mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Security Solution] Add host isolation exceptions UI (#111253)
This commit is contained in:
parent
3bd687e6ca
commit
a6670380aa
35 changed files with 1030 additions and 24 deletions
|
@ -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',
|
||||
}
|
||||
|
|
|
@ -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({});
|
||||
});
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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 = [
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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',
|
||||
});
|
||||
|
|
|
@ -47,6 +47,7 @@ export type SecurityNavKey =
|
|||
| SecurityPageName.endpoints
|
||||
| SecurityPageName.eventFilters
|
||||
| SecurityPageName.exceptions
|
||||
| SecurityPageName.hostIsolationExceptions
|
||||
| SecurityPageName.hosts
|
||||
| SecurityPageName.network
|
||||
| SecurityPageName.overview
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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[] {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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))
|
||||
)}`;
|
||||
};
|
||||
|
|
|
@ -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),
|
||||
});
|
||||
|
||||
|
|
|
@ -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,
|
||||
};
|
|
@ -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';
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
|
@ -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: '',
|
||||
},
|
||||
});
|
|
@ -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',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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
|
||||
);
|
||||
}
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
};
|
|
@ -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;
|
|
@ -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;
|
||||
}
|
|
@ -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';
|
|
@ -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]
|
||||
);
|
||||
}
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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';
|
|
@ -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>
|
||||
|
|
|
@ -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)!,
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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)
|
||||
),
|
||||
];
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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',
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue