[Security Solution] Add host isolation exceptions UI (#111253)

This commit is contained in:
Esteban Beltran 2021-09-30 13:43:22 +02:00 committed by GitHub
parent 3bd687e6ca
commit a6670380aa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 1030 additions and 24 deletions

View file

@ -12,6 +12,7 @@ export const exceptionListType = t.keyof({
detection: null,
endpoint: null,
endpoint_events: null,
endpoint_host_isolation_exceptions: null,
});
export const exceptionListTypeOrUndefined = t.union([exceptionListType, t.undefined]);
export type ExceptionListType = t.TypeOf<typeof exceptionListType>;
@ -20,4 +21,5 @@ export enum ExceptionListTypeEnum {
DETECTION = 'detection',
ENDPOINT = 'endpoint',
ENDPOINT_EVENTS = 'endpoint_events',
ENDPOINT_HOST_ISOLATION_EXCEPTIONS = 'endpoint_host_isolation_exceptions',
}

View file

@ -86,7 +86,7 @@ describe('Lists', () => {
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "1" supplied to "Array<{| id: NonEmptyString, list_id: NonEmptyString, type: "detection" | "endpoint" | "endpoint_events", namespace_type: "agnostic" | "single" |}>"',
'Invalid value "1" supplied to "Array<{| id: NonEmptyString, list_id: NonEmptyString, type: "detection" | "endpoint" | "endpoint_events" | "endpoint_host_isolation_exceptions", namespace_type: "agnostic" | "single" |}>"',
]);
expect(message.schema).toEqual({});
});
@ -117,8 +117,8 @@ describe('Lists', () => {
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "1" supplied to "(Array<{| id: NonEmptyString, list_id: NonEmptyString, type: "detection" | "endpoint" | "endpoint_events", namespace_type: "agnostic" | "single" |}> | undefined)"',
'Invalid value "[1]" supplied to "(Array<{| id: NonEmptyString, list_id: NonEmptyString, type: "detection" | "endpoint" | "endpoint_events", namespace_type: "agnostic" | "single" |}> | undefined)"',
'Invalid value "1" supplied to "(Array<{| id: NonEmptyString, list_id: NonEmptyString, type: "detection" | "endpoint" | "endpoint_events" | "endpoint_host_isolation_exceptions", namespace_type: "agnostic" | "single" |}> | undefined)"',
'Invalid value "[1]" supplied to "(Array<{| id: NonEmptyString, list_id: NonEmptyString, type: "detection" | "endpoint" | "endpoint_events" | "endpoint_host_isolation_exceptions", namespace_type: "agnostic" | "single" |}> | undefined)"',
]);
expect(message.schema).toEqual({});
});

View file

@ -70,3 +70,9 @@ export const ENDPOINT_EVENT_FILTERS_LIST_NAME = 'Endpoint Security Event Filters
/** Description of event filters agnostic list */
export const ENDPOINT_EVENT_FILTERS_LIST_DESCRIPTION = 'Endpoint Security Event Filters List';
export const ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID = 'endpoint_host_isolation_exceptions';
export const ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_NAME =
'Endpoint Security Host Isolation Exceptions List';
export const ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_DESCRIPTION =
'Endpoint Security Host Isolation Exceptions List';

View file

@ -76,6 +76,7 @@ export enum SecurityPageName {
detections = 'detections',
endpoints = 'endpoints',
eventFilters = 'event_filters',
hostIsolationExceptions = 'host_isolation_exceptions',
events = 'events',
exceptions = 'exceptions',
explore = 'explore',
@ -113,6 +114,7 @@ export const MANAGEMENT_PATH = '/administration';
export const ENDPOINTS_PATH = `${MANAGEMENT_PATH}/endpoints`;
export const TRUSTED_APPS_PATH = `${MANAGEMENT_PATH}/trusted_apps`;
export const EVENT_FILTERS_PATH = `${MANAGEMENT_PATH}/event_filters`;
export const HOST_ISOLATION_EXCEPTIONS_PATH = `${MANAGEMENT_PATH}/host_isolation_exceptions`;
export const APP_OVERVIEW_PATH = `${APP_PATH}${OVERVIEW_PATH}`;
export const APP_MANAGEMENT_PATH = `${APP_PATH}${MANAGEMENT_PATH}`;
@ -129,6 +131,7 @@ export const APP_CASES_PATH = `${APP_PATH}${CASES_PATH}`;
export const APP_ENDPOINTS_PATH = `${APP_PATH}${ENDPOINTS_PATH}`;
export const APP_TRUSTED_APPS_PATH = `${APP_PATH}${TRUSTED_APPS_PATH}`;
export const APP_EVENT_FILTERS_PATH = `${APP_PATH}${EVENT_FILTERS_PATH}`;
export const APP_HOST_ISOLATION_EXCEPTIONS_PATH = `${APP_PATH}${HOST_ISOLATION_EXCEPTIONS_PATH}`;
/** The comma-delimited list of Elasticsearch indices from which the SIEM app collects events */
export const DEFAULT_INDEX_PATTERN = [

View file

@ -30,6 +30,10 @@ import {
CASE,
MANAGE,
UEBA,
HOST_ISOLATION_EXCEPTIONS,
EVENT_FILTERS,
TRUSTED_APPLICATIONS,
ENDPOINTS,
} from '../translations';
import {
OVERVIEW_PATH,
@ -44,6 +48,7 @@ import {
TRUSTED_APPS_PATH,
EVENT_FILTERS_PATH,
UEBA_PATH,
HOST_ISOLATION_EXCEPTIONS_PATH,
} from '../../../common/constants';
import { ExperimentalFeatures } from '../../../common/experimental_features';
@ -313,26 +318,25 @@ export const securitySolutionsDeepLinks: AppDeepLink[] = [
{
id: SecurityPageName.endpoints,
navLinkStatus: AppNavLinkStatus.visible,
title: i18n.translate('xpack.securitySolution.search.administration.endpoints', {
defaultMessage: 'Endpoints',
}),
title: ENDPOINTS,
order: 9006,
path: ENDPOINTS_PATH,
},
{
id: SecurityPageName.trustedApps,
title: i18n.translate('xpack.securitySolution.search.administration.trustedApps', {
defaultMessage: 'Trusted applications',
}),
title: TRUSTED_APPLICATIONS,
path: TRUSTED_APPS_PATH,
},
{
id: SecurityPageName.eventFilters,
title: i18n.translate('xpack.securitySolution.search.administration.eventFilters', {
defaultMessage: 'Event filters',
}),
title: EVENT_FILTERS,
path: EVENT_FILTERS_PATH,
},
{
id: SecurityPageName.hostIsolationExceptions,
title: HOST_ISOLATION_EXCEPTIONS,
path: HOST_ISOLATION_EXCEPTIONS_PATH,
},
],
},
];

View file

@ -26,6 +26,7 @@ import {
APP_EVENT_FILTERS_PATH,
APP_UEBA_PATH,
SecurityPageName,
APP_HOST_ISOLATION_EXCEPTIONS_PATH,
} from '../../../common/constants';
export const navTabs: SecurityNav = {
@ -120,6 +121,13 @@ export const navTabs: SecurityNav = {
disabled: false,
urlKey: 'administration',
},
[SecurityPageName.hostIsolationExceptions]: {
id: SecurityPageName.hostIsolationExceptions,
name: i18n.HOST_ISOLATION_EXCEPTIONS,
href: APP_HOST_ISOLATION_EXCEPTIONS_PATH,
disabled: false,
urlKey: 'administration',
},
};
export const securityNavGroup: SecurityNavGroup = {

View file

@ -62,6 +62,12 @@ export const EVENT_FILTERS = i18n.translate(
}
);
export const HOST_ISOLATION_EXCEPTIONS = i18n.translate(
'xpack.securitySolution.search.administration.hostIsolationExceptions',
{
defaultMessage: 'Host Isolation Exceptions',
}
);
export const DETECT = i18n.translate('xpack.securitySolution.navigation.detect', {
defaultMessage: 'Detect',
});

View file

@ -47,6 +47,7 @@ export type SecurityNavKey =
| SecurityPageName.endpoints
| SecurityPageName.eventFilters
| SecurityPageName.exceptions
| SecurityPageName.hostIsolationExceptions
| SecurityPageName.hosts
| SecurityPageName.network
| SecurityPageName.overview

View file

@ -233,6 +233,16 @@ describe('useSecuritySolutionNavigation', () => {
"name": "Event filters",
"onClick": [Function],
},
Object {
"data-href": "securitySolution/host_isolation_exceptions",
"data-test-subj": "navigation-host_isolation_exceptions",
"disabled": false,
"href": "securitySolution/host_isolation_exceptions",
"id": "host_isolation_exceptions",
"isSelected": false,
"name": "Host Isolation Exceptions",
"onClick": [Function],
},
],
"name": "Manage",
},

