Skip to content

Commit

Permalink
[Security solution][Endpoint] Users can filter trusted apps by policy…
Browse files Browse the repository at this point in the history
… 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>
  • Loading branch information
dasansol92 and kibanamachine authored Aug 5, 2021
1 parent 3b387e2 commit d56b22f
Show file tree
Hide file tree
Showing 26 changed files with 768 additions and 101 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export const allowedExperimentalValues = Object.freeze({
ruleRegistryEnabled: false,
tGridEnabled: false,
trustedAppsByPolicyEnabled: false,
excludePoliciesInFilterEnabled: false,
uebaEnabled: false,
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export const initialAppState: AppState = {
errors: [],
enableExperimental: {
trustedAppsByPolicyEnabled: false,
excludePoliciesInFilterEnabled: false,
metricsEntitiesEnabled: false,
ruleRegistryEnabled: false,
tGridEnabled: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}`
);
});

Expand All @@ -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', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {};
Expand Down Expand Up @@ -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 => {
Expand All @@ -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,
Expand Down
30 changes: 30 additions & 0 deletions x-pack/plugins/security_solution/public/management/common/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
* 2.0.
*/

import { isEmpty } from 'lodash/fp';

export const parseQueryFilterToKQL = (filter: string, fields: Readonly<string[]>): string => {
if (!filter) return '';
const kuery = fields
Expand All @@ -19,3 +21,31 @@ export const parseQueryFilterToKQL = (filter: string, fields: Readonly<string[]>

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 ')})`;
};
Original file line number Diff line number Diff line change
@@ -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';
Original file line number Diff line number Diff line change
@@ -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<PoliciesSelectorProps>) => RenderResult;
beforeEach(() => {
onChangeSelectionMock = jest.fn();
useIsExperimentalFeatureEnabledMock.mockReturnValue(false);
getElement = (params: Partial<PoliciesSelectorProps>) => {
return render(
<I18nProvider>
<PoliciesSelector
policies={[policy]}
onChangeSelection={onChangeSelectionMock}
{...params}
/>
</I18nProvider>
);
};
});
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([]);
});
});
});
Loading

0 comments on commit d56b22f

Please sign in to comment.