From c978455544411c25c1532116f1dc10cc5e606f8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20=C3=81brah=C3=A1m?= Date: Tue, 2 Jul 2024 18:21:16 +0200 Subject: [PATCH] [EDR Workflows][Process descendants filter] Display process descendant filtering on event filter cards (#187174) ## Summary The modifications indicate to the users if an Event Filter filters process descendants, displayed in 3 places. It's a bit of a prop drilling to be honest, but that was needed to keep `ArtifactXZ` components generic by passing a 'decorator' component from the outside. ### Testing Modifications are behind feature flag: `xpack.securitySolution.enableExperimental.filterProcessDescendantsForEventFiltersEnabled` To change an Event Filter to Process descendant filtering, you just need to change the toggle on the new/edit flyout: image ### Manage / Event Filters - `ArtifactEntryCard` image image ### Manage / Policies / Event Filters tab / Assign flyout - `ArtifactEntryCardMinified` image image ### Manage / Policies / Event Filters tab - when there are assigned filters - `ArtifactEntryCollapsibleCard` image image ### Checklist Delete any items that are not applicable to this PR. - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --------- Co-authored-by: Elastic Machine --- .../artifact_card_grid.test.tsx | 1 + .../artifact_entry_card.test.tsx | 21 ++++- .../artifact_entry_card.tsx | 13 +++ .../artifact_entry_card_minified.test.tsx | 22 ++++- .../artifact_entry_card_minified.tsx | 10 +++ .../artifact_entry_collapsible_card.test.tsx | 31 ++++++- .../artifact_entry_collapsible_card.tsx | 3 + ...ters_process_descendant_indicator.test.tsx | 81 +++++++++++++++++++ ...t_filters_process_descendant_indicator.tsx | 52 ++++++++++++ .../artifact_list_page/artifact_list_page.tsx | 4 + .../artifact_list_page.test.tsx | 15 ++++ .../paginated_content.test.tsx | 1 + .../paginated_content/paginated_content.tsx | 17 ++-- .../view/components/form.test.tsx | 4 +- .../event_filters/view/components/form.tsx | 34 +------- .../components/process_descendant_tooltip.tsx | 66 +++++++++++++++ .../event_filters/view/event_filters_list.tsx | 2 + .../event_filters_list.test.tsx | 76 +++++++++++++++++ .../policy_artifacts_assignable_list.test.tsx | 4 + .../policy_artifacts_assignable_list.tsx | 11 ++- .../flyout/policy_artifacts_flyout.test.tsx | 1 + .../flyout/policy_artifacts_flyout.tsx | 5 +- .../layout/policy_artifacts_layout.tsx | 9 ++- .../list/policy_artifacts_list.test.tsx | 1 + .../artifacts/list/policy_artifacts_list.tsx | 4 + .../pages/policy/view/tabs/policy_tabs.tsx | 2 + 26 files changed, 443 insertions(+), 47 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/card_decorators/event_filters_process_descendant_indicator.test.tsx create mode 100644 x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/card_decorators/event_filters_process_descendant_indicator.tsx create mode 100644 x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/process_descendant_tooltip.tsx diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_card_grid/artifact_card_grid.test.tsx b/x-pack/plugins/security_solution/public/management/components/artifact_card_grid/artifact_card_grid.test.tsx index cb6fcf255126b..731c4076f75c4 100644 --- a/x-pack/plugins/security_solution/public/management/components/artifact_card_grid/artifact_card_grid.test.tsx +++ b/x-pack/plugins/security_solution/public/management/components/artifact_card_grid/artifact_card_grid.test.tsx @@ -57,6 +57,7 @@ describe.each([ pageIndex: 0, }, 'data-test-subj': 'testGrid', + CardDecorator: undefined, ...props, }; diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/artifact_entry_card.test.tsx b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/artifact_entry_card.test.tsx index a1e6243a67a72..100a92f11b5e8 100644 --- a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/artifact_entry_card.test.tsx +++ b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/artifact_entry_card.test.tsx @@ -5,10 +5,13 @@ * 2.0. */ -import React from 'react'; +import React, { memo } from 'react'; import type { AppContextTestRender } from '../../../common/mock/endpoint'; import { createAppRootMockRenderer } from '../../../common/mock/endpoint'; -import type { ArtifactEntryCardProps } from './artifact_entry_card'; +import type { + ArtifactEntryCardDecoratorProps, + ArtifactEntryCardProps, +} from './artifact_entry_card'; import { ArtifactEntryCard } from './artifact_entry_card'; import { act, fireEvent, getByTestId } from '@testing-library/react'; import type { AnyArtifact } from './types'; @@ -268,5 +271,19 @@ describe.each([ expect(renderResult.getByText('policy-1').textContent).not.toBeNull(); }); + + it('should pass item to decorator function and display its result', () => { + let passedItem: ArtifactEntryCardDecoratorProps['item'] | null = null; + const MockDecorator = memo(({ item: actualItem }) => { + passedItem = actualItem; + return

{'mock decorator'}

; + }); + MockDecorator.displayName = 'MockDecorator'; + + render({ Decorator: MockDecorator }); + + expect(renderResult.getByText('mock decorator')).toBeInTheDocument(); + expect(passedItem).toBe(item); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/artifact_entry_card.tsx b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/artifact_entry_card.tsx index f0037b691ceab..76b5142bf068d 100644 --- a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/artifact_entry_card.tsx +++ b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/artifact_entry_card.tsx @@ -37,6 +37,12 @@ export interface CommonArtifactEntryCardProps extends CommonProps { */ policies?: MenuItemPropsByPolicyId; loadingPoliciesList?: boolean; + /** + * Artifact specific decorator component that receives the current artifact as a prop, and + * is displayed inside the card on the top of the card section, + * above the selected OS and the condition entries. + */ + Decorator?: React.ComponentType; } export interface ArtifactEntryCardProps extends CommonArtifactEntryCardProps { @@ -46,6 +52,10 @@ export interface ArtifactEntryCardProps extends CommonArtifactEntryCardProps { hideComments?: boolean; } +export interface ArtifactEntryCardDecoratorProps extends CommonProps { + item: MaybeImmutable; +} + /** * Display Artifact Items (ex. Trusted App, Event Filter, etc) as a card. * This component is a TS Generic that allows you to set what the Item type is @@ -58,6 +68,7 @@ export const ArtifactEntryCard = memo( actions, hideDescription = false, hideComments = false, + Decorator, 'data-test-subj': dataTestSubj, ...commonProps }) => { @@ -103,6 +114,8 @@ export const ArtifactEntryCard = memo( + {Decorator && } + { + let passedItem: ArtifactEntryCardDecoratorProps['item'] | null = null; + const MockDecorator = memo(({ item: actualItem }) => { + passedItem = actualItem; + return

{'mock decorator'}

; + }); + MockDecorator.displayName = 'MockDecorator'; + + render({ + item, + isSelected: false, + onToggleSelectedArtifact: onToggleSelectedArtifactMock, + Decorator: MockDecorator, + }); + + expect(renderResult.getByText('mock decorator')).toBeInTheDocument(); + expect(passedItem).toBe(item); + }); }); diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/artifact_entry_card_minified.tsx b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/artifact_entry_card_minified.tsx index 0d17cfaf7e45a..c1acc122eb2d3 100644 --- a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/artifact_entry_card_minified.tsx +++ b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/artifact_entry_card_minified.tsx @@ -25,6 +25,7 @@ import { useNormalizedArtifact } from './hooks/use_normalized_artifact'; import { useTestIdGenerator } from '../../hooks/use_test_id_generator'; import { DESCRIPTION_LABEL } from './components/translations'; import { DescriptionField } from './components/description_field'; +import type { ArtifactEntryCardDecoratorProps } from './artifact_entry_card'; const CardContainerPanel = styled(EuiSplitPanel.Outer)` &.artifactEntryCardMinified + &.artifactEntryCardMinified { @@ -40,6 +41,12 @@ export interface ArtifactEntryCardMinifiedProps extends CommonProps { item: AnyArtifact; isSelected: boolean; onToggleSelectedArtifact: (selected: boolean) => void; + /** + * Artifact specific decorator component that receives the current artifact as a prop, and + * is displayed inside the card on the top of the card section, + * above the selected OS and the condition entries. + */ + Decorator?: React.ComponentType; } /** @@ -52,6 +59,7 @@ export const ArtifactEntryCardMinified = memo( isSelected = false, onToggleSelectedArtifact, 'data-test-subj': dataTestSubj, + Decorator, ...commonProps }: ArtifactEntryCardMinifiedProps) => { const artifact = useNormalizedArtifact(item); @@ -126,6 +134,8 @@ export const ArtifactEntryCardMinified = memo( {getAccordionTitle()} + {Decorator && } + { + let passedItem: ArtifactEntryCardDecoratorProps['item'] | null = null; + const MockDecorator = memo(({ item: actualItem }) => { + passedItem = actualItem; + return

{'mock decorator'}

; + }); + MockDecorator.displayName = 'MockDecorator'; + + render({ Decorator: MockDecorator, expanded: true }); + + expect(renderResult.getByText('mock decorator')).toBeInTheDocument(); + expect(passedItem).toBe(item); + }); + + it('should not display decorator when collapsed', () => { + let passedItem: ArtifactEntryCardDecoratorProps['item'] | null = null; + const MockDecorator = memo(({ item: actualItem }) => { + passedItem = actualItem; + return

{'mock decorator'}

; + }); + MockDecorator.displayName = 'MockDecorator'; + + render({ Decorator: MockDecorator, expanded: false }); + + expect(renderResult.queryByText('mock decorator')).not.toBeInTheDocument(); + expect(passedItem).toBe(null); + }); }); diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/artifact_entry_collapsible_card.tsx b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/artifact_entry_collapsible_card.tsx index 29d336d45aefe..ecf99fac8343b 100644 --- a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/artifact_entry_collapsible_card.tsx +++ b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/artifact_entry_collapsible_card.tsx @@ -29,6 +29,7 @@ export const ArtifactEntryCollapsibleCard = memo { const artifact = useNormalizedArtifact(item); @@ -51,6 +52,8 @@ export const ArtifactEntryCollapsibleCard = memo + {Decorator && } + { + let appTestContext: AppContextTestRender; + let renderResult: ReturnType; + let render: ( + props: ArtifactEntryCardDecoratorProps + ) => ReturnType; + + const getStandardEventFilter: () => AnyArtifact = () => + ({ + tags: [GLOBAL_ARTIFACT_TAG], + } as Partial as AnyArtifact); + + const getProcessDescendantEventFilter: () => AnyArtifact = () => + ({ + tags: [GLOBAL_ARTIFACT_TAG, FILTER_PROCESS_DESCENDANTS_TAG], + } as Partial as AnyArtifact); + + beforeEach(() => { + appTestContext = createAppRootMockRenderer(); + render = (props) => { + renderResult = appTestContext.render( + + ); + return renderResult; + }; + }); + + it('should not display anything if feature flag is disabled', () => { + appTestContext.setExperimentalFlag({ filterProcessDescendantsForEventFiltersEnabled: false }); + + render({ item: getProcessDescendantEventFilter() }); + + expect(renderResult.queryByTestId('test-processDescendantIndication')).not.toBeInTheDocument(); + }); + + it('should not display anything if Event Filter is not for process descendants', () => { + appTestContext.setExperimentalFlag({ filterProcessDescendantsForEventFiltersEnabled: true }); + + render({ item: getStandardEventFilter() }); + + expect(renderResult.queryByTestId('test-processDescendantIndication')).not.toBeInTheDocument(); + }); + + it('should display indication if Event Filter is for process descendants', () => { + appTestContext.setExperimentalFlag({ filterProcessDescendantsForEventFiltersEnabled: true }); + + render({ item: getProcessDescendantEventFilter() }); + + expect(renderResult.getByTestId('test-processDescendantIndication')).toBeInTheDocument(); + }); + + it('should mention additional `event.category is process` entry in tooltip', async () => { + const prefix = 'test-processDescendantIndicationTooltip'; + appTestContext.setExperimentalFlag({ filterProcessDescendantsForEventFiltersEnabled: true }); + render({ item: getProcessDescendantEventFilter() }); + + expect(renderResult.queryByTestId(`${prefix}-tooltipText`)).not.toBeInTheDocument(); + + userEvent.hover(renderResult.getByTestId(`${prefix}-tooltipIcon`)); + expect(await renderResult.findByTestId(`${prefix}-tooltipText`)).toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/card_decorators/event_filters_process_descendant_indicator.tsx b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/card_decorators/event_filters_process_descendant_indicator.tsx new file mode 100644 index 0000000000000..93eaeb5fbc7e3 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/card_decorators/event_filters_process_descendant_indicator.tsx @@ -0,0 +1,52 @@ +/* + * 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 type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; +import React, { memo } from 'react'; +import { EuiSpacer, EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { useTestIdGenerator } from '../../../../hooks/use_test_id_generator'; +import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features'; +import { isFilterProcessDescendantsEnabled } from '../../../../../../common/endpoint/service/artifacts/utils'; +import { ProcessDescendantsTooltip } from '../../../../pages/event_filters/view/components/process_descendant_tooltip'; +import type { ArtifactEntryCardDecoratorProps } from '../../artifact_entry_card'; + +export const EventFiltersProcessDescendantIndicator = memo( + ({ item, 'data-test-subj': dataTestSubj, ...commonProps }) => { + const getTestId = useTestIdGenerator(dataTestSubj); + const isProcessDescendantFeatureEnabled = useIsExperimentalFeatureEnabled( + 'filterProcessDescendantsForEventFiltersEnabled' + ); + + if ( + isProcessDescendantFeatureEnabled && + isFilterProcessDescendantsEnabled(item as ExceptionListItemSchema) + ) { + return ( + <> + + + + {' '} + + + + + + + ); + } + + return <>; + } +); +EventFiltersProcessDescendantIndicator.displayName = 'EventFiltersProcessDescendantIndicator'; diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_list_page/artifact_list_page.tsx b/x-pack/plugins/security_solution/public/management/components/artifact_list_page/artifact_list_page.tsx index abe52767c5d5e..49755f88562f7 100644 --- a/x-pack/plugins/security_solution/public/management/components/artifact_list_page/artifact_list_page.tsx +++ b/x-pack/plugins/security_solution/public/management/components/artifact_list_page/artifact_list_page.tsx @@ -18,6 +18,7 @@ import { AdministrationListPage } from '../administration_list_page'; import type { PaginatedContentProps } from '../paginated_content'; import { PaginatedContent } from '../paginated_content'; +import type { ArtifactEntryCardDecoratorProps } from '../artifact_entry_card'; import { ArtifactEntryCard } from '../artifact_entry_card'; import type { ArtifactListPageLabels } from './translations'; @@ -75,6 +76,7 @@ export interface ArtifactListPageProps { allowCardDeleteAction?: boolean; allowCardCreateAction?: boolean; secondaryPageInfo?: React.ReactNode; + CardDecorator?: React.ComponentType; } export const ArtifactListPage = memo( @@ -90,6 +92,7 @@ export const ArtifactListPage = memo( allowCardEditAction = true, allowCardCreateAction = true, allowCardDeleteAction = true, + CardDecorator, }) => { const { state: routeState } = useLocation(); const getTestId = useTestIdGenerator(dataTestSubj); @@ -354,6 +357,7 @@ export const ArtifactListPage = memo( pagination={uiPagination} contentClassName="card-container" data-test-subj={getTestId('list')} + CardDecorator={CardDecorator} /> )} diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_list_page/integration_tests/artifact_list_page.test.tsx b/x-pack/plugins/security_solution/public/management/components/artifact_list_page/integration_tests/artifact_list_page.test.tsx index 4ce4a80a00e9f..f67e694224713 100644 --- a/x-pack/plugins/security_solution/public/management/components/artifact_list_page/integration_tests/artifact_list_page.test.tsx +++ b/x-pack/plugins/security_solution/public/management/components/artifact_list_page/integration_tests/artifact_list_page.test.tsx @@ -5,6 +5,7 @@ * 2.0. */ +import React, { memo } from 'react'; import type { AppContextTestRender } from '../../../../common/mock/endpoint'; import type { trustedAppsAllHttpMocks } from '../../../mocks'; import type { ArtifactListPageProps } from '../artifact_list_page'; @@ -14,6 +15,7 @@ import type { ArtifactListPageRenderingSetup } from '../mocks'; import { getArtifactListPageRenderingSetup } from '../mocks'; import { getDeferred } from '../../../mocks/utils'; import { useGetEndpointSpecificPolicies } from '../../../services/policies/hooks'; +import type { ArtifactEntryCardDecoratorProps } from '../../artifact_entry_card'; jest.mock('../../../services/policies/hooks', () => ({ useGetEndpointSpecificPolicies: jest.fn(), @@ -144,6 +146,19 @@ describe('When using the ArtifactListPage component', () => { }); }); + it('should show per card decoration', async () => { + const MockCardDecorator = memo(({ item: actualItem }) => { + return

{'mock decorator'}

; + }); + MockCardDecorator.displayName = 'MockCardDecorator'; + + const { getAllByText } = await renderWithListData({ + CardDecorator: MockCardDecorator, + }); + + expect(getAllByText('mock decorator')).toHaveLength(10); + }); + it('should call useGetEndpointSpecificPolicies hook with specific perPage value', () => { expect(mockUseGetEndpointSpecificPolicies).toHaveBeenCalledWith( expect.objectContaining({ diff --git a/x-pack/plugins/security_solution/public/management/components/paginated_content/paginated_content.test.tsx b/x-pack/plugins/security_solution/public/management/components/paginated_content/paginated_content.test.tsx index 78f7580a7b168..c560f155e99f9 100644 --- a/x-pack/plugins/security_solution/public/management/components/paginated_content/paginated_content.test.tsx +++ b/x-pack/plugins/security_solution/public/management/components/paginated_content/paginated_content.test.tsx @@ -60,6 +60,7 @@ describe('when using PaginatedContent', () => { totalItemCount: 10, }, 'data-test-subj': 'test', + CardDecorator: undefined, ...(additionalProps ?? {}), }; renderResult = mockedContext.render( {...props} />); diff --git a/x-pack/plugins/security_solution/public/management/components/paginated_content/paginated_content.tsx b/x-pack/plugins/security_solution/public/management/components/paginated_content/paginated_content.tsx index 11797c0544ae6..edc5151ecfd11 100644 --- a/x-pack/plugins/security_solution/public/management/components/paginated_content/paginated_content.tsx +++ b/x-pack/plugins/security_solution/public/management/components/paginated_content/paginated_content.tsx @@ -29,6 +29,7 @@ import { v4 as generateUUI } from 'uuid'; import { useTestIdGenerator } from '../../hooks/use_test_id_generator'; import type { MaybeImmutable } from '../../../../common/endpoint/types'; import { MANAGEMENT_DEFAULT_PAGE, MANAGEMENT_DEFAULT_PAGE_SIZE } from '../../common/constants'; +import type { ArtifactEntryCardDecoratorProps } from '../artifact_entry_card'; // eslint-disable-next-line @typescript-eslint/no-explicit-any type ComponentWithAnyProps = ComponentType; @@ -52,6 +53,8 @@ export interface PaginatedContentProps exten error?: ReactNode; /** Classname applied to the area that holds the content items */ contentClassName?: string; + // Artifact specific decorations to display in the cards + CardDecorator: React.ComponentType | undefined; /** * Children can be used to define custom content if the default creation of items is not sufficient * to accommodate a use case. @@ -139,6 +142,7 @@ export const PaginatedContent = memo( 'data-test-subj': dataTestSubj, 'aria-label': ariaLabel, className, + CardDecorator, children, }: PaginatedContentProps) => { const [itemKeys] = useState>(new WeakMap()); @@ -223,21 +227,22 @@ export const PaginatedContent = memo( } } - return ; + return ; }); } if (!loading) return noItemsMessage || ; }, [ - ItemComponent, error, + ItemComponent, + items, + loading, + noItemsMessage, getTestId, - itemComponentProps, itemId, + itemComponentProps, + CardDecorator, itemKeys, - items, - noItemsMessage, - loading, ]); return ( diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form.test.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form.test.tsx index 9ea16a071a72b..c355cc8bdea0b 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form.test.tsx @@ -532,8 +532,8 @@ describe('Event filter form', () => { }); it('should display a tooltip to the user', async () => { - const tooltipIconSelector = `${formPrefix}-filterProcessDescendants-tooltipIcon`; - const tooltipTextSelector = `${formPrefix}-filterProcessDescendants-tooltipText`; + const tooltipIconSelector = `${formPrefix}-filterProcessDescendantsTooltip-tooltipIcon`; + const tooltipTextSelector = `${formPrefix}-filterProcessDescendantsTooltip-tooltipText`; render(); expect(renderResult.getByTestId(tooltipIconSelector)).toBeInTheDocument(); diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form.tsx index ecb54e57baf4b..4275502961848 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form.tsx @@ -14,8 +14,6 @@ import { EuiSpacer, EuiFlexGroup, EuiButtonGroup, - EuiToolTip, - EuiIcon, useEuiTheme, EuiForm, EuiFormRow, @@ -84,6 +82,7 @@ import { EffectedPolicySelect } from '../../../../components/effected_policy_sel import { ExceptionItemComments } from '../../../../../detection_engine/rule_exceptions/components/item_comments'; import { EventFiltersApiClient } from '../../service/api_client'; import { ShowValueListModal } from '../../../../../value_list/components/show_value_list_modal'; +import { ProcessDescendantsTooltip } from './process_descendant_tooltip'; const OPERATING_SYSTEMS: readonly OperatingSystem[] = [ OperatingSystem.MAC, @@ -458,33 +457,6 @@ export const EventFiltersForm: React.FC { - const descendantsTooltip = ( - -

- -

-

- -

- - } - data-test-subj={getTestId('filterProcessDescendants-tooltipText')} - > - -
- ); - return [ { id: 'events', @@ -509,7 +481,9 @@ export const EventFiltersForm: React.FC - {descendantsTooltip} + ), iconType: isFilterProcessDescendantsSelected ? 'checkInCircleFilled' : 'empty', diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/process_descendant_tooltip.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/process_descendant_tooltip.tsx new file mode 100644 index 0000000000000..f8709306d2099 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/process_descendant_tooltip.tsx @@ -0,0 +1,66 @@ +/* + * 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 } from 'react'; +import type { CommonProps } from '@elastic/eui'; +import { EuiToolTip, EuiText, EuiIcon } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { useTestIdGenerator } from '../../../../hooks/use_test_id_generator'; +import { PROCESS_DESCENDANT_EVENT_FILTER_EXTRA_ENTRY_TEXT } from '../../../../../../common/endpoint/service/artifacts/constants'; + +interface ProcessDescendantsTooltipProps extends CommonProps { + indicateExtraEntry?: boolean; +} + +export const ProcessDescendantsTooltip = memo( + ({ + indicateExtraEntry = false, + 'data-test-subj': dataTestSubj, + ...commonProps + }: ProcessDescendantsTooltipProps) => { + const getTestId = useTestIdGenerator(dataTestSubj); + + return ( + +

+ +

+ {indicateExtraEntry && ( + <> +

+ +

+

+ {PROCESS_DESCENDANT_EVENT_FILTER_EXTRA_ENTRY_TEXT} +

+ + )} +

+ +

+ + } + data-test-subj={getTestId('tooltipText')} + > + +
+ ); + } +); +ProcessDescendantsTooltip.displayName = 'ProcessDescendantsTooltip'; diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list.tsx index b2e2054ca0eda..87aae6a376733 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list.tsx @@ -18,6 +18,7 @@ import { ArtifactListPage } from '../../../components/artifact_list_page'; import { EventFiltersApiClient } from '../service/api_client'; import { EventFiltersForm } from './components/form'; import { SEARCHABLE_FIELDS } from '../constants'; +import { EventFiltersProcessDescendantIndicator } from '../../../components/artifact_entry_card/components/card_decorators/event_filters_process_descendant_indicator'; export const ABOUT_EVENT_FILTERS = i18n.translate('xpack.securitySolution.eventFilters.aboutInfo', { defaultMessage: @@ -155,6 +156,7 @@ export const EventFiltersList = memo(() => { allowCardCreateAction={canWriteEventFilters} allowCardEditAction={canWriteEventFilters} allowCardDeleteAction={canWriteEventFilters} + CardDecorator={EventFiltersProcessDescendantIndicator} /> ); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/integration_tests/event_filters_list.test.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/integration_tests/event_filters_list.test.tsx index c454cda4d49b2..8311c111ac8b5 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/integration_tests/event_filters_list.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/integration_tests/event_filters_list.test.tsx @@ -17,6 +17,8 @@ import { SEARCHABLE_FIELDS } from '../../constants'; import { parseQueryFilterToKQL } from '../../../../common/utils'; import type { EndpointPrivileges } from '../../../../../../common/endpoint/types'; import { useUserPrivileges } from '../../../../../common/components/user_privileges'; +import { ExceptionsListItemGenerator } from '../../../../../../common/endpoint/data_generators/exceptions_list_item_generator'; +import { FILTER_PROCESS_DESCENDANTS_TAG } from '../../../../../../common/endpoint/service/artifacts/constants'; jest.mock('../../../../../common/components/user_privileges'); const mockUserPrivileges = useUserPrivileges as jest.Mock; @@ -69,6 +71,80 @@ describe('When on the Event Filters list page', () => { ); }); + describe('filtering process descendants', () => { + let renderWithData: () => Promise>; + + beforeEach(() => { + renderWithData = async () => { + const generator = new ExceptionsListItemGenerator(); + + apiMocks.responseProvider.exceptionsFind.mockReturnValue({ + data: [ + generator.generateEventFilter(), + generator.generateEventFilter({ tags: [FILTER_PROCESS_DESCENDANTS_TAG] }), + generator.generateEventFilter({ tags: [FILTER_PROCESS_DESCENDANTS_TAG] }), + ], + total: 3, + per_page: 3, + page: 1, + }); + + render(); + + await act(async () => { + await waitFor(() => { + expect(renderResult.getByTestId('EventFiltersListPage-list')).toBeTruthy(); + }); + }); + + return renderResult; + }; + }); + + it('should not show indication if feature flag is disabled', async () => { + mockedContext.setExperimentalFlag({ filterProcessDescendantsForEventFiltersEnabled: false }); + + await renderWithData(); + + expect(renderResult.getAllByTestId('EventFiltersListPage-card')).toHaveLength(3); + expect( + renderResult.queryAllByTestId( + 'EventFiltersListPage-card-decorator-processDescendantIndication' + ) + ).toHaveLength(0); + }); + + it('should indicate to user if event filter filters process descendants', async () => { + mockedContext.setExperimentalFlag({ filterProcessDescendantsForEventFiltersEnabled: true }); + + await renderWithData(); + + expect(renderResult.getAllByTestId('EventFiltersListPage-card')).toHaveLength(3); + expect( + renderResult.getAllByTestId( + 'EventFiltersListPage-card-decorator-processDescendantIndication' + ) + ).toHaveLength(2); + }); + + it('should display additional `event.category is process` entry in tooltip', async () => { + mockedContext.setExperimentalFlag({ filterProcessDescendantsForEventFiltersEnabled: true }); + const prefix = 'EventFiltersListPage-card-decorator-processDescendantIndicationTooltip'; + + await renderWithData(); + + expect(renderResult.getAllByTestId(`${prefix}-tooltipIcon`)).toHaveLength(2); + expect(renderResult.queryByTestId(`${prefix}-tooltipText`)).not.toBeInTheDocument(); + + userEvent.hover(renderResult.getAllByTestId(`${prefix}-tooltipIcon`)[0]); + + expect(await renderResult.findByTestId(`${prefix}-tooltipText`)).toBeInTheDocument(); + expect(renderResult.getByTestId(`${prefix}-tooltipText`).textContent).toContain( + 'event.category is process' + ); + }); + }); + describe('RBAC Event Filters', () => { describe('ALL privilege', () => { beforeEach(() => { diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/assignable/policy_artifacts_assignable_list.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/assignable/policy_artifacts_assignable_list.test.tsx index c1f95b1f4d067..f86e4bfd10f4d 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/assignable/policy_artifacts_assignable_list.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/assignable/policy_artifacts_assignable_list.test.tsx @@ -35,6 +35,7 @@ describe('Policy artifacts list', () => { selectedArtifactIds: [], isListLoading: true, selectedArtifactsUpdated: selectedArtifactsUpdatedMock, + CardDecorator: undefined, }); expect(component.getByTestId('artifactsAssignableListLoader')).not.toBeNull(); @@ -47,6 +48,7 @@ describe('Policy artifacts list', () => { selectedArtifactIds: [], isListLoading: false, selectedArtifactsUpdated: selectedArtifactsUpdatedMock, + CardDecorator: undefined, }); expect(component.queryByTestId('artifactsList')).toBeNull(); }); @@ -58,6 +60,7 @@ describe('Policy artifacts list', () => { selectedArtifactIds: [], isListLoading: false, selectedArtifactsUpdated: selectedArtifactsUpdatedMock, + CardDecorator: undefined, }); expect(component.getByTestId('artifactsList')).not.toBeNull(); }); @@ -69,6 +72,7 @@ describe('Policy artifacts list', () => { selectedArtifactIds: [artifactsResponse.data[0].id], isListLoading: false, selectedArtifactsUpdated: selectedArtifactsUpdatedMock, + CardDecorator: undefined, }); const tACardCheckbox = component.getByTestId(`${getMockListResponse().data[1].name}_checkbox`); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/assignable/policy_artifacts_assignable_list.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/assignable/policy_artifacts_assignable_list.tsx index 4be70c364e0d8..d2f0ae02a2ddb 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/assignable/policy_artifacts_assignable_list.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/assignable/policy_artifacts_assignable_list.tsx @@ -12,7 +12,10 @@ import type { GetTrustedAppsListResponse, Immutable, } from '../../../../../../../common/endpoint/types'; -import type { AnyArtifact } from '../../../../../components/artifact_entry_card'; +import type { + AnyArtifact, + ArtifactEntryCardDecoratorProps, +} from '../../../../../components/artifact_entry_card'; import { ArtifactEntryCardMinified } from '../../../../../components/artifact_entry_card'; export interface PolicyArtifactsAssignableListProps { @@ -25,10 +28,11 @@ export interface PolicyArtifactsAssignableListProps { selectedArtifactIds: string[]; selectedArtifactsUpdated: (id: string, selected: boolean) => void; isListLoading: boolean; + CardDecorator: React.ComponentType | undefined; } export const PolicyArtifactsAssignableList = React.memo( - ({ artifacts, isListLoading, selectedArtifactIds, selectedArtifactsUpdated }) => { + ({ artifacts, isListLoading, selectedArtifactIds, selectedArtifactsUpdated, CardDecorator }) => { const selectedArtifactIdsByKey = useMemo( () => selectedArtifactIds.reduce( @@ -51,11 +55,12 @@ export const PolicyArtifactsAssignableList = React.memo selectedArtifactsUpdated(artifact.id, selected) } + Decorator={CardDecorator} /> ))} ); - }, [artifacts, selectedArtifactIdsByKey, selectedArtifactsUpdated]); + }, [CardDecorator, artifacts, selectedArtifactIdsByKey, selectedArtifactsUpdated]); return ( <> diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/flyout/policy_artifacts_flyout.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/flyout/policy_artifacts_flyout.test.tsx index 48d133cd41f29..90438d7eb80da 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/flyout/policy_artifacts_flyout.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/flyout/policy_artifacts_flyout.test.tsx @@ -86,6 +86,7 @@ describe('Policy details artifacts flyout', () => { apiClient={EventFiltersApiClient.getInstance(mockedContext.coreStart.http)} onClose={onCloseMock} searchableFields={[...SEARCHABLE_FIELDS]} + CardDecorator={undefined} /> ); await waitFor(mockedApi.responseProvider.eventFiltersList); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/flyout/policy_artifacts_flyout.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/flyout/policy_artifacts_flyout.tsx index 7793061be4fea..9805c72a75ca6 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/flyout/policy_artifacts_flyout.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/flyout/policy_artifacts_flyout.tsx @@ -24,6 +24,7 @@ import { EuiEmptyPrompt, useGeneratedHtmlId, } from '@elastic/eui'; +import type { ArtifactEntryCardDecoratorProps } from '../../../../../components/artifact_entry_card'; import { SearchExceptions } from '../../../../../components/search_exceptions'; import type { ImmutableObject, PolicyData } from '../../../../../../../common/endpoint/types'; import { useToasts } from '../../../../../../common/lib/kibana'; @@ -38,12 +39,13 @@ interface PolicyArtifactsFlyoutProps { searchableFields: string[]; onClose: () => void; labels: typeof POLICY_ARTIFACT_FLYOUT_LABELS; + CardDecorator: React.ComponentType | undefined; } export const MAX_ALLOWED_RESULTS = 100; export const PolicyArtifactsFlyout = React.memo( - ({ policyItem, apiClient, searchableFields, onClose, labels }) => { + ({ policyItem, apiClient, searchableFields, onClose, labels, CardDecorator }) => { const toasts = useToasts(); const queryClient = useQueryClient(); const [selectedArtifactIds, setSelectedArtifactIds] = useState([]); @@ -210,6 +212,7 @@ export const PolicyArtifactsFlyout = React.memo( selectedArtifactIds={selectedArtifactIds} isListLoading={isLoadingArtifacts || isRefetchingArtifacts} selectedArtifactsUpdated={handleSelectArtifacts} + CardDecorator={CardDecorator} /> {noItemsMessage} diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/layout/policy_artifacts_layout.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/layout/policy_artifacts_layout.tsx index 70ac3ce16aab7..75927ece7dfde 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/layout/policy_artifacts_layout.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/layout/policy_artifacts_layout.tsx @@ -17,6 +17,7 @@ import { EuiButton, EuiPageSection, } from '@elastic/eui'; +import type { ArtifactEntryCardDecoratorProps } from '../../../../../components/artifact_entry_card'; import { useAppUrl } from '../../../../../../common/lib/kibana'; import { APP_UI_ID } from '../../../../../../../common/constants'; import type { ImmutableObject, PolicyData } from '../../../../../../../common/endpoint/types'; @@ -34,7 +35,7 @@ import { policyArtifactsPageLabels } from '../translations'; import { PolicyArtifactsDeleteModal } from '../delete_modal'; import type { ArtifactListPageUrlParams } from '../../../../../components/artifact_list_page'; -interface PolicyArtifactsLayoutProps { +export interface PolicyArtifactsLayoutProps { policyItem?: ImmutableObject | undefined; /** A list of labels for the given policy artifact page. Not all have to be defined, only those that should override the defaults */ labels: PolicyArtifactsPageLabels; @@ -44,6 +45,8 @@ interface PolicyArtifactsLayoutProps { getPolicyArtifactsPath: (policyId: string) => string; /** A boolean to check if has write artifact privilege or not */ canWriteArtifact?: boolean; + // Artifact specific decorations to display in the cards + CardDecorator?: React.ComponentType; } export const PolicyArtifactsLayout = React.memo( ({ @@ -54,6 +57,7 @@ export const PolicyArtifactsLayout = React.memo( getArtifactPath, getPolicyArtifactsPath, canWriteArtifact = false, + CardDecorator, }) => { const exceptionsListApiClient = useMemo( () => getExceptionsListApiClient(), @@ -154,6 +158,7 @@ export const PolicyArtifactsLayout = React.memo( searchableFields={[...searchableFields]} onClose={handleOnCloseFlyout} labels={labels} + CardDecorator={CardDecorator} /> )} {allArtifacts && allArtifacts.total !== 0 ? ( @@ -205,6 +210,7 @@ export const PolicyArtifactsLayout = React.memo( searchableFields={[...searchableFields]} onClose={handleOnCloseFlyout} labels={labels} + CardDecorator={CardDecorator} /> )} {exceptionItemToDelete && ( @@ -228,6 +234,7 @@ export const PolicyArtifactsLayout = React.memo( canWriteArtifact={canWriteArtifact} getPolicyArtifactsPath={getPolicyArtifactsPath} getArtifactPath={getArtifactPath} + CardDecorator={CardDecorator} /> diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/list/policy_artifacts_list.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/list/policy_artifacts_list.test.tsx index 1ad26fd171c28..3d4469a4ec33f 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/list/policy_artifacts_list.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/list/policy_artifacts_list.test.tsx @@ -68,6 +68,7 @@ describe('Policy details artifacts list', () => { canWriteArtifact={canWriteArtifact} getPolicyArtifactsPath={getPolicyEventFiltersPath} getArtifactPath={getEventFiltersListPath} + CardDecorator={undefined} /> ); await waitFor(() => expect(mockedApi.responseProvider.eventFiltersList).toHaveBeenCalled()); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/list/policy_artifacts_list.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/list/policy_artifacts_list.tsx index 497caf30d4020..082012295b023 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/list/policy_artifacts_list.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/list/policy_artifacts_list.tsx @@ -9,6 +9,7 @@ import React, { useCallback, useMemo, useState } from 'react'; import type { Pagination } from '@elastic/eui'; import { EuiSpacer, EuiText } from '@elastic/eui'; import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; +import type { ArtifactEntryCardDecoratorProps } from '../../../../../components/artifact_entry_card'; import { useAppUrl } from '../../../../../../common/lib/kibana'; import { APP_UI_ID } from '../../../../../../../common/constants'; import { SearchExceptions } from '../../../../../components/search_exceptions'; @@ -38,6 +39,7 @@ interface PolicyArtifactsListProps { labels: typeof POLICY_ARTIFACT_LIST_LABELS; onDeleteActionCallback: (item: ExceptionListItemSchema) => void; canWriteArtifact?: boolean; + CardDecorator: React.ComponentType | undefined; } export const PolicyArtifactsList = React.memo( @@ -50,6 +52,7 @@ export const PolicyArtifactsList = React.memo( labels, onDeleteActionCallback, canWriteArtifact = false, + CardDecorator, }) => { useOldUrlSearchPaginationReplace(); const { getAppUrl } = useAppUrl(); @@ -192,6 +195,7 @@ export const PolicyArtifactsList = React.memo( pagination={artifacts ? pagination : undefined} loading={isLoadingArtifacts || isRefetchingArtifacts} data-test-subj={'artifacts-collapsed-list'} + CardDecorator={CardDecorator} /> ); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/tabs/policy_tabs.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/tabs/policy_tabs.tsx index fd7e8793535e2..cb480615d27a5 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/tabs/policy_tabs.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/tabs/policy_tabs.tsx @@ -11,6 +11,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useHistory, useLocation } from 'react-router-dom'; +import { EventFiltersProcessDescendantIndicator } from '../../../../components/artifact_entry_card/components/card_decorators/event_filters_process_descendant_indicator'; import { UnsavedChangesConfirmModal } from './unsaved_changes_confirm_modal'; import { useLicense } from '../../../../../common/hooks/use_license'; import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features'; @@ -290,6 +291,7 @@ export const PolicyTabs = React.memo(() => { getArtifactPath={getEventFiltersListPath} getPolicyArtifactsPath={getPolicyEventFiltersPath} canWriteArtifact={canWriteEventFilters} + CardDecorator={EventFiltersProcessDescendantIndicator} /> ),