From 3efc73ca8506562db5f1dfa6ffe945ae6960445c Mon Sep 17 00:00:00 2001 From: Paul Tavares <56442535+paul-tavares@users.noreply.github.com> Date: Fri, 4 Aug 2023 13:24:12 -0400 Subject: [PATCH 1/2] [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 --- .github/CODEOWNERS | 1 + .../index_fleet_endpoint_policy.ts | 19 +++++- .../common/types/app_features.ts | 5 ++ .../security_solution/public/app/app.tsx | 18 +++--- .../components/upselling_provider/index.ts | 8 +++ .../upselling_provider/upselling_provider.tsx | 34 +++++++++++ .../common/hooks/use_upselling.test.tsx | 23 +++++-- .../public/common/hooks/use_upselling.ts | 9 +-- .../public/common/lib/upsellings/types.ts | 2 +- .../mock/endpoint/app_context_render.tsx | 25 ++++---- .../mock/endpoint/app_root_provider.tsx | 41 ++++++++----- .../public/common/mock/test_providers.tsx | 45 ++++++++------ .../cypress/support/data_loaders.ts | 2 +- .../render_context_providers.tsx | 11 +++- .../with_security_context.tsx | 14 +++-- .../lazy_endpoint_generic_errors_list.tsx | 13 ++-- ...lazy_endpoint_package_custom_extension.tsx | 13 ++-- .../lazy_endpoint_policy_edit_extension.tsx | 13 ++-- ...azy_endpoint_policy_response_extension.tsx | 13 ++-- .../view/ingest_manager_integration/mocks.tsx | 7 ++- .../view/ingest_manager_integration/types.ts | 18 ++++++ .../cards/antivirus_registration_card.tsx | 6 ++ .../cards/attack_surface_reduction_card.tsx | 6 ++ .../cards/malware_protections_card.tsx | 11 +++- .../cards/memory_protection_card.tsx | 6 ++ .../behaviour_protection_card.tsx | 6 ++ .../cards/ransomware_protection_card.tsx | 6 ++ ...e_get_protections_unavailable_component.ts | 13 ++++ .../policy_settings_form.test.tsx | 30 +++++++++ .../policy_settings_form.tsx | 34 ++++++++--- .../security_solution/public/plugin.tsx | 16 +++-- .../app_features/security_kibana_features.ts | 3 + .../plugins/security_solution/tsconfig.json | 1 + .../common/pli/pli_config.ts | 3 +- .../upselling/register_upsellings.test.tsx | 4 +- .../public/upselling/register_upsellings.tsx | 22 +++++-- .../endpoint_policy_protections.tsx | 61 +++++++++++++++++++ .../sections/endpoint_management/index.ts | 14 +++++ .../feature_access/complete.cy.ts | 17 +++++- .../feature_access/essentials.cy.ts | 23 +++++-- ...policy_details_with_security_essentials.ts | 47 ++++++++++++++ .../endpoint_management/policy_details.ts | 12 ++++ 42 files changed, 543 insertions(+), 132 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/common/components/upselling_provider/index.ts create mode 100644 x-pack/plugins/security_solution/public/common/components/upselling_provider/upselling_provider.tsx create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/types.ts create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/hooks/use_get_protections_unavailable_component.ts create mode 100644 x-pack/plugins/security_solution_serverless/public/upselling/sections/endpoint_management/endpoint_policy_protections.tsx create mode 100644 x-pack/plugins/security_solution_serverless/public/upselling/sections/endpoint_management/index.ts create mode 100644 x-pack/test_serverless/functional/test_suites/security/cypress/e2e/endpoint_management/policy_details_with_security_essentials.ts create mode 100644 x-pack/test_serverless/functional/test_suites/security/cypress/screens/endpoint_management/policy_details.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 504b6c7878e80..eeae5ac8346ab 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -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 diff --git a/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_fleet_endpoint_policy.ts b/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_fleet_endpoint_policy.ts index 312809e2f48e4..0730eba99306c 100644 --- a/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_fleet_endpoint_policy.ts +++ b/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_fleet_endpoint_policy.ts @@ -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 => { 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('/'); + } +); diff --git a/x-pack/plugins/security_solution/common/types/app_features.ts b/x-pack/plugins/security_solution/common/types/app_features.ts index 4d2b53a97acad..27b59037f6cb0 100644 --- a/x-pack/plugins/security_solution/common/types/app_features.ts +++ b/x-pack/plugins/security_solution/common/types/app_features.ts @@ -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. diff --git a/x-pack/plugins/security_solution/public/app/app.tsx b/x-pack/plugins/security_solution/public/app/app.tsx index 10f4c007f87ca..1a64d1f9f47e7 100644 --- a/x-pack/plugins/security_solution/public/app/app.tsx +++ b/x-pack/plugins/security_solution/public/app/app.tsx @@ -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 = ({ http, triggersActionsUi: { actionTypeRegistry }, uiActions, + upselling, } = services; const { conversations, setConversations } = useConversationStore(); @@ -115,13 +117,15 @@ const StartAppComponent: FC = ({ - - {children} - + + + {children} + + diff --git a/x-pack/plugins/security_solution/public/common/components/upselling_provider/index.ts b/x-pack/plugins/security_solution/public/common/components/upselling_provider/index.ts new file mode 100644 index 0000000000000..abfaed2375ed7 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/upselling_provider/index.ts @@ -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'; diff --git a/x-pack/plugins/security_solution/public/common/components/upselling_provider/upselling_provider.tsx b/x-pack/plugins/security_solution/public/common/components/upselling_provider/upselling_provider.tsx new file mode 100644 index 0000000000000..34668ea9583a3 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/upselling_provider/upselling_provider.tsx @@ -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(null); + +export type UpsellingProviderProps = React.PropsWithChildren<{ + upsellingService: UpsellingService; +}>; + +export const UpsellingProvider = memo(({ upsellingService, children }) => { + return ( + + {children} + + ); +}); +UpsellingProvider.displayName = 'UpsellingProvider'; + +export const useUpsellingService = (): UpsellingService => { + const upsellingService = useContext(UpsellingProviderContext); + + if (!upsellingService) { + throw new Error('UpsellingProviderContext not found'); + } + + return upsellingService; +}; diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_upselling.test.tsx b/x-pack/plugins/security_solution/public/common/hooks/use_upselling.test.tsx index 3f4d352a7fbfe..99aae028a7e51 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/use_upselling.test.tsx +++ b/x-pack/plugins/security_solution/public/common/hooks/use_upselling.test.tsx @@ -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 = () =>
{'TEST 1 2 3'}
; +const RenderWrapper: React.FunctionComponent = ({ children }) => { + return {children}; +}; 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); }); diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_upselling.ts b/x-pack/plugins/security_solution/public/common/hooks/use_upselling.ts index f8737eeb6d12d..e54053978fe03 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/use_upselling.ts +++ b/x-pack/plugins/security_solution/public/common/hooks/use_upselling.ts @@ -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; diff --git a/x-pack/plugins/security_solution/public/common/lib/upsellings/types.ts b/x-pack/plugins/security_solution/public/common/lib/upsellings/types.ts index 3eb2785cd5961..2b68556817573 100644 --- a/x-pack/plugins/security_solution/public/common/lib/upsellings/types.ts +++ b/x-pack/plugins/security_solution/public/common/lib/upsellings/types.ts @@ -11,6 +11,6 @@ export type PageUpsellings = Partial>; export type SectionUpsellings = Partial>; -export type UpsellingSectionId = 'entity_analytics_panel'; +export type UpsellingSectionId = 'entity_analytics_panel' | 'endpointPolicyProtections'; export type UpsellingMessageId = 'investigation_guide'; diff --git a/x-pack/plugins/security_solution/public/common/mock/endpoint/app_context_render.tsx b/x-pack/plugins/security_solution/public/common/mock/endpoint/app_context_render.tsx index 3c0e4620a4000..ebf077de26dd1 100644 --- a/x-pack/plugins/security_solution/public/common/mock/endpoint/app_context_render.tsx +++ b/x-pack/plugins/security_solution/public/common/mock/endpoint/app_context_render.tsx @@ -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 }) => ( - - - - - {children} - - - - + + {children} + ); const render: UiRender = (ui, options) => { diff --git a/x-pack/plugins/security_solution/public/common/mock/endpoint/app_root_provider.tsx b/x-pack/plugins/security_solution/public/common/mock/endpoint/app_root_provider.tsx index 799485574a4eb..8c65ba84ff871 100644 --- a/x-pack/plugins/security_solution/public/common/mock/endpoint/app_root_provider.tsx +++ b/x-pack/plugins/security_solution/public/common/mock/endpoint/app_root_provider.tsx @@ -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; + 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(uiSettings.get$('theme:darkMode')); - const services = useMemo( - () => ({ http, notifications, application, data }), - [application, data, http, notifications] - ); + return ( - + - - - - {children} - - - + + + + + + + {children} + + + + + + diff --git a/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx b/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx index 6b662ea9e3cb8..ef8c68f9a25bb 100644 --- a/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx +++ b/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx @@ -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 = ({ }, }, }); + return ( - - ({ eui: euiDarkVars, darkMode: true })}> - - - - - Promise.resolve(cellActions)} - > - {children} - - - - - - - + + + ({ eui: euiDarkVars, darkMode: true })}> + + + + + Promise.resolve(cellActions)} + > + {children} + + + + + + + + ); }; +const UpsellingProviderMock = ({ children }: React.PropsWithChildren<{}>) => { + const upselingService = useKibana().services.upselling; + + return {children}; +}; + /** * A utility for wrapping children in the providers required to run most tests * WITH user privileges provider. diff --git a/x-pack/plugins/security_solution/public/management/cypress/support/data_loaders.ts b/x-pack/plugins/security_solution/public/management/cypress/support/data_loaders.ts index 8282f8f713130..4d8164391cb11 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/support/data_loaders.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/support/data_loaders.ts @@ -88,7 +88,7 @@ export const dataLoaders = ( agentPolicyName, }: { policyName: string; - endpointPackageVersion: string; + endpointPackageVersion?: string; agentPolicyName?: string; }) => { const { kbnClient } = await stackServicesPromise; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/components/with_security_context/render_context_providers.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/components/with_security_context/render_context_providers.tsx index 003a3debf852a..972da1a436678 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/components/with_security_context/render_context_providers.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/components/with_security_context/render_context_providers.tsx @@ -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; + upsellingService: UpsellingService; queryClient?: SecuritySolutionQueryClient; }>; export const RenderContextProviders = memo( - ({ store, depsStart, queryClient, children }) => { + ({ store, depsStart, queryClient, upsellingService, children }) => { const services = useKibana().services; const { application: { capabilities }, @@ -36,7 +39,11 @@ export const RenderContextProviders = memo( - {children} + + + {children} + + diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/components/with_security_context/with_security_context.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/components/with_security_context/with_security_context.tsx index ca488e64e50cc..e212ed8d17352 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/components/with_security_context/with_security_context.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/components/with_security_context/with_security_context.tsx @@ -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

{ - coreStart: CoreStart; - depsStart: Pick; +interface WithSecurityContextProps

extends FleetUiExtensionGetterOptions { WrappedComponent: ComponentType

; } @@ -30,6 +27,7 @@ interface WithSecurityContextProps

{ export const withSecurityContext =

({ coreStart, depsStart, + services: { upsellingService }, WrappedComponent, }: WithSecurityContextProps

): ComponentType

=> { let store: ReturnType; // created on first render @@ -41,7 +39,11 @@ export const withSecurityContext =

({ } return ( - + ); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/lazy_endpoint_generic_errors_list.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/lazy_endpoint_generic_errors_list.tsx index 55f58b0577d83..07f13e6759891 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/lazy_endpoint_generic_errors_list.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/lazy_endpoint_generic_errors_list.tsx @@ -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 -) => { +export const getLazyEndpointGenericErrorsListExtension = ({ + coreStart, + depsStart, + services, +}: FleetUiExtensionGetterOptions) => { return lazy(async () => { const [{ withSecurityContext }, { EndpointGenericErrorsList }] = await Promise.all([ import('./components/with_security_context/with_security_context'), @@ -27,6 +27,7 @@ export const getLazyEndpointGenericErrorsListExtension = ( default: withSecurityContext({ coreStart, depsStart, + services, WrappedComponent: EndpointGenericErrorsList, }), }; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/lazy_endpoint_package_custom_extension.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/lazy_endpoint_package_custom_extension.tsx index 7b1e1b386af30..e122ed4c4b26f 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/lazy_endpoint_package_custom_extension.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/lazy_endpoint_package_custom_extension.tsx @@ -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 -) => { +export const getLazyEndpointPackageCustomExtension = ({ + coreStart, + depsStart, + services, +}: FleetUiExtensionGetterOptions) => { return lazy(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, }), }; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_edit_extension.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_edit_extension.tsx index eb87daad8feae..57b1c5b23c15e 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_edit_extension.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_edit_extension.tsx @@ -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 -) => { +export const getLazyEndpointPolicyEditExtension = ({ + coreStart, + depsStart, + services, +}: FleetUiExtensionGetterOptions) => { return lazy(async () => { const [{ withSecurityContext }, { EndpointPolicyEditExtension }] = await Promise.all([ import('./components/with_security_context/with_security_context'), @@ -27,6 +27,7 @@ export const getLazyEndpointPolicyEditExtension = ( default: withSecurityContext({ coreStart, depsStart, + services, WrappedComponent: EndpointPolicyEditExtension, }), }; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_response_extension.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_response_extension.tsx index 87568afa348b9..88a49965dd89a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_response_extension.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_response_extension.tsx @@ -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 -) => { +export const getLazyEndpointPolicyResponseExtension = ({ + coreStart, + depsStart, + services, +}: FleetUiExtensionGetterOptions) => { return lazy(async () => { const [{ withSecurityContext }, { EndpointPolicyResponseExtension }] = await Promise.all([ import('./components/with_security_context/with_security_context'), @@ -27,6 +27,7 @@ export const getLazyEndpointPolicyResponseExtension = ( default: withSecurityContext({ coreStart, depsStart, + services, WrappedComponent: EndpointPolicyResponseExtension, }), }; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/mocks.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/mocks.tsx index a43424b42982c..56c0e2b9b11a2 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/mocks.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/mocks.tsx @@ -102,7 +102,12 @@ export const createFleetContextRendererMock = (): AppContextTestRender => { - + {children} diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/types.ts b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/types.ts new file mode 100644 index 0000000000000..cc27971de63ab --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/types.ts @@ -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; + services: { + upsellingService: UpsellingService; + }; +} diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/antivirus_registration_card.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/antivirus_registration_card.tsx index 3d3f6056210aa..058293911e25d 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/antivirus_registration_card.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/antivirus_registration_card.tsx @@ -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( ({ 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( [onChange, policy] ); + if (!isProtectionsAllowed) { + return null; + } + return ( ( ({ 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( [onChange, policy] ); + if (!isProtectionsAllowed) { + return null; + } + if (!isPlatinumPlus) { return ( = [ 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( ({ 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( } ); + if (!isProtectionsAllowed) { + return null; + } + return ( ( ({ 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( } ); + if (!isProtectionsAllowed) { + return null; + } + if (!isPlatinumPlus) { return ( diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/protection_seetings_card/behaviour_protection_card.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/protection_seetings_card/behaviour_protection_card.tsx index f1be996a67d70..18946da2313f1 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/protection_seetings_card/behaviour_protection_card.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/protection_seetings_card/behaviour_protection_card.tsx @@ -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( ({ 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( } ); + if (!isProtectionsAllowed) { + return null; + } + if (!isPlatinumPlus) { return ( = [ PolicyOperatingSystem.windows, @@ -38,6 +39,7 @@ export type RansomwareProtectionCardProps = PolicyFormComponentCommonProps; export const RansomwareProtectionCard = React.memo( ({ 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 { + return useUpsellingComponent('endpointPolicyProtections'); +}; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/policy_settings_form.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/policy_settings_form.test.tsx index 5aed812e9f3c9..7f204ca56d4ca 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/policy_settings_form.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/policy_settings_form.test.tsx @@ -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; let renderResult: ReturnType; + 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: () =>

{'pay up!'}
, + }); + }); + + 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')); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/policy_settings_form.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/policy_settings_form.tsx index 623c61ebbf0ee..460d685466c11 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/policy_settings_form.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/policy_settings_form.tsx @@ -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((props) => { const getTestId = useTestIdGenerator(props['data-test-subj']); + const ProtectionsUpSellingComponent = useGetProtectionsUnavailableComponent(); return (
{PROTECTIONS_SECTION_TITLE} - - + {ProtectionsUpSellingComponent && ( + <> + + + + + )} - - + {!ProtectionsUpSellingComponent && ( + <> + + - - + + - - + + - - + + + + + + + )} {SETTINGS_SECTION_TITLE} diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx index 9b8b440f6ed25..f619dead10399 100644 --- a/x-pack/plugins/security_solution/public/plugin.tsx +++ b/x-pack/plugins/security_solution/public/plugin.tsx @@ -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 { 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); diff --git a/x-pack/plugins/security_solution_serverless/public/upselling/register_upsellings.tsx b/x-pack/plugins/security_solution_serverless/public/upselling/register_upsellings.tsx index 8db5b4945ee62..b2c6fd23ca9b5 100644 --- a/x-pack/plugins/security_solution_serverless/public/upselling/register_upsellings.tsx +++ b/x-pack/plugins/security_solution_serverless/public/upselling/register_upsellings.tsx @@ -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: () => , + }; +}); interface UpsellingsConfig { pli: AppFeatureKey; - component: React.ComponentType; + component: React.LazyExoticComponent; } interface UpsellingsMessageConfig { @@ -89,9 +95,7 @@ export const upsellingPages: UpsellingPages = [ { pageName: SecurityPageName.threatIntelligence, pli: AppFeatureKey.threatIntelligence, - component: () => ( - - ), + component: ThreatIntelligencePaywallLazy, }, ]; @@ -104,6 +108,12 @@ export const upsellingSections: UpsellingSections = [ // pli: AppFeatureKey.advancedInsights, // component: () => , // }, + + { + id: 'endpointPolicyProtections', + pli: AppFeatureKey.endpointPolicyProtections, + component: EndpointPolicyProtectionsLazy, + }, ]; // Upsellings for sections, linked by arbitrary ids diff --git a/x-pack/plugins/security_solution_serverless/public/upselling/sections/endpoint_management/endpoint_policy_protections.tsx b/x-pack/plugins/security_solution_serverless/public/upselling/sections/endpoint_management/endpoint_policy_protections.tsx new file mode 100644 index 0000000000000..c3e63b88ca78c --- /dev/null +++ b/x-pack/plugins/security_solution_serverless/public/upselling/sections/endpoint_management/endpoint_policy_protections.tsx @@ -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 ( + } + betaBadgeProps={{ + 'data-test-subj': 'endpointPolicy-protectionsLockedCard-badge', + label: BADGE_TEXT, + }} + title={ +

+ {CARD_TITLE} +

+ } + > + {CARD_MESSAGE} +
+ ); +}); +EndpointPolicyProtections.displayName = 'EndpointPolicyProtections'; diff --git a/x-pack/plugins/security_solution_serverless/public/upselling/sections/endpoint_management/index.ts b/x-pack/plugins/security_solution_serverless/public/upselling/sections/endpoint_management/index.ts new file mode 100644 index 0000000000000..a76b1cc0bacc8 --- /dev/null +++ b/x-pack/plugins/security_solution_serverless/public/upselling/sections/endpoint_management/index.ts @@ -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, + })) +); diff --git a/x-pack/test_serverless/functional/test_suites/security/cypress/e2e/endpoint_management/feature_access/complete.cy.ts b/x-pack/test_serverless/functional/test_suites/security/cypress/e2e/endpoint_management/feature_access/complete.cy.ts index 0324392f81867..09491e247e07e 100644 --- a/x-pack/test_serverless/functional/test_suites/security/cypress/e2e/endpoint_management/feature_access/complete.cy.ts +++ b/x-pack/test_serverless/functional/test_suites/security/cypress/e2e/endpoint_management/feature_access/complete.cy.ts @@ -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'); diff --git a/x-pack/test_serverless/functional/test_suites/security/cypress/e2e/endpoint_management/feature_access/essentials.cy.ts b/x-pack/test_serverless/functional/test_suites/security/cypress/e2e/endpoint_management/feature_access/essentials.cy.ts index 5e910f967befa..0f86f1fad045e 100644 --- a/x-pack/test_serverless/functional/test_suites/security/cypress/e2e/endpoint_management/feature_access/essentials.cy.ts +++ b/x-pack/test_serverless/functional/test_suites/security/cypress/e2e/endpoint_management/feature_access/essentials.cy.ts @@ -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); }); } diff --git a/x-pack/test_serverless/functional/test_suites/security/cypress/e2e/endpoint_management/policy_details_with_security_essentials.ts b/x-pack/test_serverless/functional/test_suites/security/cypress/e2e/endpoint_management/policy_details_with_security_essentials.ts new file mode 100644 index 0000000000000..582a9c510c4c3 --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/security/cypress/e2e/endpoint_management/policy_details_with_security_essentials.ts @@ -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'); + }); + } +); diff --git a/x-pack/test_serverless/functional/test_suites/security/cypress/screens/endpoint_management/policy_details.ts b/x-pack/test_serverless/functional/test_suites/security/cypress/screens/endpoint_management/policy_details.ts new file mode 100644 index 0000000000000..2ba5de32cbab6 --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/security/cypress/screens/endpoint_management/policy_details.ts @@ -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}`); +}; From 339eb28861e56a465cc9ce6f458d792d39414706 Mon Sep 17 00:00:00 2001 From: Abdul Wahab Zahid Date: Fri, 4 Aug 2023 19:25:10 +0200 Subject: [PATCH 2/2] [Synthetics] Normalize monitor before mixing params. (#163176) Fixes #163042 ## Summary Normalize the monitor object before mixing global and project-wide params. --- .../synthetics_monitor/synthetics_monitor_client.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/synthetics/server/synthetics_service/synthetics_monitor/synthetics_monitor_client.ts b/x-pack/plugins/synthetics/server/synthetics_service/synthetics_monitor/synthetics_monitor_client.ts index c5c158c750beb..696746e28a0f6 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/synthetics_monitor/synthetics_monitor_client.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/synthetics_monitor/synthetics_monitor_client.ts @@ -361,13 +361,16 @@ export class SyntheticsMonitorClient { const heartbeatConfigs: HeartbeatConfig[] = []; for (const monitor of monitors) { - const attributes = monitor.attributes as unknown as MonitorFields; - const { str: paramsString } = mixParamsWithGlobalParams(paramsBySpace[spaceId], attributes); + const normalizedMonitor = normalizeSecrets(monitor).attributes as MonitorFields; + const { str: paramsString } = mixParamsWithGlobalParams( + paramsBySpace[spaceId], + normalizedMonitor + ); heartbeatConfigs.push( formatHeartbeatRequest( { - monitor: normalizeSecrets(monitor).attributes, + monitor: normalizedMonitor, configId: monitor.id, }, paramsString