Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Security Solution][Endpoint] Artifacts event filter card on integration policy edit view #121879

Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,7 @@ export const getPolicyDetailsArtifactsListPath = (
)}`;
};

export const extractEventFiltetrsPageLocation = (
export const extractEventFiltersPageLocation = (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎉

query: querystring.ParsedUrlQuery
): EventFiltersPageLocation => {
const showParamValue = extractFirstParamValue(query, 'show') as EventFiltersPageLocation['show'];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { AppAction } from '../../../../common/store/actions';
import { AppLocation, Immutable } from '../../../../../common/endpoint/types';
import { UserChangedUrl } from '../../../../common/store/routing/action';
import { MANAGEMENT_ROUTING_EVENT_FILTERS_PATH } from '../../../common/constants';
import { extractEventFiltetrsPageLocation } from '../../../common/routing';
import { extractEventFiltersPageLocation } from '../../../common/routing';
import {
isLoadedResourceState,
isUninitialisedResourceState,
Expand Down Expand Up @@ -156,7 +156,7 @@ const eventFiltersUpdateSuccess: CaseReducer<EventFiltersUpdateSuccess> = (state

const userChangedUrl: CaseReducer<UserChangedUrl> = (state, action) => {
if (isEventFiltersPageLocation(action.payload)) {
const location = extractEventFiltetrsPageLocation(parse(action.payload.search.slice(1)));
const location = extractEventFiltersPageLocation(parse(action.payload.search.slice(1)));
return {
...state,
location,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ export const StyledEuiFlexGridGroup = styled(EuiFlexGroup)`
const StyledEuiFlexGroup = styled(EuiFlexGroup)<{
isSmall: boolean;
}>`
font-size: ${({ isSmall, theme }) => (isSmall ? theme.eui.euiFontSizeXS : 'innherit')};
font-weight: ${({ isSmall }) => (isSmall ? '1px' : 'innherit')};
font-size: ${({ isSmall, theme }) => (isSmall ? theme.eui.euiFontSizeXS : 'inherit')};
font-weight: ${({ isSmall }) => (isSmall ? '1px' : 'inherit')};
Comment on lines 48 to +50
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I missed this in an earlier review! 😅

`;

