Skip to content

Commit

Permalink
[EDR Workflows][Process descendants filter] Display process descendan…
Browse files Browse the repository at this point in the history
…t 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:
<img width="400" alt="image"
src="https://github.com/elastic/kibana/assets/39014407/23c64d77-7d28-44c1-9a7f-07499652610b">


### Manage / Event Filters - `ArtifactEntryCard`
<img width="1393" alt="image"
src="https://github.com/elastic/kibana/assets/39014407/79459f08-3b30-4f66-b058-e9b2bbaed705">
<img width="675" alt="image"
src="https://github.com/elastic/kibana/assets/39014407/7be9d2d8-85d4-4a8d-b650-f1371bcaa903">


### Manage / Policies / Event Filters tab / Assign flyout -
`ArtifactEntryCardMinified`
<img width="1315" alt="image"
src="https://github.com/elastic/kibana/assets/39014407/57b6564b-8b43-4a37-9d4b-c7db5cbefbeb">

<img width="668" alt="image"
src="https://github.com/elastic/kibana/assets/39014407/7e3e1b4a-e0f0-4b20-8b9b-f7d24589ce2a">

### Manage / Policies / Event Filters tab - when there are assigned
filters - `ArtifactEntryCollapsibleCard`
<img width="1068" alt="image"
src="https://github.com/elastic/kibana/assets/39014407/af31b89e-9845-4625-a95d-f610a57203f4">

<img width="1067" alt="image"
src="https://github.com/elastic/kibana/assets/39014407/f9b98c4b-ed47-4f62-8bed-95c65420ab4f">


### 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 <elasticmachine@users.noreply.github.com>
  • Loading branch information
gergoabraham and elasticmachine authored Jul 2, 2024
1 parent 525d24e commit c978455
Show file tree
Hide file tree
Showing 26 changed files with 443 additions and 47 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ describe.each([
pageIndex: 0,
},
'data-test-subj': 'testGrid',
CardDecorator: undefined,
...props,
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<ArtifactEntryCardDecoratorProps>(({ item: actualItem }) => {
passedItem = actualItem;
return <p>{'mock decorator'}</p>;
});
MockDecorator.displayName = 'MockDecorator';

render({ Decorator: MockDecorator });

expect(renderResult.getByText('mock decorator')).toBeInTheDocument();
expect(passedItem).toBe(item);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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<ArtifactEntryCardDecoratorProps>;
}

