[Security Solution] Add host isolation exceptions UI (#111253)
This commit is contained in:
parent
3bd687e6ca
commit
a6670380aa
|
@ -12,6 +12,7 @@ export const exceptionListType = t.keyof({
|
||||||
detection: null,
|
detection: null,
|
||||||
endpoint: null,
|
endpoint: null,
|
||||||
endpoint_events: null,
|
endpoint_events: null,
|
||||||
|
endpoint_host_isolation_exceptions: null,
|
||||||
});
|
});
|
||||||
export const exceptionListTypeOrUndefined = t.union([exceptionListType, t.undefined]);
|
export const exceptionListTypeOrUndefined = t.union([exceptionListType, t.undefined]);
|
||||||
export type ExceptionListType = t.TypeOf<typeof exceptionListType>;
|
export type ExceptionListType = t.TypeOf<typeof exceptionListType>;
|
||||||
|
@ -20,4 +21,5 @@ export enum ExceptionListTypeEnum {
|
||||||
DETECTION = 'detection',
|
DETECTION = 'detection',
|
||||||
ENDPOINT = 'endpoint',
|
ENDPOINT = 'endpoint',
|
||||||
ENDPOINT_EVENTS = 'endpoint_events',
|
ENDPOINT_EVENTS = 'endpoint_events',
|
||||||
|
ENDPOINT_HOST_ISOLATION_EXCEPTIONS = 'endpoint_host_isolation_exceptions',
|
||||||
}
|
}
|
||||||
|
|
|
@ -86,7 +86,7 @@ describe('Lists', () => {
|
||||||
const message = pipe(decoded, foldLeftRight);
|
const message = pipe(decoded, foldLeftRight);
|
||||||
|
|
||||||
expect(getPaths(left(message.errors))).toEqual([
|
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({});
|
expect(message.schema).toEqual({});
|
||||||
});
|
});
|
||||||
|
@ -117,8 +117,8 @@ describe('Lists', () => {
|
||||||
const message = pipe(decoded, foldLeftRight);
|
const message = pipe(decoded, foldLeftRight);
|
||||||
|
|
||||||
expect(getPaths(left(message.errors))).toEqual([
|
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" | "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", 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({});
|
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 */
|
/** Description of event filters agnostic list */
|
||||||
export const ENDPOINT_EVENT_FILTERS_LIST_DESCRIPTION = 'Endpoint Security Event Filters 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',
|
detections = 'detections',
|
||||||
endpoints = 'endpoints',
|
endpoints = 'endpoints',
|
||||||
eventFilters = 'event_filters',
|
eventFilters = 'event_filters',
|
||||||
|
hostIsolationExceptions = 'host_isolation_exceptions',
|
||||||
events = 'events',
|
events = 'events',
|
||||||
exceptions = 'exceptions',
|
exceptions = 'exceptions',
|
||||||
explore = 'explore',
|
explore = 'explore',
|
||||||
|
@ -113,6 +114,7 @@ export const MANAGEMENT_PATH = '/administration';
|
||||||
export const ENDPOINTS_PATH = `${MANAGEMENT_PATH}/endpoints`;
|
export const ENDPOINTS_PATH = `${MANAGEMENT_PATH}/endpoints`;
|
||||||
export const TRUSTED_APPS_PATH = `${MANAGEMENT_PATH}/trusted_apps`;
|
export const TRUSTED_APPS_PATH = `${MANAGEMENT_PATH}/trusted_apps`;
|
||||||
export const EVENT_FILTERS_PATH = `${MANAGEMENT_PATH}/event_filters`;
|
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_OVERVIEW_PATH = `${APP_PATH}${OVERVIEW_PATH}`;
|
||||||
export const APP_MANAGEMENT_PATH = `${APP_PATH}${MANAGEMENT_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_ENDPOINTS_PATH = `${APP_PATH}${ENDPOINTS_PATH}`;
|
||||||
export const APP_TRUSTED_APPS_PATH = `${APP_PATH}${TRUSTED_APPS_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_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 */
|
/** The comma-delimited list of Elasticsearch indices from which the SIEM app collects events */
|
||||||
export const DEFAULT_INDEX_PATTERN = [
|
export const DEFAULT_INDEX_PATTERN = [
|
||||||
|
|
|
@ -30,6 +30,10 @@ import {
|
||||||
CASE,
|
CASE,
|
||||||
MANAGE,
|
MANAGE,
|
||||||
UEBA,
|
UEBA,
|
||||||
|
HOST_ISOLATION_EXCEPTIONS,
|
||||||
|
EVENT_FILTERS,
|
||||||
|
TRUSTED_APPLICATIONS,
|
||||||
|
ENDPOINTS,
|
||||||
} from '../translations';
|
} from '../translations';
|
||||||
import {
|
import {
|
||||||
OVERVIEW_PATH,
|
OVERVIEW_PATH,
|
||||||
|
@ -44,6 +48,7 @@ import {
|
||||||
TRUSTED_APPS_PATH,
|
TRUSTED_APPS_PATH,
|
||||||
EVENT_FILTERS_PATH,
|
EVENT_FILTERS_PATH,
|
||||||
UEBA_PATH,
|
UEBA_PATH,
|
||||||
|
HOST_ISOLATION_EXCEPTIONS_PATH,
|
||||||
} from '../../../common/constants';
|
} from '../../../common/constants';
|
||||||
import { ExperimentalFeatures } from '../../../common/experimental_features';
|
import { ExperimentalFeatures } from '../../../common/experimental_features';
|
||||||
|
|
||||||
|
@ -313,26 +318,25 @@ export const securitySolutionsDeepLinks: AppDeepLink[] = [
|
||||||
{
|
{
|
||||||
id: SecurityPageName.endpoints,
|
id: SecurityPageName.endpoints,
|
||||||
navLinkStatus: AppNavLinkStatus.visible,
|
navLinkStatus: AppNavLinkStatus.visible,
|
||||||
title: i18n.translate('xpack.securitySolution.search.administration.endpoints', {
|
title: ENDPOINTS,
|
||||||
defaultMessage: 'Endpoints',
|
|
||||||
}),
|
|
||||||
order: 9006,
|
order: 9006,
|
||||||
path: ENDPOINTS_PATH,
|
path: ENDPOINTS_PATH,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: SecurityPageName.trustedApps,
|
id: SecurityPageName.trustedApps,
|
||||||
title: i18n.translate('xpack.securitySolution.search.administration.trustedApps', {
|
title: TRUSTED_APPLICATIONS,
|
||||||
defaultMessage: 'Trusted applications',
|
|
||||||
}),
|
|
||||||
path: TRUSTED_APPS_PATH,
|
path: TRUSTED_APPS_PATH,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: SecurityPageName.eventFilters,
|
id: SecurityPageName.eventFilters,
|
||||||
title: i18n.translate('xpack.securitySolution.search.administration.eventFilters', {
|
title: EVENT_FILTERS,
|
||||||
defaultMessage: 'Event filters',
|
|
||||||
}),
|
|
||||||
path: EVENT_FILTERS_PATH,
|
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_EVENT_FILTERS_PATH,
|
||||||
APP_UEBA_PATH,
|
APP_UEBA_PATH,
|
||||||
SecurityPageName,
|
SecurityPageName,
|
||||||
|
APP_HOST_ISOLATION_EXCEPTIONS_PATH,
|
||||||
} from '../../../common/constants';
|
} from '../../../common/constants';
|
||||||
|
|
||||||
export const navTabs: SecurityNav = {
|
export const navTabs: SecurityNav = {
|
||||||
|
@ -120,6 +121,13 @@ export const navTabs: SecurityNav = {
|
||||||
disabled: false,
|
disabled: false,
|
||||||
urlKey: 'administration',
|
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 = {
|
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', {
|
export const DETECT = i18n.translate('xpack.securitySolution.navigation.detect', {
|
||||||
defaultMessage: 'Detect',
|
defaultMessage: 'Detect',
|
||||||
});
|
});
|
||||||
|
|
|
@ -47,6 +47,7 @@ export type SecurityNavKey =
|
||||||
| SecurityPageName.endpoints
|
| SecurityPageName.endpoints
|
||||||
| SecurityPageName.eventFilters
|
| SecurityPageName.eventFilters
|
||||||
| SecurityPageName.exceptions
|
| SecurityPageName.exceptions
|
||||||
|
| SecurityPageName.hostIsolationExceptions
|
||||||
| SecurityPageName.hosts
|
| SecurityPageName.hosts
|
||||||
| SecurityPageName.network
|
| SecurityPageName.network
|
||||||
| SecurityPageName.overview
|
| SecurityPageName.overview
|
||||||
|
|
|
@ -233,6 +233,16 @@ describe('useSecuritySolutionNavigation', () => {
|
||||||
"name": "Event filters",
|
"name": "Event filters",
|
||||||
"onClick": [Function],
|
"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",
|
"name": "Manage",
|
||||||
},
|
},
|
||||||
|
|
|
@ -83,7 +83,12 @@ function usePrimaryNavigationItemsToDisplay(navTabs: Record<string, NavTab>) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
...securityNavGroup.manage,
|
...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]
|
[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 { PolicyDetailsAction } from '../../management/pages/policy/store/policy_details';
|
||||||
import { TrustedAppsPageAction } from '../../management/pages/trusted_apps/store/action';
|
import { TrustedAppsPageAction } from '../../management/pages/trusted_apps/store/action';
|
||||||
import { EventFiltersPageAction } from '../../management/pages/event_filters/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 { appActions } from './app';
|
||||||
export { dragAndDropActions } from './drag_and_drop';
|
export { dragAndDropActions } from './drag_and_drop';
|
||||||
|
@ -21,4 +22,5 @@ export type AppAction =
|
||||||
| RoutingAction
|
| RoutingAction
|
||||||
| PolicyDetailsAction
|
| PolicyDetailsAction
|
||||||
| TrustedAppsPageAction
|
| TrustedAppsPageAction
|
||||||
| EventFiltersPageAction;
|
| EventFiltersPageAction
|
||||||
|
| HostIsolationExceptionsPageAction;
|
||||||
|
|
|
@ -9,12 +9,14 @@ import { ChromeBreadcrumb } from 'kibana/public';
|
||||||
import { AdministrationSubTab } from '../types';
|
import { AdministrationSubTab } from '../types';
|
||||||
import { ENDPOINTS_TAB, EVENT_FILTERS_TAB, POLICIES_TAB, TRUSTED_APPS_TAB } from './translations';
|
import { ENDPOINTS_TAB, EVENT_FILTERS_TAB, POLICIES_TAB, TRUSTED_APPS_TAB } from './translations';
|
||||||
import { AdministrationRouteSpyState } from '../../common/utils/route/types';
|
import { AdministrationRouteSpyState } from '../../common/utils/route/types';
|
||||||
|
import { HOST_ISOLATION_EXCEPTIONS } from '../../app/translations';
|
||||||
|
|
||||||
const TabNameMappedToI18nKey: Record<AdministrationSubTab, string> = {
|
const TabNameMappedToI18nKey: Record<AdministrationSubTab, string> = {
|
||||||
[AdministrationSubTab.endpoints]: ENDPOINTS_TAB,
|
[AdministrationSubTab.endpoints]: ENDPOINTS_TAB,
|
||||||
[AdministrationSubTab.policies]: POLICIES_TAB,
|
[AdministrationSubTab.policies]: POLICIES_TAB,
|
||||||
[AdministrationSubTab.trustedApps]: TRUSTED_APPS_TAB,
|
[AdministrationSubTab.trustedApps]: TRUSTED_APPS_TAB,
|
||||||
[AdministrationSubTab.eventFilters]: EVENT_FILTERS_TAB,
|
[AdministrationSubTab.eventFilters]: EVENT_FILTERS_TAB,
|
||||||
|
[AdministrationSubTab.hostIsolationExceptions]: HOST_ISOLATION_EXCEPTIONS,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getBreadcrumbs(params: AdministrationRouteSpyState): ChromeBreadcrumb[] {
|
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_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_TRUSTED_APPS_PATH = `${MANAGEMENT_PATH}/:tabName(${AdministrationSubTab.trustedApps})`;
|
||||||
export const MANAGEMENT_ROUTING_EVENT_FILTERS_PATH = `${MANAGEMENT_PATH}/:tabName(${AdministrationSubTab.eventFilters})`;
|
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 ]---------------------------------------------------------------------------
|
// --[ STORE ]---------------------------------------------------------------------------
|
||||||
/** The SIEM global store namespace where the management state will be mounted */
|
/** 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';
|
export const MANAGEMENT_STORE_TRUSTED_APPS_NAMESPACE = 'trustedApps';
|
||||||
/** Namespace within the Management state where event filters page state is maintained */
|
/** Namespace within the Management state where event filters page state is maintained */
|
||||||
export const MANAGEMENT_STORE_EVENT_FILTERS_NAMESPACE = 'eventFilters';
|
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_PAGE_SIZE_OPTIONS: readonly number[] = [10, 20, 50];
|
||||||
export const MANAGEMENT_DEFAULT_PAGE = 0;
|
export const MANAGEMENT_DEFAULT_PAGE = 0;
|
||||||
|
|
|
@ -16,6 +16,7 @@ import {
|
||||||
MANAGEMENT_PAGE_SIZE_OPTIONS,
|
MANAGEMENT_PAGE_SIZE_OPTIONS,
|
||||||
MANAGEMENT_ROUTING_ENDPOINTS_PATH,
|
MANAGEMENT_ROUTING_ENDPOINTS_PATH,
|
||||||
MANAGEMENT_ROUTING_EVENT_FILTERS_PATH,
|
MANAGEMENT_ROUTING_EVENT_FILTERS_PATH,
|
||||||
|
MANAGEMENT_ROUTING_HOST_ISOLATION_EXCEPTIONS_PATH,
|
||||||
MANAGEMENT_ROUTING_POLICIES_PATH,
|
MANAGEMENT_ROUTING_POLICIES_PATH,
|
||||||
MANAGEMENT_ROUTING_POLICY_DETAILS_FORM_PATH,
|
MANAGEMENT_ROUTING_POLICY_DETAILS_FORM_PATH,
|
||||||
MANAGEMENT_ROUTING_POLICY_DETAILS_TRUSTED_APPS_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 { EndpointIndexUIQueryParams } from '../pages/endpoint_hosts/types';
|
||||||
import { TrustedAppsListPageLocation } from '../pages/trusted_apps/state';
|
import { TrustedAppsListPageLocation } from '../pages/trusted_apps/state';
|
||||||
import { EventFiltersPageLocation } from '../pages/event_filters/types';
|
import { EventFiltersPageLocation } from '../pages/event_filters/types';
|
||||||
|
import { HostIsolationExceptionsPageLocation } from '../pages/host_isolation_exceptions/types';
|
||||||
import { PolicyDetailsArtifactsPageLocation } from '../pages/policy/types';
|
import { PolicyDetailsArtifactsPageLocation } from '../pages/policy/types';
|
||||||
|
|
||||||
// Taken from: https://github.com/microsoft/TypeScript/issues/12936#issuecomment-559034150
|
// 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)
|
* Given an object with url params, and a given key, return back only the first param value (case multiples were defined)
|
||||||
* @param query
|
* @param query
|
||||||
|
@ -327,3 +349,31 @@ export const getEventFiltersListPath = (location?: Partial<EventFiltersPageLocat
|
||||||
querystring.stringify(normalizeEventFiltersPageLocation(location))
|
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({
|
dispatch({
|
||||||
type: 'eventFiltersListPageDataExistsChanged',
|
type: 'eventFiltersListPageDataExistsChanged',
|
||||||
// Ignore will be fixed with when AsyncResourceState is refactored (#830)
|
// @ts-expect-error-next-line will be fixed with when AsyncResourceState is refactored (#830)
|
||||||
// @ts-ignore
|
|
||||||
payload: createLoadingResourceState(getListPageDataExistsState(getState())),
|
payload: createLoadingResourceState(getListPageDataExistsState(getState())),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -232,9 +231,8 @@ const refreshListDataIfNeeded: MiddlewareActionHandler = async (store, eventFilt
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'eventFiltersListPageDataChanged',
|
type: 'eventFiltersListPageDataChanged',
|
||||||
payload: {
|
payload: {
|
||||||
// Ignore will be fixed with when AsyncResourceState is refactored (#830)
|
|
||||||
// @ts-ignore
|
|
||||||
type: 'LoadingResourceState',
|
type: 'LoadingResourceState',
|
||||||
|
// @ts-expect-error-next-line will be fixed with when AsyncResourceState is refactored (#830)
|
||||||
previousState: getCurrentListPageDataState(state),
|
previousState: getCurrentListPageDataState(state),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -300,8 +298,7 @@ const eventFilterDeleteEntry: MiddlewareActionHandler = async (
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'eventFilterDeleteStatusChanged',
|
type: 'eventFilterDeleteStatusChanged',
|
||||||
// Ignore will be fixed with when AsyncResourceState is refactored (#830)
|
// @ts-expect-error-next-line will be fixed with when AsyncResourceState is refactored (#830)
|
||||||
// @ts-ignore
|
|
||||||
payload: createLoadingResourceState(getDeletionState(state).status),
|
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 {
|
import {
|
||||||
MANAGEMENT_ROUTING_ENDPOINTS_PATH,
|
MANAGEMENT_ROUTING_ENDPOINTS_PATH,
|
||||||
MANAGEMENT_ROUTING_EVENT_FILTERS_PATH,
|
MANAGEMENT_ROUTING_EVENT_FILTERS_PATH,
|
||||||
|
MANAGEMENT_ROUTING_HOST_ISOLATION_EXCEPTIONS_PATH,
|
||||||
MANAGEMENT_ROUTING_POLICIES_PATH,
|
MANAGEMENT_ROUTING_POLICIES_PATH,
|
||||||
MANAGEMENT_ROUTING_TRUSTED_APPS_PATH,
|
MANAGEMENT_ROUTING_TRUSTED_APPS_PATH,
|
||||||
} from '../common/constants';
|
} from '../common/constants';
|
||||||
|
@ -25,6 +26,7 @@ import { SpyRoute } from '../../common/utils/route/spy_routes';
|
||||||
import { EventFiltersContainer } from './event_filters';
|
import { EventFiltersContainer } from './event_filters';
|
||||||
import { getEndpointListPath } from '../common/routing';
|
import { getEndpointListPath } from '../common/routing';
|
||||||
import { useUserPrivileges } from '../../common/components/user_privileges';
|
import { useUserPrivileges } from '../../common/components/user_privileges';
|
||||||
|
import { HostIsolationExceptionsContainer } from './host_isolation_exceptions';
|
||||||
|
|
||||||
const NoPermissions = memo(() => {
|
const NoPermissions = memo(() => {
|
||||||
return (
|
return (
|
||||||
|
@ -79,6 +81,13 @@ const EventFilterTelemetry = () => (
|
||||||
</TrackApplicationView>
|
</TrackApplicationView>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const HostIsolationExceptionsTelemetry = () => (
|
||||||
|
<TrackApplicationView viewId={SecurityPageName.hostIsolationExceptions}>
|
||||||
|
<SpyRoute pageName={SecurityPageName.administration} />
|
||||||
|
<HostIsolationExceptionsContainer />
|
||||||
|
</TrackApplicationView>
|
||||||
|
);
|
||||||
|
|
||||||
export const ManagementContainer = memo(() => {
|
export const ManagementContainer = memo(() => {
|
||||||
const { loading, canAccessEndpointManagement } = useUserPrivileges().endpointPrivileges;
|
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_POLICIES_PATH} component={PolicyTelemetry} />
|
||||||
<Route path={MANAGEMENT_ROUTING_TRUSTED_APPS_PATH} component={TrustedAppTelemetry} />
|
<Route path={MANAGEMENT_ROUTING_TRUSTED_APPS_PATH} component={TrustedAppTelemetry} />
|
||||||
<Route path={MANAGEMENT_ROUTING_EVENT_FILTERS_PATH} component={EventFilterTelemetry} />
|
<Route path={MANAGEMENT_ROUTING_EVENT_FILTERS_PATH} component={EventFilterTelemetry} />
|
||||||
|
<Route
|
||||||
|
path={MANAGEMENT_ROUTING_HOST_ISOLATION_EXCEPTIONS_PATH}
|
||||||
|
component={HostIsolationExceptionsTelemetry}
|
||||||
|
/>
|
||||||
<Route path={MANAGEMENT_PATH} exact>
|
<Route path={MANAGEMENT_PATH} exact>
|
||||||
<Redirect to={getEndpointListPath({ name: 'endpointList' })} />
|
<Redirect to={getEndpointListPath({ name: 'endpointList' })} />
|
||||||
</Route>
|
</Route>
|
||||||
|
|
|
@ -412,11 +412,8 @@ const fetchEditTrustedAppIfNeeded = async (
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'trustedAppCreationEditItemStateChanged',
|
type: 'trustedAppCreationEditItemStateChanged',
|
||||||
payload: {
|
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',
|
type: 'LoadingResourceState',
|
||||||
|
// @ts-expect-error-next-line will be fixed with when AsyncResourceState is refactored (#830)
|
||||||
previousState: editItemState(currentState)!,
|
previousState: editItemState(currentState)!,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -16,11 +16,13 @@ import {
|
||||||
MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE,
|
MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE,
|
||||||
MANAGEMENT_STORE_TRUSTED_APPS_NAMESPACE,
|
MANAGEMENT_STORE_TRUSTED_APPS_NAMESPACE,
|
||||||
MANAGEMENT_STORE_EVENT_FILTERS_NAMESPACE,
|
MANAGEMENT_STORE_EVENT_FILTERS_NAMESPACE,
|
||||||
|
MANAGEMENT_STORE_HOST_ISOLATION_EXCEPTIONS_NAMESPACE,
|
||||||
} from '../common/constants';
|
} from '../common/constants';
|
||||||
import { policyDetailsMiddlewareFactory } from '../pages/policy/store/policy_details';
|
import { policyDetailsMiddlewareFactory } from '../pages/policy/store/policy_details';
|
||||||
import { endpointMiddlewareFactory } from '../pages/endpoint_hosts/store/middleware';
|
import { endpointMiddlewareFactory } from '../pages/endpoint_hosts/store/middleware';
|
||||||
import { trustedAppsPageMiddlewareFactory } from '../pages/trusted_apps/store/middleware';
|
import { trustedAppsPageMiddlewareFactory } from '../pages/trusted_apps/store/middleware';
|
||||||
import { eventFiltersPageMiddlewareFactory } from '../pages/event_filters/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];
|
type ManagementSubStateKey = keyof State[typeof MANAGEMENT_STORE_GLOBAL_NAMESPACE];
|
||||||
|
|
||||||
|
@ -50,5 +52,9 @@ export const managementMiddlewareFactory: SecuritySubPluginMiddlewareFactory = (
|
||||||
createSubStateSelector(MANAGEMENT_STORE_EVENT_FILTERS_NAMESPACE),
|
createSubStateSelector(MANAGEMENT_STORE_EVENT_FILTERS_NAMESPACE),
|
||||||
eventFiltersPageMiddlewareFactory(coreStart, depsStart)
|
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_POLICY_DETAILS_NAMESPACE,
|
||||||
MANAGEMENT_STORE_TRUSTED_APPS_NAMESPACE,
|
MANAGEMENT_STORE_TRUSTED_APPS_NAMESPACE,
|
||||||
MANAGEMENT_STORE_EVENT_FILTERS_NAMESPACE,
|
MANAGEMENT_STORE_EVENT_FILTERS_NAMESPACE,
|
||||||
|
MANAGEMENT_STORE_HOST_ISOLATION_EXCEPTIONS_NAMESPACE,
|
||||||
} from '../common/constants';
|
} from '../common/constants';
|
||||||
import { ImmutableCombineReducers } from '../../common/store';
|
import { ImmutableCombineReducers } from '../../common/store';
|
||||||
import { Immutable } from '../../../common/endpoint/types';
|
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 { initialEventFiltersPageState } from '../pages/event_filters/store/builders';
|
||||||
import { eventFiltersPageReducer } from '../pages/event_filters/store/reducer';
|
import { eventFiltersPageReducer } from '../pages/event_filters/store/reducer';
|
||||||
import { initialEndpointPageState } from '../pages/endpoint_hosts/store/builders';
|
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;
|
const immutableCombineReducers: ImmutableCombineReducers = combineReducers;
|
||||||
|
|
||||||
|
@ -36,6 +39,7 @@ export const mockManagementState: Immutable<ManagementState> = {
|
||||||
[MANAGEMENT_STORE_ENDPOINTS_NAMESPACE]: initialEndpointPageState(),
|
[MANAGEMENT_STORE_ENDPOINTS_NAMESPACE]: initialEndpointPageState(),
|
||||||
[MANAGEMENT_STORE_TRUSTED_APPS_NAMESPACE]: initialTrustedAppsPageState(),
|
[MANAGEMENT_STORE_TRUSTED_APPS_NAMESPACE]: initialTrustedAppsPageState(),
|
||||||
[MANAGEMENT_STORE_EVENT_FILTERS_NAMESPACE]: initialEventFiltersPageState(),
|
[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_ENDPOINTS_NAMESPACE]: endpointListReducer,
|
||||||
[MANAGEMENT_STORE_TRUSTED_APPS_NAMESPACE]: trustedAppsPageReducer,
|
[MANAGEMENT_STORE_TRUSTED_APPS_NAMESPACE]: trustedAppsPageReducer,
|
||||||
[MANAGEMENT_STORE_EVENT_FILTERS_NAMESPACE]: eventFiltersPageReducer,
|
[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 { EndpointState } from './pages/endpoint_hosts/types';
|
||||||
import { TrustedAppsListPageState } from './pages/trusted_apps/state';
|
import { TrustedAppsListPageState } from './pages/trusted_apps/state';
|
||||||
import { EventFiltersListPageState } from './pages/event_filters/types';
|
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
|
* The type for the management store global namespace. Used mostly internally to reference
|
||||||
|
@ -23,6 +24,7 @@ export type ManagementState = CombinedState<{
|
||||||
endpoints: EndpointState;
|
endpoints: EndpointState;
|
||||||
trustedApps: TrustedAppsListPageState;
|
trustedApps: TrustedAppsListPageState;
|
||||||
eventFilters: EventFiltersListPageState;
|
eventFilters: EventFiltersListPageState;
|
||||||
|
hostIsolationExceptions: HostIsolationExceptionsPageState;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -33,6 +35,7 @@ export enum AdministrationSubTab {
|
||||||
policies = 'policy',
|
policies = 'policy',
|
||||||
trustedApps = 'trusted_apps',
|
trustedApps = 'trusted_apps',
|
||||||
eventFilters = 'event_filters',
|
eventFilters = 'event_filters',
|
||||||
|
hostIsolationExceptions = 'host_isolation_exceptions',
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Loading…
Reference in a new issue