[Security Solution] [Fix] [Performance] Alert Page Controls should query only from Alert Index. (#157286)

## Summary

Handles : #157217


|Before|After|
|---|---|
| <video
src="aa892faf-8055-46e0-b23b-87061ecef67f"
/> | <video
src="2f392ea4-cfc5-4759-96c7-94561f60c3f4"
/> |
This commit is contained in:
Jatin Kathuria 2023-06-21 05:21:16 -07:00 committed by GitHub
parent 87b80cb21b
commit ac5207cca9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 131 additions and 44 deletions

View file

@ -10,12 +10,15 @@ import { getNewRule } from '../../../objects/rule';
import {
CONTROL_FRAMES,
CONTROL_FRAME_TITLE,
CONTROL_POPOVER,
FILTER_GROUP_CHANGED_BANNER,
FILTER_GROUP_EDIT_CONTROL_PANEL_ITEMS,
OPTION_IGNORED,
OPTION_LIST_LABELS,
OPTION_LIST_VALUES,
OPTION_SELECTABLE,
OPTION_SELECTABLE_COUNT,
FILTER_GROUP_CONTROL_ACTION_EDIT,
FILTER_GROUP_EDIT_CONTROL_PANEL_ITEMS,
} from '../../../screens/common/filter_group';
import { createRule } from '../../../tasks/api_calls/rules';
import { cleanKibana } from '../../../tasks/common';
@ -26,15 +29,17 @@ import { formatPageFilterSearchParam } from '../../../../common/utils/format_pag
import {
closePageFilterPopover,
markAcknowledgedFirstAlert,
openFirstAlert,
openPageFilterPopover,
resetFilters,
selectCountTable,
togglePageFilterPopover,
visitAlertsPageWithCustomFilters,
waitForAlerts,
waitForPageFilters,
} from '../../../tasks/alerts';
import { ALERTS_COUNT } from '../../../screens/alerts';
import { navigateFromHeaderTo } from '../../../tasks/security_header';
import { ALERTS_COUNT, ALERTS_REFRESH_BTN } from '../../../screens/alerts';
import { kqlSearch, navigateFromHeaderTo } from '../../../tasks/security_header';
import { ALERTS, CASES } from '../../../screens/security_header';
import {
addNewFilterGroupControlValues,
@ -45,6 +50,9 @@ import {
editFilterGroupControls,
saveFilterGroupControls,
} from '../../../tasks/common/filter_group';
import { TOASTER } from '../../../screens/alerts_detection_rules';
import { setEndDate, setStartDate } from '../../../tasks/date_picker';
import { fillAddFilterForm, openAddFilterPopover } from '../../../tasks/search_bar';
const customFilters = [
{
@ -233,13 +241,21 @@ describe('Detections : Page Filters', () => {
markAcknowledgedFirstAlert();
waitForAlerts();
cy.get(OPTION_LIST_VALUES(0)).click();
cy.get(OPTION_SELECTABLE(0, 'acknowledged')).should('be.visible');
cy.get(OPTION_SELECTABLE(0, 'acknowledged')).should('be.visible').trigger('click');
cy.get(ALERTS_COUNT)
.invoke('text')
.should((newAlertCount) => {
expect(newAlertCount.split(' ')[0]).eq(String(parseInt(originalAlertCount, 10) - 1));
});
});
// cleanup
// revert the changes so that data does not change for further tests.
// It would make sure that tests can run in any order.
cy.get(OPTION_SELECTABLE(0, 'open')).trigger('click');
togglePageFilterPopover(0);
openFirstAlert();
waitForAlerts();
});
it(`URL is updated when filters are updated`, () => {
@ -256,14 +272,14 @@ describe('Detections : Page Filters', () => {
openPageFilterPopover(1);
cy.get(OPTION_SELECTABLE(1, 'high')).should('be.visible');
cy.get(OPTION_SELECTABLE(1, 'high')).click({ force: true });
cy.get(OPTION_SELECTABLE(1, 'high')).click({});
closePageFilterPopover(1);
});
it(`Filters are restored from localstorage when user navigates back to the page.`, () => {
cy.get(OPTION_LIST_VALUES(1)).click();
cy.get(OPTION_SELECTABLE(1, 'high')).should('be.visible');
cy.get(OPTION_SELECTABLE(1, 'high')).click({ force: true });
cy.get(OPTION_SELECTABLE(1, 'high')).click({});
// high should be scuccessfully selected.
cy.get(OPTION_LIST_VALUES(1)).contains('high');
@ -311,6 +327,57 @@ describe('Detections : Page Filters', () => {
cy.get(FILTER_GROUP_CHANGED_BANNER).should('not.exist');
});
context('Impact of inputs', () => {
afterEach(() => {
resetFilters();
});
it('should recover from invalide kql Query result', () => {
// do an invalid search
//
kqlSearch('\\');
cy.get(ALERTS_REFRESH_BTN).trigger('click');
waitForPageFilters();
cy.get(TOASTER).should('contain.text', 'KQLSyntaxError');
togglePageFilterPopover(0);
cy.get(OPTION_SELECTABLE(0, 'open')).should('be.visible');
cy.get(OPTION_SELECTABLE(0, 'open')).should('contain.text', 'open');
cy.get(OPTION_SELECTABLE(0, 'open')).get(OPTION_SELECTABLE_COUNT).should('have.text', 2);
});
it('should take kqlQuery into account', () => {
kqlSearch('kibana.alert.workflow_status: "nothing"');
cy.get(ALERTS_REFRESH_BTN).trigger('click');
waitForPageFilters();
togglePageFilterPopover(0);
cy.get(CONTROL_POPOVER(0)).should('contain.text', 'No options found');
cy.get(OPTION_IGNORED(0, 'open')).should('be.visible');
});
it('should take filters into account', () => {
openAddFilterPopover();
fillAddFilterForm({
key: 'kibana.alert.workflow_status',
value: 'invalid',
});
waitForPageFilters();
togglePageFilterPopover(0);
cy.get(CONTROL_POPOVER(0)).should('contain.text', 'No options found');
cy.get(OPTION_IGNORED(0, 'open')).should('be.visible');
});
it('should take timeRange into account', () => {
const startDateWithZeroAlerts = 'Jan 1, 2002 @ 00:00:00.000';
const endDateWithZeroAlerts = 'Jan 1, 2010 @ 00:00:00.000';
setStartDate(startDateWithZeroAlerts);
setEndDate(endDateWithZeroAlerts);
cy.get(ALERTS_REFRESH_BTN).trigger('click');
waitForPageFilters();
togglePageFilterPopover(0);
cy.get(CONTROL_POPOVER(0)).should('contain.text', 'No options found');
cy.get(OPTION_IGNORED(0, 'open')).should('be.visible');
});
});
it('Number fields are not visible in field edit panel', () => {
const idx = 3;
const { FILTER_FIELD_TYPE, FIELD_TYPES } = FILTER_GROUP_EDIT_CONTROL_PANEL_ITEMS;

View file

@ -36,6 +36,12 @@ export const OPTION_SELECTABLE = (popoverIndex: number, value: string) =>
export const OPTION_IGNORED = (popoverIndex: number, value: string) =>
`#control-popover-${popoverIndex} [data-test-subj="optionsList-control-ignored-selection-${value}"]`;
export const OPTION_SELECTABLE_COUNT = getDataTestSubjectSelector(
'optionsList-document-count-badge'
);
export const CONTROL_POPOVER = (popoverIdx: number) => `#control-popover-${popoverIdx}`;
export const DETECTION_PAGE_FILTER_GROUP_WRAPPER = '.filter-group__wrapper';
export const DETECTION_PAGE_FILTERS_LOADING = '.securityPageWrapper .controlFrame--controlLoading';

View file

@ -33,6 +33,8 @@ const mockedDataViewServiceGetter = jest.fn(() => {
} as unknown as DataView);
});
const mockDataViewCreator = jest.fn();
const getKibanaServiceWithMockedGetter = (
mockedDataViewGetter: DataViewsServicePublic['get'] = mockedDataViewServiceGetter
) => {
@ -42,6 +44,7 @@ const getKibanaServiceWithMockedGetter = (
...basicKibanaServicesMock.dataViews,
clearInstanceCache: jest.fn(),
get: mockedDataViewGetter,
create: mockDataViewCreator,
},
};
};
@ -55,7 +58,6 @@ const TestComponent = (props: Partial<ComponentProps<typeof DetectionPageFilterS
<TestProviders>
<DetectionPageFilterSet
chainingSystem="HIERARCHICAL"
dataViewId=""
onFilterChange={onFilterChangeMock}
{...props}
/>
@ -84,29 +86,32 @@ describe('Detection Page Filters', () => {
});
});
it('should check all the fields till any absent field is found', async () => {
it('should create dataview on render', async () => {
render(<TestComponent />);
expect(screen.getByTestId(TEST_IDS.FILTER_LOADING)).toBeInTheDocument();
await waitFor(() => {
expect(getFieldByNameMock).toHaveBeenCalledTimes(4);
expect(kibanaServiceDefaultMock.dataViews.clearInstanceCache).toHaveBeenCalledTimes(0);
expect(mockDataViewCreator).toHaveBeenCalledTimes(1);
expect(mockDataViewCreator).toHaveBeenCalledWith(
expect.objectContaining({
id: 'security_solution_alerts_dv',
name: 'Security Solution Alerts DataView',
allowNoIndex: true,
title: '.siem-signals-spacename',
})
);
});
});
it('should stop checking fields if blank field is found and clear the cache', async () => {
const getFieldByNameLocalMock = jest.fn(() => false);
const mockGetter = jest.fn(() =>
Promise.resolve({ getFieldByName: getFieldByNameLocalMock } as unknown as DataView)
);
const modifiedKibanaServicesMock = getKibanaServiceWithMockedGetter(mockGetter);
(useKibana as jest.Mock).mockReturnValueOnce({ services: modifiedKibanaServicesMock });
it('should clear cache on unmount', async () => {
const { unmount } = render(<TestComponent />);
render(<TestComponent />);
expect(screen.getByTestId(TEST_IDS.FILTER_LOADING)).toBeInTheDocument();
await waitFor(() => {
expect(getFieldByNameLocalMock).toHaveBeenCalledTimes(1);
expect(modifiedKibanaServicesMock.dataViews.clearInstanceCache).toHaveBeenCalledTimes(1);
expect(screen.getByTestId(TEST_IDS.MOCKED_CONTROL)).toBeInTheDocument();
// wait for the document to completely load.
unmount();
});
await waitFor(() => {
expect(kibanaServiceDefaultMock.dataViews.clearInstanceCache).toHaveBeenCalledTimes(1);
});
});

View file

@ -10,15 +10,30 @@ import React, { useEffect, useState, useCallback } from 'react';
import type { Filter } from '@kbn/es-query';
import { isEqual } from 'lodash';
import { EuiFlexItem } from '@elastic/eui';
import { SourcererScopeName } from '../../../common/store/sourcerer/model';
import { FilterGroupLoading } from '../../../common/components/filter_group/loading';
import { useKibana } from '../../../common/lib/kibana';
import { DEFAULT_DETECTION_PAGE_FILTERS } from '../../../../common/constants';
import { FilterGroup } from '../../../common/components/filter_group';
import { useSourcererDataView } from '../../../common/containers/sourcerer';
type FilterItemSetProps = Omit<ComponentProps<typeof FilterGroup>, 'initialControls'>;
type FilterItemSetProps = Omit<
ComponentProps<typeof FilterGroup>,
'initialControls' | 'dataViewId'
>;
const SECURITY_ALERT_DATA_VIEW = {
id: 'security_solution_alerts_dv',
name: 'Security Solution Alerts DataView',
};
const FilterItemSetComponent = (props: FilterItemSetProps) => {
const { dataViewId, onFilterChange, ...restFilterItemGroupProps } = props;
const { onFilterChange, ...restFilterItemGroupProps } = props;
const {
indexPattern: { title },
dataViewId,
} = useSourcererDataView(SourcererScopeName.detections);
const [loadingPageFilters, setLoadingPageFilters] = useState(true);
@ -27,24 +42,21 @@ const FilterItemSetComponent = (props: FilterItemSetProps) => {
} = useKibana();
useEffect(() => {
// this makes sure, that if fields are not present in existing copy of the
// dataView, clear the cache before filter group is loaded. This is only
// applicable to `alert` page as new alert mappings are added when first alert
// is encountered
(async () => {
const dataView = await dataViewService.get(dataViewId ?? '');
if (!dataView) return;
for (const filter of DEFAULT_DETECTION_PAGE_FILTERS) {
const fieldExists = dataView.getFieldByName(filter.fieldName);
if (!fieldExists) {
dataViewService.clearInstanceCache(dataViewId ?? '');
setLoadingPageFilters(false);
return;
}
}
// creates an adhoc dataview if it does not already exists just for alert index
const { timeFieldName = '@timestamp' } = await dataViewService.get(dataViewId ?? '');
await dataViewService.create({
id: SECURITY_ALERT_DATA_VIEW.id,
name: SECURITY_ALERT_DATA_VIEW.name,
title,
allowNoIndex: true,
timeFieldName,
});
setLoadingPageFilters(false);
})();
}, [dataViewId, dataViewService]);
return () => dataViewService.clearInstanceCache();
}, [title, dataViewService, dataViewId]);
const [initialFilterControls] = useState(DEFAULT_DETECTION_PAGE_FILTERS);
@ -78,7 +90,7 @@ const FilterItemSetComponent = (props: FilterItemSetProps) => {
return (
<FilterGroup
dataViewId={dataViewId}
dataViewId={SECURITY_ALERT_DATA_VIEW.id}
onFilterChange={filterChangesHandler}
initialControls={initialFilterControls}
{...restFilterItemGroupProps}

View file

@ -153,7 +153,6 @@ const DetectionEnginePageComponent: React.FC<DetectionEngineComponentProps> = ({
const {
indexPattern,
runtimeMappings,
dataViewId,
loading: isLoadingIndexPattern,
} = useSourcererDataView(SourcererScopeName.detections);
@ -351,7 +350,6 @@ const DetectionEnginePageComponent: React.FC<DetectionEngineComponentProps> = ({
</EuiFlexGroup>
) : (
<DetectionPageFilterSet
dataViewId={dataViewId}
onFilterChange={pageFiltersUpdateHandler}
filters={topLevelFilters}
query={query}
@ -367,7 +365,6 @@ const DetectionEnginePageComponent: React.FC<DetectionEngineComponentProps> = ({
[
topLevelFilters,
arePageFiltersEnabled,
dataViewId,
statusFilter,
onFilterGroupChangedCallback,
pageFiltersUpdateHandler,