export interface ArtifactEntryCardProps extends CommonArtifactEntryCardProps {
Expand All @@ -46,6 +52,10 @@ export interface ArtifactEntryCardProps extends CommonArtifactEntryCardProps {
hideComments?: boolean;
}

export interface ArtifactEntryCardDecoratorProps extends CommonProps {
item: MaybeImmutable<AnyArtifact>;
}

/**
* 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
Expand All @@ -58,6 +68,7 @@ export const ArtifactEntryCard = memo<ArtifactEntryCardProps>(
actions,
hideDescription = false,
hideComments = false,
Decorator,
'data-test-subj': dataTestSubj,
...commonProps
}) => {
Expand Down Expand Up @@ -103,6 +114,8 @@ export const ArtifactEntryCard = memo<ArtifactEntryCardProps>(
<EuiHorizontalRule margin="none" />

<CardSectionPanel className="bottom-section">
{Decorator && <Decorator item={item} data-test-subj={getTestId('decorator')} />}

<CriteriaConditions
os={artifact.os as CriteriaConditionsProps['os']}
entries={artifact.entries}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@
* 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 { ArtifactEntryCardMinifiedProps } from './artifact_entry_card_minified';
import { ArtifactEntryCardMinified } from './artifact_entry_card_minified';
import { act, fireEvent } from '@testing-library/react';
import type { AnyArtifact } from './types';
import { getTrustedAppProviderMock, getExceptionProviderMock } from './test_utils';
import type { ArtifactEntryCardDecoratorProps } from './artifact_entry_card';

describe.each([
['trusted apps', getTrustedAppProviderMock],
Expand Down Expand Up @@ -94,4 +95,23 @@ describe.each([
expect(onToggleSelectedArtifactMock).toHaveBeenCalledTimes(1);
expect(onToggleSelectedArtifactMock).toHaveBeenCalledWith(false);
});

it('should pass item to Decorator component and display the component', () => {
let passedItem: ArtifactEntryCardDecoratorProps['item'] | null = null;
const MockDecorator = memo<ArtifactEntryCardDecoratorProps>(({ item: actualItem }) => {
passedItem = actualItem;
return <p>{'mock decorator'}</p>;
});
MockDecorator.displayName = 'MockDecorator';

render({
item,
isSelected: false,
onToggleSelectedArtifact: onToggleSelectedArtifactMock,
Decorator: MockDecorator,
});

expect(renderResult.getByText('mock decorator')).toBeInTheDocument();
expect(passedItem).toBe(item);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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<ArtifactEntryCardDecoratorProps>;
}

/**
Expand All @@ -52,6 +59,7 @@ export const ArtifactEntryCardMinified = memo(
isSelected = false,
onToggleSelectedArtifact,
'data-test-subj': dataTestSubj,
Decorator,
...commonProps
}: ArtifactEntryCardMinifiedProps) => {
const artifact = useNormalizedArtifact(item);
Expand Down Expand Up @@ -126,6 +134,8 @@ export const ArtifactEntryCardMinified = memo(
{getAccordionTitle()}
</EuiButtonEmpty>
<EuiAccordion id="showDetails" arrowDisplay="none" forceState={accordionTrigger}>
{Decorator && <Decorator item={item} data-test-subj={getTestId('decorator')} />}

<CriteriaConditions
os={artifact.os as CriteriaConditionsProps['os']}
entries={artifact.entries}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@
* 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 { act, fireEvent } from '@testing-library/react';
import type { AnyArtifact } from './types';
import { getTrustedAppProviderMock, getExceptionProviderMock } from './test_utils';
import type { ArtifactEntryCollapsibleCardProps } from './artifact_entry_collapsible_card';
import { ArtifactEntryCollapsibleCard } from './artifact_entry_collapsible_card';
import type { ArtifactEntryCardDecoratorProps } from './artifact_entry_card';

describe.each([
['trusted apps', getTrustedAppProviderMock],
Expand Down Expand Up @@ -119,4 +120,32 @@ describe.each([

expect(renderResult.getByTestId(testSubjId).classList.contains('eui-textTruncate')).toBe(false);
});

it('should pass item to decorator function and display its result when expanded', () => {
let passedItem: ArtifactEntryCardDecoratorProps['item'] | null = null;
const MockDecorator = memo<ArtifactEntryCardDecoratorProps>(({ item: actualItem }) => {
passedItem = actualItem;
return <p>{'mock decorator'}</p>;
});
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<ArtifactEntryCardDecoratorProps>(({ item: actualItem }) => {
passedItem = actualItem;
return <p>{'mock decorator'}</p>;
});
MockDecorator.displayName = 'MockDecorator';

render({ Decorator: MockDecorator, expanded: false });

expect(renderResult.queryByText('mock decorator')).not.toBeInTheDocument();
expect(passedItem).toBe(null);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export const ArtifactEntryCollapsibleCard = memo<ArtifactEntryCollapsibleCardPro
actions,
expanded = false,
'data-test-subj': dataTestSubj,
Decorator,
...commonProps
}) => {
const artifact = useNormalizedArtifact(item);
Expand All @@ -51,6 +52,8 @@ export const ArtifactEntryCollapsibleCard = memo<ArtifactEntryCollapsibleCardPro
<EuiHorizontalRule margin="xs" />

<CardSectionPanel>
{Decorator && <Decorator item={item} data-test-subj={getTestId('decorator')} />}

<CriteriaConditions
os={artifact.os as CriteriaConditionsProps['os']}
entries={artifact.entries}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* 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 from 'react';
import userEvent from '@testing-library/user-event';
import { EventFiltersProcessDescendantIndicator } from './event_filters_process_descendant_indicator';
import type { AnyArtifact } from '../../types';
import type { AppContextTestRender } from '../../../../../common/mock/endpoint';
import { createAppRootMockRenderer } from '../../../../../common/mock/endpoint';
import {
FILTER_PROCESS_DESCENDANTS_TAG,
GLOBAL_ARTIFACT_TAG,
} from '../../../../../../common/endpoint/service/artifacts/constants';
import type { ArtifactEntryCardDecoratorProps } from '../../artifact_entry_card';

describe('EventFiltersProcessDescendantIndicator', () => {
let appTestContext: AppContextTestRender;
let renderResult: ReturnType<AppContextTestRender['render']>;
let render: (
props: ArtifactEntryCardDecoratorProps
) => ReturnType<AppContextTestRender['render']>;

const getStandardEventFilter: () => AnyArtifact = () =>
({
tags: [GLOBAL_ARTIFACT_TAG],
} as Partial<AnyArtifact> as AnyArtifact);

const getProcessDescendantEventFilter: () => AnyArtifact = () =>
({
tags: [GLOBAL_ARTIFACT_TAG, FILTER_PROCESS_DESCENDANTS_TAG],
} as Partial<AnyArtifact> as AnyArtifact);

beforeEach(() => {
appTestContext = createAppRootMockRenderer();
render = (props) => {
renderResult = appTestContext.render(
<EventFiltersProcessDescendantIndicator data-test-subj="test" {...props} />
);
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();
});
});
Original file line number Diff line number Diff line change
@@ -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<ArtifactEntryCardDecoratorProps>(
({ item, 'data-test-subj': dataTestSubj, ...commonProps }) => {
const getTestId = useTestIdGenerator(dataTestSubj);
const isProcessDescendantFeatureEnabled = useIsExperimentalFeatureEnabled(
'filterProcessDescendantsForEventFiltersEnabled'
);

if (
isProcessDescendantFeatureEnabled &&
isFilterProcessDescendantsEnabled(item as ExceptionListItemSchema)
) {
return (
<>
<EuiText {...commonProps} data-test-subj={getTestId('processDescendantIndication')}>
<code>
<strong>
<FormattedMessage
defaultMessage="Filtering descendants of process"
id="xpack.securitySolution.eventFilters.filteringProcessDescendants"
/>{' '}
<ProcessDescendantsTooltip
indicateExtraEntry
data-test-subj={getTestId('processDescendantIndicationTooltip')}
/>
</strong>
</code>
</EuiText>
<EuiSpacer size="m" />
</>
);
}

return <></>;
}
);
EventFiltersProcessDescendantIndicator.displayName = 'EventFiltersProcessDescendantIndicator';
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -75,6 +76,7 @@ export interface ArtifactListPageProps {
allowCardDeleteAction?: boolean;
allowCardCreateAction?: boolean;
secondaryPageInfo?: React.ReactNode;
CardDecorator?: React.ComponentType<ArtifactEntryCardDecoratorProps>;
}

export const ArtifactListPage = memo<ArtifactListPageProps>(
Expand All @@ -90,6 +92,7 @@ export const ArtifactListPage = memo<ArtifactListPageProps>(
allowCardEditAction = true,
allowCardCreateAction = true,
allowCardDeleteAction = true,
CardDecorator,
}) => {
const { state: routeState } = useLocation<ListPageRouteState | undefined>();
const getTestId = useTestIdGenerator(dataTestSubj);
Expand Down Expand Up @@ -354,6 +357,7 @@ export const ArtifactListPage = memo<ArtifactListPageProps>(
pagination={uiPagination}
contentClassName="card-container"
data-test-subj={getTestId('list')}
CardDecorator={CardDecorator}
/>
</>
)}
Expand Down
Loading

0 comments on commit c978455

Please sign in to comment.