From d56b22fccbab559d662725bf35130a0de28e6c5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20S=C3=A1nchez?= Date: Thu, 5 Aug 2021 15:08:30 +0200 Subject: [PATCH] [Security solution][Endpoint] Users can filter trusted apps by policy name (#106710) * Allow users select policies from a dropdown * Policy filters are passed throguh the API call and the results are now filtered by policy * Moved policies selector inside search component and triggers search only when refresh button is clicked * Fixes tests * Triggers policy filter when policy is selected. Also fix unit test because now policies are loaded at the trusted apps list * Renamed components and added an index.ts for the exports * Adds unit tests for policies selector component * Fix unit tests and changed camelcase by snack case for url params * adds multilang * Fixes i18n keys * Move mock resonse to the mocks file * Use string templating in test * remove === true from boolean comparison * Set function in useCallback. Renames some variables and types. Use reourceState helper function to get the prev state. Use generated data for policies in tests * Fix ts errors * Removes unused type and fix type name for Item * Puts exclude clause on policy dropdown behind a feature flag * Adds missing feature flags in some tests and in global reducer * Fix test adding useExperimentalValua mock for FF * Wrapp handlers in a useCallback in order to prevent useless rerenders Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../common/experimental_features.ts | 1 + .../public/common/store/app/reducer.ts | 1 + .../public/management/common/routing.test.ts | 74 +++++-- .../public/management/common/routing.ts | 22 +- .../public/management/common/utils.ts | 30 +++ .../components/policies_selector/index.ts | 8 + .../policies_selector.test.tsx | 128 +++++++++++ .../policies_selector/policies_selector.tsx | 205 ++++++++++++++++++ .../components/search_bar/index.tsx | 51 ----- .../components/search_exceptions/index.ts | 8 + .../search_exceptions.test.tsx} | 14 +- .../search_exceptions/search_exceptions.tsx | 116 ++++++++++ .../view/event_filters_list_page.tsx | 4 +- .../state/trusted_apps_list_page_state.ts | 6 + .../pages/trusted_apps/store/builders.ts | 2 + .../trusted_apps/store/middleware.test.ts | 27 +++ .../pages/trusted_apps/store/middleware.ts | 22 +- .../pages/trusted_apps/store/mocks.ts | 17 ++ .../pages/trusted_apps/store/reducer.test.ts | 22 +- .../trusted_apps/store/selectors.test.ts | 6 + .../pages/trusted_apps/store/selectors.ts | 20 ++ .../pages/trusted_apps/test_utils/index.ts | 2 + .../view/trusted_apps_page.test.tsx | 43 +++- .../trusted_apps/view/trusted_apps_page.tsx | 38 +++- .../signals/get_input_output_index.test.ts | 1 + .../factory/hosts/details/index.test.tsx | 1 + 26 files changed, 768 insertions(+), 101 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/management/components/policies_selector/index.ts create mode 100644 x-pack/plugins/security_solution/public/management/components/policies_selector/policies_selector.test.tsx create mode 100644 x-pack/plugins/security_solution/public/management/components/policies_selector/policies_selector.tsx delete mode 100644 x-pack/plugins/security_solution/public/management/components/search_bar/index.tsx create mode 100644 x-pack/plugins/security_solution/public/management/components/search_exceptions/index.ts rename x-pack/plugins/security_solution/public/management/components/{search_bar/index.test.tsx => search_exceptions/search_exceptions.test.tsx} (89%) create mode 100644 x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.tsx create mode 100644 x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/mocks.ts diff --git a/x-pack/plugins/security_solution/common/experimental_features.ts b/x-pack/plugins/security_solution/common/experimental_features.ts index 6d4a2b78840ea..0ae42d4baaec4 100644 --- a/x-pack/plugins/security_solution/common/experimental_features.ts +++ b/x-pack/plugins/security_solution/common/experimental_features.ts @@ -16,6 +16,7 @@ export const allowedExperimentalValues = Object.freeze({ ruleRegistryEnabled: false, tGridEnabled: false, trustedAppsByPolicyEnabled: false, + excludePoliciesInFilterEnabled: false, uebaEnabled: false, }); diff --git a/x-pack/plugins/security_solution/public/common/store/app/reducer.ts b/x-pack/plugins/security_solution/public/common/store/app/reducer.ts index 5b0a2330a408d..6ab572490f5d7 100644 --- a/x-pack/plugins/security_solution/public/common/store/app/reducer.ts +++ b/x-pack/plugins/security_solution/public/common/store/app/reducer.ts @@ -19,6 +19,7 @@ export const initialAppState: AppState = { errors: [], enableExperimental: { trustedAppsByPolicyEnabled: false, + excludePoliciesInFilterEnabled: false, metricsEntitiesEnabled: false, ruleRegistryEnabled: false, tGridEnabled: false, diff --git a/x-pack/plugins/security_solution/public/management/common/routing.test.ts b/x-pack/plugins/security_solution/public/management/common/routing.test.ts index 82b7a15d642e4..e0e6ed2a08037 100644 --- a/x-pack/plugins/security_solution/public/management/common/routing.test.ts +++ b/x-pack/plugins/security_solution/public/management/common/routing.test.ts @@ -106,26 +106,28 @@ describe('routing', () => { }); it('builds proper path when only page size provided', () => { - expect(getTrustedAppsListPath({ page_size: 20 })).toEqual( - '/administration/trusted_apps?page_size=20' + const pageSize = 20; + expect(getTrustedAppsListPath({ page_size: pageSize })).toEqual( + `/administration/trusted_apps?page_size=${pageSize}` ); }); it('builds proper path when only page index provided', () => { - expect(getTrustedAppsListPath({ page_index: 2 })).toEqual( - '/administration/trusted_apps?page_index=2' + const pageIndex = 2; + expect(getTrustedAppsListPath({ page_index: pageIndex })).toEqual( + `/administration/trusted_apps?page_index=${pageIndex}` ); }); it('builds proper path when only "show" provided', () => { - expect(getTrustedAppsListPath({ show: 'create' })).toEqual( - '/administration/trusted_apps?show=create' - ); + const show = 'create'; + expect(getTrustedAppsListPath({ show })).toEqual(`/administration/trusted_apps?show=${show}`); }); it('builds proper path when only view type provided', () => { - expect(getTrustedAppsListPath({ view_type: 'list' })).toEqual( - '/administration/trusted_apps?view_type=list' + const viewType = 'list'; + expect(getTrustedAppsListPath({ view_type: viewType })).toEqual( + `/administration/trusted_apps?view_type=${viewType}` ); }); @@ -135,56 +137,82 @@ describe('routing', () => { page_size: 20, show: 'create', view_type: 'list', - filter: '', + filter: 'test', + included_policies: 'globally', + excluded_policies: 'unassigned', }; expect(getTrustedAppsListPath(location)).toEqual( - '/administration/trusted_apps?page_index=2&page_size=20&view_type=list&show=create' + `/administration/trusted_apps?page_index=${location.page_index}&page_size=${location.page_size}&view_type=${location.view_type}&show=${location.show}&filter=${location.filter}&included_policies=${location.included_policies}&excluded_policies=${location.excluded_policies}` ); }); it('builds proper path when page index is equal to default', () => { - const path = getTrustedAppsListPath({ + const location: TrustedAppsListPageLocation = { page_index: MANAGEMENT_DEFAULT_PAGE, page_size: 20, show: 'create', view_type: 'list', - }); + filter: '', + included_policies: '', + excluded_policies: '', + }; + const path = getTrustedAppsListPath(location); - expect(path).toEqual('/administration/trusted_apps?page_size=20&view_type=list&show=create'); + expect(path).toEqual( + `/administration/trusted_apps?page_size=${location.page_size}&view_type=${location.view_type}&show=${location.show}` + ); }); it('builds proper path when page size is equal to default', () => { - const path = getTrustedAppsListPath({ + const location: TrustedAppsListPageLocation = { page_index: 2, page_size: MANAGEMENT_DEFAULT_PAGE_SIZE, show: 'create', view_type: 'list', - }); + filter: '', + included_policies: '', + excluded_policies: '', + }; + const path = getTrustedAppsListPath(location); - expect(path).toEqual('/administration/trusted_apps?page_index=2&view_type=list&show=create'); + expect(path).toEqual( + `/administration/trusted_apps?page_index=${location.page_index}&view_type=${location.view_type}&show=${location.show}` + ); }); it('builds proper path when "show" is equal to default', () => { - const path = getTrustedAppsListPath({ + const location: TrustedAppsListPageLocation = { page_index: 2, page_size: 20, show: undefined, view_type: 'list', - }); + filter: '', + included_policies: '', + excluded_policies: '', + }; + const path = getTrustedAppsListPath(location); - expect(path).toEqual('/administration/trusted_apps?page_index=2&page_size=20&view_type=list'); + expect(path).toEqual( + `/administration/trusted_apps?page_index=${location.page_index}&page_size=${location.page_size}&view_type=${location.view_type}` + ); }); it('builds proper path when view type is equal to default', () => { - const path = getTrustedAppsListPath({ + const location: TrustedAppsListPageLocation = { page_index: 2, page_size: 20, show: 'create', view_type: 'grid', - }); + filter: '', + included_policies: '', + excluded_policies: '', + }; + const path = getTrustedAppsListPath(location); - expect(path).toEqual('/administration/trusted_apps?page_index=2&page_size=20&show=create'); + expect(path).toEqual( + `/administration/trusted_apps?page_index=${location.page_index}&page_size=${location.page_size}&show=${location.show}` + ); }); it('builds proper path when params are equal to default', () => { diff --git a/x-pack/plugins/security_solution/public/management/common/routing.ts b/x-pack/plugins/security_solution/public/management/common/routing.ts index e6c02ffefa3c5..d044fc0f1f2f6 100644 --- a/x-pack/plugins/security_solution/public/management/common/routing.ts +++ b/x-pack/plugins/security_solution/public/management/common/routing.ts @@ -140,6 +140,12 @@ const normalizeTrustedAppsPageLocation = ( ...(!isDefaultOrMissing(location.show, undefined) ? { show: location.show } : {}), ...(!isDefaultOrMissing(location.id, undefined) ? { id: location.id } : {}), ...(!isDefaultOrMissing(location.filter, '') ? { filter: location.filter } : ''), + ...(!isDefaultOrMissing(location.included_policies, '') + ? { included_policies: location.included_policies } + : ''), + ...(!isDefaultOrMissing(location.excluded_policies, '') + ? { excluded_policies: location.excluded_policies } + : ''), }; } else { return {}; @@ -196,12 +202,26 @@ const extractFilter = (query: querystring.ParsedUrlQuery): string => { return extractFirstParamValue(query, 'filter') || ''; }; +const extractIncludedPolicies = (query: querystring.ParsedUrlQuery): string => { + return extractFirstParamValue(query, 'included_policies') || ''; +}; + +const extractExcludedPolicies = (query: querystring.ParsedUrlQuery): string => { + return extractFirstParamValue(query, 'excluded_policies') || ''; +}; + export const extractListPaginationParams = (query: querystring.ParsedUrlQuery) => ({ page_index: extractPageIndex(query), page_size: extractPageSize(query), filter: extractFilter(query), }); +export const extractTrustedAppsListPaginationParams = (query: querystring.ParsedUrlQuery) => ({ + ...extractListPaginationParams(query), + included_policies: extractIncludedPolicies(query), + excluded_policies: extractExcludedPolicies(query), +}); + export const extractTrustedAppsListPageLocation = ( query: querystring.ParsedUrlQuery ): TrustedAppsListPageLocation => { @@ -211,7 +231,7 @@ export const extractTrustedAppsListPageLocation = ( ) as TrustedAppsListPageLocation['show']; return { - ...extractListPaginationParams(query), + ...extractTrustedAppsListPaginationParams(query), view_type: extractFirstParamValue(query, 'view_type') === 'list' ? 'list' : 'grid', show: showParamValue && ['edit', 'create'].includes(showParamValue) ? showParamValue : undefined, diff --git a/x-pack/plugins/security_solution/public/management/common/utils.ts b/x-pack/plugins/security_solution/public/management/common/utils.ts index 616e395c8ad47..3fbe5662f338c 100644 --- a/x-pack/plugins/security_solution/public/management/common/utils.ts +++ b/x-pack/plugins/security_solution/public/management/common/utils.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { isEmpty } from 'lodash/fp'; + export const parseQueryFilterToKQL = (filter: string, fields: Readonly): string => { if (!filter) return ''; const kuery = fields @@ -19,3 +21,31 @@ export const parseQueryFilterToKQL = (filter: string, fields: Readonly return `(${kuery})`; }; + +const getPolicyQuery = (policyId: string): string => { + if (policyId === 'global') return 'exception-list-agnostic.attributes.tags:"policy:all"'; + if (policyId === 'unassigned') return '(not exception-list-agnostic.attributes.tags:*)'; + return `exception-list-agnostic.attributes.tags:"policy:${policyId}"`; +}; + +export const parsePoliciesToKQL = (includedPolicies: string, excludedPolicies: string): string => { + if (isEmpty(includedPolicies) && isEmpty(excludedPolicies)) return ''; + + const parsedIncludedPolicies = includedPolicies ? includedPolicies.split(',') : undefined; + const parsedExcludedPolicies = excludedPolicies ? excludedPolicies.split(',') : undefined; + + const includedPoliciesKuery = parsedIncludedPolicies + ? parsedIncludedPolicies.map(getPolicyQuery).join(' OR ') + : ''; + + const excludedPoliciesKuery = parsedExcludedPolicies + ? parsedExcludedPolicies.map((policyId) => `not ${getPolicyQuery(policyId)}`).join(' AND ') + : ''; + + const kuery = []; + + if (includedPoliciesKuery) kuery.push(includedPoliciesKuery); + if (excludedPoliciesKuery) kuery.push(excludedPoliciesKuery); + + return `(${kuery.join(' AND ')})`; +}; diff --git a/x-pack/plugins/security_solution/public/management/components/policies_selector/index.ts b/x-pack/plugins/security_solution/public/management/components/policies_selector/index.ts new file mode 100644 index 0000000000000..1b25a8af9d3fc --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/components/policies_selector/index.ts @@ -0,0 +1,8 @@ +/* + * 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. + */ + +export { PoliciesSelector, PolicySelectionItem, PoliciesSelectorProps } from './policies_selector'; diff --git a/x-pack/plugins/security_solution/public/management/components/policies_selector/policies_selector.test.tsx b/x-pack/plugins/security_solution/public/management/components/policies_selector/policies_selector.test.tsx new file mode 100644 index 0000000000000..f93c7399cac33 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/components/policies_selector/policies_selector.test.tsx @@ -0,0 +1,128 @@ +/* + * 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 { I18nProvider } from '@kbn/i18n/react'; +import { render, act, fireEvent, RenderResult } from '@testing-library/react'; +import React from 'react'; +import { EndpointDocGenerator } from '../../../../common/endpoint/generate_data'; + +import { PoliciesSelector, PoliciesSelectorProps } from '.'; +import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features'; + +// TODO: remove this mock when feature flag is removed +jest.mock('../../../common/hooks/use_experimental_features'); +const useIsExperimentalFeatureEnabledMock = useIsExperimentalFeatureEnabled as jest.Mock; + +let onChangeSelectionMock: jest.Mock; + +describe('Policies selector', () => { + let getElement: (params: Partial) => RenderResult; + beforeEach(() => { + onChangeSelectionMock = jest.fn(); + useIsExperimentalFeatureEnabledMock.mockReturnValue(false); + getElement = (params: Partial) => { + return render( + + + + ); + }; + }); + const generator = new EndpointDocGenerator('policy-list'); + const policy = generator.generatePolicyPackagePolicy(); + policy.name = 'test policy A'; + policy.id = 'abc123'; + + describe('When click on policy', () => { + it('should have a default value', () => { + const defaultIncludedPolicies = 'abc123'; + const defaultExcludedPolicies = 'global'; + const element = getElement({ defaultExcludedPolicies, defaultIncludedPolicies }); + act(() => { + fireEvent.click(element.getByTestId('policiesSelectorButton')); + }); + expect(element.getByText(policy.name)).toHaveTextContent(policy.name); + act(() => { + fireEvent.click(element.getByText('Unassigned entries')); + }); + expect(onChangeSelectionMock).toHaveBeenCalledWith([ + { checked: 'on', id: 'abc123', name: 'test policy A' }, + { checked: 'off', id: 'global', name: 'Global entries' }, + { checked: 'on', id: 'unassigned', name: 'Unassigned entries' }, + ]); + }); + + it('should disable enabled default value', () => { + useIsExperimentalFeatureEnabledMock.mockReturnValue(true); + const defaultIncludedPolicies = 'abc123'; + const defaultExcludedPolicies = 'global'; + const element = getElement({ defaultExcludedPolicies, defaultIncludedPolicies }); + act(() => { + fireEvent.click(element.getByTestId('policiesSelectorButton')); + }); + act(() => { + fireEvent.click(element.getByText(policy.name)); + }); + expect(onChangeSelectionMock).toHaveBeenCalledWith([ + { checked: 'off', id: 'abc123', name: 'test policy A' }, + { checked: 'off', id: 'global', name: 'Global entries' }, + { checked: undefined, id: 'unassigned', name: 'Unassigned entries' }, + ]); + }); + + it('should remove disabled default value', () => { + const defaultIncludedPolicies = 'abc123'; + const defaultExcludedPolicies = 'global'; + const element = getElement({ defaultExcludedPolicies, defaultIncludedPolicies }); + act(() => { + fireEvent.click(element.getByTestId('policiesSelectorButton')); + }); + act(() => { + fireEvent.click(element.getByText('Global entries')); + }); + expect(onChangeSelectionMock).toHaveBeenCalledWith([ + { checked: 'on', id: 'abc123', name: 'test policy A' }, + { checked: undefined, id: 'global', name: 'Global entries' }, + { checked: undefined, id: 'unassigned', name: 'Unassigned entries' }, + ]); + }); + }); + + describe('When filter policy', () => { + it('should filter policy by name', () => { + const element = getElement({}); + act(() => { + fireEvent.click(element.getByTestId('policiesSelectorButton')); + }); + act(() => { + fireEvent.change(element.getByTestId('policiesSelectorSearch'), { + target: { value: policy.name }, + }); + }); + expect(element.queryAllByText('Global entries')).toStrictEqual([]); + expect(element.getByText(policy.name)).toHaveTextContent(policy.name); + }); + it('should filter with no results', () => { + const element = getElement({}); + act(() => { + fireEvent.click(element.getByTestId('policiesSelectorButton')); + }); + act(() => { + fireEvent.change(element.getByTestId('policiesSelectorSearch'), { + target: { value: 'no results' }, + }); + }); + expect(element.queryAllByText('Global entries')).toStrictEqual([]); + expect(element.queryAllByText('Unassigned entries')).toStrictEqual([]); + expect(element.queryAllByText(policy.name)).toStrictEqual([]); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/management/components/policies_selector/policies_selector.tsx b/x-pack/plugins/security_solution/public/management/components/policies_selector/policies_selector.tsx new file mode 100644 index 0000000000000..b86c8f6de7abd --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/components/policies_selector/policies_selector.tsx @@ -0,0 +1,205 @@ +/* + * 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 React, { memo, useCallback, useMemo, useState, useEffect, ChangeEvent } from 'react'; + +import { i18n } from '@kbn/i18n'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiFilterGroup, + EuiPopover, + EuiPopoverTitle, + EuiFieldSearch, + EuiFilterButton, + EuiFilterSelectItem, + FilterChecked, + EuiText, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { ImmutableArray, PolicyData } from '../../../../common/endpoint/types'; +import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features'; + +export interface PoliciesSelectorProps { + policies: ImmutableArray; + defaultIncludedPolicies?: string; + defaultExcludedPolicies?: string; + onChangeSelection: (items: PolicySelectionItem[]) => void; +} + +export interface PolicySelectionItem { + name: string; + id?: string; + checked?: FilterChecked; +} + +interface DefaultPoliciesByKey { + [key: string]: boolean; +} + +const GLOBAL_ENTRIES = i18n.translate( + 'xpack.securitySolution.management.policiesSelector.globalEntries', + { + defaultMessage: 'Global entries', + } +); +const UNASSIGNED_ENTRIES = i18n.translate( + 'xpack.securitySolution.management.policiesSelector.unassignedEntries', + { + defaultMessage: 'Unassigned entries', + } +); + +export const PoliciesSelector = memo( + ({ policies, onChangeSelection, defaultExcludedPolicies, defaultIncludedPolicies }) => { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const [query, setQuery] = useState(''); + const [itemsList, setItemsList] = useState([]); + + const isExcludePoliciesInFilterEnabled = useIsExperimentalFeatureEnabled( + 'excludePoliciesInFilterEnabled' + ); + + useEffect(() => { + const defaultIncludedPoliciesByKey: DefaultPoliciesByKey = defaultIncludedPolicies + ? defaultIncludedPolicies.split(',').reduce((acc, val) => ({ ...acc, [val]: true }), {}) + : {}; + + const defaultExcludedPoliciesByKey: DefaultPoliciesByKey = defaultExcludedPolicies + ? defaultExcludedPolicies.split(',').reduce((acc, val) => ({ ...acc, [val]: true }), {}) + : {}; + + const getCheckedValue = (id: string): FilterChecked | undefined => + defaultIncludedPoliciesByKey[id] + ? 'on' + : defaultExcludedPoliciesByKey[id] + ? 'off' + : undefined; + + setItemsList([ + ...policies.map((policy) => ({ + name: policy.name, + id: policy.id, + checked: getCheckedValue(policy.id), + })), + { name: GLOBAL_ENTRIES, id: 'global', checked: getCheckedValue('global') }, + { name: UNASSIGNED_ENTRIES, id: 'unassigned', checked: getCheckedValue('unassigned') }, + ]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [policies]); + + const onButtonClick = useCallback(() => { + setIsPopoverOpen((prevIsPopoverOpen) => !prevIsPopoverOpen); + }, []); + + const closePopover = useCallback(() => { + setIsPopoverOpen(false); + }, []); + + const onChange = useCallback((ev: ChangeEvent) => { + const value = ev.target.value || ''; + setQuery(value); + }, []); + + const updateItem = useCallback( + (index: number) => { + if (!itemsList[index]) { + return; + } + + const newItems = [...itemsList]; + + switch (newItems[index].checked) { + case 'on': + newItems[index].checked = isExcludePoliciesInFilterEnabled ? 'off' : undefined; + break; + + case 'off': + newItems[index].checked = undefined; + break; + + default: + newItems[index].checked = 'on'; + } + + setItemsList(newItems); + onChangeSelection(newItems); + }, + [itemsList, onChangeSelection, isExcludePoliciesInFilterEnabled] + ); + + const dropdownItems = useMemo( + () => + itemsList.map((item, index) => + item.name.match(new RegExp(query, 'i')) ? ( + updateItem(index)} + > + {item.name} + + ) : null + ), + [itemsList, query, updateItem] + ); + + const button = useMemo( + () => ( + item.checked === 'on')} + numActiveFilters={itemsList.filter((item) => item.checked === 'on').length} + > + + + + + ), + [isPopoverOpen, itemsList, onButtonClick] + ); + + return ( + + + + + + + +
{dropdownItems}
+
+
+
+
+ ); + } +); + +PoliciesSelector.displayName = 'PoliciesSelector'; diff --git a/x-pack/plugins/security_solution/public/management/components/search_bar/index.tsx b/x-pack/plugins/security_solution/public/management/components/search_bar/index.tsx deleted file mode 100644 index 5ace2b901da11..0000000000000 --- a/x-pack/plugins/security_solution/public/management/components/search_bar/index.tsx +++ /dev/null @@ -1,51 +0,0 @@ -/* - * 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 React, { memo, useCallback, useState } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiFieldSearch, EuiButton } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; - -export interface SearchBarProps { - defaultValue?: string; - placeholder: string; - onSearch(value: string): void; -} - -export const SearchBar = memo(({ defaultValue = '', onSearch, placeholder }) => { - const [query, setQuery] = useState(defaultValue); - - const handleOnChangeSearchField = useCallback( - (ev: React.ChangeEvent) => setQuery(ev.target.value), - [setQuery] - ); - const handleOnSearch = useCallback(() => onSearch(query), [query, onSearch]); - - return ( - - - - - - - {i18n.translate('xpack.securitySolution.management.search.button', { - defaultMessage: 'Refresh', - })} - - - - ); -}); - -SearchBar.displayName = 'SearchBar'; diff --git a/x-pack/plugins/security_solution/public/management/components/search_exceptions/index.ts b/x-pack/plugins/security_solution/public/management/components/search_exceptions/index.ts new file mode 100644 index 0000000000000..6a870dbb06c66 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/components/search_exceptions/index.ts @@ -0,0 +1,8 @@ +/* + * 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. + */ + +export { SearchExceptions, SearchExceptionsProps } from './search_exceptions'; diff --git a/x-pack/plugins/security_solution/public/management/components/search_bar/index.test.tsx b/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.test.tsx similarity index 89% rename from x-pack/plugins/security_solution/public/management/components/search_bar/index.test.tsx rename to x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.test.tsx index 707a96938655a..5c909e062ceb9 100644 --- a/x-pack/plugins/security_solution/public/management/components/search_bar/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.test.tsx @@ -8,7 +8,7 @@ import { mount } from 'enzyme'; import React from 'react'; -import { SearchBar } from '.'; +import { SearchExceptions } from '.'; let onSearchMock: jest.Mock; @@ -16,13 +16,17 @@ interface EuiFieldSearchPropsFake { onSearch(value: string): void; } -describe('Search bar', () => { +describe('Search exceptions', () => { beforeEach(() => { onSearchMock = jest.fn(); }); const getElement = (defaultValue: string = '') => ( - + ); it('should have a default value', () => { @@ -45,7 +49,7 @@ describe('Search bar', () => { searchFieldProps.onSearch(expectedDefaultValue); expect(onSearchMock).toHaveBeenCalledTimes(1); - expect(onSearchMock).toHaveBeenCalledWith(expectedDefaultValue); + expect(onSearchMock).toHaveBeenCalledWith(expectedDefaultValue, '', ''); }); it('should dispatch search action when click on button', () => { @@ -55,6 +59,6 @@ describe('Search bar', () => { element.find('[data-test-subj="searchButton"]').first().simulate('click'); expect(onSearchMock).toHaveBeenCalledTimes(1); - expect(onSearchMock).toHaveBeenCalledWith(expectedDefaultValue); + expect(onSearchMock).toHaveBeenCalledWith(expectedDefaultValue, '', ''); }); }); diff --git a/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.tsx b/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.tsx new file mode 100644 index 0000000000000..a737b53e2d9b9 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.tsx @@ -0,0 +1,116 @@ +/* + * 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 React, { memo, useCallback, useState } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiFieldSearch, EuiButton } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { PolicySelectionItem, PoliciesSelector } from '../policies_selector'; +import { ImmutableArray, PolicyData } from '../../../../common/endpoint/types'; + +export interface SearchExceptionsProps { + defaultValue?: string; + placeholder: string; + hasPolicyFilter?: boolean; + policyList?: ImmutableArray; + defaultExcludedPolicies?: string; + defaultIncludedPolicies?: string; + onSearch(query: string, includedPolicies?: string, excludedPolicies?: string): void; +} + +export const SearchExceptions = memo( + ({ + defaultValue = '', + onSearch, + placeholder, + hasPolicyFilter, + policyList, + defaultIncludedPolicies, + defaultExcludedPolicies, + }) => { + const [query, setQuery] = useState(defaultValue); + const [includedPolicies, setIncludedPolicies] = useState(defaultIncludedPolicies || ''); + const [excludedPolicies, setExcludedPolicies] = useState(defaultExcludedPolicies || ''); + + const onChangeSelection = useCallback( + (items: PolicySelectionItem[]) => { + const includePoliciesNew = items + .filter((item) => item.checked === 'on') + .map((item) => item.id) + .join(','); + const excludePoliciesNew = items + .filter((item) => item.checked === 'off') + .map((item) => item.id) + .join(','); + + setIncludedPolicies(includePoliciesNew); + setExcludedPolicies(excludePoliciesNew); + + onSearch(query, includePoliciesNew, excludePoliciesNew); + }, + [onSearch, query] + ); + + const handleOnChangeSearchField = useCallback( + (ev: React.ChangeEvent) => setQuery(ev.target.value), + [setQuery] + ); + const handleOnSearch = useCallback(() => onSearch(query, includedPolicies, excludedPolicies), [ + onSearch, + query, + includedPolicies, + excludedPolicies, + ]); + + const handleOnSearchQuery = useCallback( + (value) => { + onSearch(value, includedPolicies, excludedPolicies); + }, + [onSearch, includedPolicies, excludedPolicies] + ); + + return ( + + + + + {hasPolicyFilter && policyList ? ( + + + + ) : null} + + + + {i18n.translate('xpack.securitySolution.management.search.button', { + defaultMessage: 'Refresh', + })} + + + + ); + } +); + +SearchExceptions.displayName = 'SearchExceptions'; diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.tsx index 95f3e856a6ff6..3c537320bc92a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.tsx @@ -42,7 +42,7 @@ import { } from '../../../../common/components/exceptions/viewer/exception_item'; import { EventFilterDeleteModal } from './components/event_filter_delete_modal'; -import { SearchBar } from '../../../components/search_bar'; +import { SearchExceptions } from '../../../components/search_exceptions'; import { BackToExternalAppButton } from '../../../components/back_to_external_app_button'; import { ABOUT_EVENT_FILTERS } from './translations'; @@ -226,7 +226,7 @@ export const EventFiltersListPage = memo(() => { {doesDataExist && ( <> - ({ id: undefined, view_type: 'grid', filter: '', + included_policies: '', + excluded_policies: '', }, active: false, forceRefresh: false, diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.test.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.test.ts index 9624987c8af56..f64003ec6ad91 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.test.ts @@ -26,6 +26,7 @@ import { initialTrustedAppsPageState } from './builders'; import { trustedAppsPageReducer } from './reducer'; import { createTrustedAppsPageMiddleware } from './middleware'; import { Immutable } from '../../../../../common/endpoint/types'; +import { getGeneratedPolicyResponse } from './mocks'; const initialNow = 111111; const dateNowMock = jest.fn(); @@ -189,8 +190,10 @@ describe('middleware', () => { const location = createLocationState(); const service = createTrustedAppsServiceMock(); const { store, spyMiddleware } = createStoreSetup(service); + const policiesResponse = getGeneratedPolicyResponse(); service.getTrustedAppsList.mockResolvedValue(createGetTrustedListAppsResponse(pagination)); + service.getPolicyList.mockResolvedValue(policiesResponse); store.dispatch(createUserChangedUrlAction('/administration/trusted_apps')); @@ -215,10 +218,15 @@ describe('middleware', () => { }); await spyMiddleware.waitForAction('trustedAppsListResourceStateChanged'); + await spyMiddleware.waitForAction('trustedAppsPoliciesStateChanged'); expect(store.getState()).toStrictEqual({ ...initialState, ...entriesExistLoadedState(), + policies: { + data: policiesResponse, + type: 'LoadedResourceState', + }, listView: createLoadedListViewWithPagination(newNow, pagination), active: true, location, @@ -304,9 +312,11 @@ describe('middleware', () => { it('submits successfully when entry is defined', async () => { const service = createTrustedAppsServiceMock(); const { store, spyMiddleware } = createStoreSetup(service); + const policiesResponse = getGeneratedPolicyResponse(); service.getTrustedAppsList.mockResolvedValue(getTrustedAppsListResponse); service.deleteTrustedApp.mockResolvedValue(); + service.getPolicyList.mockResolvedValue(policiesResponse); store.dispatch(createUserChangedUrlAction('/administration/trusted_apps')); @@ -335,6 +345,10 @@ describe('middleware', () => { expect(store.getState()).toStrictEqual({ ...testStartState, ...entriesExistLoadedState(), + policies: { + data: policiesResponse, + type: 'LoadedResourceState', + }, listView: listViewNew, }); expect(service.deleteTrustedApp).toBeCalledWith({ id: '3' }); @@ -344,9 +358,11 @@ describe('middleware', () => { it('does not submit twice', async () => { const service = createTrustedAppsServiceMock(); const { store, spyMiddleware } = createStoreSetup(service); + const policiesResponse = getGeneratedPolicyResponse(); service.getTrustedAppsList.mockResolvedValue(getTrustedAppsListResponse); service.deleteTrustedApp.mockResolvedValue(); + service.getPolicyList.mockResolvedValue(policiesResponse); store.dispatch(createUserChangedUrlAction('/administration/trusted_apps')); @@ -376,6 +392,10 @@ describe('middleware', () => { expect(store.getState()).toStrictEqual({ ...testStartState, ...entriesExistLoadedState(), + policies: { + data: policiesResponse, + type: 'LoadedResourceState', + }, listView: listViewNew, }); expect(service.deleteTrustedApp).toBeCalledWith({ id: '3' }); @@ -385,9 +405,11 @@ describe('middleware', () => { it('does not submit when server response with failure', async () => { const service = createTrustedAppsServiceMock(); const { store, spyMiddleware } = createStoreSetup(service); + const policiesResponse = getGeneratedPolicyResponse(); service.getTrustedAppsList.mockResolvedValue(getTrustedAppsListResponse); service.deleteTrustedApp.mockRejectedValue({ body: notFoundError }); + service.getPolicyList.mockResolvedValue(policiesResponse); store.dispatch(createUserChangedUrlAction('/administration/trusted_apps')); @@ -409,10 +431,15 @@ describe('middleware', () => { }); await spyMiddleware.waitForAction('trustedAppDeletionSubmissionResourceStateChanged'); + await spyMiddleware.waitForAction('trustedAppsPoliciesStateChanged'); expect(store.getState()).toStrictEqual({ ...testStartState, ...entriesExistLoadedState(), + policies: { + data: policiesResponse, + type: 'LoadedResourceState', + }, deletionDialog: { entry, confirmed: true, diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.ts index da6394a9ab896..cf7cff30d6d6d 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.ts @@ -26,6 +26,7 @@ import { isLoadedResourceState, isLoadingResourceState, isStaleResourceState, + isUninitialisedResourceState, StaleResourceState, TrustedAppsListData, TrustedAppsListPageState, @@ -63,8 +64,10 @@ import { editingTrustedApp, getListItems, editItemState, + getCurrentLocationIncludedPolicies, + getCurrentLocationExcludedPolicies, } from './selectors'; -import { parseQueryFilterToKQL } from '../../../common/utils'; +import { parsePoliciesToKQL, parseQueryFilterToKQL } from '../../../common/utils'; import { toUpdateTrustedApp } from '../../../../../common/endpoint/service/trusted_apps/to_update_trusted_app'; import { SEARCHABLE_FIELDS } from '../constants'; @@ -95,11 +98,21 @@ const refreshListIfNeeded = async ( const pageIndex = getCurrentLocationPageIndex(store.getState()); const pageSize = getCurrentLocationPageSize(store.getState()); const filter = getCurrentLocationFilter(store.getState()); + const includedPolicies = getCurrentLocationIncludedPolicies(store.getState()); + const excludedPolicies = getCurrentLocationExcludedPolicies(store.getState()); + + const kuery = []; + + const filterKuery = parseQueryFilterToKQL(filter, SEARCHABLE_FIELDS) || undefined; + if (filterKuery) kuery.push(filterKuery); + + const policiesKuery = parsePoliciesToKQL(includedPolicies, excludedPolicies) || undefined; + if (policiesKuery) kuery.push(policiesKuery); const response = await trustedAppsService.getTrustedAppsList({ page: pageIndex + 1, per_page: pageSize, - kuery: parseQueryFilterToKQL(filter, SEARCHABLE_FIELDS) || undefined, + kuery: kuery.join(' AND ') || undefined, }); store.dispatch( @@ -112,6 +125,8 @@ const refreshListIfNeeded = async ( totalItemsCount: response.total, timestamp: Date.now(), filter, + includedPolicies, + excludedPolicies, }, }) ); @@ -311,8 +326,9 @@ export const retrieveListOfPoliciesIfNeeded = async ( const isLoading = isLoadingResourceState(currentPoliciesState); const isPageActive = trustedAppsListPageActive(currentState); const isCreateFlow = isCreationDialogLocation(currentState); + const isUninitialized = isUninitialisedResourceState(currentPoliciesState); - if (isPageActive && isCreateFlow && !isLoading) { + if (isPageActive && ((isCreateFlow && !isLoading) || isUninitialized)) { dispatch({ type: 'trustedAppsPoliciesStateChanged', payload: { diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/mocks.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/mocks.ts new file mode 100644 index 0000000000000..c97dd37db6bbf --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/mocks.ts @@ -0,0 +1,17 @@ +/* + * 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 { GetPolicyListResponse } from '../../policy/types'; + +import { EndpointDocGenerator } from '../../../../../common/endpoint/generate_data'; + +export const getGeneratedPolicyResponse = (): GetPolicyListResponse => ({ + items: [new EndpointDocGenerator('seed').generatePolicyPackagePolicy()], + total: 1, + perPage: 1, + page: 1, +}); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.test.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.test.ts index ac4d29a6016b2..71bea62030676 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.test.ts @@ -31,7 +31,7 @@ describe('reducer', () => { initialState, createUserChangedUrlAction( '/administration/trusted_apps', - '?page_index=5&page_size=50&show=create&view_type=list&filter=test' + '?page_index=5&page_size=50&show=create&view_type=list&filter=test&included_policies=global&excluded_policies=unassigned' ) ); @@ -44,6 +44,8 @@ describe('reducer', () => { view_type: 'list', id: undefined, filter: 'test', + included_policies: 'global', + excluded_policies: 'unassigned', }, active: true, }); @@ -53,7 +55,14 @@ describe('reducer', () => { const result = trustedAppsPageReducer( { ...initialState, - location: { page_index: 5, page_size: 50, view_type: 'grid', filter: '' }, + location: { + page_index: 5, + page_size: 50, + view_type: 'grid', + filter: '', + included_policies: '', + excluded_policies: '', + }, }, createUserChangedUrlAction( '/administration/trusted_apps', @@ -68,7 +77,14 @@ describe('reducer', () => { const result = trustedAppsPageReducer( { ...initialState, - location: { page_index: 5, page_size: 50, view_type: 'grid', filter: '' }, + location: { + page_index: 5, + page_size: 50, + view_type: 'grid', + filter: '', + included_policies: '', + excluded_policies: '', + }, }, createUserChangedUrlAction('/administration/trusted_apps') ); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.test.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.test.ts index 4d8ce097a7263..387a354fe38c4 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.test.ts @@ -94,6 +94,8 @@ describe('selectors', () => { page_size: 10, view_type: 'grid', filter: '', + included_policies: '', + excluded_policies: '', }; expect(needsRefreshOfListData({ ...initialState, listView, active: true, location })).toBe( @@ -176,6 +178,8 @@ describe('selectors', () => { page_size: 10, view_type: 'grid', filter: '', + included_policies: '', + excluded_policies: '', }; expect(getCurrentLocationPageIndex({ ...initialState, location })).toBe(3); @@ -189,6 +193,8 @@ describe('selectors', () => { page_size: 20, view_type: 'grid', filter: '', + included_policies: '', + excluded_policies: '', }; expect(getCurrentLocationPageSize({ ...initialState, location })).toBe(20); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.ts index 338f30b447a8a..388d8b5e6ed66 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.ts @@ -74,6 +74,18 @@ export const getCurrentLocationFilter = (state: Immutable +): string => { + return state.location.included_policies; +}; + +export const getCurrentLocationExcludedPolicies = ( + state: Immutable +): string => { + return state.location.excluded_policies; +}; + export const getListTotalItemsCount = (state: Immutable): number => { return getLastLoadedResourceState(state.listView.listResourceState)?.data.totalItemsCount || 0; }; @@ -188,6 +200,14 @@ export const entriesExist: (state: Immutable) => boole } ); +export const prevEntriesExist: ( + state: Immutable +) => boolean = createSelector(entriesExistState, (doEntriesExists) => { + return ( + isLoadingResourceState(doEntriesExists) && !!getLastLoadedResourceState(doEntriesExists)?.data + ); +}); + export const trustedAppsListPageActive: (state: Immutable) => boolean = ( state ) => state.active; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/test_utils/index.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/test_utils/index.ts index 7783ac96e192d..cad8f606f85bb 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/test_utils/index.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/test_utils/index.ts @@ -80,6 +80,8 @@ export const createTrustedAppsListData = ( totalItemsCount: fullPagination.totalItemCount, timestamp, filter: '', + excludedPolicies: '', + includedPolicies: '', }; }; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx index 5c627d1d7a837..2ba357a349b5d 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx @@ -173,9 +173,9 @@ describe('When on the Trusted Apps Page', () => { expect(addButton.textContent).toBe('Add Trusted Application'); }); - it('should display the searchbar', async () => { + it('should display the searchExceptions', async () => { const renderResult = await renderWithListData(); - expect(await renderResult.findByTestId('searchBar')).not.toBeNull(); + expect(await renderResult.findByTestId('searchExceptions')).not.toBeNull(); }); describe('and the Grid view is being displayed', () => { @@ -774,6 +774,20 @@ describe('When on the Trusted Apps Page', () => { return releaseListResponse(); } } + + if (path === PACKAGE_POLICY_API_ROUTES.LIST_PATTERN) { + const policy = generator.generatePolicyPackagePolicy(); + policy.name = 'test policy A'; + policy.id = 'abc123'; + + const response: GetPackagePoliciesResponse = { + items: [policy], + page: 1, + perPage: 1000, + total: 1, + }; + return response; + } if (priorMockImplementation) { return priorMockImplementation(path); } @@ -874,12 +888,12 @@ describe('When on the Trusted Apps Page', () => { expect(await renderResult.findByTestId('trustedAppEmptyState')).not.toBeNull(); }); - it('should not display the searchbar', async () => { + it('should not display the searchExceptions', async () => { const renderResult = render(); await act(async () => { await waitForAction('trustedAppsExistStateChanged'); }); - expect(renderResult.queryByTestId('searchBar')).toBeNull(); + expect(renderResult.queryByTestId('searchExceptions')).toBeNull(); }); }); @@ -922,6 +936,27 @@ describe('When on the Trusted Apps Page', () => { backButtonUrl: '/fleet', }); }); + + const priorMockImplementation = coreStart.http.get.getMockImplementation(); + // @ts-ignore + coreStart.http.get.mockImplementation((path, options) => { + if (path === PACKAGE_POLICY_API_ROUTES.LIST_PATTERN) { + const policy = generator.generatePolicyPackagePolicy(); + policy.name = 'test policy A'; + policy.id = 'abc123'; + + const response: GetPackagePoliciesResponse = { + items: [policy], + page: 1, + perPage: 1000, + total: 1, + }; + return response; + } + if (priorMockImplementation) { + return priorMockImplementation(path); + } + }); }); it('back button is present', () => { diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx index ec80b4c5ae21b..48ff54a0e3b56 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx @@ -26,6 +26,8 @@ import { entriesExist, getCurrentLocation, getListTotalItemsCount, + listOfPolicies, + prevEntriesExist, } from '../store/selectors'; import { useTrustedAppsNavigateCallback, useTrustedAppsSelector } from './hooks'; import { AdministrationListPage } from '../../../components/administration_list_page'; @@ -38,18 +40,31 @@ import { TrustedAppsNotifications } from './trusted_apps_notifications'; import { AppAction } from '../../../../common/store/actions'; import { ABOUT_TRUSTED_APPS, SEARCH_TRUSTED_APP_PLACEHOLDER } from './translations'; import { EmptyState } from './components/empty_state'; -import { SearchBar } from '../../../components/search_bar'; +import { SearchExceptions } from '../../../components/search_exceptions'; import { BackToExternalAppButton } from '../../../components/back_to_external_app_button'; import { ListPageRouteState } from '../../../../../common/endpoint/types'; +import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; export const TrustedAppsPage = memo(() => { + const isTrustedAppsByPolicyEnabled = useIsExperimentalFeatureEnabled( + 'trustedAppsByPolicyEnabled' + ); const dispatch = useDispatch>(); const { state: routeState } = useLocation(); const location = useTrustedAppsSelector(getCurrentLocation); const totalItemsCount = useTrustedAppsSelector(getListTotalItemsCount); const isCheckingIfEntriesExists = useTrustedAppsSelector(checkingIfEntriesExist); - const doEntriesExist = useTrustedAppsSelector(entriesExist) === true; - const navigationCallback = useTrustedAppsNavigateCallback((query: string) => ({ filter: query })); + const policyList = useTrustedAppsSelector(listOfPolicies); + const doEntriesExist = useTrustedAppsSelector(entriesExist); + const didEntriesExist = useTrustedAppsSelector(prevEntriesExist); + const navigationCallbackQuery = useTrustedAppsNavigateCallback( + (query: string, includedPolicies?: string, excludedPolicies?: string) => ({ + filter: query, + included_policies: includedPolicies, + excluded_policies: excludedPolicies, + }) + ); + const handleAddButtonClick = useTrustedAppsNavigateCallback(() => ({ show: 'create', id: undefined, @@ -61,12 +76,13 @@ export const TrustedAppsPage = memo(() => { const handleViewTypeChange = useTrustedAppsNavigateCallback((viewType: ViewType) => ({ view_type: viewType, })); + const handleOnSearch = useCallback( - (query: string) => { + (query: string, includedPolicies?: string, excludedPolicies?: string) => { dispatch({ type: 'trustedAppForceRefresh', payload: { forceRefresh: true } }); - navigationCallback(query); + navigationCallbackQuery(query, includedPolicies, excludedPolicies); }, - [dispatch, navigationCallback] + [dispatch, navigationCallbackQuery] ); const showCreateFlyout = !!location.show; @@ -105,12 +121,16 @@ export const TrustedAppsPage = memo(() => { /> )} - {doEntriesExist ? ( + {doEntriesExist || (isCheckingIfEntriesExists && didEntriesExist) ? ( <> - { > - {isCheckingIfEntriesExists ? ( + {isCheckingIfEntriesExists && !didEntriesExist ? ( } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_input_output_index.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_input_output_index.test.ts index 5058056b169a3..c8ef0093291d5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_input_output_index.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_input_output_index.test.ts @@ -34,6 +34,7 @@ describe('get_input_output_index', () => { index: ['test-input-index-1'], experimentalFeatures: { trustedAppsByPolicyEnabled: false, + excludePoliciesInFilterEnabled: false, metricsEntitiesEnabled: false, ruleRegistryEnabled: false, tGridEnabled: false, diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/index.test.tsx b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/index.test.tsx index 60602d75602bf..e921e8420eb96 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/index.test.tsx +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/index.test.tsx @@ -31,6 +31,7 @@ const mockDeps = { config: jest.fn().mockResolvedValue({}), experimentalFeatures: { trustedAppsByPolicyEnabled: false, + excludePoliciesInFilterEnabled: false, metricsEntitiesEnabled: false, ruleRegistryEnabled: false, tGridEnabled: false,