[Security Solution][Investigations] Fix favorite filter behaviour in timeline search (#122265)

* fix: apply correct search options for favorited timelines

* test: add timeline overview page tests

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Jan Monschke 2022-01-07 14:52:37 +01:00 committed by GitHub
parent 5316d08c31
commit ef2610a8f9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 143 additions and 18 deletions

View file

@ -0,0 +1,88 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import {
TIMELINES_OVERVIEW_TABLE,
TIMELINES_OVERVIEW_ONLY_FAVORITES,
TIMELINES_OVERVIEW_SEARCH,
} from '../../screens/timelines';
import {
getTimeline,
getFavoritedTimeline,
sharedTimelineTitleFragment,
} from '../../objects/timeline';
import { cleanKibana } from '../../tasks/common';
import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login';
import { createTimeline, favoriteTimeline } from '../../tasks/api_calls/timelines';
import { TIMELINES_URL } from '../../urls/navigation';
describe('timeline overview search', () => {
before(() => {
cleanKibana();
createTimeline(getFavoritedTimeline())
.then((response) => response.body.data.persistTimeline.timeline.savedObjectId)
.then((timelineId) => favoriteTimeline({ timelineId, timelineType: 'default' }));
createTimeline(getTimeline());
loginAndWaitForPageWithoutDateRange(TIMELINES_URL);
});
beforeEach(() => {
cy.get(TIMELINES_OVERVIEW_SEARCH).clear();
});
it('should show all timelines when no search term was entered', () => {
cy.get(TIMELINES_OVERVIEW_TABLE).contains(getTimeline().title);
cy.get(TIMELINES_OVERVIEW_TABLE).contains(getFavoritedTimeline().title);
});
it('should show the correct favorite count without search', () => {
cy.get(TIMELINES_OVERVIEW_ONLY_FAVORITES).contains(1);
});
it('should show the correct timelines when the favorite filter is activated', () => {
cy.get(TIMELINES_OVERVIEW_ONLY_FAVORITES).click(); // enable the filter
cy.get(TIMELINES_OVERVIEW_TABLE).contains(getTimeline().title).should('not.exist');
cy.get(TIMELINES_OVERVIEW_TABLE).contains(getFavoritedTimeline().title);
cy.get(TIMELINES_OVERVIEW_ONLY_FAVORITES).contains(1);
cy.get(TIMELINES_OVERVIEW_ONLY_FAVORITES).click(); // disable the filter
});
it('should find the correct timeline and have the correct favorite count when searching by timeline title', () => {
cy.get(TIMELINES_OVERVIEW_SEARCH).type(`"${getTimeline().title}"{enter}`);
cy.get(TIMELINES_OVERVIEW_TABLE).contains(getFavoritedTimeline().title).should('not.exist');
cy.get(TIMELINES_OVERVIEW_TABLE).contains(getTimeline().title);
cy.get(TIMELINES_OVERVIEW_ONLY_FAVORITES).contains(0);
});
it('should find the correct timelines when searching for favorited timelines', () => {
cy.get(TIMELINES_OVERVIEW_ONLY_FAVORITES).click(); // enable the filter
cy.get(TIMELINES_OVERVIEW_SEARCH).type(`"${getFavoritedTimeline().title}"{enter}`);
cy.get(TIMELINES_OVERVIEW_TABLE).contains(getTimeline().title).should('not.exist');
cy.get(TIMELINES_OVERVIEW_TABLE).contains(getFavoritedTimeline().title);
cy.get(TIMELINES_OVERVIEW_ONLY_FAVORITES).contains(1);
cy.get(TIMELINES_OVERVIEW_ONLY_FAVORITES).click(); // disable the filter
});
it('should find the correct timelines when both favorited and non-favorited timelines match', () => {
cy.get(TIMELINES_OVERVIEW_SEARCH).type(`"${sharedTimelineTitleFragment}"{enter}`);
cy.get(TIMELINES_OVERVIEW_TABLE).contains(getTimeline().title);
cy.get(TIMELINES_OVERVIEW_TABLE).contains(getFavoritedTimeline().title);
cy.get(TIMELINES_OVERVIEW_ONLY_FAVORITES).contains(1);
});
});

View file

@ -34,14 +34,24 @@ export const getFilter = (): TimelineFilter => ({
value: 'exists',
});
export const sharedTimelineTitleFragment = 'Timeline';
export const getTimeline = (): CompleteTimeline => ({
title: 'Security Timeline',
title: `Security ${sharedTimelineTitleFragment}`,
description: 'This is the best timeline',
query: 'host.name: *',
notes: 'Yes, the best timeline',
filter: getFilter(),
});
export const getFavoritedTimeline = (): CompleteTimeline => ({
title: `Darkest ${sharedTimelineTitleFragment}`,
description: 'This is the darkest timeline',
query: 'host.name: *',
notes: 'Yes, the darkest timeline, you heard me right',
filter: getFilter(),
});
export const getIndicatorMatchTimelineTemplate = (): CompleteTimeline => ({
...getTimeline(),
title: 'Generic Threat Match Timeline',

View file

@ -45,3 +45,11 @@ export const TIMELINES_TABLE = '[data-test-subj="timelines-table"]';
export const TIMELINES_USERNAME = '[data-test-subj="username"]';
export const REFRESH_BUTTON = '[data-test-subj="refreshButton-linkIcon"]';
export const TIMELINES_OVERVIEW = '[data-test-subj="timelines-container"]';
export const TIMELINES_OVERVIEW_ONLY_FAVORITES = `${TIMELINES_OVERVIEW} [data-test-subj="only-favorites-toggle"]`;
export const TIMELINES_OVERVIEW_SEARCH = `${TIMELINES_OVERVIEW} [data-test-subj="search-bar"]`;
export const TIMELINES_OVERVIEW_TABLE = `${TIMELINES_OVERVIEW} [data-test-subj="timelines-table"]`;

View file

@ -79,7 +79,7 @@ export const TimelinesPageComponent: React.FC = () => {
</EuiFlexGroup>
</HeaderPage>
<TimelinesContainer>
<TimelinesContainer data-test-subj="timelines-container">
<StatefulOpenTimeline
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
isModal={false}

View file

@ -222,11 +222,10 @@ describe('saved_object', () => {
test('should send correct options for counts of favorite timeline', async () => {
expect(mockFindSavedObject.mock.calls[5][0]).toEqual({
filter:
'not siem-ui-timeline.attributes.status: draft and not siem-ui-timeline.attributes.status: immutable',
'not siem-ui-timeline.attributes.status: draft and not siem-ui-timeline.attributes.status: immutable and siem-ui-timeline.attributes.favorite.keySearch: dXNlcm5hbWU=',
page: 1,
perPage: 1,
search: ' dXNlcm5hbWU=',
searchFields: ['title', 'description', 'favorite.keySearch'],
searchFields: ['title', 'description'],
type: 'siem-ui-timeline',
});
});

View file

@ -161,9 +161,26 @@ const getTimelineTypeFilter = (
: `not siem-ui-timeline.attributes.status: ${TimelineStatus.immutable}`;
const filters = [typeFilter, draftFilter, immutableFilter];
return filters.filter((f) => f != null).join(' and ');
return combineFilters(filters);
};
const getTimelineFavoriteFilter = ({
onlyUserFavorite,
request,
}: {
onlyUserFavorite: boolean | null;
request: FrameworkRequest;
}) => {
if (!onlyUserFavorite) {
return null;
}
const username = request.user?.username ?? UNAUTHENTICATED_USER;
return `siem-ui-timeline.attributes.favorite.keySearch: ${convertStringToBase64(username)}`;
};
const combineFilters = (filters: Array<string | null>) =>
filters.filter((f) => f != null).join(' and ');
export const getExistingPrepackagedTimelines = async (
request: FrameworkRequest,
countsOnly?: boolean,
@ -197,15 +214,19 @@ export const getAllTimeline = async (
status: TimelineStatusLiteralWithNull,
timelineType: TimelineTypeLiteralWithNull
): Promise<AllTimelinesResponse> => {
const searchTerm = search != null ? search : undefined;
const searchFields = ['title', 'description'];
const filter = combineFilters([
getTimelineTypeFilter(timelineType ?? null, status ?? null),
getTimelineFavoriteFilter({ onlyUserFavorite, request }),
]);
const options: SavedObjectsFindOptions = {
type: timelineSavedObjectType,
perPage: pageInfo.pageSize,
page: pageInfo.pageIndex,
search: search != null ? search : undefined,
searchFields: onlyUserFavorite
? ['title', 'description', 'favorite.keySearch']
: ['title', 'description'],
filter: getTimelineTypeFilter(timelineType ?? null, status ?? null),
filter,
search: searchTerm,
searchFields,
sortField: sort != null ? sort.sortField : undefined,
sortOrder: sort != null ? sort.sortOrder : undefined,
};
@ -233,10 +254,14 @@ export const getAllTimeline = async (
const favoriteTimelineOptions = {
type: timelineSavedObjectType,
searchFields: ['title', 'description', 'favorite.keySearch'],
search: searchTerm,
searchFields,
perPage: 1,
page: 1,
filter: getTimelineTypeFilter(timelineType ?? null, TimelineStatus.active),
filter: combineFilters([
getTimelineTypeFilter(timelineType ?? null, TimelineStatus.active),
getTimelineFavoriteFilter({ onlyUserFavorite: true, request }),
]),
};
const result = await Promise.all([
@ -623,11 +648,6 @@ const getSavedTimeline = async (request: FrameworkRequest, timelineId: string) =
const getAllSavedTimeline = async (request: FrameworkRequest, options: SavedObjectsFindOptions) => {
const userName = request.user?.username ?? UNAUTHENTICATED_USER;
const savedObjectsClient = request.context.core.savedObjects.client;
if (options.searchFields != null && options.searchFields.includes('favorite.keySearch')) {
options.search = `${options.search != null ? options.search : ''} ${
userName != null ? convertStringToBase64(userName) : null
}`;
}
const savedObjects = await savedObjectsClient.find<TimelineWithoutExternalRefs>(options);