View file

@ -83,7 +83,12 @@ function usePrimaryNavigationItemsToDisplay(navTabs: Record<string, NavTab>) {
},
{
...securityNavGroup.manage,
items: [navTabs.endpoints, navTabs.trusted_apps, navTabs.event_filters],
items: [
navTabs.endpoints,
navTabs.trusted_apps,
navTabs.event_filters,
navTabs.host_isolation_exceptions,
],
},
],
[navTabs, hasCasesReadPermissions]

View file

@ -9,6 +9,7 @@ import { EndpointAction } from '../../management/pages/endpoint_hosts/store/acti
import { PolicyDetailsAction } from '../../management/pages/policy/store/policy_details';
import { TrustedAppsPageAction } from '../../management/pages/trusted_apps/store/action';
import { EventFiltersPageAction } from '../../management/pages/event_filters/store/action';
import { HostIsolationExceptionsPageAction } from '../../management/pages/host_isolation_exceptions/store/action';
export { appActions } from './app';
export { dragAndDropActions } from './drag_and_drop';
@ -21,4 +22,5 @@ export type AppAction =
| RoutingAction
| PolicyDetailsAction
| TrustedAppsPageAction
| EventFiltersPageAction;
| EventFiltersPageAction
| HostIsolationExceptionsPageAction;

View file

@ -9,12 +9,14 @@ import { ChromeBreadcrumb } from 'kibana/public';
import { AdministrationSubTab } from '../types';
import { ENDPOINTS_TAB, EVENT_FILTERS_TAB, POLICIES_TAB, TRUSTED_APPS_TAB } from './translations';
import { AdministrationRouteSpyState } from '../../common/utils/route/types';
import { HOST_ISOLATION_EXCEPTIONS } from '../../app/translations';
const TabNameMappedToI18nKey: Record<AdministrationSubTab, string> = {
[AdministrationSubTab.endpoints]: ENDPOINTS_TAB,
[AdministrationSubTab.policies]: POLICIES_TAB,
[AdministrationSubTab.trustedApps]: TRUSTED_APPS_TAB,
[AdministrationSubTab.eventFilters]: EVENT_FILTERS_TAB,
[AdministrationSubTab.hostIsolationExceptions]: HOST_ISOLATION_EXCEPTIONS,
};
export function getBreadcrumbs(params: AdministrationRouteSpyState): ChromeBreadcrumb[] {

View file

@ -17,6 +17,7 @@ export const MANAGEMENT_ROUTING_POLICY_DETAILS_TRUSTED_APPS_PATH = `${MANAGEMENT
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})`;
export const MANAGEMENT_ROUTING_EVENT_FILTERS_PATH = `${MANAGEMENT_PATH}/:tabName(${AdministrationSubTab.eventFilters})`;
export const MANAGEMENT_ROUTING_HOST_ISOLATION_EXCEPTIONS_PATH = `${MANAGEMENT_PATH}/:tabName(${AdministrationSubTab.hostIsolationExceptions})`;
// --[ STORE ]---------------------------------------------------------------------------
/** The SIEM global store namespace where the management state will be mounted */
@ -29,6 +30,7 @@ export const MANAGEMENT_STORE_ENDPOINTS_NAMESPACE = 'endpoints';
export const MANAGEMENT_STORE_TRUSTED_APPS_NAMESPACE = 'trustedApps';
/** Namespace within the Management state where event filters page state is maintained */
export const MANAGEMENT_STORE_EVENT_FILTERS_NAMESPACE = 'eventFilters';
export const MANAGEMENT_STORE_HOST_ISOLATION_EXCEPTIONS_NAMESPACE = 'hostIsolationExceptions';
export const MANAGEMENT_PAGE_SIZE_OPTIONS: readonly number[] = [10, 20, 50];
export const MANAGEMENT_DEFAULT_PAGE = 0;

View file

@ -16,6 +16,7 @@ import {
MANAGEMENT_PAGE_SIZE_OPTIONS,
MANAGEMENT_ROUTING_ENDPOINTS_PATH,
MANAGEMENT_ROUTING_EVENT_FILTERS_PATH,
MANAGEMENT_ROUTING_HOST_ISOLATION_EXCEPTIONS_PATH,
MANAGEMENT_ROUTING_POLICIES_PATH,
MANAGEMENT_ROUTING_POLICY_DETAILS_FORM_PATH,
MANAGEMENT_ROUTING_POLICY_DETAILS_TRUSTED_APPS_PATH,
@ -26,6 +27,7 @@ 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
@ -200,6 +202,26 @@ const normalizeEventFiltersPageLocation = (
}
};
const normalizeHostIsolationExceptionsPageLocation = (
location?: Partial<EventFiltersPageLocation>
): Partial<EventFiltersPageLocation> => {
if (location) {
return {
...(!isDefaultOrMissing(location.page_index, MANAGEMENT_DEFAULT_PAGE)
? { page_index: location.page_index }
: {}),
...(!isDefaultOrMissing(location.page_size, MANAGEMENT_DEFAULT_PAGE_SIZE)
? { page_size: location.page_size }
: {}),
...(!isDefaultOrMissing(location.show, undefined) ? { show: location.show } : {}),
...(!isDefaultOrMissing(location.id, undefined) ? { id: location.id } : {}),
...(!isDefaultOrMissing(location.filter, '') ? { filter: location.filter } : ''),
};
} else {
return {};
}
};
/**
* Given an object with url params, and a given key, return back only the first param value (case multiples were defined)
* @param query
@ -327,3 +349,31 @@ export const getEventFiltersListPath = (location?: Partial<EventFiltersPageLocat
querystring.stringify(normalizeEventFiltersPageLocation(location))
)}`;
};
export const extractHostIsolationExceptionsPageLocation = (
query: querystring.ParsedUrlQuery
): HostIsolationExceptionsPageLocation => {
const showParamValue = extractFirstParamValue(
query,
'show'
) as HostIsolationExceptionsPageLocation['show'];
return {
...extractListPaginationParams(query),
show:
showParamValue && ['edit', 'create'].includes(showParamValue) ? showParamValue : undefined,
id: extractFirstParamValue(query, 'id'),
};
};
export const getHostIsolationExceptionsListPath = (
location?: Partial<HostIsolationExceptionsPageLocation>
): string => {
const path = generatePath(MANAGEMENT_ROUTING_HOST_ISOLATION_EXCEPTIONS_PATH, {
tabName: AdministrationSubTab.hostIsolationExceptions,
});
return `${path}${appendSearch(
querystring.stringify(normalizeHostIsolationExceptionsPageLocation(location))
)}`;
};

View file

@ -203,8 +203,7 @@ const checkIfEventFilterDataExist: MiddlewareActionHandler = async (
) => {
dispatch({
type: 'eventFiltersListPageDataExistsChanged',
// Ignore will be fixed with when AsyncResourceState is refactored (#830)
// @ts-ignore
// @ts-expect-error-next-line will be fixed with when AsyncResourceState is refactored (#830)
payload: createLoadingResourceState(getListPageDataExistsState(getState())),
});
@ -232,9 +231,8 @@ const refreshListDataIfNeeded: MiddlewareActionHandler = async (store, eventFilt
dispatch({
type: 'eventFiltersListPageDataChanged',
payload: {
// Ignore will be fixed with when AsyncResourceState is refactored (#830)
// @ts-ignore
type: 'LoadingResourceState',
// @ts-expect-error-next-line will be fixed with when AsyncResourceState is refactored (#830)
previousState: getCurrentListPageDataState(state),
},
});
@ -300,8 +298,7 @@ const eventFilterDeleteEntry: MiddlewareActionHandler = async (
dispatch({
type: 'eventFilterDeleteStatusChanged',
// Ignore will be fixed with when AsyncResourceState is refactored (#830)
// @ts-ignore
// @ts-expect-error-next-line will be fixed with when AsyncResourceState is refactored (#830)
payload: createLoadingResourceState(getDeletionState(state).status),
});

View file

@ -0,0 +1,23 @@
/*
* 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 { ExceptionListType, ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types';
import {
ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_DESCRIPTION,
ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID,
ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_NAME,
} from '@kbn/securitysolution-list-constants';
export const HOST_ISOLATION_EXCEPTIONS_LIST_TYPE: ExceptionListType =
ExceptionListTypeEnum.ENDPOINT_HOST_ISOLATION_EXCEPTIONS;
export const HOST_ISOLATION_EXCEPTIONS_LIST = {
name: ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_NAME,
namespace_type: 'agnostic',
description: ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_DESCRIPTION,
list_id: ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID,
type: HOST_ISOLATION_EXCEPTIONS_LIST_TYPE,
};

View file

@ -0,0 +1,30 @@
/*
* 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 { Switch, Route } from 'react-router-dom';
import React, { memo } from 'react';
import { MANAGEMENT_ROUTING_HOST_ISOLATION_EXCEPTIONS_PATH } from '../../common/constants';
import { NotFoundPage } from '../../../app/404';
import { HostIsolationExceptionsList } from './view/host_isolation_exceptions_list';
/**
* Provides the routing container for the hosts related views
*/
export const HostIsolationExceptionsContainer = memo(() => {
return (
<Switch>
<Route
path={MANAGEMENT_ROUTING_HOST_ISOLATION_EXCEPTIONS_PATH}
exact
component={HostIsolationExceptionsList}
/>
<Route path="*" component={NotFoundPage} />
</Switch>
);
});
HostIsolationExceptionsContainer.displayName = 'HostIsolationExceptionsContainer';

View file

@ -0,0 +1,66 @@
/*
* 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 {
ExceptionListItemSchema,
FoundExceptionListItemSchema,
} from '@kbn/securitysolution-io-ts-list-types';
import { ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID } from '@kbn/securitysolution-list-constants';
import { HttpStart } from 'kibana/public';
import { EXCEPTION_LIST_ITEM_URL, EXCEPTION_LIST_URL } from '../event_filters/constants';
import { HOST_ISOLATION_EXCEPTIONS_LIST } from './constants';
async function createHostIsolationExceptionList(http: HttpStart): Promise<void> {
try {
await http.post<ExceptionListItemSchema>(EXCEPTION_LIST_URL, {
body: JSON.stringify(HOST_ISOLATION_EXCEPTIONS_LIST),
});
} catch (err) {
// Ignore 409 errors. List already created
if (err.response?.status !== 409) {
throw err;
}
}
}
let listExistsPromise: Promise<void>;
async function ensureHostIsolationExceptionsListExists(http: HttpStart): Promise<void> {
if (!listExistsPromise) {
listExistsPromise = createHostIsolationExceptionList(http);
}
await listExistsPromise;
}
export async function getHostIsolationExceptionItems({
http,
perPage,
page,
sortField,
sortOrder,
filter,
}: {
http: HttpStart;
page?: number;
perPage?: number;
sortField?: keyof ExceptionListItemSchema;
sortOrder?: 'asc' | 'desc';
filter?: string;
}): Promise<FoundExceptionListItemSchema> {
await ensureHostIsolationExceptionsListExists(http);
const entries: FoundExceptionListItemSchema = await http.get(`${EXCEPTION_LIST_ITEM_URL}/_find`, {
query: {
list_id: [ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID],
namespace_type: ['agnostic'],
page,
per_page: perPage,
sort_field: sortField,
sort_order: sortOrder,
filter,
},
});
return entries;
}

View file

@ -0,0 +1,16 @@
/*
* 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 { Action } from 'redux';
import { HostIsolationExceptionsPageState } from '../types';
export type HostIsolationExceptionsPageDataChanged =
Action<'hostIsolationExceptionsPageDataChanged'> & {
payload: HostIsolationExceptionsPageState['entries'];
};
export type HostIsolationExceptionsPageAction = HostIsolationExceptionsPageDataChanged;

View file

@ -0,0 +1,19 @@
/*
* 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 { MANAGEMENT_DEFAULT_PAGE, MANAGEMENT_DEFAULT_PAGE_SIZE } from '../../../common/constants';
import { createUninitialisedResourceState } from '../../../state';
import { HostIsolationExceptionsPageState } from '../types';
export const initialHostIsolationExceptionsPageState = (): HostIsolationExceptionsPageState => ({
entries: createUninitialisedResourceState(),
location: {
page_index: MANAGEMENT_DEFAULT_PAGE,
page_size: MANAGEMENT_DEFAULT_PAGE_SIZE,
filter: '',
},
});

View file

@ -0,0 +1,142 @@
/*
* 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 { applyMiddleware, createStore, Store } from 'redux';
import { HOST_ISOLATION_EXCEPTIONS_PATH } from '../../../../../common/constants';
import { coreMock } from '../../../../../../../../src/core/public/mocks';
import { getFoundExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/found_exception_list_item_schema.mock';
import { AppAction } from '../../../../common/store/actions';
import {
createSpyMiddleware,
MiddlewareActionSpyHelper,
} from '../../../../common/store/test_utils';
import { isFailedResourceState, isLoadedResourceState } from '../../../state';
import { getHostIsolationExceptionItems } from '../service';
import { HostIsolationExceptionsPageState } from '../types';
import { initialHostIsolationExceptionsPageState } from './builders';
import { createHostIsolationExceptionsPageMiddleware } from './middleware';
import { hostIsolationExceptionsPageReducer } from './reducer';
import { getListFetchError } from './selector';
jest.mock('../service');
const getHostIsolationExceptionItemsMock = getHostIsolationExceptionItems as jest.Mock;
const fakeCoreStart = coreMock.createStart({ basePath: '/mock' });
const createStoreSetup = () => {
const spyMiddleware = createSpyMiddleware<HostIsolationExceptionsPageState>();
return {
spyMiddleware,
store: createStore(
hostIsolationExceptionsPageReducer,
applyMiddleware(
createHostIsolationExceptionsPageMiddleware(fakeCoreStart),
spyMiddleware.actionSpyMiddleware
)
),
};
};
describe('Host isolation exceptions middleware', () => {
let store: Store<HostIsolationExceptionsPageState>;
let spyMiddleware: MiddlewareActionSpyHelper<HostIsolationExceptionsPageState, AppAction>;
let initialState: HostIsolationExceptionsPageState;
beforeEach(() => {
initialState = initialHostIsolationExceptionsPageState();
const storeSetup = createStoreSetup();
store = storeSetup.store as Store<HostIsolationExceptionsPageState>;
spyMiddleware = storeSetup.spyMiddleware;
});
describe('initial state', () => {
it('sets initial state properly', async () => {
expect(createStoreSetup().store.getState()).toStrictEqual(initialState);
});
});
describe('when on the List page', () => {
const changeUrl = (searchParams: string = '') => {
store.dispatch({
type: 'userChangedUrl',
payload: {
pathname: HOST_ISOLATION_EXCEPTIONS_PATH,
search: searchParams,
hash: '',
key: 'miniMe',
},
});
};
beforeEach(() => {
getHostIsolationExceptionItemsMock.mockClear();
getHostIsolationExceptionItemsMock.mockImplementation(getFoundExceptionListItemSchemaMock);
});
it.each([
[undefined, undefined],
[3, 50],
])(
'should trigger api call to retrieve host isolation exceptions params page_index[%s] page_size[%s]',
async (pageIndex, perPage) => {
changeUrl((pageIndex && perPage && `?page_index=${pageIndex}&page_size=${perPage}`) || '');
await spyMiddleware.waitForAction('hostIsolationExceptionsPageDataChanged', {
validate({ payload }) {
return isLoadedResourceState(payload);
},
});
expect(getHostIsolationExceptionItemsMock).toHaveBeenCalledWith(
expect.objectContaining({
page: (pageIndex ?? 0) + 1,
perPage: perPage ?? 10,
filter: undefined,
})
);
}
);
it('should clear up previous page and apply a filter configuration when a filter is used', async () => {
changeUrl('?filter=testMe');
await spyMiddleware.waitForAction('hostIsolationExceptionsPageDataChanged', {
validate({ payload }) {
return isLoadedResourceState(payload);
},
});
expect(getHostIsolationExceptionItemsMock).toHaveBeenCalledWith(
expect.objectContaining({
page: 1,
perPage: 10,
filter:
'(exception-list-agnostic.attributes.name:(*testMe*) OR exception-list-agnostic.attributes.description:(*testMe*) OR exception-list-agnostic.attributes.entries.value:(*testMe*))',
})
);
});
it('should dispatch a Failure if an API error was encountered', async () => {
getHostIsolationExceptionItemsMock.mockRejectedValue({
body: { message: 'error message', statusCode: 500, error: 'Internal Server Error' },
});
changeUrl();
await spyMiddleware.waitForAction('hostIsolationExceptionsPageDataChanged', {
validate({ payload }) {
return isFailedResourceState(payload);
},
});
expect(getListFetchError(store.getState())).toEqual({
message: 'error message',
statusCode: 500,
error: 'Internal Server Error',
});
});
});
});

View file

@ -0,0 +1,90 @@
/*
* 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 { FoundExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types';
import { CoreStart, HttpStart } from 'kibana/public';
import { matchPath } from 'react-router-dom';
import { AppLocation, Immutable } from '../../../../../common/endpoint/types';
import { ImmutableMiddleware, ImmutableMiddlewareAPI } from '../../../../common/store';
import { AppAction } from '../../../../common/store/actions';
import { MANAGEMENT_ROUTING_HOST_ISOLATION_EXCEPTIONS_PATH } from '../../../common/constants';
import { parseQueryFilterToKQL } from '../../../common/utils';
import {
createFailedResourceState,
createLoadedResourceState,
} from '../../../state/async_resource_builders';
import { getHostIsolationExceptionItems } from '../service';
import { HostIsolationExceptionsPageState } from '../types';
import { getCurrentListPageDataState, getCurrentLocation } from './selector';
export const SEARCHABLE_FIELDS: Readonly<string[]> = [`name`, `description`, `entries.value`];
export function hostIsolationExceptionsMiddlewareFactory(coreStart: CoreStart) {
return createHostIsolationExceptionsPageMiddleware(coreStart);
}
export const createHostIsolationExceptionsPageMiddleware = (
coreStart: CoreStart
): ImmutableMiddleware<HostIsolationExceptionsPageState, AppAction> => {
return (store) => (next) => async (action) => {
next(action);
if (action.type === 'userChangedUrl' && isHostIsolationExceptionsPage(action.payload)) {
loadHostIsolationExceptionsList(store, coreStart.http);
}
};
};
async function loadHostIsolationExceptionsList(
store: ImmutableMiddlewareAPI<HostIsolationExceptionsPageState, AppAction>,
http: HttpStart
) {
const { dispatch } = store;
try {
const {
page_size: pageSize,
page_index: pageIndex,
filter,
} = getCurrentLocation(store.getState());
const query = {
http,
page: pageIndex + 1,
perPage: pageSize,
filter: parseQueryFilterToKQL(filter, SEARCHABLE_FIELDS) || undefined,
};
dispatch({
type: 'hostIsolationExceptionsPageDataChanged',
payload: {
type: 'LoadingResourceState',
// @ts-expect-error-next-line will be fixed with when AsyncResourceState is refactored (#830)
previousState: getCurrentListPageDataState(store.getState()),
},
});
const entries = await getHostIsolationExceptionItems(query);
dispatch({
type: 'hostIsolationExceptionsPageDataChanged',
payload: createLoadedResourceState(entries),
});
} catch (error) {
dispatch({
type: 'hostIsolationExceptionsPageDataChanged',
payload: createFailedResourceState<FoundExceptionListItemSchema>(error.body ?? error),
});
}
}
function isHostIsolationExceptionsPage(location: Immutable<AppLocation>) {
return (
matchPath(location.pathname ?? '', {
path: MANAGEMENT_ROUTING_HOST_ISOLATION_EXCEPTIONS_PATH,
exact: true,
}) !== null
);
}

View file

@ -0,0 +1,44 @@
/*
* 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 { UserChangedUrl } from '../../../../common/store/routing/action';
import { HostIsolationExceptionsPageState } from '../types';
import { initialHostIsolationExceptionsPageState } from './builders';
import { HOST_ISOLATION_EXCEPTIONS_PATH } from '../../../../../common/constants';
import { hostIsolationExceptionsPageReducer } from './reducer';
import { getCurrentLocation } from './selector';
describe('Host Isolation Exceptions Reducer', () => {
let initialState: HostIsolationExceptionsPageState;
beforeEach(() => {
initialState = initialHostIsolationExceptionsPageState();
});
describe('UserChangedUrl', () => {
const userChangedUrlAction = (
search = '',
pathname = HOST_ISOLATION_EXCEPTIONS_PATH
): UserChangedUrl => ({
type: 'userChangedUrl',
payload: { search, pathname, hash: '' },
});
describe('When the url is set to host isolation exceptions', () => {
it('should set the default page size and index', () => {
const result = hostIsolationExceptionsPageReducer(initialState, userChangedUrlAction());
expect(getCurrentLocation(result)).toEqual({
filter: '',
id: undefined,
page_index: 0,
page_size: 10,
show: undefined,
});
});
});
});
});

View file

@ -0,0 +1,63 @@
/*
* 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.
*/
// eslint-disable-next-line import/no-nodejs-modules
import { parse } from 'querystring';
import { matchPath } from 'react-router-dom';
import { ImmutableReducer } from '../../../../common/store';
import { AppAction } from '../../../../common/store/actions';
import { AppLocation, Immutable } from '../../../../../common/endpoint/types';
import { extractHostIsolationExceptionsPageLocation } from '../../../common/routing';
import { HostIsolationExceptionsPageState } from '../types';
import { initialHostIsolationExceptionsPageState } from './builders';
import { MANAGEMENT_ROUTING_HOST_ISOLATION_EXCEPTIONS_PATH } from '../../../common/constants';
import { UserChangedUrl } from '../../../../common/store/routing/action';
type StateReducer = ImmutableReducer<HostIsolationExceptionsPageState, AppAction>;
type CaseReducer<T extends AppAction> = (
state: Immutable<HostIsolationExceptionsPageState>,
action: Immutable<T>
) => Immutable<HostIsolationExceptionsPageState>;
const isHostIsolationExceptionsPageLocation = (location: Immutable<AppLocation>) => {
return (
matchPath(location.pathname ?? '', {
path: MANAGEMENT_ROUTING_HOST_ISOLATION_EXCEPTIONS_PATH,
exact: true,
}) !== null
);
};
export const hostIsolationExceptionsPageReducer: StateReducer = (
state = initialHostIsolationExceptionsPageState(),
action
) => {
switch (action.type) {
case 'hostIsolationExceptionsPageDataChanged': {
return {
...state,
entries: action.payload,
};
}
case 'userChangedUrl':
return userChangedUrl(state, action);
}
return state;
};
const userChangedUrl: CaseReducer<UserChangedUrl> = (state, action) => {
if (isHostIsolationExceptionsPageLocation(action.payload)) {
const location = extractHostIsolationExceptionsPageLocation(
parse(action.payload.search.slice(1))
);
return {
...state,
location,
};
}
return state;
};

View file

@ -0,0 +1,75 @@
/*
* 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 { Pagination } from '@elastic/eui';
import {
ExceptionListItemSchema,
FoundExceptionListItemSchema,
} from '@kbn/securitysolution-io-ts-list-types';
import { createSelector } from 'reselect';
import { Immutable } from '../../../../../common/endpoint/types';
import { ServerApiError } from '../../../../common/types';
import {
MANAGEMENT_DEFAULT_PAGE_SIZE,
MANAGEMENT_PAGE_SIZE_OPTIONS,
} from '../../../common/constants';
import {
getLastLoadedResourceState,
isFailedResourceState,
isLoadingResourceState,
} from '../../../state/async_resource_state';
import { HostIsolationExceptionsPageState } from '../types';
type StoreState = Immutable<HostIsolationExceptionsPageState>;
type HostIsolationExceptionsSelector<T> = (state: StoreState) => T;
export const getCurrentListPageState: HostIsolationExceptionsSelector<StoreState> = (state) => {
return state;
};
export const getCurrentListPageDataState: HostIsolationExceptionsSelector<StoreState['entries']> = (
state
) => state.entries;
const getListApiSuccessResponse: HostIsolationExceptionsSelector<
Immutable<FoundExceptionListItemSchema> | undefined
> = createSelector(getCurrentListPageDataState, (listPageData) => {
return getLastLoadedResourceState(listPageData)?.data;
});
export const getListItems: HostIsolationExceptionsSelector<Immutable<ExceptionListItemSchema[]>> =
createSelector(getListApiSuccessResponse, (apiResponseData) => {
return apiResponseData?.data || [];
});
export const getListPagination: HostIsolationExceptionsSelector<Pagination> = createSelector(
getListApiSuccessResponse,
// memoized via `reselect` until the API response changes
(response) => {
return {
totalItemCount: response?.total ?? 0,
pageSize: response?.per_page ?? MANAGEMENT_DEFAULT_PAGE_SIZE,
pageSizeOptions: [...MANAGEMENT_PAGE_SIZE_OPTIONS],
pageIndex: (response?.page ?? 1) - 1,
};
}
);
export const getListIsLoading: HostIsolationExceptionsSelector<boolean> = createSelector(
getCurrentListPageDataState,
(listDataState) => isLoadingResourceState(listDataState)
);
export const getListFetchError: HostIsolationExceptionsSelector<
Immutable<ServerApiError> | undefined
> = createSelector(getCurrentListPageDataState, (listPageDataState) => {
return (isFailedResourceState(listPageDataState) && listPageDataState.error) || undefined;
});
export const getCurrentLocation: HostIsolationExceptionsSelector<StoreState['location']> = (
state
) => state.location;

View file

@ -0,0 +1,23 @@
/*
* 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 { FoundExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types';
import { AsyncResourceState } from '../../state/async_resource_state';
export interface HostIsolationExceptionsPageLocation {
page_index: number;
page_size: number;
show?: 'create' | 'edit';
/** Used for editing. The ID of the selected event filter */
id?: string;
filter: string;
}
export interface HostIsolationExceptionsPageState {
entries: AsyncResourceState<FoundExceptionListItemSchema>;
location: HostIsolationExceptionsPageLocation;
}

