Skip to content

Commit

Permalink
[Security Solution] host isolation exceptions listing under policy in…
Browse files Browse the repository at this point in the history
…tegration details tab (elastic#120361)
  • Loading branch information
academo authored and TinLe committed Dec 22, 2021
1 parent 3d242c5 commit 8a1e6ef
Show file tree
Hide file tree
Showing 19 changed files with 793 additions and 53 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ import type { FoundExceptionListItemSchema } from '@kbn/securitysolution-io-ts-l

import { getExceptionListItemSchemaMock } from './exception_list_item_schema.mock';

export const getFoundExceptionListItemSchemaMock = (): FoundExceptionListItemSchema => ({
data: [getExceptionListItemSchemaMock()],
export const getFoundExceptionListItemSchemaMock = (
count: number = 1
): FoundExceptionListItemSchema => ({
data: Array.from({ length: count }, getExceptionListItemSchemaMock),
page: 1,
per_page: 1,
total: 1,
total: count,
});
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const MANAGEMENT_ROUTING_POLICIES_PATH = `${MANAGEMENT_PATH}/:tabName(${A
export const MANAGEMENT_ROUTING_POLICY_DETAILS_FORM_PATH = `${MANAGEMENT_PATH}/:tabName(${AdministrationSubTab.policies})/:policyId/settings`;
export const MANAGEMENT_ROUTING_POLICY_DETAILS_TRUSTED_APPS_PATH = `${MANAGEMENT_PATH}/:tabName(${AdministrationSubTab.policies})/:policyId/trustedApps`;
export const MANAGEMENT_ROUTING_POLICY_DETAILS_EVENT_FILTERS_PATH = `${MANAGEMENT_PATH}/:tabName(${AdministrationSubTab.policies})/:policyId/eventFilters`;
export const MANAGEMENT_ROUTING_POLICY_DETAILS_HOST_ISOLATION_EXCEPTIONS_PATH = `${MANAGEMENT_PATH}/:tabName(${AdministrationSubTab.policies})/:policyId/hostIsolationExceptions`;
/** @deprecated use the paths defined above instead */
export const MANAGEMENT_ROUTING_POLICY_DETAILS_PATH_OLD = `${MANAGEMENT_PATH}/:tabName(${AdministrationSubTab.policies})/:policyId`;
export const MANAGEMENT_ROUTING_TRUSTED_APPS_PATH = `${MANAGEMENT_PATH}/:tabName(${AdministrationSubTab.trustedApps})`;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,16 @@
*/

import { isEmpty } from 'lodash/fp';
import { generatePath } from 'react-router-dom';
// eslint-disable-next-line import/no-nodejs-modules
import querystring from 'querystring';

import { generatePath } from 'react-router-dom';
import { appendSearch } from '../../common/components/link_to/helpers';
import { EndpointIndexUIQueryParams } from '../pages/endpoint_hosts/types';
import { EventFiltersPageLocation } from '../pages/event_filters/types';
import { HostIsolationExceptionsPageLocation } from '../pages/host_isolation_exceptions/types';
import { PolicyDetailsArtifactsPageLocation } from '../pages/policy/types';
import { TrustedAppsListPageLocation } from '../pages/trusted_apps/state';
import { AdministrationSubTab } from '../types';
import {
MANAGEMENT_DEFAULT_PAGE,
MANAGEMENT_DEFAULT_PAGE_SIZE,
Expand All @@ -19,17 +25,11 @@ import {
MANAGEMENT_ROUTING_HOST_ISOLATION_EXCEPTIONS_PATH,
MANAGEMENT_ROUTING_POLICIES_PATH,
MANAGEMENT_ROUTING_POLICY_DETAILS_FORM_PATH,
MANAGEMENT_ROUTING_POLICY_DETAILS_HOST_ISOLATION_EXCEPTIONS_PATH,
MANAGEMENT_ROUTING_POLICY_DETAILS_TRUSTED_APPS_PATH,
MANAGEMENT_ROUTING_POLICY_DETAILS_EVENT_FILTERS_PATH,
MANAGEMENT_ROUTING_TRUSTED_APPS_PATH,
} from './constants';
import { AdministrationSubTab } from '../types';
import { appendSearch } from '../../common/components/link_to/helpers';
import { EndpointIndexUIQueryParams } from '../pages/endpoint_hosts/types';
import { TrustedAppsListPageLocation } from '../pages/trusted_apps/state';
import { EventFiltersPageLocation } from '../pages/event_filters/types';
import { HostIsolationExceptionsPageLocation } from '../pages/host_isolation_exceptions/types';
import { PolicyDetailsArtifactsPageLocation } from '../pages/policy/types';

// Taken from: https://github.com/microsoft/TypeScript/issues/12936#issuecomment-559034150
type ExactKeys<T1, T2> = Exclude<keyof T1, keyof T2> extends never ? T1 : never;
Expand Down Expand Up @@ -390,3 +390,16 @@ export const getHostIsolationExceptionsListPath = (
querystring.stringify(normalizeHostIsolationExceptionsPageLocation(location))
)}`;
};

export const getPolicyHostIsolationExceptionsPath = (
policyId: string,
location?: Partial<PolicyDetailsArtifactsPageLocation>
) => {
const path = generatePath(MANAGEMENT_ROUTING_POLICY_DETAILS_HOST_ISOLATION_EXCEPTIONS_PATH, {
tabName: AdministrationSubTab.policies,
policyId,
});
return `${path}${appendSearch(
querystring.stringify(normalizePolicyDetailsArtifactsListPageLocation(location))
)}`;
};
21 changes: 21 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 @@ -49,3 +49,24 @@ export const parsePoliciesToKQL = (includedPolicies: string, excludedPolicies: s

return `(${kuery.join(' AND ')})`;
};

/**
* Takes a list of policies (string[]) and an existing kuery
* (string) and returns an unified KQL with and AND
* @param policies string[] a list of policies ids
* @param kuery string an existing KQL.
*/
export const parsePoliciesAndFilterToKql = ({
policies,
kuery,
}: {
policies?: string[];
kuery?: string;
}): string | undefined => {
if (!policies || !policies.length) {
return kuery;
}

const policiesKQL = parsePoliciesToKQL(policies.join(','), '');
return `(${policiesKQL})${kuery ? ` AND (${kuery})` : ''}`;
};
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
MANAGEMENT_STORE_HOST_ISOLATION_EXCEPTIONS_NAMESPACE,
} from '../../../common/constants';
import { getHostIsolationExceptionsListPath } from '../../../common/routing';
import { parseQueryFilterToKQL } from '../../../common/utils';
import { parsePoliciesAndFilterToKql, parseQueryFilterToKQL } from '../../../common/utils';
import {
getHostIsolationExceptionItems,
getHostIsolationExceptionSummary,
Expand Down Expand Up @@ -87,23 +87,37 @@ export function useCanSeeHostIsolationExceptionsMenu(): boolean {

const SEARCHABLE_FIELDS: Readonly<string[]> = [`name`, `description`, `entries.value`];

export function useFetchHostIsolationExceptionsList(): QueryObserverResult<
FoundExceptionListItemSchema,
ServerApiError
> {
export function useFetchHostIsolationExceptionsList({
filter,
page,
perPage,
policies,
enabled = true,
}: {
filter?: string;
page: number;
perPage: number;
policies?: string[];
enabled?: boolean;
}): QueryObserverResult<FoundExceptionListItemSchema, ServerApiError> {
const http = useHttp();
const location = useHostIsolationExceptionsSelector(getCurrentLocation);

return useQuery<FoundExceptionListItemSchema, ServerApiError>(
['hostIsolationExceptions', 'list', location.filter, location.page_size, location.page_index],
['hostIsolationExceptions', 'list', filter, perPage, page, policies],
() => {
const kql = parsePoliciesAndFilterToKql({
policies,
kuery: filter ? parseQueryFilterToKQL(filter, SEARCHABLE_FIELDS) : undefined,
});

return getHostIsolationExceptionItems({
http,
page: location.page_index + 1,
perPage: location.page_size,
filter: parseQueryFilterToKQL(location.filter, SEARCHABLE_FIELDS) || undefined,
page: page + 1,
perPage,
filter: kql,
});
}
},
{ enabled }
);
}

Expand All @@ -114,7 +128,7 @@ export function useGetHostIsolationExceptionFormEntry({
}: {
id?: string;
onSuccess: (data: CreateExceptionListItemSchema | UpdateExceptionListItemSchema) => void;
onError: (error: ServerApiError) => void;
onError?: (error: ServerApiError) => void;
}): QueryObserverResult {
const http = useHttp();
return useQuery<UpdateExceptionListItemSchema | CreateExceptionListItemSchema, ServerApiError>(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,9 @@ describe('When on the host isolation exceptions page', () => {

describe('And data exists', () => {
beforeEach(async () => {
getHostIsolationExceptionItemsMock.mockImplementation(getFoundExceptionListItemSchemaMock);
getHostIsolationExceptionItemsMock.mockImplementation(() =>
getFoundExceptionListItemSchemaMock(1)
);
});

it('should show loading indicator while retrieving data and hide it when it gets it', async () => {
Expand Down Expand Up @@ -185,7 +187,9 @@ describe('When on the host isolation exceptions page', () => {
describe('has canIsolateHost privileges', () => {
beforeEach(async () => {
setEndpointPrivileges({ canIsolateHost: true });
getHostIsolationExceptionItemsMock.mockImplementation(getFoundExceptionListItemSchemaMock);
getHostIsolationExceptionItemsMock.mockImplementation(() =>
getFoundExceptionListItemSchemaMock(1)
);
});

it('should show the create flyout when the add button is pressed', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,12 @@ export const HostIsolationExceptionsList = () => {

const [itemToDelete, setItemToDelete] = useState<ExceptionListItemSchema | null>(null);

const { isLoading, data, error, refetch } = useFetchHostIsolationExceptionsList();
const { isLoading, data, error, refetch } = useFetchHostIsolationExceptionsList({
filter: location.filter,
page: location.page_index,
perPage: location.page_size,
});

const toasts = useToasts();

// load the list of policies>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
MANAGEMENT_ROUTING_POLICY_DETAILS_TRUSTED_APPS_PATH,
MANAGEMENT_ROUTING_POLICY_DETAILS_EVENT_FILTERS_PATH,
MANAGEMENT_ROUTING_POLICY_DETAILS_PATH_OLD,
MANAGEMENT_ROUTING_POLICY_DETAILS_HOST_ISOLATION_EXCEPTIONS_PATH,
} from '../../common/constants';
import { NotFoundPage } from '../../../app/404';
import { getPolicyDetailPath } from '../../common/routing';
Expand All @@ -25,6 +26,7 @@ export const PolicyContainer = memo(() => {
MANAGEMENT_ROUTING_POLICY_DETAILS_FORM_PATH,
MANAGEMENT_ROUTING_POLICY_DETAILS_TRUSTED_APPS_PATH,
MANAGEMENT_ROUTING_POLICY_DETAILS_EVENT_FILTERS_PATH,
MANAGEMENT_ROUTING_POLICY_DETAILS_HOST_ISOLATION_EXCEPTIONS_PATH,
]}
exact
component={PolicyDetails}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@

import { matchPath } from 'react-router-dom';
import { createSelector } from 'reselect';
import { PolicyDetailsSelector, PolicyDetailsState } from '../../../types';
import {
MANAGEMENT_ROUTING_POLICY_DETAILS_FORM_PATH,
MANAGEMENT_ROUTING_POLICY_DETAILS_HOST_ISOLATION_EXCEPTIONS_PATH,
MANAGEMENT_ROUTING_POLICY_DETAILS_TRUSTED_APPS_PATH,
MANAGEMENT_ROUTING_POLICY_DETAILS_EVENT_FILTERS_PATH,
} from '../../../../../common/constants';
import { PolicyDetailsSelector, PolicyDetailsState } from '../../../types';

/**
* Returns current artifacts location
Expand All @@ -37,7 +38,7 @@ export const isOnPolicyFormView: PolicyDetailsSelector<boolean> = createSelector
}
);

/** Returns a boolean of whether the user is on the policy trusted app page or not */
/** Returns a boolean of whether the user is on the policy trusted apps page or not */
export const isOnPolicyTrustedAppsView: PolicyDetailsSelector<boolean> = createSelector(
getUrlLocationPathname,
(pathname) => {
Expand All @@ -62,3 +63,16 @@ export const isOnPolicyEventFiltersView: PolicyDetailsSelector<boolean> = create
);
}
);

/** Returns a boolean of whether the user is on the host isolation exceptions page or not */
export const isOnHostIsolationExceptionsView: PolicyDetailsSelector<boolean> = createSelector(
getUrlLocationPathname,
(pathname) => {
return (
matchPath(pathname ?? '', {
path: MANAGEMENT_ROUTING_POLICY_DETAILS_HOST_ISOLATION_EXCEPTIONS_PATH,
exact: true,
}) !== null
);
}
);
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
* 2.0.
*/

import { createSelector } from 'reselect';
import { matchPath } from 'react-router-dom';
import { createSelector } from 'reselect';
import { ILicense } from '../../../../../../../../licensing/common/types';
import { unsetPolicyFeaturesAccordingToLicenseLevel } from '../../../../../../../common/license/policy_config';
import { PolicyDetailsState } from '../../../types';
Expand All @@ -20,6 +20,7 @@ import {
import { policyFactory as policyConfigFactory } from '../../../../../../../common/endpoint/models/policy_config';
import {
MANAGEMENT_ROUTING_POLICY_DETAILS_FORM_PATH,
MANAGEMENT_ROUTING_POLICY_DETAILS_HOST_ISOLATION_EXCEPTIONS_PATH,
MANAGEMENT_ROUTING_POLICY_DETAILS_TRUSTED_APPS_PATH,
MANAGEMENT_ROUTING_POLICY_DETAILS_EVENT_FILTERS_PATH,
} from '../../../../../common/constants';
Expand All @@ -28,6 +29,7 @@ import { getPolicyDataForUpdate } from '../../../../../../../common/endpoint/ser
import {
isOnPolicyTrustedAppsView,
isOnPolicyEventFiltersView,
isOnHostIsolationExceptionsView,
isOnPolicyFormView,
} from './policy_common_selectors';

Expand Down Expand Up @@ -90,7 +92,8 @@ export const needsToRefresh = (state: Immutable<PolicyDetailsState>): boolean =>
export const isOnPolicyDetailsPage = (state: Immutable<PolicyDetailsState>) =>
isOnPolicyFormView(state) ||
isOnPolicyTrustedAppsView(state) ||
isOnPolicyEventFiltersView(state);
isOnPolicyEventFiltersView(state) ||
isOnHostIsolationExceptionsView(state);

/** Returns the license info fetched from the license service */
export const license = (state: Immutable<PolicyDetailsState>) => {
Expand All @@ -107,6 +110,7 @@ export const policyIdFromParams: (state: Immutable<PolicyDetailsState>) => strin
MANAGEMENT_ROUTING_POLICY_DETAILS_FORM_PATH,
MANAGEMENT_ROUTING_POLICY_DETAILS_TRUSTED_APPS_PATH,
MANAGEMENT_ROUTING_POLICY_DETAILS_EVENT_FILTERS_PATH,
MANAGEMENT_ROUTING_POLICY_DETAILS_HOST_ISOLATION_EXCEPTIONS_PATH,
],
exact: true,
})?.params?.policyId ?? ''
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* 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 { EuiEmptyPrompt, EuiLink, EuiPageTemplate } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import React from 'react';

export const PolicyHostIsolationExceptionsEmptyUnassigned = ({
policyName,
toHostIsolationList,
}: {
policyName: string;
toHostIsolationList: string;
}) => {
return (
<EuiPageTemplate template="centeredContent">
<EuiEmptyPrompt
iconType="plusInCircle"
data-test-subj="policy-host-isolation-exceptions-empty-unassigned"
title={
<h2>
<FormattedMessage
id="xpack.securitySolution.endpoint.policy.hostIsolationExceptions.empty.unassigned.title"
defaultMessage="No assigned host isolation exceptions"
/>
</h2>
}
body={
<FormattedMessage
id="xpack.securitySolution.endpoint.policy.hostIsolationExceptions.empty.unassigned.content"
defaultMessage="There are currently no host isolation exceptions assigned to {policyName}. Assign exceptions now or add and manage them on the host isolation exceptions page."
values={{ policyName }}
/>
}
actions={[
<EuiLink href={toHostIsolationList}>
<FormattedMessage
id="xpack.securitySolution.endpoint.policy.hostIsolationExceptions.empty.unassigned.secondaryAction"
defaultMessage="Manage host isolation exceptions"
/>
</EuiLink>,
]}
/>
</EuiPageTemplate>
);
};

PolicyHostIsolationExceptionsEmptyUnassigned.displayName =
'PolicyHostIsolationExceptionsEmptyUnassigned';
Loading

0 comments on commit 8a1e6ef

Please sign in to comment.