[Security Solution][Endpoint] Add RBAC support to the endpoint artifact cards displayed in Fleet pages (#144747)

## Summary

- Endpoint artifact cards, displayed in fleet under an Endpoint
integration policy edit form, or under the Advanced settings tab when
looking at the details of the Endpoint Integration details page:
- will now only display the associated artifact card based on RBAC
controls set via Kibana feature privileges
- If no cards are displayed (due to missing authz), then a Permission
empty state will be displayed
- When the cards are displayed under the Fleet Edit Endpoint integration
form, if the user does not have Endpoint policy management Authz, the
link displayed on the Artifact Card will take the user to the Artifact
list page filtered by the policy the user was viewing in fleet. If the
user has authz to manage endpoint policies, then the link will continue
to be the same as is pre-RBAC (takes user to the Endpoint Policy
details, focused on the respective Artifact tab.
This commit is contained in:
Paul Tavares 2022-11-23 17:31:14 -05:00 committed by GitHub
parent 25c65358a0
commit 14cd9ce0ba
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 748 additions and 561 deletions

View file

@ -14,7 +14,6 @@ import { appendSearch } from '../../common/components/link_to/helpers';
import type { ArtifactListPageUrlParams } from '../components/artifact_list_page';
import { paginationFromUrlParams } from '../hooks/use_url_pagination';
import type { EndpointIndexUIQueryParams } from '../pages/endpoint_hosts/types';
import type { EventFiltersPageLocation } from '../pages/event_filters/types';
import type { PolicyDetailsArtifactsPageLocation } from '../pages/policy/types';
import { AdministrationSubTab } from '../types';
import {
@ -168,29 +167,6 @@ const normalizePolicyDetailsArtifactsListPageLocation = (
}
};
const normalizeEventFiltersPageLocation = (
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 } : ''),
...(!isDefaultOrMissing(location.included_policies, '')
? { included_policies: location.included_policies }
: ''),
};
} 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
@ -265,14 +241,12 @@ export const getPolicyDetailsArtifactsListPath = (
)}`;
};
export const getEventFiltersListPath = (location?: Partial<EventFiltersPageLocation>): string => {
export const getEventFiltersListPath = (location?: Partial<ArtifactListPageUrlParams>): string => {
const path = generatePath(MANAGEMENT_ROUTING_EVENT_FILTERS_PATH, {
tabName: AdministrationSubTab.eventFilters,
});
return `${path}${appendSearch(
querystring.stringify(normalizeEventFiltersPageLocation(location))
)}`;
return getArtifactListPageUrlPath(path, location);
};
export const getHostIsolationExceptionsListPath = (

View file

@ -1,16 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export interface EventFiltersPageLocation {
page_index: number;
page_size: number;
show?: 'create' | 'edit';
/** Used for editing. The ID of the selected event filter */
id?: string;
filter: string;
included_policies: string;
}

View file

@ -16,7 +16,6 @@ import { usePolicyDetailsArtifactsNavigateCallback } from '../../policy_hooks';
import { useGetLinkTo } from './use_policy_artifacts_empty_hooks';
import { useUserPrivileges } from '../../../../../../common/components/user_privileges';
import type { POLICY_ARTIFACT_EMPTY_UNASSIGNED_LABELS } from './translations';
import type { EventFiltersPageLocation } from '../../../../event_filters/types';
import type { ArtifactListPageUrlParams } from '../../../../../components/artifact_list_page';
interface CommonProps {
policyId: string;
@ -24,9 +23,7 @@ interface CommonProps {
listId: string;
labels: typeof POLICY_ARTIFACT_EMPTY_UNASSIGNED_LABELS;
getPolicyArtifactsPath: (policyId: string) => string;
getArtifactPath: (
location?: Partial<EventFiltersPageLocation> | Partial<ArtifactListPageUrlParams>
) => string;
getArtifactPath: (location?: Partial<ArtifactListPageUrlParams>) => string;
}
export const PolicyArtifactsEmptyUnassigned = memo<CommonProps>(

View file

@ -13,7 +13,6 @@ import {
} from '@elastic/eui';
import { useGetLinkTo } from './use_policy_artifacts_empty_hooks';
import type { POLICY_ARTIFACT_EMPTY_UNEXISTING_LABELS } from './translations';
import type { EventFiltersPageLocation } from '../../../../event_filters/types';
import type { ArtifactListPageUrlParams } from '../../../../../components/artifact_list_page';
interface CommonProps {
@ -21,9 +20,7 @@ interface CommonProps {
policyName: string;
labels: typeof POLICY_ARTIFACT_EMPTY_UNEXISTING_LABELS;
getPolicyArtifactsPath: (policyId: string) => string;
getArtifactPath: (
location?: Partial<EventFiltersPageLocation> | Partial<ArtifactListPageUrlParams>
) => string;
getArtifactPath: (location?: Partial<ArtifactListPageUrlParams>) => string;
}
export const PolicyArtifactsEmptyUnexisting = memo<CommonProps>(

View file

@ -10,16 +10,13 @@ import { i18n } from '@kbn/i18n';
import { useNavigateToAppEventHandler } from '../../../../../../common/hooks/endpoint/use_navigate_to_app_event_handler';
import { useAppUrl } from '../../../../../../common/lib/kibana/hooks';
import { APP_UI_ID } from '../../../../../../../common/constants';
import type { EventFiltersPageLocation } from '../../../../event_filters/types';
import type { ArtifactListPageUrlParams } from '../../../../../components/artifact_list_page';
export const useGetLinkTo = (
policyId: string,
policyName: string,
getPolicyArtifactsPath: (policyId: string) => string,
getArtifactPath: (
location?: Partial<EventFiltersPageLocation> | Partial<ArtifactListPageUrlParams>
) => string,
getArtifactPath: (location?: Partial<ArtifactListPageUrlParams>) => string,
location?: Partial<{ show: 'create' }>
) => {
const { getAppUrl } = useAppUrl();

View file

@ -32,7 +32,6 @@ import { PolicyArtifactsFlyout } from '../flyout';
import type { PolicyArtifactsPageLabels } from '../translations';
import { policyArtifactsPageLabels } from '../translations';
import { PolicyArtifactsDeleteModal } from '../delete_modal';
import type { EventFiltersPageLocation } from '../../../../event_filters/types';
import type { ArtifactListPageUrlParams } from '../../../../../components/artifact_list_page';
interface PolicyArtifactsLayoutProps {
@ -41,9 +40,7 @@ interface PolicyArtifactsLayoutProps {
labels: PolicyArtifactsPageLabels;
getExceptionsListApiClient: () => ExceptionsListApiClient;
searchableFields: readonly string[];
getArtifactPath: (
location?: Partial<EventFiltersPageLocation> | Partial<ArtifactListPageUrlParams>
) => string;
getArtifactPath: (location?: Partial<ArtifactListPageUrlParams>) => string;
getPolicyArtifactsPath: (policyId: string) => string;
/** A boolean to check extra privileges for restricted actions, true when it's allowed, false when not */
externalPrivileges?: boolean;

View file

@ -27,16 +27,13 @@ import { useGetLinkTo } from '../empty/use_policy_artifacts_empty_hooks';
import type { ExceptionsListApiClient } from '../../../../../services/exceptions_list/exceptions_list_api_client';
import { useListArtifact } from '../../../../../hooks/artifacts';
import type { POLICY_ARTIFACT_LIST_LABELS } from './translations';
import type { EventFiltersPageLocation } from '../../../../event_filters/types';
import type { ArtifactListPageUrlParams } from '../../../../../components/artifact_list_page';
interface PolicyArtifactsListProps {
policy: ImmutableObject<PolicyData>;
apiClient: ExceptionsListApiClient;
searchableFields: string[];
getArtifactPath: (
location?: Partial<EventFiltersPageLocation> | Partial<ArtifactListPageUrlParams>
) => string;
getArtifactPath: (location?: Partial<ArtifactListPageUrlParams>) => string;
getPolicyArtifactsPath: (policyId: string) => string;
labels: typeof POLICY_ARTIFACT_LIST_LABELS;
onDeleteActionCallback: (item: ExceptionListItemSchema) => void;

View file

@ -10,8 +10,8 @@ import { ThemeProvider } from 'styled-components';
import { I18nProvider } from '@kbn/i18n-react';
import { ExceptionItemsSummary } from './exception_items_summary';
import * as reactTestingLibrary from '@testing-library/react';
import { getMockTheme } from '../../../../../../../common/lib/kibana/kibana_react.mock';
import type { GetExceptionSummaryResponse } from '../../../../../../../../common/endpoint/types';
import { getMockTheme } from '../../../../../../common/lib/kibana/kibana_react.mock';
import type { GetExceptionSummaryResponse } from '../../../../../../../common/endpoint/types';
const mockTheme = getMockTheme({
eui: {

View file

@ -9,9 +9,9 @@ import type { FC } from 'react';
import React, { memo, useCallback } from 'react';
import type { EuiBadgeProps } from '@elastic/eui';
import { EuiBadge, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import styled from 'styled-components';
import type { GetExceptionSummaryResponse } from '../../../../../../../../common/endpoint/types';
import { i18n } from '@kbn/i18n';
import type { GetExceptionSummaryResponse } from '../../../../../../../common/endpoint/types';
const SUMMARY_KEYS: Readonly<Array<keyof GetExceptionSummaryResponse>> = [
'windows',

View file

@ -9,8 +9,8 @@ import styled from 'styled-components';
import type { FC } from 'react';
import React, { memo } from 'react';
import { EuiIcon } from '@elastic/eui';
import type { LinkToAppProps } from '../../../../../../../common/components/endpoint/link_to_app';
import { LinkToApp } from '../../../../../../../common/components/endpoint/link_to_app';
import type { LinkToAppProps } from '../../../../../../common/components/endpoint/link_to_app';
import { LinkToApp } from '../../../../../../common/components/endpoint/link_to_app';
const LinkLabel = styled.span<{
size?: 'm' | 'l';

View file

@ -9,11 +9,11 @@ import type { PropsWithChildren } from 'react';
import React, { memo } from 'react';
import { Provider as ReduxStoreProvider } from 'react-redux';
import type { Store } from 'redux';
import type { SecuritySolutionQueryClient } from '../../../../../../common/containers/query_client/query_client_provider';
import { ReactQueryClientProvider } from '../../../../../../common/containers/query_client/query_client_provider';
import { SecuritySolutionStartDependenciesContext } from '../../../../../../common/components/user_privileges/endpoint/security_solution_start_dependencies';
import { CurrentLicense } from '../../../../../../common/components/current_license';
import type { StartPlugins } from '../../../../../../types';
import type { SecuritySolutionQueryClient } from '../../../../../../../common/containers/query_client/query_client_provider';
import { ReactQueryClientProvider } from '../../../../../../../common/containers/query_client/query_client_provider';
import { SecuritySolutionStartDependenciesContext } from '../../../../../../../common/components/user_privileges/endpoint/security_solution_start_dependencies';
import { CurrentLicense } from '../../../../../../../common/components/current_license';
import type { StartPlugins } from '../../../../../../../types';
export type RenderContextProvidersProps = PropsWithChildren<{
store: Store;

View file

@ -8,14 +8,14 @@
import type { Dispatch, Middleware, PreloadedState, ReducersMapObject } from 'redux';
import { applyMiddleware, combineReducers, compose, createStore } from 'redux';
import type { CoreStart } from '@kbn/core/public';
import { managementReducer } from '../../../../../store/reducer';
import { appReducer } from '../../../../../../common/store/app';
import { ExperimentalFeaturesService } from '../../../../../../common/experimental_features_service';
import { managementMiddlewareFactory } from '../../../../../store/middleware';
import type { StartPlugins } from '../../../../../../types';
import type { State } from '../../../../../../common/store';
import type { AppAction } from '../../../../../../common/store/actions';
import type { Immutable } from '../../../../../../../common/endpoint/types';
import { managementReducer } from '../../../../../../store/reducer';
import { appReducer } from '../../../../../../../common/store/app';
import { ExperimentalFeaturesService } from '../../../../../../../common/experimental_features_service';
import { managementMiddlewareFactory } from '../../../../../../store/middleware';
import type { StartPlugins } from '../../../../../../../types';
import type { State } from '../../../../../../../common/store';
import type { AppAction } from '../../../../../../../common/store/actions';
import type { Immutable } from '../../../../../../../../common/endpoint/types';
type ComposeType = typeof compose;
declare global {

View file

@ -8,7 +8,7 @@
import type { ComponentType } from 'react';
import React, { memo } from 'react';
import type { CoreStart } from '@kbn/core/public';
import type { StartPlugins } from '../../../../../../types';
import type { StartPlugins } from '../../../../../../../types';
import { createFleetContextReduxStore } from './store';
import { RenderContextProviders } from './render_context_providers';

View file

@ -15,8 +15,8 @@ import { INTEGRATIONS_PLUGIN_ID } from '@kbn/fleet-plugin/common';
import type { ListPageRouteState } from '../../../../../../../../common/endpoint/types';
import { useToasts } from '../../../../../../../common/lib/kibana';
import { useAppUrl } from '../../../../../../../common/lib/kibana/hooks';
import { LinkWithIcon } from './link_with_icon';
import { ExceptionItemsSummary } from './exception_items_summary';
import { LinkWithIcon } from '../../components/link_with_icon';
import { ExceptionItemsSummary } from '../../components/exception_items_summary';
import { StyledEuiFlexGridGroup, StyledEuiFlexGridItem } from './styled_components';
import { useSummaryArtifact } from '../../../../../../hooks/artifacts';
import type { ExceptionsListApiClient } from '../../../../../../services/exceptions_list/exceptions_list_api_client';

View file

@ -59,8 +59,13 @@ describe('When displaying the EndpointPackageCustomExtension fleet UI extension'
it('should NOT show artifact cards if no endpoint management authz', async () => {
useEndpointPrivilegesMock.mockReturnValue({
...getEndpointPrivilegesInitialStateMock(),
canAccessEndpointManagement: false,
...getEndpointPrivilegesInitialStateMock({
canReadBlocklist: false,
canReadEventFilters: false,
canReadHostIsolationExceptions: false,
canReadTrustedApplications: false,
canIsolateHost: false,
}),
});
render();
@ -68,7 +73,7 @@ describe('When displaying the EndpointPackageCustomExtension fleet UI extension'
artifactCards.forEach((artifactCard) => {
expect(renderResult.queryByTestId(artifactCard)).toBeNull();
});
expect(renderResult.queryByTestId('noIngestPermissions')).toBeTruthy();
expect(renderResult.queryByTestId('noPrivilegesPage')).toBeTruthy();
});
});

View file

@ -5,10 +5,13 @@
* 2.0.
*/
import type { ReactElement } from 'react';
import React, { memo, useMemo } from 'react';
import { EuiSpacer, EuiLoadingSpinner } from '@elastic/eui';
import { EuiSpacer, EuiLoadingSpinner, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import type { PackageCustomExtensionComponentProps } from '@kbn/fleet-plugin/public';
import { useHttp } from '../../../../../../common/lib/kibana';
import { NoPrivileges } from '../../../../../../common/components/no_privileges';
import { useCanAccessSomeArtifacts } from '../hooks/use_can_access_some_artifacts';
import { useHttp, useKibana } from '../../../../../../common/lib/kibana';
import { TrustedAppsApiClient } from '../../../../trusted_apps/service/api_client';
import { EventFiltersApiClient } from '../../../../event_filters/service/api_client';
import { HostIsolationExceptionsApiClient } from '../../../../host_isolation_exceptions/host_isolation_exceptions_api_client';
@ -27,91 +30,155 @@ import {
TRUSTED_APPS_LABELS,
} from './translations';
import { useEndpointPrivileges } from '../../../../../../common/components/user_privileges/endpoint';
import { NoPermissions } from '../../../../../components/no_permissons';
const TrustedAppsArtifactCard = memo<PackageCustomExtensionComponentProps>((props) => {
const http = useHttp();
const trustedAppsApiClientInstance = useMemo(
() => TrustedAppsApiClient.getInstance(http),
[http]
);
return (
<FleetArtifactsCard
{...props}
artifactApiClientInstance={trustedAppsApiClientInstance}
getArtifactsPath={getTrustedAppsListPath}
labels={TRUSTED_APPS_LABELS}
data-test-subj="trustedApps"
/>
);
});
TrustedAppsArtifactCard.displayName = 'TrustedAppsArtifactCard';
const EventFiltersArtifactCard = memo<PackageCustomExtensionComponentProps>((props) => {
const http = useHttp();
const eventFiltersApiClientInstance = useMemo(
() => EventFiltersApiClient.getInstance(http),
[http]
);
return (
<FleetArtifactsCard
{...props}
artifactApiClientInstance={eventFiltersApiClientInstance}
getArtifactsPath={getEventFiltersListPath}
labels={EVENT_FILTERS_LABELS}
data-test-subj="eventFilters"
/>
);
});
EventFiltersArtifactCard.displayName = 'EventFiltersArtifactCard';
const HostIsolationExceptionsArtifactCard = memo<PackageCustomExtensionComponentProps>((props) => {
const http = useHttp();
const hostIsolationExceptionsApiClientInstance = useMemo(
() => HostIsolationExceptionsApiClient.getInstance(http),
[http]
);
return (
<FleetArtifactsCard
{...props}
artifactApiClientInstance={hostIsolationExceptionsApiClientInstance}
getArtifactsPath={getHostIsolationExceptionsListPath}
labels={HOST_ISOLATION_EXCEPTIONS_LABELS}
data-test-subj="hostIsolationExceptions"
/>
);
});
HostIsolationExceptionsArtifactCard.displayName = 'HostIsolationExceptionsArtifactCard';
const BlockListArtifactCard = memo<PackageCustomExtensionComponentProps>((props) => {
const http = useHttp();
const blocklistsApiClientInstance = useMemo(() => BlocklistsApiClient.getInstance(http), [http]);
return (
<FleetArtifactsCard
{...props}
artifactApiClientInstance={blocklistsApiClientInstance}
getArtifactsPath={getBlocklistsListPath}
labels={BLOCKLISTS_LABELS}
data-test-subj="blocklists"
/>
);
});
BlockListArtifactCard.displayName = 'BlockListArtifactCard';
/**
* The UI displayed in Fleet's Endpoint integration page, under the `Advanced` tab
*/
export const EndpointPackageCustomExtension = memo<PackageCustomExtensionComponentProps>(
(props) => {
const http = useHttp();
const { loading, canAccessEndpointManagement, canReadHostIsolationExceptions } =
useEndpointPrivileges();
const {
loading,
canReadBlocklist,
canReadEventFilters,
canReadTrustedApplications,
canReadHostIsolationExceptions,
} = useEndpointPrivileges();
const { docLinks } = useKibana().services;
const trustedAppsApiClientInstance = useMemo(
() => TrustedAppsApiClient.getInstance(http),
[http]
);
const userCanAccessContent = useCanAccessSomeArtifacts();
const eventFiltersApiClientInstance = useMemo(
() => EventFiltersApiClient.getInstance(http),
[http]
);
const artifactCards: ReactElement = useMemo(() => {
if (loading) {
return <></>;
}
const hostIsolationExceptionsApiClientInstance = useMemo(
() => HostIsolationExceptionsApiClient.getInstance(http),
[http]
);
if (!userCanAccessContent) {
return <NoPrivileges documentationUrl={docLinks.links.securitySolution.privileges} />;
}
const blocklistsApiClientInstance = useMemo(
() => BlocklistsApiClient.getInstance(http),
[http]
);
const artifactCards = useMemo(
() => (
return (
<div data-test-subj="fleetEndpointPackageCustomContent">
<FleetArtifactsCard
{...props}
artifactApiClientInstance={trustedAppsApiClientInstance}
getArtifactsPath={getTrustedAppsListPath}
labels={TRUSTED_APPS_LABELS}
data-test-subj="trustedApps"
/>
<EuiSpacer />
<FleetArtifactsCard
{...props}
artifactApiClientInstance={eventFiltersApiClientInstance}
getArtifactsPath={getEventFiltersListPath}
labels={EVENT_FILTERS_LABELS}
data-test-subj="eventFilters"
/>
{canReadHostIsolationExceptions && (
{canReadTrustedApplications && (
<>
<TrustedAppsArtifactCard {...props} />
<EuiSpacer />
<FleetArtifactsCard
{...props}
artifactApiClientInstance={hostIsolationExceptionsApiClientInstance}
getArtifactsPath={getHostIsolationExceptionsListPath}
labels={HOST_ISOLATION_EXCEPTIONS_LABELS}
data-test-subj="hostIsolationExceptions"
/>
</>
)}
<EuiSpacer />
<FleetArtifactsCard
{...props}
artifactApiClientInstance={blocklistsApiClientInstance}
getArtifactsPath={getBlocklistsListPath}
labels={BLOCKLISTS_LABELS}
data-test-subj="blocklists"
/>
</div>
),
[
blocklistsApiClientInstance,
canReadHostIsolationExceptions,
eventFiltersApiClientInstance,
hostIsolationExceptionsApiClientInstance,
trustedAppsApiClientInstance,
props,
]
);
return loading ? (
<EuiLoadingSpinner data-test-subj="endpointExtensionLoadingSpinner" />
) : canAccessEndpointManagement ? (
artifactCards
) : (
<NoPermissions />
);
{canReadEventFilters && (
<>
<EventFiltersArtifactCard {...props} />
<EuiSpacer />
</>
)}
{canReadHostIsolationExceptions && (
<>
<HostIsolationExceptionsArtifactCard {...props} />
<EuiSpacer />
</>
)}
{canReadBlocklist && <BlockListArtifactCard {...props} />}
</div>
);
}, [
canReadBlocklist,
canReadEventFilters,
canReadTrustedApplications,
canReadHostIsolationExceptions,
docLinks.links.securitySolution.privileges,
loading,
props,
userCanAccessContent,
]);
if (loading) {
return (
<EuiFlexGroup alignItems="center" justifyContent={'spaceAround'}>
<EuiFlexItem grow={false}>
<EuiSpacer size="xl" />
<EuiLoadingSpinner size="l" data-test-subj="endpointExtensionLoadingSpinner" />
<EuiSpacer size="xl" />
</EuiFlexItem>
</EuiFlexGroup>
);
}
return artifactCards;
}
);

View file

@ -12,7 +12,7 @@ import { FormattedMessage } from '@kbn/i18n-react';
export const TRUSTED_APPS_LABELS = {
artifactsSummaryApiError: (error: string) =>
i18n.translate(
'xpack.securitySolution.endpoint.fleetCustomExtension.trustedAppsSummarySummary.error',
'xpack.securitySolution.endpoint.fleetCustomExtension.trustedAppsSummary.error',
{
defaultMessage: 'There was an error trying to fetch trusted applications stats: "{error}"',
values: { error },
@ -29,7 +29,7 @@ export const TRUSTED_APPS_LABELS = {
export const EVENT_FILTERS_LABELS = {
artifactsSummaryApiError: (error: string) =>
i18n.translate(
'xpack.securitySolution.endpoint.fleetCustomExtension.eventFiltersSummarySummary.error',
'xpack.securitySolution.endpoint.fleetCustomExtension.eventFiltersSummary.error',
{
defaultMessage: 'There was an error trying to fetch event filters stats: "{error}"',
values: { error },
@ -46,7 +46,7 @@ export const EVENT_FILTERS_LABELS = {
export const HOST_ISOLATION_EXCEPTIONS_LABELS = {
artifactsSummaryApiError: (error: string) =>
i18n.translate(
'xpack.securitySolution.endpoint.fleetCustomExtension.hostIsolationExceptionsSummarySummary.error',
'xpack.securitySolution.endpoint.fleetCustomExtension.hostIsolationExceptionsSummary.error',
{
defaultMessage:
'There was an error trying to fetch host isolation exceptions stats: "{error}"',
@ -63,13 +63,10 @@ export const HOST_ISOLATION_EXCEPTIONS_LABELS = {
export const BLOCKLISTS_LABELS = {
artifactsSummaryApiError: (error: string) =>
i18n.translate(
'xpack.securitySolution.endpoint.fleetCustomExtension.blocklistsSummarySummary.error',
{
defaultMessage: 'There was an error trying to fetch blocklist stats: "{error}"',
values: { error },
}
),
i18n.translate('xpack.securitySolution.endpoint.fleetCustomExtension.blocklistsSummary.error', {
defaultMessage: 'There was an error trying to fetch blocklist stats: "{error}"',
values: { error },
}),
cardTitle: (
<FormattedMessage
id="xpack.securitySolution.endpoint.blocklists.fleetIntegration.title"

View file

@ -1,331 +0,0 @@
/*
* 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, useEffect, useState, useMemo } from 'react';
import { EuiCallOut, EuiLoadingSpinner, EuiSpacer, EuiText } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { useDispatch } from 'react-redux';
import type {
PackagePolicyEditExtensionComponentProps,
NewPackagePolicy,
} from '@kbn/fleet-plugin/public';
import { useHttp } from '../../../../../common/lib/kibana/hooks';
import {
getPolicyDetailPath,
getPolicyTrustedAppsPath,
getPolicyBlocklistsPath,
getPolicyHostIsolationExceptionsPath,
getPolicyEventFiltersPath,
} from '../../../../common/routing';
import { PolicyDetailsForm } from '../policy_details_form';
import type { AppAction } from '../../../../../common/store/actions';
import { usePolicyDetailsSelector } from '../policy_hooks';
import {
apiError,
policyDetails,
policyDetailsForUpdate,
} from '../../store/policy_details/selectors';
import { useUserPrivileges } from '../../../../../common/components/user_privileges';
import { FleetIntegrationArtifactsCard } from './endpoint_package_custom_extension/components/fleet_integration_artifacts_card';
import { BlocklistsApiClient } from '../../../blocklist/services';
import { HostIsolationExceptionsApiClient } from '../../../host_isolation_exceptions/host_isolation_exceptions_api_client';
import { EventFiltersApiClient } from '../../../event_filters/service/api_client';
import { TrustedAppsApiClient } from '../../../trusted_apps/service/api_client';
import { SEARCHABLE_FIELDS as BLOCKLIST_SEARCHABLE_FIELDS } from '../../../blocklist/constants';
import { SEARCHABLE_FIELDS as HOST_ISOLATION_EXCEPTIONS_SEARCHABLE_FIELDS } from '../../../host_isolation_exceptions/constants';
import { SEARCHABLE_FIELDS as EVENT_FILTERS_SEARCHABLE_FIELDS } from '../../../event_filters/constants';
import { SEARCHABLE_FIELDS as TRUSTED_APPS_SEARCHABLE_FIELDS } from '../../../trusted_apps/constants';
import { useEndpointPrivileges } from '../../../../../common/components/user_privileges/endpoint';
export const BLOCKLISTS_LABELS = {
artifactsSummaryApiError: (error: string) =>
i18n.translate('xpack.securitySolution.endpoint.fleetIntegrationCard.blocklistsSummary.error', {
defaultMessage: 'There was an error trying to fetch blocklists stats: "{error}"',
values: { error },
}),
cardTitle: (
<FormattedMessage
id="xpack.securitySolution.endpoint.blocklist.fleetIntegration.title"
defaultMessage="Blocklist"
/>
),
linkLabel: (
<FormattedMessage
id="xpack.securitySolution.endpoint.fleetIntegrationCard.blocklistsManageLabel"
defaultMessage="Manage blocklist"
/>
),
};
export const HOST_ISOLATION_EXCEPTIONS_LABELS = {
artifactsSummaryApiError: (error: string) =>
i18n.translate(
'xpack.securitySolution.endpoint.fleetIntegrationCard.hostIsolationExceptionsSummary.error',
{
defaultMessage:
'There was an error trying to fetch host isolation exceptions stats: "{error}"',
values: { error },
}
),
cardTitle: (
<FormattedMessage
id="xpack.securitySolution.endpoint.hostIsolationExceptions.fleetIntegration.title"
defaultMessage="Host isolation exceptions"
/>
),
linkLabel: (
<FormattedMessage
id="xpack.securitySolution.endpoint.fleetIntegrationCard.hostIsolationExceptionsManageLabel"
defaultMessage="Manage host isolation exceptions"
/>
),
};
export const EVENT_FILTERS_LABELS = {
artifactsSummaryApiError: (error: string) =>
i18n.translate(
'xpack.securitySolution.endpoint.fleetIntegrationCard.eventFiltersSummarySummary.error',
{
defaultMessage: 'There was an error trying to fetch event filters stats: "{error}"',
values: { error },
}
),
cardTitle: (
<FormattedMessage
id="xpack.securitySolution.endpoint.eventFilters.fleetIntegration.title"
defaultMessage="Event filters"
/>
),
linkLabel: (
<FormattedMessage
id="xpack.securitySolution.endpoint.fleetIntegrationCard.eventFiltersManageLabel"
defaultMessage="Manage event filters"
/>
),
};
export const TRUSTED_APPS_LABELS = {
artifactsSummaryApiError: (error: string) =>
i18n.translate(
'xpack.securitySolution.endpoint.fleetIntegrationCard.trustedAppsSummarySummary.error',
{
defaultMessage: 'There was an error trying to fetch trusted apps stats: "{error}"',
values: { error },
}
),
cardTitle: (
<FormattedMessage
id="xpack.securitySolution.endpoint.trustedApps.fleetIntegration.title"
defaultMessage="Trusted applications"
/>
),
linkLabel: (
<FormattedMessage
id="xpack.securitySolution.endpoint.fleetIntegrationCard.trustedAppsManageLabel"
defaultMessage="Manage trusted applications"
/>
),
};
/**
* Exports Endpoint-specific package policy instructions
* for use in the Ingest app create / edit package policy
*/
export const EndpointPolicyEditExtension = memo<PackagePolicyEditExtensionComponentProps>(
({ policy, onChange }) => {
return (
<>
<EuiSpacer size="m" />
<WrappedPolicyDetailsForm policyId={policy.id} onChange={onChange} />
</>
);
}
);
EndpointPolicyEditExtension.displayName = 'EndpointPolicyEditExtension';
const WrappedPolicyDetailsForm = memo<{
policyId: string;
onChange: PackagePolicyEditExtensionComponentProps['onChange'];
}>(({ policyId, onChange }) => {
const dispatch = useDispatch<(a: AppAction) => void>();
const updatedPolicy = usePolicyDetailsSelector(policyDetailsForUpdate);
const endpointPolicyDetails = usePolicyDetailsSelector(policyDetails);
const endpointDetailsLoadingError = usePolicyDetailsSelector(apiError);
const [, setLastUpdatedPolicy] = useState(updatedPolicy);
const privileges = useUserPrivileges().endpointPrivileges;
const http = useHttp();
const blocklistsApiClientInstance = useMemo(() => BlocklistsApiClient.getInstance(http), [http]);
const { canAccessEndpointManagement } = useEndpointPrivileges();
const hostIsolationExceptionsApiClientInstance = useMemo(
() => HostIsolationExceptionsApiClient.getInstance(http),
[http]
);
const eventFiltersApiClientInstance = useMemo(
() => EventFiltersApiClient.getInstance(http),
[http]
);
const trustedAppsApiClientInstance = useMemo(
() => TrustedAppsApiClient.getInstance(http),
[http]
);
// When the form is initially displayed, trigger the Redux middleware which is based on
// the location information stored via the `userChangedUrl` action.
useEffect(() => {
dispatch({
type: 'userChangedUrl',
payload: {
hash: '',
pathname: getPolicyDetailPath(policyId, ''),
search: '',
},
});
// When form is unloaded, reset the redux store
return () => {
dispatch({
type: 'userChangedUrl',
payload: {
hash: '',
pathname: '/',
search: '',
},
});
};
}, [dispatch, policyId]);
useEffect(() => {
// Currently, the `onChange` callback provided by the fleet UI extension is regenerated every
// time the policy data is updated, which means this will go into a continious loop if we don't
// actually check to see if an update should be reported back to fleet
setLastUpdatedPolicy((prevState) => {
if (prevState === updatedPolicy) {
return prevState;
}
if (updatedPolicy) {
onChange({
isValid: true,
// send up only the updated policy data which is stored in the `inputs` section.
// All other attributes (like name, id) are updated from the Fleet form, so we want to
// ensure we don't override it.
updatedPolicy: {
// Casting is needed due to the use of `Immutable<>` in our store data
inputs: updatedPolicy.inputs as unknown as NewPackagePolicy['inputs'],
},
});
}
return updatedPolicy;
});
}, [onChange, updatedPolicy]);
const artifactCards = useMemo(
() => (
<>
<div>
<EuiText>
<h5>
<FormattedMessage
id="xpack.securitySolution.endpoint.policyDetails.artifacts.title"
defaultMessage="Artifacts"
/>
</h5>
</EuiText>
<EuiSpacer size="s" />
<FleetIntegrationArtifactsCard
policyId={policyId}
artifactApiClientInstance={trustedAppsApiClientInstance}
getArtifactsPath={getPolicyTrustedAppsPath}
searchableFields={TRUSTED_APPS_SEARCHABLE_FIELDS}
labels={TRUSTED_APPS_LABELS}
data-test-subj="trustedApps"
/>
<EuiSpacer size="s" />
<FleetIntegrationArtifactsCard
policyId={policyId}
artifactApiClientInstance={eventFiltersApiClientInstance}
getArtifactsPath={getPolicyEventFiltersPath}
searchableFields={EVENT_FILTERS_SEARCHABLE_FIELDS}
labels={EVENT_FILTERS_LABELS}
data-test-subj="eventFilters"
/>
<EuiSpacer size="s" />
<FleetIntegrationArtifactsCard
policyId={policyId}
artifactApiClientInstance={hostIsolationExceptionsApiClientInstance}
getArtifactsPath={getPolicyHostIsolationExceptionsPath}
searchableFields={HOST_ISOLATION_EXCEPTIONS_SEARCHABLE_FIELDS}
labels={HOST_ISOLATION_EXCEPTIONS_LABELS}
privileges={privileges.canIsolateHost}
data-test-subj="hostIsolationExceptions"
/>
<EuiSpacer size="s" />
<FleetIntegrationArtifactsCard
policyId={policyId}
artifactApiClientInstance={blocklistsApiClientInstance}
getArtifactsPath={getPolicyBlocklistsPath}
searchableFields={BLOCKLIST_SEARCHABLE_FIELDS}
labels={BLOCKLISTS_LABELS}
data-test-subj="blocklists"
/>
</div>
<EuiSpacer size="l" />
</>
),
[
blocklistsApiClientInstance,
eventFiltersApiClientInstance,
hostIsolationExceptionsApiClientInstance,
policyId,
privileges.canIsolateHost,
trustedAppsApiClientInstance,
]
);
return (
<div data-test-subj="endpointIntegrationPolicyForm">
<>
{canAccessEndpointManagement && artifactCards}
<div>
<EuiText>
<h5>
<FormattedMessage
id="xpack.securitySolution.endpoint.policyDetails.settings.title"
defaultMessage="Policy settings"
/>
</h5>
</EuiText>
<EuiSpacer size="s" />
{endpointDetailsLoadingError ? (
<EuiCallOut
title={
<FormattedMessage
id="xpack.securitySolution.endpoint.policyDetails.loadError"
defaultMessage="Failed to load endpoint policy settings"
/>
}
iconType="alert"
color="warning"
data-test-subj="endpiontPolicySettingsLoadingError"
>
{endpointDetailsLoadingError.message}
</EuiCallOut>
) : !endpointPolicyDetails ? (
<EuiLoadingSpinner size="l" className="essentialAnimation" />
) : (
<PolicyDetailsForm />
)}
</div>
</>
</div>
);
});
WrappedPolicyDetailsForm.displayName = 'WrappedPolicyDetailsForm';

View file

@ -0,0 +1,230 @@
/*
* 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, useCallback, useMemo } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiLoadingContent, EuiSpacer, EuiText } from '@elastic/eui';
import {
BLOCKLISTS_LABELS,
EVENT_FILTERS_LABELS,
HOST_ISOLATION_EXCEPTIONS_LABELS,
TRUSTED_APPS_LABELS,
} from '../translations';
import { useCanAccessSomeArtifacts } from '../../hooks/use_can_access_some_artifacts';
import { BlocklistsApiClient } from '../../../../../blocklist/services';
import { HostIsolationExceptionsApiClient } from '../../../../../host_isolation_exceptions/host_isolation_exceptions_api_client';
import { EventFiltersApiClient } from '../../../../../event_filters/service/api_client';
import { TrustedAppsApiClient } from '../../../../../trusted_apps/service';
import {
getBlocklistsListPath,
getEventFiltersListPath,
getHostIsolationExceptionsListPath,
getPolicyBlocklistsPath,
getPolicyEventFiltersPath,
getPolicyHostIsolationExceptionsPath,
getPolicyTrustedAppsPath,
getTrustedAppsListPath,
} from '../../../../../../common/routing';
import { SEARCHABLE_FIELDS as TRUSTED_APPS_SEARCHABLE_FIELDS } from '../../../../../trusted_apps/constants';
import type { FleetIntegrationArtifactCardProps } from './fleet_integration_artifacts_card';
import { FleetIntegrationArtifactsCard } from './fleet_integration_artifacts_card';
import { SEARCHABLE_FIELDS as EVENT_FILTERS_SEARCHABLE_FIELDS } from '../../../../../event_filters/constants';
import { SEARCHABLE_FIELDS as HOST_ISOLATION_EXCEPTIONS_SEARCHABLE_FIELDS } from '../../../../../host_isolation_exceptions/constants';
import { SEARCHABLE_FIELDS as BLOCKLIST_SEARCHABLE_FIELDS } from '../../../../../blocklist/constants';
import { useHttp } from '../../../../../../../common/lib/kibana';
import { useEndpointPrivileges } from '../../../../../../../common/components/user_privileges/endpoint';
interface PolicyArtifactCardProps {
policyId: string;
}
const TrustedAppsPolicyCard = memo<PolicyArtifactCardProps>(({ policyId }) => {
const http = useHttp();
const trustedAppsApiClientInstance = useMemo(
() => TrustedAppsApiClient.getInstance(http),
[http]
);
const { canReadPolicyManagement } = useEndpointPrivileges();
const getArtifactPathHandler: FleetIntegrationArtifactCardProps['getArtifactsPath'] =
useCallback(() => {
if (canReadPolicyManagement) {
return getPolicyTrustedAppsPath(policyId);
}
return getTrustedAppsListPath({ includedPolicies: `${policyId},global` });
}, [canReadPolicyManagement, policyId]);
return (
<FleetIntegrationArtifactsCard
policyId={policyId}
artifactApiClientInstance={trustedAppsApiClientInstance}
getArtifactsPath={getArtifactPathHandler}
searchableFields={TRUSTED_APPS_SEARCHABLE_FIELDS}
labels={TRUSTED_APPS_LABELS}
data-test-subj="trustedApps"
/>
);
});
TrustedAppsPolicyCard.displayName = 'TrustedAppsPolicyCard';
const EventFiltersPolicyCard = memo<PolicyArtifactCardProps>(({ policyId }) => {
const http = useHttp();
const eventFiltersApiClientInstance = useMemo(
() => EventFiltersApiClient.getInstance(http),
[http]
);
const { canReadPolicyManagement } = useEndpointPrivileges();
const getArtifactPathHandler: FleetIntegrationArtifactCardProps['getArtifactsPath'] =
useCallback(() => {
if (canReadPolicyManagement) {
return getPolicyEventFiltersPath(policyId);
}
return getEventFiltersListPath({ includedPolicies: `${policyId},global` });
}, [canReadPolicyManagement, policyId]);
return (
<FleetIntegrationArtifactsCard
policyId={policyId}
artifactApiClientInstance={eventFiltersApiClientInstance}
getArtifactsPath={getArtifactPathHandler}
searchableFields={EVENT_FILTERS_SEARCHABLE_FIELDS}
labels={EVENT_FILTERS_LABELS}
data-test-subj="eventFilters"
/>
);
});
EventFiltersPolicyCard.displayName = 'EventFiltersPolicyCard';
const HostIsolationExceptionsPolicyCard = memo<PolicyArtifactCardProps>(({ policyId }) => {
const http = useHttp();
const hostIsolationExceptionsApiClientInstance = useMemo(
() => HostIsolationExceptionsApiClient.getInstance(http),
[http]
);
const { canReadPolicyManagement } = useEndpointPrivileges();
const getArtifactPathHandler: FleetIntegrationArtifactCardProps['getArtifactsPath'] =
useCallback(() => {
if (canReadPolicyManagement) {
return getPolicyHostIsolationExceptionsPath(policyId);
}
return getHostIsolationExceptionsListPath({ includedPolicies: `${policyId},global` });
}, [canReadPolicyManagement, policyId]);
return (
<FleetIntegrationArtifactsCard
policyId={policyId}
artifactApiClientInstance={hostIsolationExceptionsApiClientInstance}
getArtifactsPath={getArtifactPathHandler}
searchableFields={HOST_ISOLATION_EXCEPTIONS_SEARCHABLE_FIELDS}
labels={HOST_ISOLATION_EXCEPTIONS_LABELS}
data-test-subj="hostIsolationExceptions"
/>
);
});
HostIsolationExceptionsPolicyCard.displayName = 'HostIsolationExceptionsPolicyCard';
const BlocklistPolicyCard = memo<PolicyArtifactCardProps>(({ policyId }) => {
const http = useHttp();
const blocklistsApiClientInstance = useMemo(() => BlocklistsApiClient.getInstance(http), [http]);
const { canReadPolicyManagement } = useEndpointPrivileges();
const getArtifactPathHandler: FleetIntegrationArtifactCardProps['getArtifactsPath'] =
useCallback(() => {
if (canReadPolicyManagement) {
return getPolicyBlocklistsPath(policyId);
}
return getBlocklistsListPath({ includedPolicies: `${policyId},global` });
}, [canReadPolicyManagement, policyId]);
return (
<FleetIntegrationArtifactsCard
policyId={policyId}
artifactApiClientInstance={blocklistsApiClientInstance}
getArtifactsPath={getArtifactPathHandler}
searchableFields={BLOCKLIST_SEARCHABLE_FIELDS}
labels={BLOCKLISTS_LABELS}
data-test-subj="blocklists"
/>
);
});
BlocklistPolicyCard.displayName = 'BlocklistPolicyCard';
export interface EndpointPolicyArtifactCardsProps {
policyId: string;
}
/**
* Displays the Artifact cards on the Edit Integration Policy form within Fleet according to the
* current user's authz
*/
export const EndpointPolicyArtifactCards = memo<EndpointPolicyArtifactCardsProps>(
({ policyId }) => {
const {
loading,
canReadBlocklist,
canReadEventFilters,
canReadTrustedApplications,
canReadHostIsolationExceptions,
} = useEndpointPrivileges();
const canAccessArtifactContent = useCanAccessSomeArtifacts();
if (loading) {
return <EuiLoadingContent lines={4} />;
}
if (!canAccessArtifactContent) {
return null;
}
return (
<>
<div>
<EuiText>
<h5>
<FormattedMessage
id="xpack.securitySolution.endpoint.policyDetails.artifacts.title"
defaultMessage="Artifacts"
/>
</h5>
</EuiText>
<EuiSpacer size="s" />
{canReadTrustedApplications && (
<>
<TrustedAppsPolicyCard policyId={policyId} />
<EuiSpacer size="s" />
</>
)}
{canReadEventFilters && (
<>
<EventFiltersPolicyCard policyId={policyId} />
<EuiSpacer size="s" />
</>
)}
{canReadHostIsolationExceptions && (
<>
<HostIsolationExceptionsPolicyCard policyId={policyId} />
<EuiSpacer size="s" />
</>
)}
{canReadBlocklist && <BlocklistPolicyCard policyId={policyId} />}
</div>
<EuiSpacer size="l" />
</>
);
}
);
EndpointPolicyArtifactCards.displayName = 'EndpointPolicyArtifactCards';

View file

@ -19,7 +19,7 @@ import type { PolicyData } from '../../../../../../../../common/endpoint/types';
import { getSummaryExceptionListSchemaMock } from '@kbn/lists-plugin/common/schemas/response/exception_list_summary_schema.mock';
import { EventFiltersApiClient } from '../../../../../event_filters/service/api_client';
import { SEARCHABLE_FIELDS } from '../../../../../event_filters/constants';
import { EVENT_FILTERS_LABELS } from '../../endpoint_policy_edit_extension';
import { EVENT_FILTERS_LABELS } from '../translations';
const endpointGenerator = new EndpointDocGenerator('seed');

View file

@ -6,16 +6,16 @@
*/
import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiText } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import React, { memo, useMemo } from 'react';
import { INTEGRATIONS_PLUGIN_ID } from '@kbn/fleet-plugin/common';
import { pagePathGetters } from '@kbn/fleet-plugin/public';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import type { PolicyDetailsRouteState } from '../../../../../../../../common/endpoint/types';
import { useAppUrl, useToasts } from '../../../../../../../common/lib/kibana';
import { ExceptionItemsSummary } from './exception_items_summary';
import { LinkWithIcon } from './link_with_icon';
import { StyledEuiFlexItem } from './styled_components';
import { ExceptionItemsSummary } from '../../components/exception_items_summary';
import { LinkWithIcon } from '../../components/link_with_icon';
import { StyledEuiFlexItem } from '../../endpoint_package_custom_extension/components/styled_components';
import { useSummaryArtifact } from '../../../../../../hooks/artifacts';
import type { ExceptionsListApiClient } from '../../../../../../services/exceptions_list/exceptions_list_api_client';
import { useTestIdGenerator } from '../../../../../../hooks/use_test_id_generator';
@ -42,7 +42,7 @@ const ARTIFACTS_LABELS = {
export type ARTIFACTS_LABELS_TYPE = typeof ARTIFACTS_LABELS;
export const FleetIntegrationArtifactsCard = memo<{
export interface FleetIntegrationArtifactCardProps {
policyId: string;
artifactApiClientInstance: ExceptionsListApiClient;
getArtifactsPath: (policyId: string) => string;
@ -50,7 +50,9 @@ export const FleetIntegrationArtifactsCard = memo<{
labels?: ARTIFACTS_LABELS_TYPE;
privileges?: boolean;
'data-test-subj': string;
}>(
}
export const FleetIntegrationArtifactsCard = memo<FleetIntegrationArtifactCardProps>(
({
policyId,
artifactApiClientInstance,

View file

@ -9,20 +9,20 @@ import React from 'react';
import { waitFor } from '@testing-library/react';
import type { PackagePolicy, NewPackagePolicy } from '@kbn/fleet-plugin/common';
import { useEndpointPrivileges } from '../../../../../common/components/user_privileges/endpoint/use_endpoint_privileges';
import { useUserPrivileges } from '../../../../../common/components/user_privileges';
import { getEndpointPrivilegesInitialStateMock } from '../../../../../common/components/user_privileges/endpoint/mocks';
import { composeHttpHandlerMocks } from '../../../../../common/mock/endpoint/http_handler_mock_factory';
import type { AppContextTestRender } from '../../../../../common/mock/endpoint';
import { useEndpointPrivileges } from '../../../../../../common/components/user_privileges/endpoint/use_endpoint_privileges';
import { useUserPrivileges } from '../../../../../../common/components/user_privileges';
import { getEndpointPrivilegesInitialStateMock } from '../../../../../../common/components/user_privileges/endpoint/mocks';
import { composeHttpHandlerMocks } from '../../../../../../common/mock/endpoint/http_handler_mock_factory';
import type { AppContextTestRender } from '../../../../../../common/mock/endpoint';
import {
fleetGetAgentStatusHttpMock,
fleetGetEndpointPackagePolicyHttpMock,
} from '../../../../mocks';
} from '../../../../../mocks';
import { EndpointPolicyEditExtension } from './endpoint_policy_edit_extension';
import { createFleetContextRendererMock } from './mocks';
import { createFleetContextRendererMock } from '../mocks';
jest.mock('../../../../../common/components/user_privileges/endpoint/use_endpoint_privileges');
jest.mock('../../../../../common/components/user_privileges');
jest.mock('../../../../../../common/components/user_privileges/endpoint/use_endpoint_privileges');
jest.mock('../../../../../../common/components/user_privileges');
const useEndpointPrivilegesMock = useEndpointPrivileges as jest.Mock;
const useUserPrivilegesMock = useUserPrivileges as jest.Mock;
@ -65,8 +65,12 @@ describe('When displaying the EndpointPolicyEditExtension fleet UI extension', (
it('should NOT show artifact cards if no endpoint management authz', async () => {
useEndpointPrivilegesMock.mockReturnValue({
...getEndpointPrivilegesInitialStateMock(),
canAccessEndpointManagement: false,
...getEndpointPrivilegesInitialStateMock({
canReadTrustedApplications: false,
canReadEventFilters: false,
canReadBlocklist: false,
canReadHostIsolationExceptions: false,
}),
});
const renderResult = render();
@ -76,4 +80,28 @@ describe('When displaying the EndpointPolicyEditExtension fleet UI extension', (
});
});
});
it.each([
['trustedApps', 'trusted_apps'],
['eventFilters', 'event_filters'],
['hostIsolationExceptions', 'host_isolation_exceptions'],
['blocklists', 'blocklist'],
])(
'should link to the %s list page if no Authz for policy management',
async (artifactTestIdPrefix, pageUrlName) => {
useEndpointPrivilegesMock.mockReturnValue({
...getEndpointPrivilegesInitialStateMock({
canReadPolicyManagement: false,
}),
});
const { getByTestId } = render();
await waitFor(() => {
expect(
getByTestId(`${artifactTestIdPrefix}-link-to-exceptions`).getAttribute('href')
).toEqual(`/app/security/administration/${pageUrlName}?includedPolicies=someid%2Cglobal`);
});
}
);
});

View file

@ -0,0 +1,142 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { memo, useEffect, useState } from 'react';
import { EuiCallOut, EuiLoadingSpinner, EuiSpacer, EuiText } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { useDispatch } from 'react-redux';
import type {
PackagePolicyEditExtensionComponentProps,
NewPackagePolicy,
} from '@kbn/fleet-plugin/public';
import { EndpointPolicyArtifactCards } from './components/endpoint_policy_artifact_cards';
import { getPolicyDetailPath } from '../../../../../common/routing';
import { PolicyDetailsForm } from '../../policy_details_form';
import type { AppAction } from '../../../../../../common/store/actions';
import { usePolicyDetailsSelector } from '../../policy_hooks';
import {
apiError,
policyDetails,
policyDetailsForUpdate,
} from '../../../store/policy_details/selectors';
/**
* Exports Endpoint-specific package policy instructions
* for use in the Ingest app create / edit package policy
*/
export const EndpointPolicyEditExtension = memo<PackagePolicyEditExtensionComponentProps>(
({ policy, onChange }) => {
return (
<>
<EuiSpacer size="m" />
<WrappedPolicyDetailsForm policyId={policy.id} onChange={onChange} />
</>
);
}
);
EndpointPolicyEditExtension.displayName = 'EndpointPolicyEditExtension';
const WrappedPolicyDetailsForm = memo<{
policyId: string;
onChange: PackagePolicyEditExtensionComponentProps['onChange'];
}>(({ policyId, onChange }) => {
const dispatch = useDispatch<(a: AppAction) => void>();
const updatedPolicy = usePolicyDetailsSelector(policyDetailsForUpdate);
const endpointPolicyDetails = usePolicyDetailsSelector(policyDetails);
const endpointDetailsLoadingError = usePolicyDetailsSelector(apiError);
const [, setLastUpdatedPolicy] = useState(updatedPolicy);
// When the form is initially displayed, trigger the Redux middleware which is based on
// the location information stored via the `userChangedUrl` action.
useEffect(() => {
dispatch({
type: 'userChangedUrl',
payload: {
hash: '',
pathname: getPolicyDetailPath(policyId, ''),
search: '',
},
});
// When form is unloaded, reset the redux store
return () => {
dispatch({
type: 'userChangedUrl',
payload: {
hash: '',
pathname: '/',
search: '',
},
});
};
}, [dispatch, policyId]);
useEffect(() => {
// Currently, the `onChange` callback provided by the fleet UI extension is regenerated every
// time the policy data is updated, which means this will go into a continuous loop if we don't
// actually check to see if an update should be reported back to fleet
setLastUpdatedPolicy((prevState) => {
if (prevState === updatedPolicy) {
return prevState;
}
if (updatedPolicy) {
onChange({
isValid: true,
// send up only the updated policy data which is stored in the `inputs` section.
// All other attributes (like name, id) are updated from the Fleet form, so we want to
// ensure we don't override it.
updatedPolicy: {
// Casting is needed due to the use of `Immutable<>` in our store data
inputs: updatedPolicy.inputs as unknown as NewPackagePolicy['inputs'],
},
});
}
return updatedPolicy;
});
}, [onChange, updatedPolicy]);
return (
<div data-test-subj="endpointIntegrationPolicyForm">
<>
<EndpointPolicyArtifactCards policyId={policyId} />
<div>
<EuiText>
<h5>
<FormattedMessage
id="xpack.securitySolution.endpoint.policyDetails.settings.title"
defaultMessage="Policy settings"
/>
</h5>
</EuiText>
<EuiSpacer size="s" />
{endpointDetailsLoadingError ? (
<EuiCallOut
title={
<FormattedMessage
id="xpack.securitySolution.endpoint.policyDetails.loadError"
defaultMessage="Failed to load endpoint policy settings"
/>
}
iconType="alert"
color="warning"
data-test-subj="endpiontPolicySettingsLoadingError"
>
{endpointDetailsLoadingError.message}
</EuiCallOut>
) : !endpointPolicyDetails ? (
<EuiLoadingSpinner size="l" className="essentialAnimation" />
) : (
<PolicyDetailsForm />
)}
</div>
</>
</div>
);
});
WrappedPolicyDetailsForm.displayName = 'WrappedPolicyDetailsForm';

View file

@ -0,0 +1,97 @@
/*
* 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 { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import React from 'react';
export const BLOCKLISTS_LABELS = {
artifactsSummaryApiError: (error: string) =>
i18n.translate('xpack.securitySolution.endpoint.fleetIntegrationCard.blocklistsSummary.error', {
defaultMessage: 'There was an error trying to fetch blocklist stats: "{error}"',
values: { error },
}),
cardTitle: (
<FormattedMessage
id="xpack.securitySolution.endpoint.blocklist.fleetIntegration.title"
defaultMessage="Blocklist"
/>
),
linkLabel: (
<FormattedMessage
id="xpack.securitySolution.endpoint.fleetIntegrationCard.blocklistsManageLabel"
defaultMessage="Manage blocklist"
/>
),
};
export const HOST_ISOLATION_EXCEPTIONS_LABELS = {
artifactsSummaryApiError: (error: string) =>
i18n.translate(
'xpack.securitySolution.endpoint.fleetIntegrationCard.hostIsolationExceptionsSummary.error',
{
defaultMessage:
'There was an error trying to fetch host isolation exceptions stats: "{error}"',
values: { error },
}
),
cardTitle: (
<FormattedMessage
id="xpack.securitySolution.endpoint.hostIsolationExceptions.fleetIntegration.title"
defaultMessage="Host isolation exceptions"
/>
),
linkLabel: (
<FormattedMessage
id="xpack.securitySolution.endpoint.fleetIntegrationCard.hostIsolationExceptionsManageLabel"
defaultMessage="Manage host isolation exceptions"
/>
),
};
export const EVENT_FILTERS_LABELS = {
artifactsSummaryApiError: (error: string) =>
i18n.translate(
'xpack.securitySolution.endpoint.fleetIntegrationCard.eventFiltersSummary.error',
{
defaultMessage: 'There was an error trying to fetch event filters stats: "{error}"',
values: { error },
}
),
cardTitle: (
<FormattedMessage
id="xpack.securitySolution.endpoint.eventFilters.fleetIntegration.title"
defaultMessage="Event filters"
/>
),
linkLabel: (
<FormattedMessage
id="xpack.securitySolution.endpoint.fleetIntegrationCard.eventFiltersManageLabel"
defaultMessage="Manage event filters"
/>
),
};
export const TRUSTED_APPS_LABELS = {
artifactsSummaryApiError: (error: string) =>
i18n.translate(
'xpack.securitySolution.endpoint.fleetIntegrationCard.trustedAppsSummary.error',
{
defaultMessage: 'There was an error trying to fetch trusted apps stats: "{error}"',
values: { error },
}
),
cardTitle: (
<FormattedMessage
id="xpack.securitySolution.endpoint.trustedApps.fleetIntegration.title"
defaultMessage="Trusted applications"
/>
),
linkLabel: (
<FormattedMessage
id="xpack.securitySolution.endpoint.fleetIntegrationCard.trustedAppsManageLabel"
defaultMessage="Manage trusted applications"
/>
),
};

View file

@ -0,0 +1,36 @@
/*
* 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 { useMemo } from 'react';
import { useEndpointPrivileges } from '../../../../../../common/components/user_privileges/endpoint';
/**
* Checks to see if the current user can access at least one artifact page.
* Note that this hook will return `false` if the Authz is still being loaded.
*/
export const useCanAccessSomeArtifacts = (): boolean => {
const {
canReadBlocklist,
canReadEventFilters,
canReadTrustedApplications,
canReadHostIsolationExceptions,
} = useEndpointPrivileges();
return useMemo(() => {
return (
canReadBlocklist ||
canReadEventFilters ||
canReadTrustedApplications ||
canReadHostIsolationExceptions
);
}, [
canReadBlocklist,
canReadEventFilters,
canReadTrustedApplications,
canReadHostIsolationExceptions,
]);
};

View file

@ -19,7 +19,7 @@ export const getLazyEndpointGenericErrorsListExtension = (
) => {
return lazy<PackageGenericErrorsListComponent>(async () => {
const [{ withSecurityContext }, { EndpointGenericErrorsList }] = await Promise.all([
import('./with_security_context/with_security_context'),
import('./components/with_security_context/with_security_context'),
import('./endpoint_generic_errors_list'),
]);

View file

@ -16,7 +16,7 @@ export const getLazyEndpointPackageCustomExtension = (
) => {
return lazy<PackageCustomExtensionComponent>(async () => {
const [{ withSecurityContext }, { EndpointPackageCustomExtension }] = await Promise.all([
import('./with_security_context/with_security_context'),
import('./components/with_security_context/with_security_context'),
import('./endpoint_package_custom_extension'),
]);
return {

View file

@ -19,8 +19,8 @@ export const getLazyEndpointPolicyEditExtension = (
) => {
return lazy<PackagePolicyEditExtensionComponent>(async () => {
const [{ withSecurityContext }, { EndpointPolicyEditExtension }] = await Promise.all([
import('./with_security_context/with_security_context'),
import('./endpoint_policy_edit_extension'),
import('./components/with_security_context/with_security_context'),
import('./endpoint_policy_edit_extension/endpoint_policy_edit_extension'),
]);
return {

View file

@ -19,7 +19,7 @@ export const getLazyEndpointPolicyResponseExtension = (
) => {
return lazy<PackagePolicyResponseExtensionComponent>(async () => {
const [{ withSecurityContext }, { EndpointPolicyResponseExtension }] = await Promise.all([
import('./with_security_context/with_security_context'),
import('./components/with_security_context/with_security_context'),
import('./endpoint_policy_response_extension'),
]);

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React, { useEffect, useMemo } from 'react';
import React, { useEffect } from 'react';
import type { Action, Reducer } from 'redux';
import type { RenderOptions } from '@testing-library/react';
// eslint-disable-next-line import/no-extraneous-dependencies
@ -17,7 +17,7 @@ import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import { SecuritySolutionQueryClient } from '../../../../../common/containers/query_client/query_client_provider';
import type { AppContextTestRender, UiRender } from '../../../../../common/mock/endpoint';
import { createAppRootMockRenderer } from '../../../../../common/mock/endpoint';
import { createFleetContextReduxStore } from './with_security_context/store';
import { createFleetContextReduxStore } from './components/with_security_context/store';
import type { ExperimentalFeatures } from '../../../../../../common/experimental_features';
import { allowedExperimentalValues } from '../../../../../../common/experimental_features';
import type { State } from '../../../../../common/store';
@ -25,7 +25,7 @@ import { mockGlobalState } from '../../../../../common/mock';
import { managementReducer } from '../../../../store/reducer';
import { appReducer } from '../../../../../common/store/app';
import { ExperimentalFeaturesService } from '../../../../../common/experimental_features_service';
import { RenderContextProviders } from './with_security_context/render_context_providers';
import { RenderContextProviders } from './components/with_security_context/render_context_providers';
import type { AppAction } from '../../../../../common/store/actions';
// Defined a private custom reducer that reacts to an action that enables us to update the
@ -84,17 +84,6 @@ export const createFleetContextRendererMock = (): AppContextTestRender => {
const queryClient = new SecuritySolutionQueryClient();
const Wrapper: RenderOptions['wrapper'] = ({ children }) => {
const services = useMemo(() => {
const { http, notifications, application } = mockedContext.coreStart;
return {
http,
notifications,
application,
data: mockedContext.depsStart.data,
};
}, []);
useEffect(() => {
return () => {
// When the component un-mounts, reset the Experimental features since
@ -108,7 +97,7 @@ export const createFleetContextRendererMock = (): AppContextTestRender => {
return (
<I18nProvider>
<EuiThemeProvider>
<KibanaContextProvider services={services}>
<KibanaContextProvider services={mockedContext.startServices}>
<RenderContextProviders
store={store}
depsStart={mockedContext.depsStart}

View file

@ -25486,15 +25486,9 @@
"xpack.securitySolution.endpoint.details.policy.revisionNumber": "rév. {revNumber}",
"xpack.securitySolution.endpoint.details.policyStatusValue": "{policyStatus, select, success {Succès} warning {Avertissement} failure {Échec} other {Inconnu}}",
"xpack.securitySolution.endpoint.fleetCustomExtension.artifactsSummaryError": "Une erreur s'est produite lors de la tentative de récupération des statistiques d'artefacts : \"{error}\"",
"xpack.securitySolution.endpoint.fleetCustomExtension.blocklistsSummarySummary.error": "Une erreur s'est produite lors de la tentative de récupération des statistiques de liste noire : \"{error}\"",
"xpack.securitySolution.endpoint.fleetCustomExtension.eventFiltersSummarySummary.error": "Une erreur s'est produite lors de la tentative de récupération des statistiques de filtres d'événements : \"{error}\"",
"xpack.securitySolution.endpoint.fleetCustomExtension.hostIsolationExceptionsSummarySummary.error": "Une erreur s'est produite lors de la tentative de récupération des statistiques d'exceptions d'isolation de l'hôte : \"{error}\"",
"xpack.securitySolution.endpoint.fleetCustomExtension.trustedAppsSummarySummary.error": "Une erreur s'est produite lors de la tentative de récupération des statistiques des applications de confiance : \"{error}\"",
"xpack.securitySolution.endpoint.fleetIntegrationCard.artifactsSummary.error": "Une erreur s'est produite lors de la tentative de récupération des statistiques d'artefacts : \"{error}\"",
"xpack.securitySolution.endpoint.fleetIntegrationCard.blocklistsSummary.error": "Une erreur s'est produite lors de la tentative de récupération des statistiques de listes noires : \"{error}\"",
"xpack.securitySolution.endpoint.fleetIntegrationCard.eventFiltersSummarySummary.error": "Une erreur s'est produite lors de la tentative de récupération des statistiques de filtres d'événements : \"{error}\"",
"xpack.securitySolution.endpoint.fleetIntegrationCard.hostIsolationExceptionsSummary.error": "Une erreur s'est produite lors de la tentative de récupération des statistiques d'exceptions d'isolation de l'hôte : \"{error}\"",
"xpack.securitySolution.endpoint.fleetIntegrationCard.trustedAppsSummarySummary.error": "Une erreur s'est produite lors de la tentative de récupération des statistiques des applications de confiance : \"{error}\"",
"xpack.securitySolution.endpoint.hostIsolation.isolateHost.casesAssociatedWithAlert": "{caseCount} {caseCount, plural, one {cas associé} other {cas associés}} à cet hôte",
"xpack.securitySolution.endpoint.hostIsolation.isolateThisHost": "Isoler l'hôte {hostName} du réseau.",
"xpack.securitySolution.endpoint.hostIsolation.isolation.successfulMessage": "L'isolation sur l'hôte {hostName} a été soumise avec succès",

View file

@ -25462,15 +25462,9 @@
"xpack.securitySolution.endpoint.details.policy.revisionNumber": "rev. {revNumber}",
"xpack.securitySolution.endpoint.details.policyStatusValue": "{policyStatus, select, success {成功} warning {警告} failure {失敗} other {不明}}",
"xpack.securitySolution.endpoint.fleetCustomExtension.artifactsSummaryError": "アーティファクト統計情報の取得中にエラーが発生しました:\"{error}\"",
"xpack.securitySolution.endpoint.fleetCustomExtension.blocklistsSummarySummary.error": "ブロックリスト統計情報の取得中にエラーが発生しました:\"{error}\"",
"xpack.securitySolution.endpoint.fleetCustomExtension.eventFiltersSummarySummary.error": "イベントフィルター統計情報の取得中にエラーが発生しました:\"{error}\"",
"xpack.securitySolution.endpoint.fleetCustomExtension.hostIsolationExceptionsSummarySummary.error": "ホスト分離例外統計情報の取得中にエラーが発生しました:\"{error}\"",
"xpack.securitySolution.endpoint.fleetCustomExtension.trustedAppsSummarySummary.error": "信頼できるアプリケーション統計情報の取得中にエラーが発生しました:\"{error}\"",
"xpack.securitySolution.endpoint.fleetIntegrationCard.artifactsSummary.error": "アーティファクト統計情報の取得中にエラーが発生しました:\"{error}\"",
"xpack.securitySolution.endpoint.fleetIntegrationCard.blocklistsSummary.error": "ブロックリスト統計情報の取得中にエラーが発生しました:\"{error}\"",
"xpack.securitySolution.endpoint.fleetIntegrationCard.eventFiltersSummarySummary.error": "イベントフィルター統計情報の取得中にエラーが発生しました:\"{error}\"",
"xpack.securitySolution.endpoint.fleetIntegrationCard.hostIsolationExceptionsSummary.error": "ホスト分離例外統計情報の取得中にエラーが発生しました:\"{error}\"",
"xpack.securitySolution.endpoint.fleetIntegrationCard.trustedAppsSummarySummary.error": "信頼できるアプリ統計情報の取得中にエラーが発生しました:\"{error}\"",
"xpack.securitySolution.endpoint.hostIsolation.isolateHost.casesAssociatedWithAlert": "{caseCount} {caseCount, plural, other {個のケース}}がこのホストに関連付けられています",
"xpack.securitySolution.endpoint.hostIsolation.isolateThisHost": "ホスト{hostName}をネットワークから分離します。",
"xpack.securitySolution.endpoint.hostIsolation.isolation.successfulMessage": "ホスト{hostName}での分離は正常に送信されました",

View file

@ -25495,15 +25495,9 @@
"xpack.securitySolution.endpoint.details.policy.revisionNumber": "修订版 {revNumber}",
"xpack.securitySolution.endpoint.details.policyStatusValue": "{policyStatus, select, success {成功} warning {警告} failure {失败} other {未知}}",
"xpack.securitySolution.endpoint.fleetCustomExtension.artifactsSummaryError": "尝试提取项目统计时出错:“{error}”",
"xpack.securitySolution.endpoint.fleetCustomExtension.blocklistsSummarySummary.error": "尝试提取阻止列表统计时出错:“{error}”",
"xpack.securitySolution.endpoint.fleetCustomExtension.eventFiltersSummarySummary.error": "尝试提取事件筛选统计时出错:“{error}”",
"xpack.securitySolution.endpoint.fleetCustomExtension.hostIsolationExceptionsSummarySummary.error": "尝试提取主机隔离例外统计时出错:“{error}”",
"xpack.securitySolution.endpoint.fleetCustomExtension.trustedAppsSummarySummary.error": "尝试提取受信任应用统计时出错:“{error}”",
"xpack.securitySolution.endpoint.fleetIntegrationCard.artifactsSummary.error": "尝试提取项目统计时出错:“{error}”",
"xpack.securitySolution.endpoint.fleetIntegrationCard.blocklistsSummary.error": "尝试提取阻止列表统计时出错:“{error}”",
"xpack.securitySolution.endpoint.fleetIntegrationCard.eventFiltersSummarySummary.error": "尝试提取事件筛选统计时出错:“{error}”",
"xpack.securitySolution.endpoint.fleetIntegrationCard.hostIsolationExceptionsSummary.error": "尝试提取主机隔离例外统计时出错:“{error}”",
"xpack.securitySolution.endpoint.fleetIntegrationCard.trustedAppsSummarySummary.error": "尝试提取受信任应用统计时出错:“{error}”",
"xpack.securitySolution.endpoint.hostIsolation.isolateHost.casesAssociatedWithAlert": "与此主机关联的 {caseCount} 个{caseCount, plural, other {案例}}",
"xpack.securitySolution.endpoint.hostIsolation.isolateThisHost": "从网络中隔离主机 {hostName}。",
"xpack.securitySolution.endpoint.hostIsolation.isolation.successfulMessage": "已成功提交主机 {hostName} 的隔离",