View file

@ -0,0 +1,42 @@
/*
* 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, { memo } from 'react';
import styled, { css } from 'styled-components';
import { EuiEmptyPrompt } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
const EmptyPrompt = styled(EuiEmptyPrompt)`
${() => css`
max-width: 100%;
`}
`;
export const HostIsolationExceptionsEmptyState = memo<{}>(() => {
return (
<EmptyPrompt
data-test-subj="hostIsolationExceptionsEmpty"
iconType="plusInCircle"
title={
<h2>
<FormattedMessage
id="xpack.securitySolution.hostIsolationExceptions.listEmpty.title"
defaultMessage="Add your first Host Isolation Exception"
/>
</h2>
}
body={
<FormattedMessage
id="xpack.securitySolution.hostIsolationExceptions.listEmpty.message"
defaultMessage="There are currently no host isolation exceptions"
/>
}
/>
);
});
HostIsolationExceptionsEmptyState.displayName = 'HostIsolationExceptionsEmptyState';

View file

@ -0,0 +1,38 @@
/*
* 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 { useCallback } from 'react';
import { useSelector } from 'react-redux';
import { useHistory } from 'react-router-dom';
import { State } from '../../../../common/store';
import {
MANAGEMENT_STORE_GLOBAL_NAMESPACE,
MANAGEMENT_STORE_HOST_ISOLATION_EXCEPTIONS_NAMESPACE,
} from '../../../common/constants';
import { getHostIsolationExceptionsListPath } from '../../../common/routing';
import { getCurrentLocation } from '../store/selector';
import { HostIsolationExceptionsPageLocation, HostIsolationExceptionsPageState } from '../types';
export function useHostIsolationExceptionsSelector<R>(
selector: (state: HostIsolationExceptionsPageState) => R
): R {
return useSelector((state: State) =>
selector(
state[MANAGEMENT_STORE_GLOBAL_NAMESPACE][MANAGEMENT_STORE_HOST_ISOLATION_EXCEPTIONS_NAMESPACE]
)
);
}
export function useHostIsolationExceptionsNavigateCallback() {
const location = useHostIsolationExceptionsSelector(getCurrentLocation);
const history = useHistory();
return useCallback(
(args: Partial<HostIsolationExceptionsPageLocation>) =>
history.push(getHostIsolationExceptionsListPath({ ...location, ...args })),
[history, location]
);
}

View file

@ -0,0 +1,107 @@
/*
* 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 { act } from '@testing-library/react';
import { AppContextTestRender, createAppRootMockRenderer } from '../../../../common/mock/endpoint';
import { HOST_ISOLATION_EXCEPTIONS_PATH } from '../../../../../common/constants';
import { HostIsolationExceptionsList } from './host_isolation_exceptions_list';
import { isFailedResourceState, isLoadedResourceState } from '../../../state';
import { getHostIsolationExceptionItems } from '../service';
import { getFoundExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/found_exception_list_item_schema.mock';
jest.mock('../service');
const getHostIsolationExceptionItemsMock = getHostIsolationExceptionItems as jest.Mock;
describe('When on the host isolation exceptions page', () => {
let render: () => ReturnType<AppContextTestRender['render']>;
let renderResult: ReturnType<typeof render>;
let history: AppContextTestRender['history'];
let waitForAction: AppContextTestRender['middlewareSpy']['waitForAction'];
beforeEach(() => {
getHostIsolationExceptionItemsMock.mockReset();
const mockedContext = createAppRootMockRenderer();
({ history } = mockedContext);
render = () => (renderResult = mockedContext.render(<HostIsolationExceptionsList />));
waitForAction = mockedContext.middlewareSpy.waitForAction;
act(() => {
history.push(HOST_ISOLATION_EXCEPTIONS_PATH);
});
});
describe('When on the host isolation list page', () => {
const dataReceived = () =>
act(async () => {
await waitForAction('hostIsolationExceptionsPageDataChanged', {
validate(action) {
return isLoadedResourceState(action.payload);
},
});
});
describe('And no data exists', () => {
beforeEach(async () => {
getHostIsolationExceptionItemsMock.mockReturnValue({
data: [],
page: 1,
per_page: 10,
total: 0,
});
});
it('should show the Empty message', async () => {
render();
await dataReceived();
expect(renderResult.getByTestId('hostIsolationExceptionsEmpty')).toBeTruthy();
});
});
describe('And data exists', () => {
beforeEach(async () => {
getHostIsolationExceptionItemsMock.mockImplementation(getFoundExceptionListItemSchemaMock);
});
it('should show loading indicator while retrieving data', async () => {
let releaseApiResponse: (value?: unknown) => void;
getHostIsolationExceptionItemsMock.mockReturnValue(
new Promise((resolve) => (releaseApiResponse = resolve))
);
render();
expect(renderResult.getByTestId('hostIsolationExceptionsContent-loader')).toBeTruthy();
const wasReceived = dataReceived();
releaseApiResponse!();
await wasReceived;
expect(renderResult.container.querySelector('.euiProgress')).toBeNull();
});
it('should show items on the list', async () => {
render();
await dataReceived();
expect(renderResult.getByTestId('hostIsolationExceptionsCard')).toBeTruthy();
});
it('should show API error if one is encountered', async () => {
getHostIsolationExceptionItemsMock.mockImplementation(() => {
throw new Error('Server is too far away');
});
const errorDispatched = act(async () => {
await waitForAction('hostIsolationExceptionsPageDataChanged', {
validate(action) {
return isFailedResourceState(action.payload);
},
});
});
render();
await errorDispatched;
expect(
renderResult.getByTestId('hostIsolationExceptionsContent-error').textContent
).toEqual(' Server is too far away');
});
});
});
});

View file

@ -0,0 +1,106 @@
/*
* 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 { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types';
import { i18n } from '@kbn/i18n';
import React, { useCallback } from 'react';
import { EuiSpacer } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { ExceptionItem } from '../../../../common/components/exceptions/viewer/exception_item';
import {
getCurrentLocation,
getListFetchError,
getListIsLoading,
getListItems,
getListPagination,
} from '../store/selector';
import {
useHostIsolationExceptionsNavigateCallback,
useHostIsolationExceptionsSelector,
} from './hooks';
import { PaginatedContent, PaginatedContentProps } from '../../../components/paginated_content';
import { Immutable } from '../../../../../common/endpoint/types';
import { AdministrationListPage } from '../../../components/administration_list_page';
import { SearchExceptions } from '../../../components/search_exceptions';
import { ArtifactEntryCard, ArtifactEntryCardProps } from '../../../components/artifact_entry_card';
import { HostIsolationExceptionsEmptyState } from './components/empty';
type HostIsolationExceptionPaginatedContent = PaginatedContentProps<
Immutable<ExceptionListItemSchema>,
typeof ExceptionItem
>;
export const HostIsolationExceptionsList = () => {
const listItems = useHostIsolationExceptionsSelector(getListItems);
const pagination = useHostIsolationExceptionsSelector(getListPagination);
const isLoading = useHostIsolationExceptionsSelector(getListIsLoading);
const fetchError = useHostIsolationExceptionsSelector(getListFetchError);
const location = useHostIsolationExceptionsSelector(getCurrentLocation);
const navigateCallback = useHostIsolationExceptionsNavigateCallback();
const handleOnSearch = useCallback(
(query: string) => {
navigateCallback({ filter: query });
},
[navigateCallback]
);
const handleItemComponentProps = (element: ExceptionListItemSchema): ArtifactEntryCardProps => ({
item: element,
'data-test-subj': `hostIsolationExceptionsCard`,
});
const handlePaginatedContentChange: HostIsolationExceptionPaginatedContent['onChange'] =
useCallback(
({ pageIndex, pageSize }) => {
navigateCallback({
page_index: pageIndex,
page_size: pageSize,
});
},
[navigateCallback]
);
return (
<AdministrationListPage
title={
<FormattedMessage
id="xpack.securitySolution.hostIsolationExceptions.list.pageTitle"
defaultMessage="Host Isolation Exceptions"
/>
}
actions={[]}
>
<SearchExceptions
defaultValue={location.filter}
onSearch={handleOnSearch}
placeholder={i18n.translate(
'xpack.securitySolution.hostIsolationExceptions.search.placeholder',
{
defaultMessage: 'Search on the fields below: name, description, ip',
}
)}
/>
<EuiSpacer size="l" />
<PaginatedContent<ExceptionListItemSchema, typeof ArtifactEntryCard>
items={listItems}
ItemComponent={ArtifactEntryCard}
itemComponentProps={handleItemComponentProps}
onChange={handlePaginatedContentChange}
error={fetchError?.message}
loading={isLoading}
pagination={pagination}
contentClassName="host-isolation-exceptions-container"
data-test-subj="hostIsolationExceptionsContent"
noItemsMessage={<HostIsolationExceptionsEmptyState />}
/>
</AdministrationListPage>
);
};
HostIsolationExceptionsList.displayName = 'HostIsolationExceptionsList';

View file

@ -12,6 +12,7 @@ import { FormattedMessage } from '@kbn/i18n/react';
import {
MANAGEMENT_ROUTING_ENDPOINTS_PATH,
MANAGEMENT_ROUTING_EVENT_FILTERS_PATH,
MANAGEMENT_ROUTING_HOST_ISOLATION_EXCEPTIONS_PATH,
MANAGEMENT_ROUTING_POLICIES_PATH,
MANAGEMENT_ROUTING_TRUSTED_APPS_PATH,
} from '../common/constants';
@ -25,6 +26,7 @@ import { SpyRoute } from '../../common/utils/route/spy_routes';
import { EventFiltersContainer } from './event_filters';
import { getEndpointListPath } from '../common/routing';
import { useUserPrivileges } from '../../common/components/user_privileges';
import { HostIsolationExceptionsContainer } from './host_isolation_exceptions';
const NoPermissions = memo(() => {
return (
@ -79,6 +81,13 @@ const EventFilterTelemetry = () => (
</TrackApplicationView>
);
const HostIsolationExceptionsTelemetry = () => (
<TrackApplicationView viewId={SecurityPageName.hostIsolationExceptions}>
<SpyRoute pageName={SecurityPageName.administration} />
<HostIsolationExceptionsContainer />
</TrackApplicationView>
);
export const ManagementContainer = memo(() => {
const { loading, canAccessEndpointManagement } = useUserPrivileges().endpointPrivileges;
@ -97,6 +106,10 @@ export const ManagementContainer = memo(() => {
<Route path={MANAGEMENT_ROUTING_POLICIES_PATH} component={PolicyTelemetry} />
<Route path={MANAGEMENT_ROUTING_TRUSTED_APPS_PATH} component={TrustedAppTelemetry} />
<Route path={MANAGEMENT_ROUTING_EVENT_FILTERS_PATH} component={EventFilterTelemetry} />
<Route
path={MANAGEMENT_ROUTING_HOST_ISOLATION_EXCEPTIONS_PATH}
component={HostIsolationExceptionsTelemetry}
/>
<Route path={MANAGEMENT_PATH} exact>
<Redirect to={getEndpointListPath({ name: 'endpointList' })} />
</Route>

View file

@ -412,11 +412,8 @@ const fetchEditTrustedAppIfNeeded = async (
dispatch({
type: 'trustedAppCreationEditItemStateChanged',
payload: {
// No easy way to get around this that I can see. `previousState` does not
// seem to allow everything that `editItem` state can hold, so not even sure if using
// type guards would work here
// @ts-ignore
type: 'LoadingResourceState',
// @ts-expect-error-next-line will be fixed with when AsyncResourceState is refactored (#830)
previousState: editItemState(currentState)!,
},
});

View file

@ -16,11 +16,13 @@ import {
MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE,
MANAGEMENT_STORE_TRUSTED_APPS_NAMESPACE,
MANAGEMENT_STORE_EVENT_FILTERS_NAMESPACE,
MANAGEMENT_STORE_HOST_ISOLATION_EXCEPTIONS_NAMESPACE,
} from '../common/constants';
import { policyDetailsMiddlewareFactory } from '../pages/policy/store/policy_details';
import { endpointMiddlewareFactory } from '../pages/endpoint_hosts/store/middleware';
import { trustedAppsPageMiddlewareFactory } from '../pages/trusted_apps/store/middleware';
import { eventFiltersPageMiddlewareFactory } from '../pages/event_filters/store/middleware';
import { hostIsolationExceptionsMiddlewareFactory } from '../pages/host_isolation_exceptions/store/middleware';
type ManagementSubStateKey = keyof State[typeof MANAGEMENT_STORE_GLOBAL_NAMESPACE];
@ -50,5 +52,9 @@ export const managementMiddlewareFactory: SecuritySubPluginMiddlewareFactory = (
createSubStateSelector(MANAGEMENT_STORE_EVENT_FILTERS_NAMESPACE),
eventFiltersPageMiddlewareFactory(coreStart, depsStart)
),
substateMiddlewareFactory(
createSubStateSelector(MANAGEMENT_STORE_HOST_ISOLATION_EXCEPTIONS_NAMESPACE),
hostIsolationExceptionsMiddlewareFactory(coreStart)
),
];
};

View file

@ -15,6 +15,7 @@ import {
MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE,
MANAGEMENT_STORE_TRUSTED_APPS_NAMESPACE,
MANAGEMENT_STORE_EVENT_FILTERS_NAMESPACE,
MANAGEMENT_STORE_HOST_ISOLATION_EXCEPTIONS_NAMESPACE,
} from '../common/constants';
import { ImmutableCombineReducers } from '../../common/store';
import { Immutable } from '../../../common/endpoint/types';
@ -25,6 +26,8 @@ import { trustedAppsPageReducer } from '../pages/trusted_apps/store/reducer';
import { initialEventFiltersPageState } from '../pages/event_filters/store/builders';
import { eventFiltersPageReducer } from '../pages/event_filters/store/reducer';
import { initialEndpointPageState } from '../pages/endpoint_hosts/store/builders';
import { initialHostIsolationExceptionsPageState } from '../pages/host_isolation_exceptions/store/builders';
import { hostIsolationExceptionsPageReducer } from '../pages/host_isolation_exceptions/store/reducer';
const immutableCombineReducers: ImmutableCombineReducers = combineReducers;
@ -36,6 +39,7 @@ export const mockManagementState: Immutable<ManagementState> = {
[MANAGEMENT_STORE_ENDPOINTS_NAMESPACE]: initialEndpointPageState(),
[MANAGEMENT_STORE_TRUSTED_APPS_NAMESPACE]: initialTrustedAppsPageState(),
[MANAGEMENT_STORE_EVENT_FILTERS_NAMESPACE]: initialEventFiltersPageState(),
[MANAGEMENT_STORE_HOST_ISOLATION_EXCEPTIONS_NAMESPACE]: initialHostIsolationExceptionsPageState(),
};
/**
@ -46,4 +50,5 @@ export const managementReducer = immutableCombineReducers({
[MANAGEMENT_STORE_ENDPOINTS_NAMESPACE]: endpointListReducer,
[MANAGEMENT_STORE_TRUSTED_APPS_NAMESPACE]: trustedAppsPageReducer,
[MANAGEMENT_STORE_EVENT_FILTERS_NAMESPACE]: eventFiltersPageReducer,
[MANAGEMENT_STORE_HOST_ISOLATION_EXCEPTIONS_NAMESPACE]: hostIsolationExceptionsPageReducer,
});

View file

@ -11,6 +11,7 @@ import { PolicyDetailsState } from './pages/policy/types';
import { EndpointState } from './pages/endpoint_hosts/types';
import { TrustedAppsListPageState } from './pages/trusted_apps/state';
import { EventFiltersListPageState } from './pages/event_filters/types';
import { HostIsolationExceptionsPageState } from './pages/host_isolation_exceptions/types';
/**
* The type for the management store global namespace. Used mostly internally to reference
@ -23,6 +24,7 @@ export type ManagementState = CombinedState<{
endpoints: EndpointState;
trustedApps: TrustedAppsListPageState;
eventFilters: EventFiltersListPageState;
hostIsolationExceptions: HostIsolationExceptionsPageState;
}>;
/**
@ -33,6 +35,7 @@ export enum AdministrationSubTab {
policies = 'policy',
trustedApps = 'trusted_apps',
eventFilters = 'event_filters',
hostIsolationExceptions = 'host_isolation_exceptions',
}
/**