Skip to content

Commit

Permalink
[Security Solution][Endpoint] Artifacts event filter card on integrat…
Browse files Browse the repository at this point in the history
…ion policy edit view (#121879)

* fix typo

refs elastic/security-team/issues/2031

* Add artifact event filters card to policy edit view on endpoint integration

fixes elastic/security-team/issues/2031

* add tests

fixes elastic/security-team/issues/2031

* fix typo

refs /pull/111708

* use `eventFiltersListQueryHttpMock` instead

review suggestion

* add a test for verifying error toast

review suggestion

* fix tests

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
  • Loading branch information
ashokaditya and kibanamachine authored Jan 6, 2022
1 parent 9507d79 commit 34d7ca6
Show file tree
Hide file tree
Showing 6 changed files with 262 additions and 5 deletions.
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 = (
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')};
`;

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}"`
);
});
});
});
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

0 comments on commit 34d7ca6

Please sign in to comment.