const CSS_BOLD: Readonly<React.CSSProperties> = { fontWeight: 'bold' };
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/*
* 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 { waitFor, act } from '@testing-library/react';
import * as reactTestingLibrary from '@testing-library/react';
import {
AppContextTestRender,
createAppRootMockRenderer,
} from '../../../../../../../common/mock/endpoint';

import { eventFiltersListQueryHttpMock } from '../../../../../event_filters/test_utils';
import { FleetIntegrationEventFiltersCard } from './fleet_integration_event_filters_card';
import { EndpointDocGenerator } from '../../../../../../../../common/endpoint/generate_data';
import { getPolicyEventFiltersPath } from '../../../../../../common/routing';
import { PolicyData } from '../../../../../../../../common/endpoint/types';
import { getFoundExceptionListItemSchemaMock } from '../../../../../../../../../lists/common/schemas/response/found_exception_list_item_schema.mock';

const endpointGenerator = new EndpointDocGenerator('seed');

describe('Fleet integration policy endpoint security event filters card', () => {
let render: () => Promise<ReturnType<AppContextTestRender['render']>>;
let renderResult: ReturnType<AppContextTestRender['render']>;
let history: AppContextTestRender['history'];
let mockedContext: AppContextTestRender;
let mockedApi: ReturnType<typeof eventFiltersListQueryHttpMock>;
let policy: PolicyData;

beforeEach(() => {
policy = endpointGenerator.generatePolicyPackagePolicy();
mockedContext = createAppRootMockRenderer();
mockedApi = eventFiltersListQueryHttpMock(mockedContext.coreStart.http);
({ history } = mockedContext);
render = async () => {
await act(async () => {
renderResult = mockedContext.render(
<FleetIntegrationEventFiltersCard policyId={policy.id} />
);
await waitFor(() => expect(mockedApi.responseProvider.eventFiltersList).toHaveBeenCalled());
});
return renderResult;
};

history.push(getPolicyEventFiltersPath(policy.id));
});

afterEach(() => reactTestingLibrary.cleanup());

it('should call the API and render the card correctly', async () => {
mockedApi.responseProvider.eventFiltersList.mockReturnValue(
getFoundExceptionListItemSchemaMock(3)
);

await render();
expect(renderResult.getByTestId('eventFilters-fleet-integration-card')).toHaveTextContent(
'Event filters3'
);
});

it('should show the card even when no event filters associated with the policy', async () => {
mockedApi.responseProvider.eventFiltersList.mockReturnValue(
getFoundExceptionListItemSchemaMock(0)
);

await render();
expect(renderResult.getByTestId('eventFilters-fleet-integration-card')).toBeTruthy();
});

it('should have the correct manage event filters link', async () => {
mockedApi.responseProvider.eventFiltersList.mockReturnValue(
getFoundExceptionListItemSchemaMock(1)
);

await render();
expect(renderResult.getByTestId('eventFilters-link-to-exceptions')).toHaveAttribute(
'href',
`/app/security/administration/policy/${policy.id}/eventFilters`
);
});

it('should show an error toast when API request fails', async () => {
const error = new Error('Uh oh! API error!');
mockedApi.responseProvider.eventFiltersList.mockImplementation(() => {
throw error;
});

await render();
await waitFor(() => {
expect(mockedContext.coreStart.notifications.toasts.addDanger).toHaveBeenCalledTimes(1);
expect(mockedContext.coreStart.notifications.toasts.addDanger).toHaveBeenCalledWith(
`There was an error trying to fetch event filters stats: "${error}"`
);
});
});
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a test case for the error handling? A test that checks a toast is shown when there is an API error.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done 3bc3a04

Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
/*
* 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 { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiText } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import React, { memo, useEffect, useMemo, useRef, useState } from 'react';
import { INTEGRATIONS_PLUGIN_ID } from '../../../../../../../../../fleet/common';
import { pagePathGetters } from '../../../../../../../../../fleet/public';
import {
GetExceptionSummaryResponse,
PolicyDetailsRouteState,
} from '../../../../../../../../common/endpoint/types';
import { useAppUrl, useHttp, useToasts } from '../../../../../../../common/lib/kibana';
import { getPolicyEventFiltersPath } from '../../../../../../common/routing';
import { parsePoliciesToKQL } from '../../../../../../common/utils';
import { ExceptionItemsSummary } from './exception_items_summary';
import { LinkWithIcon } from './link_with_icon';
import { StyledEuiFlexItem } from './styled_components';
import { EventFiltersHttpService } from '../../../../../event_filters/service';

export const FleetIntegrationEventFiltersCard = memo<{
policyId: string;
}>(({ policyId }) => {
const toasts = useToasts();
const http = useHttp();
const [stats, setStats] = useState<GetExceptionSummaryResponse | undefined>();
const isMounted = useRef<boolean>();
const { getAppUrl } = useAppUrl();

const eventFiltersApi = useMemo(() => new EventFiltersHttpService(http), [http]);
const policyEventFiltersPath = getPolicyEventFiltersPath(policyId);

const policyEventFiltersRouteState = useMemo<PolicyDetailsRouteState>(() => {
const fleetPackageIntegrationCustomUrlPath = `#${
pagePathGetters.integration_policy_edit({ packagePolicyId: policyId })[1]
}`;

return {
backLink: {
label: i18n.translate(
'xpack.securitySolution.endpoint.fleetCustomExtension.artifacts.backButtonLabel',
{
defaultMessage: `Back to Fleet integration policy`,
}
),
navigateTo: [
INTEGRATIONS_PLUGIN_ID,
{
path: fleetPackageIntegrationCustomUrlPath,
},
],
href: getAppUrl({
appId: INTEGRATIONS_PLUGIN_ID,
path: fleetPackageIntegrationCustomUrlPath,
}),
},
};
}, [getAppUrl, policyId]);

const linkToEventFilters = useMemo(
() => (
<LinkWithIcon
href={getAppUrl({
path: policyEventFiltersPath,
})}
appPath={policyEventFiltersPath}
appState={policyEventFiltersRouteState}
data-test-subj="eventFilters-link-to-exceptions"
size="m"
>
<FormattedMessage
id="xpack.securitySolution.endpoint.fleetCustomExtension.eventFiltersManageLabel"
defaultMessage="Manage event filters"
/>
</LinkWithIcon>
),
[getAppUrl, policyEventFiltersPath, policyEventFiltersRouteState]
);

useEffect(() => {
isMounted.current = true;
const fetchStats = async () => {
try {
const summary = await eventFiltersApi.getList({
perPage: 1,
page: 1,
filter: parsePoliciesToKQL([policyId, 'all']),
});
if (isMounted.current) {
setStats({
total: summary.total,
windows: 0,
linux: 0,
macos: 0,
});
}
} catch (error) {
if (isMounted.current) {
toasts.addDanger(
i18n.translate(
'xpack.securitySolution.endpoint.fleetCustomExtension.eventFiltersSummary.error',
{
defaultMessage: 'There was an error trying to fetch event filters stats: "{error}"',
values: { error },
}
)
);
}
}
};
fetchStats();
return () => {
isMounted.current = false;
};
}, [eventFiltersApi, policyId, toasts]);

return (
<EuiPanel
hasShadow={false}
paddingSize="l"
hasBorder
data-test-subj="eventFilters-fleet-integration-card"
>
<EuiFlexGroup
alignItems="baseline"
justifyContent="flexStart"
gutterSize="s"
direction="row"
responsive={false}
>
<EuiFlexItem grow={false}>
<EuiText>
<h5>
<FormattedMessage
id="xpack.securitySolution.endpoint.eventFilters.fleetIntegration.title"
defaultMessage="Event filters"
/>
</h5>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<ExceptionItemsSummary stats={stats} isSmall={true} />
</EuiFlexItem>
<StyledEuiFlexItem grow={1}>{linkToEventFilters}</StyledEuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
);
});

FleetIntegrationEventFiltersCard.displayName = 'FleetIntegrationEventFiltersCard';
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
import { FleetTrustedAppsCard } from './endpoint_package_custom_extension/components/fleet_trusted_apps_card';
import { LinkWithIcon } from './endpoint_package_custom_extension/components/link_with_icon';
import { FleetIntegrationHostIsolationExceptionsCard } from './endpoint_package_custom_extension/components/fleet_integration_host_isolation_exceptions_card';
import { FleetIntegrationEventFiltersCard } from './endpoint_package_custom_extension/components/fleet_integration_event_filters_card';
/**
* Exports Endpoint-specific package policy instructions
* for use in the Ingest app create / edit package policy
Expand Down Expand Up @@ -181,6 +182,8 @@ const WrappedPolicyDetailsForm = memo<{
customLink={policyTrustedAppsLink}
/>
<EuiSpacer size="s" />
<FleetIntegrationEventFiltersCard policyId={policyId} />
<EuiSpacer size="s" />
<FleetIntegrationHostIsolationExceptionsCard policyId={policyId} />
</div>
<EuiSpacer size="l" />
Expand Down