[Security Solution][Endpoint] Add Endpoint with event collection only to Serverless Security Essentials PLI (#162927)

## Summary

- Adds Endpoint Management and Policy Management to the base Security
Essentials Product Line Item in serverless
- Removes access to Endpoint policy protections (Malware, Ransomware,
etc) from the policy form when endpoint is being used without the
Endpoint Essentials/Complete addon
This commit is contained in:
Paul Tavares 2023-08-04 13:24:12 -04:00 committed by GitHub
parent 0069062fb4
commit 3efc73ca85
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
42 changed files with 543 additions and 132 deletions

1
.github/CODEOWNERS vendored
View file

@ -1242,6 +1242,7 @@ x-pack/plugins/cloud_integrations/cloud_full_story/server/config.ts @elastic/kib
/x-pack/test_serverless/functional/test_suites/security/cypress/e2e/endpoint_management @elastic/security-defend-workflows
/x-pack/test_serverless/functional/test_suites/security/cypress/screens/endpoint_management @elastic/security-defend-workflows
/x-pack/test_serverless/functional/test_suites/security/cypress/tasks/endpoint_management @elastic/security-defend-workflows
/x-pack/plugins/security_solution_serverless/public/upselling/sections/endpoint_management @elastic/security-defend-workflows
## Security Solution sub teams - security-telemetry (Data Engineering)
x-pack/plugins/security_solution/server/usage/ @elastic/security-data-analytics

View file

@ -16,8 +16,9 @@ import type {
DeleteAgentPolicyResponse,
PostDeletePackagePoliciesResponse,
} from '@kbn/fleet-plugin/common';
import { kibanaPackageJson } from '@kbn/repo-info';
import { AGENT_POLICY_API_ROUTES, PACKAGE_POLICY_API_ROUTES } from '@kbn/fleet-plugin/common';
import { memoize } from 'lodash';
import { getEndpointPackageInfo } from '../utils/package';
import type { PolicyData } from '../types';
import { policyFactory as policyConfigFactory } from '../models/policy_config';
import { wrapErrorAndRejectPromise } from './utils';
@ -34,7 +35,7 @@ export interface IndexedFleetEndpointPolicyResponse {
export const indexFleetEndpointPolicy = async (
kbnClient: KbnClient,
policyName: string,
endpointPackageVersion: string = kibanaPackageJson.version,
endpointPackageVersion?: string,
agentPolicyName?: string
): Promise<IndexedFleetEndpointPolicyResponse> => {
const response: IndexedFleetEndpointPolicyResponse = {
@ -42,6 +43,9 @@ export const indexFleetEndpointPolicy = async (
agentPolicies: [],
};
const packageVersion =
endpointPackageVersion ?? (await getDefaultEndpointPackageVersion(kbnClient));
// Create Agent Policy first
const newAgentPolicyData: CreateAgentPolicyRequest['body'] = {
name:
@ -89,7 +93,7 @@ export const indexFleetEndpointPolicy = async (
package: {
name: 'endpoint',
title: 'Elastic Defend',
version: endpointPackageVersion,
version: packageVersion,
},
};
const packagePolicy = (await kbnClient
@ -162,3 +166,12 @@ export const deleteIndexedFleetEndpointPolicies = async (
return response;
};
const getDefaultEndpointPackageVersion = memoize(
async (kbnClient: KbnClient) => {
return (await getEndpointPackageInfo(kbnClient)).version;
},
(kbnClient: KbnClient) => {
return kbnClient.resolveUrl('/');
}
);

View file

@ -27,6 +27,11 @@ export enum AppFeatureSecurityKey {
*/
endpointPolicyManagement = 'endpoint_policy_management',
/**
* Enables Endpoint Policy protections (like Malware, Ransomware, etc)
*/
endpointPolicyProtections = 'endpoint_policy_protections',
/**
* Enables management of all endpoint related artifacts (ex. Trusted Applications, Event Filters,
* Host Isolation Exceptions, Blocklist.

View file

@ -20,6 +20,7 @@ import { EuiThemeProvider } from '@kbn/kibana-react-plugin/common';
import { CellActionsProvider } from '@kbn/cell-actions';
import { NavigationProvider } from '@kbn/security-solution-navigation';
import { UpsellingProvider } from '../common/components/upselling_provider';
import { getComments } from '../assistant/get_comments';
import { augmentMessageCodeBlocks, LOCAL_STORAGE_KEY } from '../assistant/helpers';
import { useConversationStore } from '../assistant/use_conversation_store';
@ -65,6 +66,7 @@ const StartAppComponent: FC<StartAppComponent> = ({
http,
triggersActionsUi: { actionTypeRegistry },
uiActions,
upselling,
} = services;
const { conversations, setConversations } = useConversationStore();
@ -115,13 +117,15 @@ const StartAppComponent: FC<StartAppComponent> = ({
<CellActionsProvider
getTriggerCompatibleActions={uiActions.getTriggerCompatibleActions}
>
<PageRouter
history={history}
onAppLeave={onAppLeave}
setHeaderActionMenu={setHeaderActionMenu}
>
{children}
</PageRouter>
<UpsellingProvider upsellingService={upselling}>
<PageRouter
history={history}
onAppLeave={onAppLeave}
setHeaderActionMenu={setHeaderActionMenu}
>
{children}
</PageRouter>
</UpsellingProvider>
</CellActionsProvider>
</ReactQueryClientProvider>
</NavigationProvider>

View file

@ -0,0 +1,8 @@
/*
* 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 * from './upselling_provider';

View file

@ -0,0 +1,34 @@
/*
* 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, useContext } from 'react';
import type { UpsellingService } from '../../..';
export const UpsellingProviderContext = React.createContext<UpsellingService | null>(null);
export type UpsellingProviderProps = React.PropsWithChildren<{
upsellingService: UpsellingService;
}>;
export const UpsellingProvider = memo<UpsellingProviderProps>(({ upsellingService, children }) => {
return (
<UpsellingProviderContext.Provider value={upsellingService}>
{children}
</UpsellingProviderContext.Provider>
);
});
UpsellingProvider.displayName = 'UpsellingProvider';
export const useUpsellingService = (): UpsellingService => {
const upsellingService = useContext(UpsellingProviderContext);
if (!upsellingService) {
throw new Error('UpsellingProviderContext not found');
}
return upsellingService;
};

View file

@ -10,6 +10,7 @@ import React from 'react';
import { SecurityPageName } from '../../../common';
import { UpsellingService } from '../lib/upsellings';
import { useUpsellingComponent, useUpsellingMessage, useUpsellingPage } from './use_upselling';
import { UpsellingProvider } from '../components/upselling_provider';
const mockUpselling = new UpsellingService();
@ -28,6 +29,9 @@ jest.mock('../lib/kibana', () => {
});
const TestComponent = () => <div>{'TEST 1 2 3'}</div>;
const RenderWrapper: React.FunctionComponent = ({ children }) => {
return <UpsellingProvider upsellingService={mockUpselling}>{children}</UpsellingProvider>;
};
describe('use_upselling', () => {
test('useUpsellingComponent returns sections', () => {
@ -35,7 +39,9 @@ describe('use_upselling', () => {
entity_analytics_panel: TestComponent,
});
const { result } = renderHook(() => useUpsellingComponent('entity_analytics_panel'));
const { result } = renderHook(() => useUpsellingComponent('entity_analytics_panel'), {
wrapper: RenderWrapper,
});
expect(result.current).toBe(TestComponent);
});
@ -44,7 +50,9 @@ describe('use_upselling', () => {
[SecurityPageName.hosts]: TestComponent,
});
const { result } = renderHook(() => useUpsellingPage(SecurityPageName.hosts));
const { result } = renderHook(() => useUpsellingPage(SecurityPageName.hosts), {
wrapper: RenderWrapper,
});
expect(result.current).toBe(TestComponent);
});
@ -54,7 +62,9 @@ describe('use_upselling', () => {
investigation_guide: testMessage,
});
const { result } = renderHook(() => useUpsellingMessage('investigation_guide'));
const { result } = renderHook(() => useUpsellingMessage('investigation_guide'), {
wrapper: RenderWrapper,
});
expect(result.current).toBe(testMessage);
});
@ -62,8 +72,11 @@ describe('use_upselling', () => {
const emptyMessages = {};
mockUpselling.registerMessages(emptyMessages);
const { result } = renderHook(() =>
useUpsellingMessage('my_fake_message_id' as 'investigation_guide')
const { result } = renderHook(
() => useUpsellingMessage('my_fake_message_id' as 'investigation_guide'),
{
wrapper: RenderWrapper,
}
);
expect(result.current).toBe(null);
});

View file

@ -7,27 +7,28 @@
import { useMemo } from 'react';
import useObservable from 'react-use/lib/useObservable';
import type React from 'react';
import { useUpsellingService } from '../components/upselling_provider';
import type { UpsellingSectionId } from '../lib/upsellings';
import { useKibana } from '../lib/kibana';
import type { SecurityPageName } from '../../../common';
import type { UpsellingMessageId } from '../lib/upsellings/types';
export const useUpsellingComponent = (id: UpsellingSectionId): React.ComponentType | null => {
const { upselling } = useKibana().services;
const upselling = useUpsellingService();
const upsellingSections = useObservable(upselling.sections$);
return useMemo(() => upsellingSections?.get(id) ?? null, [id, upsellingSections]);
};
export const useUpsellingMessage = (id: UpsellingMessageId): string | null => {
const { upselling } = useKibana().services;
const upselling = useUpsellingService();
const upsellingMessages = useObservable(upselling.messages$);
return useMemo(() => upsellingMessages?.get(id) ?? null, [id, upsellingMessages]);
};
export const useUpsellingPage = (pageName: SecurityPageName): React.ComponentType | null => {
const { upselling } = useKibana().services;
const upselling = useUpsellingService();
const UpsellingPage = useMemo(() => upselling.getPageUpselling(pageName), [pageName, upselling]);
return UpsellingPage ?? null;

View file

@ -11,6 +11,6 @@ export type PageUpsellings = Partial<Record<SecurityPageName, React.ComponentTyp
export type MessageUpsellings = Partial<Record<UpsellingMessageId, string>>;
export type SectionUpsellings = Partial<Record<UpsellingSectionId, React.ComponentType>>;
export type UpsellingSectionId = 'entity_analytics_panel';
export type UpsellingSectionId = 'entity_analytics_panel' | 'endpointPolicyProtections';
export type UpsellingMessageId = 'investigation_guide';

View file

@ -12,7 +12,7 @@ import { createMemoryHistory } from 'history';
import type { RenderOptions, RenderResult } from '@testing-library/react';
import { render as reactRender } from '@testing-library/react';
import type { Action, Reducer, Store } from 'redux';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { QueryClient } from '@tanstack/react-query';
import { coreMock } from '@kbn/core/public/mocks';
import { PLUGIN_ID } from '@kbn/fleet-plugin/common';
import type { RenderHookOptions, RenderHookResult } from '@testing-library/react-hooks';
@ -23,11 +23,9 @@ import type {
} from '@testing-library/react-hooks/src/types/react';
import type { UseBaseQueryResult } from '@tanstack/react-query';
import ReactDOM from 'react-dom';
import { NavigationProvider } from '@kbn/security-solution-navigation';
import type { AppLinkItems } from '../../links/types';
import { ExperimentalFeaturesService } from '../../experimental_features_service';
import { applyIntersectionObserverMock } from '../intersection_observer_mock';
import { ConsoleManager } from '../../../management/components/console';
import type { StartPlugins, StartServices } from '../../../types';
import { depsStartMock } from './dependencies_start_mock';
import type { MiddlewareActionSpyHelper } from '../../store/test_utils';
@ -41,7 +39,7 @@ import { createStartServicesMock } from '../../lib/kibana/kibana_react.mock';
import { SUB_PLUGINS_REDUCER, mockGlobalState, createSecuritySolutionStorageMock } from '..';
import type { ExperimentalFeatures } from '../../../../common/experimental_features';
import { APP_UI_ID, APP_PATH } from '../../../../common/constants';
import { KibanaContextProvider, KibanaServices } from '../../lib/kibana';
import { KibanaServices } from '../../lib/kibana';
import { links } from '../../links/app_links';
import { fleetGetPackageHttpMock } from '../../../management/mocks';
import { allowedExperimentalValues } from '../../../../common/experimental_features';
@ -238,15 +236,16 @@ export const createAppRootMockRenderer = (): AppContextTestRender => {
});
const AppWrapper: React.FC<{ children: React.ReactElement }> = ({ children }) => (
<KibanaContextProvider services={startServices}>
<AppRootProvider store={store} history={history} coreStart={coreStart} depsStart={depsStart}>
<QueryClientProvider client={queryClient}>
<NavigationProvider core={startServices}>
<ConsoleManager>{children}</ConsoleManager>
</NavigationProvider>
</QueryClientProvider>
</AppRootProvider>
</KibanaContextProvider>
<AppRootProvider
store={store}
history={history}
coreStart={coreStart}
depsStart={depsStart}
startServices={startServices}
queryClient={queryClient}
>
{children}
</AppRootProvider>
);
const render: UiRender = (ui, options) => {

View file

@ -6,7 +6,7 @@
*/
import type { ReactNode } from 'react';
import React, { memo, useMemo } from 'react';
import React, { memo } from 'react';
import { Provider } from 'react-redux';
import { I18nProvider } from '@kbn/i18n-react';
import { Router } from '@kbn/shared-ux-router';
@ -17,9 +17,13 @@ import { EuiThemeProvider } from '@kbn/kibana-react-plugin/common';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import type { CoreStart } from '@kbn/core/public';
import { NavigationProvider } from '@kbn/security-solution-navigation';
import type { QueryClient } from '@tanstack/react-query';
import { QueryClientProvider } from '@tanstack/react-query';
import { UpsellingProvider } from '../../components/upselling_provider';
import { ConsoleManager } from '../../../management/components/console';
import { MockAssistantProvider } from '../mock_assistant_provider';
import { RouteCapture } from '../../components/endpoint/route_capture';
import type { StartPlugins } from '../../../types';
import type { StartPlugins, StartServices } from '../../../types';
/**
* Provides the context for rendering the endpoint app
@ -29,26 +33,31 @@ export const AppRootProvider = memo<{
history: History;
coreStart: CoreStart;
depsStart: Pick<StartPlugins, 'data' | 'fleet'>;
startServices: StartServices;
queryClient: QueryClient;
children: ReactNode | ReactNode[];
}>(({ store, history, coreStart, depsStart: { data }, children }) => {
const { http, notifications, uiSettings, application } = coreStart;
}>(({ store, history, coreStart, depsStart: { data }, queryClient, startServices, children }) => {
const { uiSettings } = coreStart;
const isDarkMode = useObservable<boolean>(uiSettings.get$('theme:darkMode'));
const services = useMemo(
() => ({ http, notifications, application, data }),
[application, data, http, notifications]
);
return (
<Provider store={store}>
<I18nProvider>
<KibanaContextProvider services={services}>
<KibanaContextProvider services={startServices}>
<EuiThemeProvider darkMode={isDarkMode}>
<MockAssistantProvider>
<NavigationProvider core={coreStart}>
<Router history={history}>
<RouteCapture>{children}</RouteCapture>
</Router>
</NavigationProvider>
</MockAssistantProvider>
<QueryClientProvider client={queryClient}>
<UpsellingProvider upsellingService={startServices.upselling}>
<MockAssistantProvider>
<NavigationProvider core={coreStart}>
<Router history={history}>
<ConsoleManager>
<RouteCapture>{children}</RouteCapture>
</ConsoleManager>
</Router>
</NavigationProvider>
</MockAssistantProvider>
</UpsellingProvider>
</QueryClientProvider>
</EuiThemeProvider>
</KibanaContextProvider>
</I18nProvider>

View file

@ -21,6 +21,8 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import type { Action } from '@kbn/ui-actions-plugin/public';
import { CellActionsProvider } from '@kbn/cell-actions';
import { ExpandableFlyoutProvider } from '@kbn/expandable-flyout';
import { useKibana } from '../lib/kibana';
import { UpsellingProvider } from '../components/upselling_provider';
import { MockAssistantProvider } from './mock_assistant_provider';
import { ConsoleManager } from '../../management/components/console';
import type { State } from '../store';
@ -68,31 +70,40 @@ export const TestProvidersComponent: React.FC<Props> = ({
},
},
});
return (
<I18nProvider>
<MockKibanaContextProvider>
<ReduxStoreProvider store={store}>
<ThemeProvider theme={() => ({ eui: euiDarkVars, darkMode: true })}>
<MockAssistantProvider>
<QueryClientProvider client={queryClient}>
<ExpandableFlyoutProvider>
<ConsoleManager>
<CellActionsProvider
getTriggerCompatibleActions={() => Promise.resolve(cellActions)}
>
<DragDropContext onDragEnd={onDragEnd}>{children}</DragDropContext>
</CellActionsProvider>
</ConsoleManager>
</ExpandableFlyoutProvider>
</QueryClientProvider>
</MockAssistantProvider>
</ThemeProvider>
</ReduxStoreProvider>
<UpsellingProviderMock>
<ReduxStoreProvider store={store}>
<ThemeProvider theme={() => ({ eui: euiDarkVars, darkMode: true })}>
<MockAssistantProvider>
<QueryClientProvider client={queryClient}>
<ExpandableFlyoutProvider>
<ConsoleManager>
<CellActionsProvider
getTriggerCompatibleActions={() => Promise.resolve(cellActions)}
>
<DragDropContext onDragEnd={onDragEnd}>{children}</DragDropContext>
</CellActionsProvider>
</ConsoleManager>
</ExpandableFlyoutProvider>
</QueryClientProvider>
</MockAssistantProvider>
</ThemeProvider>
</ReduxStoreProvider>
</UpsellingProviderMock>
</MockKibanaContextProvider>
</I18nProvider>
);
};
const UpsellingProviderMock = ({ children }: React.PropsWithChildren<{}>) => {
const upselingService = useKibana().services.upselling;
return <UpsellingProvider upsellingService={upselingService}>{children}</UpsellingProvider>;
};
/**
* A utility for wrapping children in the providers required to run most tests
* WITH user privileges provider.

View file

@ -88,7 +88,7 @@ export const dataLoaders = (
agentPolicyName,
}: {
policyName: string;
endpointPackageVersion: string;
endpointPackageVersion?: string;
agentPolicyName?: string;
}) => {
const { kbnClient } = await stackServicesPromise;

View file

@ -10,6 +10,7 @@ import React, { memo } from 'react';
import { Provider as ReduxStoreProvider } from 'react-redux';
import type { Store } from 'redux';
import { NavigationProvider } from '@kbn/security-solution-navigation';
import { UpsellingProvider } from '../../../../../../../common/components/upselling_provider';
import { UserPrivilegesProvider } from '../../../../../../../common/components/user_privileges/user_privileges_context';
import type { SecuritySolutionQueryClient } from '../../../../../../../common/containers/query_client/query_client_provider';
import { ReactQueryClientProvider } from '../../../../../../../common/containers/query_client/query_client_provider';
@ -17,15 +18,17 @@ import { SecuritySolutionStartDependenciesContext } from '../../../../../../../c
import { CurrentLicense } from '../../../../../../../common/components/current_license';
import type { StartPlugins } from '../../../../../../../types';
import { useKibana } from '../../../../../../../common/lib/kibana';
import type { UpsellingService } from '../../../../../../..';
export type RenderContextProvidersProps = PropsWithChildren<{
store: Store;
depsStart: Pick<StartPlugins, 'data' | 'fleet'>;
upsellingService: UpsellingService;
queryClient?: SecuritySolutionQueryClient;
}>;
export const RenderContextProviders = memo<RenderContextProvidersProps>(
({ store, depsStart, queryClient, children }) => {
({ store, depsStart, queryClient, upsellingService, children }) => {
const services = useKibana().services;
const {
application: { capabilities },
@ -36,7 +39,11 @@ export const RenderContextProviders = memo<RenderContextProvidersProps>(
<SecuritySolutionStartDependenciesContext.Provider value={depsStart}>
<UserPrivilegesProvider kibanaCapabilities={capabilities}>
<NavigationProvider core={services}>
<CurrentLicense>{children}</CurrentLicense>
<CurrentLicense>
<UpsellingProvider upsellingService={upsellingService}>
{children}
</UpsellingProvider>
</CurrentLicense>
</NavigationProvider>
</UserPrivilegesProvider>
</SecuritySolutionStartDependenciesContext.Provider>

View file

@ -7,14 +7,11 @@
import type { ComponentType } from 'react';
import React, { memo } from 'react';
import type { CoreStart } from '@kbn/core/public';
import type { StartPlugins } from '../../../../../../../types';
import { createFleetContextReduxStore } from './store';
import { RenderContextProviders } from './render_context_providers';
import type { FleetUiExtensionGetterOptions } from '../../types';
interface WithSecurityContextProps<P extends {}> {
coreStart: CoreStart;
depsStart: Pick<StartPlugins, 'data' | 'fleet'>;
interface WithSecurityContextProps<P extends {}> extends FleetUiExtensionGetterOptions {
WrappedComponent: ComponentType<P>;
}
@ -30,6 +27,7 @@ interface WithSecurityContextProps<P extends {}> {
export const withSecurityContext = <P extends {}>({
coreStart,
depsStart,
services: { upsellingService },
WrappedComponent,
}: WithSecurityContextProps<P>): ComponentType<P> => {
let store: ReturnType<typeof createFleetContextReduxStore>; // created on first render
@ -41,7 +39,11 @@ export const withSecurityContext = <P extends {}>({
}
return (
<RenderContextProviders store={store} depsStart={depsStart}>
<RenderContextProviders
store={store}
depsStart={depsStart}
upsellingService={upsellingService}
>
<WrappedComponent {...props} />
</RenderContextProviders>
);

View file

@ -6,17 +6,17 @@
*/
import { lazy } from 'react';
import type { CoreStart } from '@kbn/core/public';
import type {
PackageGenericErrorsListComponent,
PackageGenericErrorsListProps,
} from '@kbn/fleet-plugin/public';
import type { StartPlugins } from '../../../../../types';
import type { FleetUiExtensionGetterOptions } from './types';
export const getLazyEndpointGenericErrorsListExtension = (
coreStart: CoreStart,
depsStart: Pick<StartPlugins, 'data' | 'fleet'>
) => {
export const getLazyEndpointGenericErrorsListExtension = ({
coreStart,
depsStart,
services,
}: FleetUiExtensionGetterOptions) => {
return lazy<PackageGenericErrorsListComponent>(async () => {
const [{ withSecurityContext }, { EndpointGenericErrorsList }] = await Promise.all([
import('./components/with_security_context/with_security_context'),
@ -27,6 +27,7 @@ export const getLazyEndpointGenericErrorsListExtension = (
default: withSecurityContext<PackageGenericErrorsListProps>({
coreStart,
depsStart,
services,
WrappedComponent: EndpointGenericErrorsList,
}),
};

View file

@ -5,15 +5,15 @@
* 2.0.
*/
import type { CoreStart } from '@kbn/core/public';
import { lazy } from 'react';
import type { PackageCustomExtensionComponent } from '@kbn/fleet-plugin/public';
import type { StartPlugins } from '../../../../../types';
import type { FleetUiExtensionGetterOptions } from './types';
export const getLazyEndpointPackageCustomExtension = (
coreStart: CoreStart,
depsStart: Pick<StartPlugins, 'data' | 'fleet'>
) => {
export const getLazyEndpointPackageCustomExtension = ({
coreStart,
depsStart,
services,
}: FleetUiExtensionGetterOptions) => {
return lazy<PackageCustomExtensionComponent>(async () => {
const [{ withSecurityContext }, { EndpointPackageCustomExtension }] = await Promise.all([
import('./components/with_security_context/with_security_context'),
@ -23,6 +23,7 @@ export const getLazyEndpointPackageCustomExtension = (
default: withSecurityContext({
coreStart,
depsStart,
services,
WrappedComponent: EndpointPackageCustomExtension,
}),
};

View file

@ -6,17 +6,17 @@
*/
import { lazy } from 'react';
import type { CoreStart } from '@kbn/core/public';
import type {
PackagePolicyEditExtensionComponent,
PackagePolicyEditExtensionComponentProps,
} from '@kbn/fleet-plugin/public';
import type { StartPlugins } from '../../../../../types';
import type { FleetUiExtensionGetterOptions } from './types';
export const getLazyEndpointPolicyEditExtension = (
coreStart: CoreStart,
depsStart: Pick<StartPlugins, 'data' | 'fleet'>
) => {
export const getLazyEndpointPolicyEditExtension = ({
coreStart,
depsStart,
services,
}: FleetUiExtensionGetterOptions) => {
return lazy<PackagePolicyEditExtensionComponent>(async () => {
const [{ withSecurityContext }, { EndpointPolicyEditExtension }] = await Promise.all([
import('./components/with_security_context/with_security_context'),
@ -27,6 +27,7 @@ export const getLazyEndpointPolicyEditExtension = (
default: withSecurityContext<PackagePolicyEditExtensionComponentProps>({
coreStart,
depsStart,
services,
WrappedComponent: EndpointPolicyEditExtension,
}),
};

View file

@ -6,17 +6,17 @@
*/
import { lazy } from 'react';
import type { CoreStart } from '@kbn/core/public';
import type {
PackagePolicyResponseExtensionComponent,
PackagePolicyResponseExtensionComponentProps,
} from '@kbn/fleet-plugin/public';
import type { StartPlugins } from '../../../../../types';
import type { FleetUiExtensionGetterOptions } from './types';
export const getLazyEndpointPolicyResponseExtension = (
coreStart: CoreStart,
depsStart: Pick<StartPlugins, 'data' | 'fleet'>
) => {
export const getLazyEndpointPolicyResponseExtension = ({
coreStart,
depsStart,
services,
}: FleetUiExtensionGetterOptions) => {
return lazy<PackagePolicyResponseExtensionComponent>(async () => {
const [{ withSecurityContext }, { EndpointPolicyResponseExtension }] = await Promise.all([
import('./components/with_security_context/with_security_context'),
@ -27,6 +27,7 @@ export const getLazyEndpointPolicyResponseExtension = (
default: withSecurityContext<PackagePolicyResponseExtensionComponentProps>({
coreStart,
depsStart,
services,
WrappedComponent: EndpointPolicyResponseExtension,
}),
};

View file

@ -102,7 +102,12 @@ export const createFleetContextRendererMock = (): AppContextTestRender => {
<I18nProvider>
<EuiThemeProvider>
<KibanaContextProvider services={startServices}>
<RenderContextProviders store={store} depsStart={depsStart} queryClient={queryClient}>
<RenderContextProviders
store={store}
depsStart={depsStart}
queryClient={queryClient}
upsellingService={startServices.upselling}
>
{children}
</RenderContextProviders>
</KibanaContextProvider>

View file

@ -0,0 +1,18 @@
/*
* 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 { CoreStart } from '@kbn/core-lifecycle-browser';
import type { StartPlugins } from '../../../../../types';
import type { UpsellingService } from '../../../../..';
export interface FleetUiExtensionGetterOptions {
coreStart: CoreStart;
depsStart: Pick<StartPlugins, 'data' | 'fleet'>;
services: {
upsellingService: UpsellingService;
};
}

View file

@ -10,6 +10,7 @@ import { OperatingSystem } from '@kbn/securitysolution-utils';
import { i18n } from '@kbn/i18n';
import { EuiSpacer, EuiSwitch, EuiText } from '@elastic/eui';
import { cloneDeep } from 'lodash';
import { useGetProtectionsUnavailableComponent } from '../../hooks/use_get_protections_unavailable_component';
import { useTestIdGenerator } from '../../../../../../hooks/use_test_id_generator';
import { SettingCard } from '../setting_card';
import type { PolicyFormComponentCommonProps } from '../../types';
@ -49,6 +50,7 @@ export type AntivirusRegistrationCardProps = PolicyFormComponentCommonProps;
export const AntivirusRegistrationCard = memo<AntivirusRegistrationCardProps>(
({ policy, onChange, mode, 'data-test-subj': dataTestSubj }) => {
const getTestId = useTestIdGenerator(dataTestSubj);
const isProtectionsAllowed = !useGetProtectionsUnavailableComponent();
const isChecked = policy.windows.antivirus_registration.enabled;
const isEditMode = mode === 'edit';
const label = isChecked ? REGISTERED_LABEL : NOT_REGISTERED_LABEL;
@ -63,6 +65,10 @@ export const AntivirusRegistrationCard = memo<AntivirusRegistrationCardProps>(
[onChange, policy]
);
if (!isProtectionsAllowed) {
return null;
}
return (
<SettingCard
type={CARD_TITLE}

View file

@ -10,6 +10,7 @@ import { OperatingSystem } from '@kbn/securitysolution-utils';
import { EuiSwitch } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { cloneDeep } from 'lodash';
import { useGetProtectionsUnavailableComponent } from '../../hooks/use_get_protections_unavailable_component';
import { useTestIdGenerator } from '../../../../../../hooks/use_test_id_generator';
import { useLicense } from '../../../../../../../common/hooks/use_license';
import { SettingLockedCard } from '../setting_locked_card';
@ -52,6 +53,7 @@ export const AttackSurfaceReductionCard = memo<AttackSurfaceReductionCardProps>(
({ policy, onChange, mode, 'data-test-subj': dataTestSubj }) => {
const isPlatinumPlus = useLicense().isPlatinumPlus();
const getTestId = useTestIdGenerator(dataTestSubj);
const isProtectionsAllowed = !useGetProtectionsUnavailableComponent();
const isChecked = policy.windows.attack_surface_reduction.credential_hardening.enabled;
const isEditMode = mode === 'edit';
const label = isChecked ? SWITCH_ENABLED_LABEL : SWITCH_DISABLED_LABEL;
@ -68,6 +70,10 @@ export const AttackSurfaceReductionCard = memo<AttackSurfaceReductionCardProps>(
[onChange, policy]
);
if (!isProtectionsAllowed) {
return null;
}
if (!isPlatinumPlus) {
return (
<SettingLockedCard

View file

@ -11,6 +11,7 @@ import { FormattedMessage } from '@kbn/i18n-react';
import { EuiSpacer, EuiSwitch, EuiFlexGroup, EuiFlexItem, EuiIconTip } from '@elastic/eui';
import { OperatingSystem } from '@kbn/securitysolution-utils';
import { cloneDeep } from 'lodash';
import { useGetProtectionsUnavailableComponent } from '../../hooks/use_get_protections_unavailable_component';
import { RelatedDetectionRulesCallout } from '../related_detection_rules_callout';
import { NotifyUserOption } from '../notify_user_option';
import { SettingCard } from '../setting_card';
@ -55,12 +56,14 @@ const MALWARE_OS_VALUES: Immutable<MalwareProtectionOSes[]> = [
export type MalwareProtectionsProps = PolicyFormComponentCommonProps;
/** The Malware Protections form for policy details
* which will configure for all relevant OSes.
/**
* The Malware Protections form for policy details
* which will configure for all relevant OSes.
*/
export const MalwareProtectionsCard = React.memo<MalwareProtectionsProps>(
({ policy, onChange, mode = 'edit', 'data-test-subj': dataTestSubj }) => {
const getTestId = useTestIdGenerator(dataTestSubj);
const isProtectionsAllowed = !useGetProtectionsUnavailableComponent();
const protection = 'malware';
const protectionLabel = i18n.translate(
'xpack.securitySolution.endpoint.policy.protections.malware',
@ -69,6 +72,10 @@ export const MalwareProtectionsCard = React.memo<MalwareProtectionsProps>(
}
);
if (!isProtectionsAllowed) {
return null;
}
return (
<SettingCard
type={i18n.translate('xpack.securitySolution.endpoint.policy.details.malware', {

View file

@ -9,6 +9,7 @@ import React, { memo } from 'react';
import { i18n } from '@kbn/i18n';
import { OperatingSystem } from '@kbn/securitysolution-utils';
import { EuiSpacer } from '@elastic/eui';
import { useGetProtectionsUnavailableComponent } from '../../hooks/use_get_protections_unavailable_component';
import { useTestIdGenerator } from '../../../../../../hooks/use_test_id_generator';
import { NotifyUserOption } from '../notify_user_option';
import { DetectPreventProtectionLevel } from '../detect_prevent_protection_level';
@ -41,6 +42,7 @@ export const MemoryProtectionCard = memo<MemoryProtectionCardProps>(
({ policy, onChange, mode, 'data-test-subj': dataTestSubj }) => {
const isPlatinumPlus = useLicense().isPlatinumPlus();
const getTestId = useTestIdGenerator(dataTestSubj);
const isProtectionsAllowed = !useGetProtectionsUnavailableComponent();
const protection = 'memory_protection';
const protectionLabel = i18n.translate(
'xpack.securitySolution.endpoint.policy.protections.memory',
@ -49,6 +51,10 @@ export const MemoryProtectionCard = memo<MemoryProtectionCardProps>(
}
);
if (!isProtectionsAllowed) {
return null;
}
if (!isPlatinumPlus) {
return (
<SettingLockedCard title={LOCKED_CARD_MEMORY_TITLE} data-test-subj={getTestId('locked')} />

View file

@ -9,6 +9,7 @@ import React, { memo } from 'react';
import { i18n } from '@kbn/i18n';
import { OperatingSystem } from '@kbn/securitysolution-utils';
import { EuiSpacer } from '@elastic/eui';
import { useGetProtectionsUnavailableComponent } from '../../../hooks/use_get_protections_unavailable_component';
import { RelatedDetectionRulesCallout } from '../../related_detection_rules_callout';
import { ReputationService } from './components/reputation_service';
import { useTestIdGenerator } from '../../../../../../../hooks/use_test_id_generator';
@ -42,6 +43,7 @@ export const BehaviourProtectionCard = memo<BehaviourProtectionCardProps>(
({ policy, onChange, mode, 'data-test-subj': dataTestSubj }) => {
const isPlatinumPlus = useLicense().isPlatinumPlus();
const getTestId = useTestIdGenerator(dataTestSubj);
const isProtectionsAllowed = !useGetProtectionsUnavailableComponent();
const protection = 'behavior_protection';
const protectionLabel = i18n.translate(
'xpack.securitySolution.endpoint.policy.protections.behavior',
@ -50,6 +52,10 @@ export const BehaviourProtectionCard = memo<BehaviourProtectionCardProps>(
}
);
if (!isProtectionsAllowed) {
return null;
}
if (!isPlatinumPlus) {
return (
<SettingLockedCard

View file

@ -21,6 +21,7 @@ import type { RansomwareProtectionOSes } from '../../../../types';
import { useLicense } from '../../../../../../../common/hooks/use_license';
import { SettingLockedCard } from '../setting_locked_card';
import { RelatedDetectionRulesCallout } from '../related_detection_rules_callout';
import { useGetProtectionsUnavailableComponent } from '../../hooks/use_get_protections_unavailable_component';
const RANSOMEWARE_OS_VALUES: Immutable<RansomwareProtectionOSes[]> = [
PolicyOperatingSystem.windows,
@ -38,6 +39,7 @@ export type RansomwareProtectionCardProps = PolicyFormComponentCommonProps;
export const RansomwareProtectionCard = React.memo<RansomwareProtectionCardProps>(
({ policy, onChange, mode, 'data-test-subj': dataTestSubj }) => {
const isPlatinumPlus = useLicense().isPlatinumPlus();
const isProtectionsAllowed = !useGetProtectionsUnavailableComponent();
const getTestId = useTestIdGenerator(dataTestSubj);
const protection = 'ransomware';
const protectionLabel = i18n.translate(
@ -47,6 +49,10 @@ export const RansomwareProtectionCard = React.memo<RansomwareProtectionCardProps
}
);
if (!isProtectionsAllowed) {
return null;
}
if (!isPlatinumPlus) {
return (
<SettingLockedCard

View file

@ -0,0 +1,13 @@
/*
* 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 React from 'react';
import { useUpsellingComponent } from '../../../../../../common/hooks/use_upselling';
export const useGetProtectionsUnavailableComponent = (): React.ComponentType | null => {
return useUpsellingComponent('endpointPolicyProtections');
};

View file

@ -12,6 +12,7 @@ import { createAppRootMockRenderer } from '../../../../../common/mock/endpoint';
import type { PolicySettingsFormProps } from './policy_settings_form';
import { PolicySettingsForm } from './policy_settings_form';
import { FleetPackagePolicyGenerator } from '../../../../../../common/endpoint/data_generators/fleet_package_policy_generator';
import type { UpsellingService } from '../../../../../common/lib/upsellings';
jest.mock('../../../../../common/hooks/use_license');
@ -21,10 +22,13 @@ describe('Endpoint Policy Settings Form', () => {
let formProps: PolicySettingsFormProps;
let render: () => ReturnType<AppContextTestRender['render']>;
let renderResult: ReturnType<typeof render>;
let upsellingService: UpsellingService;
beforeEach(() => {
const mockedContext = createAppRootMockRenderer();
upsellingService = mockedContext.startServices.upselling;
formProps = {
policy: new FleetPackagePolicyGenerator('seed').generateEndpointPackagePolicy().inputs[0]
.config.policy.value,
@ -59,4 +63,30 @@ describe('Endpoint Policy Settings Form', () => {
expectIsViewOnly(renderResult.getByTestId('test'));
});
describe('and when policy protections are not available', () => {
beforeEach(() => {
upsellingService.registerSections({
endpointPolicyProtections: () => <div data-test-subj="paywall">{'pay up!'}</div>,
});
});
it.each([
['malware', testSubj.malware.card],
['ransomware', testSubj.ransomware.card],
['memory', testSubj.memory.card],
['behaviour', testSubj.behaviour.card],
['attack surface', testSubj.attackSurface.card],
['antivirus registration', testSubj.antivirusRegistration.card],
])('should include %s card', (_, testSubjSelector) => {
render();
expect(renderResult.queryByTestId(testSubjSelector)).toBeNull();
});
it('should display upselling component', () => {
render();
expect(renderResult.getByTestId('paywall'));
});
});
});

View file

@ -8,6 +8,7 @@
import React, { memo } from 'react';
import { EuiSpacer, EuiText } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useGetProtectionsUnavailableComponent } from './hooks/use_get_protections_unavailable_component';
import { AntivirusRegistrationCard } from './components/cards/antivirus_registration_card';
import { LinuxEventCollectionCard } from './components/cards/linux_event_collection_card';
import { MacEventCollectionCard } from './components/cards/mac_event_collection_card';
@ -35,26 +36,39 @@ export type PolicySettingsFormProps = PolicyFormComponentCommonProps;
export const PolicySettingsForm = memo<PolicySettingsFormProps>((props) => {
const getTestId = useTestIdGenerator(props['data-test-subj']);
const ProtectionsUpSellingComponent = useGetProtectionsUnavailableComponent();
return (
<div data-test-subj={getTestId()}>
<FormSectionTitle>{PROTECTIONS_SECTION_TITLE}</FormSectionTitle>
<EuiSpacer size="s" />
<MalwareProtectionsCard {...props} data-test-subj={getTestId('malware')} />
<EuiSpacer size="l" />
{ProtectionsUpSellingComponent && (
<>
<EuiSpacer size="m" />
<ProtectionsUpSellingComponent />
<EuiSpacer size="l" />
</>
)}
<RansomwareProtectionCard {...props} data-test-subj={getTestId('ransomware')} />
<EuiSpacer size="l" />
{!ProtectionsUpSellingComponent && (
<>
<MalwareProtectionsCard {...props} data-test-subj={getTestId('malware')} />
<EuiSpacer size="l" />
<MemoryProtectionCard {...props} data-test-subj={getTestId('memory')} />
<EuiSpacer size="l" />
<RansomwareProtectionCard {...props} data-test-subj={getTestId('ransomware')} />
<EuiSpacer size="l" />
<BehaviourProtectionCard {...props} data-test-subj={getTestId('behaviour')} />
<EuiSpacer size="l" />
<MemoryProtectionCard {...props} data-test-subj={getTestId('memory')} />
<EuiSpacer size="l" />
<AttackSurfaceReductionCard {...props} data-test-subj={getTestId('attackSurface')} />
<EuiSpacer size="l" />
<BehaviourProtectionCard {...props} data-test-subj={getTestId('behaviour')} />
<EuiSpacer size="l" />
<AttackSurfaceReductionCard {...props} data-test-subj={getTestId('attackSurface')} />
<EuiSpacer size="l" />
</>
)}
<FormSectionTitle>{SETTINGS_SECTION_TITLE}</FormSectionTitle>
<EuiSpacer size="s" />

View file

@ -20,6 +20,7 @@ import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
import { FilterManager, NowProvider, QueryService } from '@kbn/data-plugin/public';
import { DEFAULT_APP_CATEGORIES, AppNavLinkStatus } from '@kbn/core/public';
import { Storage } from '@kbn/kibana-utils-plugin/public';
import type { FleetUiExtensionGetterOptions } from './management/pages/policy/view/ingest_manager_integration/types';
import type {
PluginSetup,
PluginStart,
@ -276,23 +277,30 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
if (plugins.fleet) {
const { registerExtension } = plugins.fleet;
const registerOptions: FleetUiExtensionGetterOptions = {
coreStart: core,
depsStart: plugins,
services: {
upsellingService: this.contract.upsellingService,
},
};
registerExtension({
package: 'endpoint',
view: 'package-policy-edit',
Component: getLazyEndpointPolicyEditExtension(core, plugins),
Component: getLazyEndpointPolicyEditExtension(registerOptions),
});
registerExtension({
package: 'endpoint',
view: 'package-policy-response',
Component: getLazyEndpointPolicyResponseExtension(core, plugins),
Component: getLazyEndpointPolicyResponseExtension(registerOptions),
});
registerExtension({
package: 'endpoint',
view: 'package-generic-errors-list',
Component: getLazyEndpointGenericErrorsListExtension(core, plugins),
Component: getLazyEndpointGenericErrorsListExtension(registerOptions),
});
registerExtension({
@ -310,7 +318,7 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
registerExtension({
package: 'endpoint',
view: 'package-detail-custom',
Component: getLazyEndpointPackageCustomExtension(core, plugins),
Component: getLazyEndpointPackageCustomExtension(registerOptions),
});
registerExtension({

View file

@ -183,6 +183,9 @@ export const getSecurityAppFeaturesConfig = (
subFeatureIds: [SecuritySubFeatureId.policyManagement],
},
// Adds no additional kibana feature controls
[AppFeatureSecurityKey.endpointPolicyProtections]: {},
[AppFeatureSecurityKey.endpointArtifactManagement]: {
subFeatureIds: [
SecuritySubFeatureId.trustedApplications,

View file

@ -168,5 +168,6 @@
"@kbn/navigation-plugin",
"@kbn/alerts-ui-shared",
"@kbn/core-logging-server-mocks",
"@kbn/core-lifecycle-browser",
]
}

View file

@ -14,7 +14,7 @@ type PliAppFeatures = Readonly<
export const PLI_APP_FEATURES: PliAppFeatures = {
security: {
essentials: [],
essentials: [AppFeatureKey.endpointHostManagement, AppFeatureKey.endpointPolicyManagement],
complete: [
AppFeatureKey.advancedInsights,
AppFeatureKey.investigationGuide,
@ -26,6 +26,7 @@ export const PLI_APP_FEATURES: PliAppFeatures = {
essentials: [
AppFeatureKey.endpointHostManagement,
AppFeatureKey.endpointPolicyManagement,
AppFeatureKey.endpointPolicyProtections,
AppFeatureKey.endpointArtifactManagement,
],
complete: [AppFeatureKey.endpointResponseActions],

View file

@ -68,13 +68,13 @@ describe('registerUpsellings', () => {
registerUpsellings(upselling, allProductTypes);
const expectedPagesObject = Object.fromEntries(
upsellingPages.map(({ pageName }) => [pageName, expect.any(Function)])
upsellingPages.map(({ pageName }) => [pageName, expect.any(Object)])
);
expect(registerPages).toHaveBeenCalledTimes(1);
expect(registerPages).toHaveBeenCalledWith(expectedPagesObject);
const expectedSectionsObject = Object.fromEntries(
upsellingSections.map(({ id }) => [id, expect.any(Function)])
upsellingSections.map(({ id }) => [id, expect.any(Object)])
);
expect(registerSections).toHaveBeenCalledTimes(1);
expect(registerSections).toHaveBeenCalledWith(expectedSectionsObject);

View file

@ -11,19 +11,25 @@ import type {
SectionUpsellings,
UpsellingSectionId,
} from '@kbn/security-solution-plugin/public';
import React, { lazy } from 'react';
import type {
MessageUpsellings,
UpsellingMessageId,
} from '@kbn/security-solution-plugin/public/common/lib/upsellings/types';
import React, { lazy } from 'react';
import { EndpointPolicyProtectionsLazy } from './sections/endpoint_management';
import type { SecurityProductTypes } from '../../common/config';
import { getProductAppFeatures } from '../../common/pli/pli_features';
import investigationGuideUpselling from './pages/investigation_guide_upselling';
const ThreatIntelligencePaywallLazy = lazy(() => import('./pages/threat_intelligence_paywall'));
const ThreatIntelligencePaywallLazy = lazy(async () => {
const ThreatIntelligencePaywall = (await import('./pages/threat_intelligence_paywall')).default;
return {
default: () => <ThreatIntelligencePaywall requiredPLI={AppFeatureKey.threatIntelligence} />,
};
});
interface UpsellingsConfig {
pli: AppFeatureKey;
component: React.ComponentType;
component: React.LazyExoticComponent<React.ComponentType>;
}
interface UpsellingsMessageConfig {
@ -89,9 +95,7 @@ export const upsellingPages: UpsellingPages = [
{
pageName: SecurityPageName.threatIntelligence,
pli: AppFeatureKey.threatIntelligence,
component: () => (
<ThreatIntelligencePaywallLazy requiredPLI={AppFeatureKey.threatIntelligence} />
),
component: ThreatIntelligencePaywallLazy,
},
];
@ -104,6 +108,12 @@ export const upsellingSections: UpsellingSections = [
// pli: AppFeatureKey.advancedInsights,
// component: () => <GenericUpsellingSectionLazy requiredPLI={AppFeatureKey.advancedInsights} />,
// },
{
id: 'endpointPolicyProtections',
pli: AppFeatureKey.endpointPolicyProtections,
component: EndpointPolicyProtectionsLazy,
},
];
// Upsellings for sections, linked by arbitrary ids

View file

@ -0,0 +1,61 @@
/*
* 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 { EuiCard, EuiIcon } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import styled from '@emotion/styled';
const CARD_TITLE = i18n.translate(
'xpack.securitySolutionServerless.endpointPolicyProtections.cardTitle',
{
defaultMessage: 'Policy Protections',
}
);
const CARD_MESSAGE = i18n.translate(
'xpack.securitySolutionServerless.endpointPolicyProtections.cardMessage',
{
defaultMessage:
'To turn on policy protections, like malware, ransomware and others, you must add at least Endpoint Essentials to your project. ',
}
);
const BADGE_TEXT = i18n.translate(
'xpack.securitySolutionServerless.endpointPolicyProtections.badgeText',
{
defaultMessage: 'Endpoint Essentials',
}
);
const CardDescription = styled.p`
padding: 0 33.3%;
`;
/**
* Component displayed when a given product tier is not allowed to use endpoint policy protections.
*/
export const EndpointPolicyProtections = memo(() => {
return (
<EuiCard
data-test-subj="endpointPolicy-protectionsLockedCard"
isDisabled={true}
description={false}
icon={<EuiIcon size="xl" type="lock" />}
betaBadgeProps={{
'data-test-subj': 'endpointPolicy-protectionsLockedCard-badge',
label: BADGE_TEXT,
}}
title={
<h3 data-test-subj="endpointPolicy-protectionsLockedCard-title">
<strong>{CARD_TITLE}</strong>
</h3>
}
>
<CardDescription>{CARD_MESSAGE}</CardDescription>
</EuiCard>
);
});
EndpointPolicyProtections.displayName = 'EndpointPolicyProtections';

View file

@ -0,0 +1,14 @@
/*
* 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 { lazy } from 'react';
export const EndpointPolicyProtectionsLazy = lazy(() =>
import('./endpoint_policy_protections').then(({ EndpointPolicyProtections }) => ({
default: EndpointPolicyProtections,
}))
);

View file

@ -19,7 +19,13 @@ describe(
},
},
() => {
const pages = getEndpointManagementPageList();
const allPages = getEndpointManagementPageList();
const deniedPages = allPages.filter(({ id }) => {
return id !== 'endpointList' && id !== 'policyList';
});
const allowedPages = allPages.filter(({ id }) => {
return id === 'endpointList' || id === 'policyList';
});
let username: string;
let password: string;
@ -30,7 +36,14 @@ describe(
});
});
for (const { url, title } of pages) {
for (const { url, title, pageTestSubj } of allowedPages) {
it(`should allow access to ${title}`, () => {
cy.visit(url);
cy.getByTestSubj(pageTestSubj).should('exist');
});
}
for (const { url, title } of deniedPages) {
it(`should not allow access to ${title}`, () => {
cy.visit(url);
getNoPrivilegesPage().should('exist');

View file

@ -8,8 +8,8 @@
import { RESPONSE_ACTION_API_COMMANDS_NAMES } from '@kbn/security-solution-plugin/common/endpoint/service/response_actions/constants';
import { login } from '../../../tasks/login';
import { getNoPrivilegesPage } from '../../../screens/endpoint_management/common';
import { getEndpointManagementPageList } from '../../../screens/endpoint_management';
import { ensureResponseActionAuthzAccess } from '../../../tasks/endpoint_management';
import { getEndpointManagementPageList } from '../../../screens/endpoint_management';
describe(
'App Features for Essential PLI',
@ -21,7 +21,13 @@ describe(
},
},
() => {
const pages = getEndpointManagementPageList();
const allPages = getEndpointManagementPageList();
const deniedPages = allPages.filter(({ id }) => {
return id !== 'endpointList' && id !== 'policyList';
});
const allowedPages = allPages.filter(({ id }) => {
return id === 'endpointList' || id === 'policyList';
});
let username: string;
let password: string;
@ -32,15 +38,22 @@ describe(
});
});
for (const { url, title } of pages) {
it(`should not allow access to ${title}`, () => {
for (const { url, title, pageTestSubj } of allowedPages) {
it(`should allow access to ${title}`, () => {
cy.visit(url);
cy.getByTestSubj(pageTestSubj).should('exist');
});
}
for (const { url, title } of deniedPages) {
it(`should NOT allow access to ${title}`, () => {
cy.visit(url);
getNoPrivilegesPage().should('exist');
});
}
for (const actionName of RESPONSE_ACTION_API_COMMANDS_NAMES) {
it(`should not allow access to Response Action: ${actionName}`, () => {
it(`should NOT allow access to Response Action: ${actionName}`, () => {
ensureResponseActionAuthzAccess('none', actionName, username, password);
});
}

View file

@ -0,0 +1,47 @@
/*
* 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 { IndexedFleetEndpointPolicyResponse } from '@kbn/security-solution-plugin/common/endpoint/data_loaders/index_fleet_endpoint_policy';
import { login } from '../../tasks/login';
import { visitPolicyDetails } from '../../screens/endpoint_management/policy_details';
describe(
'When displaying the Policy Details in Security Essentials PLI',
{
env: {
ftrConfig: {
productTypes: [{ product_line: 'security', product_tier: 'essentials' }],
},
},
},
() => {
let loadedPolicyData: IndexedFleetEndpointPolicyResponse;
before(() => {
cy.task('indexFleetEndpointPolicy', { policyName: 'tests-serverless' }).then((response) => {
loadedPolicyData = response as IndexedFleetEndpointPolicyResponse;
});
});
after(() => {
if (loadedPolicyData) {
cy.task('deleteIndexedFleetEndpointPolicies', loadedPolicyData);
}
});
beforeEach(() => {
login();
visitPolicyDetails(loadedPolicyData.integrationPolicies[0].id);
});
it('should display upselling section for protections', () => {
cy.getByTestSubj('endpointPolicy-protectionsLockedCard', { timeout: 60000 })
.should('exist')
.and('be.visible');
});
}
);

View file

@ -0,0 +1,12 @@
/*
* 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 { APP_POLICIES_PATH } from '@kbn/security-solution-plugin/common/constants';
export const visitPolicyDetails = (policyId: string): Cypress.Chainable => {
return cy.visit(`${APP_POLICIES_PATH}/${policyId}`);
};