diff --git a/packages/kbn-es/src/serverless_resources/project_roles/security/roles.yml b/packages/kbn-es/src/serverless_resources/project_roles/security/roles.yml index b05bb0de2f2c8..3c008407d5c46 100644 --- a/packages/kbn-es/src/serverless_resources/project_roles/security/roles.yml +++ b/packages/kbn-es/src/serverless_resources/project_roles/security/roles.yml @@ -47,6 +47,7 @@ viewer: - feature_siem.endpoint_list_read - feature_securitySolutionCases.read - feature_securitySolutionAssistant.all + - feature_securitySolutionAttackDiscovery.all - feature_actions.read - feature_builtInAlerts.read - feature_osquery.read @@ -124,6 +125,7 @@ editor: - feature_siem.file_operations_all - feature_securitySolutionCases.all - feature_securitySolutionAssistant.all + - feature_securitySolutionAttackDiscovery.all - feature_actions.read - feature_builtInAlerts.all - feature_osquery.all @@ -171,6 +173,7 @@ t1_analyst: - feature_siem.endpoint_list_read - feature_securitySolutionCases.read - feature_securitySolutionAssistant.all + - feature_securitySolutionAttackDiscovery.all - feature_actions.read - feature_builtInAlerts.read - feature_osquery.read @@ -224,6 +227,7 @@ t2_analyst: - feature_siem.endpoint_list_read - feature_securitySolutionCases.all - feature_securitySolutionAssistant.all + - feature_securitySolutionAttackDiscovery.all - feature_actions.read - feature_builtInAlerts.read - feature_osquery.read @@ -292,6 +296,7 @@ t3_analyst: - feature_siem.scan_operations_all - feature_securitySolutionCases.all - feature_securitySolutionAssistant.all + - feature_securitySolutionAttackDiscovery.all - feature_actions.read - feature_builtInAlerts.all - feature_osquery.all @@ -352,6 +357,7 @@ threat_intelligence_analyst: - feature_siem.blocklist_all - feature_securitySolutionCases.all - feature_securitySolutionAssistant.all + - feature_securitySolutionAttackDiscovery.all - feature_actions.read - feature_builtInAlerts.read - feature_osquery.all @@ -418,6 +424,7 @@ rule_author: - feature_siem.actions_log_management_read - feature_securitySolutionCases.all - feature_securitySolutionAssistant.all + - feature_securitySolutionAttackDiscovery.all - feature_actions.read - feature_builtInAlerts.all - feature_osquery.all @@ -488,6 +495,7 @@ soc_manager: - feature_siem.scan_operations_all - feature_securitySolutionCases.all - feature_securitySolutionAssistant.all + - feature_securitySolutionAttackDiscovery.all - feature_actions.all - feature_builtInAlerts.all - feature_osquery.all @@ -546,6 +554,7 @@ detections_admin: - feature_siem.crud_alerts - feature_securitySolutionCases.all - feature_securitySolutionAssistant.all + - feature_securitySolutionAttackDiscovery.all - feature_actions.all - feature_builtInAlerts.all - feature_dev_tools.all @@ -603,6 +612,7 @@ platform_engineer: - feature_siem.actions_log_management_read - feature_securitySolutionCases.all - feature_securitySolutionAssistant.all + - feature_securitySolutionAttackDiscovery.all - feature_actions.all - feature_builtInAlerts.all - feature_fleet.all @@ -674,6 +684,7 @@ endpoint_operations_analyst: - feature_siem.scan_operations_all - feature_securitySolutionCases.all - feature_securitySolutionAssistant.all + - feature_securitySolutionAttackDiscovery.all - feature_actions.all - feature_builtInAlerts.all - feature_osquery.all @@ -747,6 +758,7 @@ endpoint_policy_manager: - feature_siem.blocklist_all # Elastic Defend Policy Management - feature_securitySolutionCases.all - feature_securitySolutionAssistant.all + - feature_securitySolutionAttackDiscovery.all - feature_actions.all - feature_builtInAlerts.all - feature_osquery.all diff --git a/packages/kbn-es/src/serverless_resources/security_roles.json b/packages/kbn-es/src/serverless_resources/security_roles.json index 8ba9dc4c6bc73..0554853b82df9 100644 --- a/packages/kbn-es/src/serverless_resources/security_roles.json +++ b/packages/kbn-es/src/serverless_resources/security_roles.json @@ -34,6 +34,7 @@ "ml": ["read"], "siem": ["read", "read_alerts"], "securitySolutionAssistant": ["all"], + "securitySolutionAttackDiscovery": ["all"], "securitySolutionCases": ["read"], "actions": ["read"], "builtInAlerts": ["read"] @@ -80,6 +81,7 @@ "ml": ["read"], "siem": ["read", "read_alerts"], "securitySolutionAssistant": ["all"], + "securitySolutionAttackDiscovery": ["all"], "securitySolutionCases": ["read"], "actions": ["read"], "builtInAlerts": ["read"] @@ -145,6 +147,7 @@ ], "securitySolutionCases": ["all"], "securitySolutionAssistant": ["all"], + "securitySolutionAttackDiscovery": ["all"], "actions": ["read"], "builtInAlerts": ["all"], "osquery": ["all"], @@ -201,6 +204,7 @@ "ml": ["read"], "siem": ["all", "read_alerts", "crud_alerts"], "securitySolutionAssistant": ["all"], + "securitySolutionAttackDiscovery": ["all"], "securitySolutionCases": ["all"], "actions": ["read"], "builtInAlerts": ["all"] @@ -253,6 +257,7 @@ "ml": ["read"], "siem": ["all", "read_alerts", "crud_alerts"], "securitySolutionAssistant": ["all"], + "securitySolutionAttackDiscovery": ["all"], "securitySolutionCases": ["all"], "actions": ["all"], "builtInAlerts": ["all"] @@ -300,6 +305,7 @@ "ml": ["all"], "siem": ["all", "read_alerts", "crud_alerts"], "securitySolutionAssistant": ["all"], + "securitySolutionAttackDiscovery": ["all"], "securitySolutionCases": ["all"], "actions": ["read"], "builtInAlerts": ["all"], @@ -354,6 +360,7 @@ "ml": ["all"], "siem": ["all", "read_alerts", "crud_alerts"], "securitySolutionAssistant": ["all"], + "securitySolutionAttackDiscovery": ["all"], "securitySolutionCases": ["all"], "actions": ["all"], "builtInAlerts": ["all"] diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 84055d662a65b..0356fb4fd6541 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -144,7 +144,7 @@ pageLoadAssetSize: searchprofiler: 67080 security: 81771 securitySolution: 98429 - securitySolutionEss: 16573 + securitySolutionEss: 31781 securitySolutionServerless: 62488 serverless: 16573 serverlessObservability: 68747 diff --git a/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_selector_inline/action_type_selector_modal.tsx b/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_selector_inline/action_type_selector_modal.tsx index 83437bb7dc69b..9e019d10140a4 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_selector_inline/action_type_selector_modal.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_selector_inline/action_type_selector_modal.tsx @@ -41,7 +41,7 @@ export const ActionTypeSelectorModal = React.memo( ({ actionTypes, actionTypeRegistry, onClose, onSelect, actionTypeSelectorInline }: Props) => { const content = useMemo( () => ( - + {actionTypes?.map((actionType: ActionType) => { const fullAction = actionTypeRegistry.get(actionType.id); return ( diff --git a/x-pack/packages/security-solution/features/config.ts b/x-pack/packages/security-solution/features/config.ts index 8f36393ffd530..9fa14ab0ba7c2 100644 --- a/x-pack/packages/security-solution/features/config.ts +++ b/x-pack/packages/security-solution/features/config.ts @@ -8,5 +8,6 @@ export { securityDefaultProductFeaturesConfig } from './src/security/product_feature_config'; export { getCasesDefaultProductFeaturesConfig } from './src/cases/product_feature_config'; export { assistantDefaultProductFeaturesConfig } from './src/assistant/product_feature_config'; +export { attackDiscoveryDefaultProductFeaturesConfig } from './src/attack_discovery/product_feature_config'; export { createEnabledProductFeaturesConfigMap } from './src/helpers'; diff --git a/x-pack/packages/security-solution/features/product_features.ts b/x-pack/packages/security-solution/features/product_features.ts index b9209441cff85..b2c524ff6de1d 100644 --- a/x-pack/packages/security-solution/features/product_features.ts +++ b/x-pack/packages/security-solution/features/product_features.ts @@ -8,3 +8,4 @@ export { getSecurityFeature } from './src/security'; export { getCasesFeature } from './src/cases'; export { getAssistantFeature } from './src/assistant'; +export { getAttackDiscoveryFeature } from './src/attack_discovery'; diff --git a/x-pack/packages/security-solution/features/src/attack_discovery/index.ts b/x-pack/packages/security-solution/features/src/attack_discovery/index.ts new file mode 100644 index 0000000000000..c72485c8f229e --- /dev/null +++ b/x-pack/packages/security-solution/features/src/attack_discovery/index.ts @@ -0,0 +1,15 @@ +/* + * 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 { getAttackDiscoveryBaseKibanaFeature } from './kibana_features'; +import type { ProductFeatureParams } from '../types'; + +export const getAttackDiscoveryFeature = (): ProductFeatureParams => ({ + baseKibanaFeature: getAttackDiscoveryBaseKibanaFeature(), + baseKibanaSubFeatureIds: [], + subFeaturesMap: new Map(), +}); diff --git a/x-pack/packages/security-solution/features/src/attack_discovery/kibana_features.ts b/x-pack/packages/security-solution/features/src/attack_discovery/kibana_features.ts new file mode 100644 index 0000000000000..130ee907b8e83 --- /dev/null +++ b/x-pack/packages/security-solution/features/src/attack_discovery/kibana_features.ts @@ -0,0 +1,48 @@ +/* + * 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 { DEFAULT_APP_CATEGORIES } from '@kbn/core-application-common'; +import { i18n } from '@kbn/i18n'; + +import { APP_ID, ATTACK_DISCOVERY_FEATURE_ID } from '../constants'; +import { type BaseKibanaFeatureConfig } from '../types'; + +export const getAttackDiscoveryBaseKibanaFeature = (): BaseKibanaFeatureConfig => ({ + id: ATTACK_DISCOVERY_FEATURE_ID, + name: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.linkSecuritySolutionAttackDiscoveryTitle', + { + defaultMessage: 'Attack discovery', + } + ), + order: 1100, + category: DEFAULT_APP_CATEGORIES.security, + app: [ATTACK_DISCOVERY_FEATURE_ID, 'kibana'], + catalogue: [APP_ID], + minimumLicense: 'enterprise', + privileges: { + all: { + api: ['elasticAssistant'], + app: [ATTACK_DISCOVERY_FEATURE_ID, 'kibana'], + catalogue: [APP_ID], + savedObject: { + all: [], + read: [], + }, + ui: [], + }, + read: { + // No read-only mode currently supported + disabled: true, + savedObject: { + all: [], + read: [], + }, + ui: [], + }, + }, +}); diff --git a/x-pack/packages/security-solution/features/src/attack_discovery/product_feature_config.ts b/x-pack/packages/security-solution/features/src/attack_discovery/product_feature_config.ts new file mode 100644 index 0000000000000..94af601307849 --- /dev/null +++ b/x-pack/packages/security-solution/features/src/attack_discovery/product_feature_config.ts @@ -0,0 +1,33 @@ +/* + * 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 { ProductFeatureAttackDiscoveryKey } from '../product_features_keys'; +import type { ProductFeatureKibanaConfig } from '../types'; + +/** + * App features privileges configuration for the Attack discovery feature. + * These are the configs that are shared between both offering types (ess and serverless). + * They can be extended on each offering plugin to register privileges using different way on each offering type. + * + * Privileges can be added in different ways: + * - `privileges`: the privileges that will be added directly into the main Security feature. + * - `subFeatureIds`: the ids of the sub-features that will be added into the Security subFeatures entry. + * - `subFeaturesPrivileges`: the privileges that will be added into the existing Security subFeature with the privilege `id` specified. + */ +export const attackDiscoveryDefaultProductFeaturesConfig: Record< + ProductFeatureAttackDiscoveryKey, + ProductFeatureKibanaConfig +> = { + [ProductFeatureAttackDiscoveryKey.attackDiscovery]: { + privileges: { + all: { + ui: ['attack-discovery'], + }, + }, + subFeatureIds: [], + }, +}; diff --git a/x-pack/packages/security-solution/features/src/constants.ts b/x-pack/packages/security-solution/features/src/constants.ts index c92376fd36209..5027a7c8d393b 100644 --- a/x-pack/packages/security-solution/features/src/constants.ts +++ b/x-pack/packages/security-solution/features/src/constants.ts @@ -11,6 +11,7 @@ export const SERVER_APP_ID = 'siem' as const; export const CASES_FEATURE_ID = 'securitySolutionCases' as const; export const ASSISTANT_FEATURE_ID = 'securitySolutionAssistant' as const; +export const ATTACK_DISCOVERY_FEATURE_ID = 'securitySolutionAttackDiscovery' as const; // Same as the plugin id defined by Cloud Security Posture export const CLOUD_POSTURE_APP_ID = 'csp' as const; diff --git a/x-pack/packages/security-solution/features/src/product_features_keys.ts b/x-pack/packages/security-solution/features/src/product_features_keys.ts index 9f1a268b6f674..2bb8cdc0f1e9f 100644 --- a/x-pack/packages/security-solution/features/src/product_features_keys.ts +++ b/x-pack/packages/security-solution/features/src/product_features_keys.ts @@ -93,17 +93,26 @@ export enum ProductFeatureAssistantKey { assistant = 'assistant', } +export enum ProductFeatureAttackDiscoveryKey { + /** + * Enables Attack discovery + */ + attackDiscovery = 'attack_discovery', +} + // Merges the two enums. export const ProductFeatureKey = { ...ProductFeatureSecurityKey, ...ProductFeatureCasesKey, ...ProductFeatureAssistantKey, + ...ProductFeatureAttackDiscoveryKey, }; // We need to merge the value and the type and export both to replicate how enum works. export type ProductFeatureKeyType = | ProductFeatureSecurityKey | ProductFeatureCasesKey - | ProductFeatureAssistantKey; + | ProductFeatureAssistantKey + | ProductFeatureAttackDiscoveryKey; export const ALL_PRODUCT_FEATURE_KEYS = Object.freeze(Object.values(ProductFeatureKey)); diff --git a/x-pack/packages/security-solution/features/src/types.ts b/x-pack/packages/security-solution/features/src/types.ts index 29778ed8cad7b..17e4869fa66f1 100644 --- a/x-pack/packages/security-solution/features/src/types.ts +++ b/x-pack/packages/security-solution/features/src/types.ts @@ -13,6 +13,7 @@ import type { import type { RecursivePartial } from '@kbn/utility-types'; import type { ProductFeatureAssistantKey, + ProductFeatureAttackDiscoveryKey, ProductFeatureCasesKey, ProductFeatureKeyType, ProductFeatureSecurityKey, @@ -51,6 +52,11 @@ export type ProductFeaturesAssistantConfig = Map< ProductFeatureKibanaConfig >; +export type ProductFeaturesAttackDiscoveryConfig = Map< + ProductFeatureAttackDiscoveryKey, + ProductFeatureKibanaConfig +>; + export type AppSubFeaturesMap = Map; export interface ProductFeatureParams { diff --git a/x-pack/packages/security-solution/upselling/pages/attack_discovery/index.test.tsx b/x-pack/packages/security-solution/upselling/pages/attack_discovery/index.test.tsx new file mode 100644 index 0000000000000..3301f27c74079 --- /dev/null +++ b/x-pack/packages/security-solution/upselling/pages/attack_discovery/index.test.tsx @@ -0,0 +1,56 @@ +/* + * 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 { render, screen } from '@testing-library/react'; +import React from 'react'; + +import { AttackDiscoveryUpsellingPage } from '.'; + +const availabilityMessage = 'This feature is available...'; +const upgradeMessage = 'Please upgrade...'; + +const mockActions =
; + +jest.mock('@kbn/security-solution-navigation', () => { + const original = jest.requireActual('@kbn/security-solution-navigation'); + return { + ...original, + useNavigation: () => ({ + navigateTo: jest.fn(), + }), + }; +}); + +describe('AttackDiscoveryUpsellingPage', () => { + beforeEach(() => { + render( + + ); + }); + + it('renders the availability message', () => { + const attackDiscoveryIsAvailable = screen.getByTestId('availabilityMessage'); + + expect(attackDiscoveryIsAvailable).toHaveTextContent(availabilityMessage); + }); + + it('renders the upgrade message', () => { + const pleaseUpgrade = screen.getByTestId('upgradeMessage'); + + expect(pleaseUpgrade).toHaveTextContent(upgradeMessage); + }); + + it('renders the actions', () => { + const actions = screen.getByTestId('mockActions'); + + expect(actions).toBeInTheDocument(); + }); +}); diff --git a/x-pack/packages/security-solution/upselling/pages/attack_discovery/index.tsx b/x-pack/packages/security-solution/upselling/pages/attack_discovery/index.tsx new file mode 100644 index 0000000000000..7d3353e6431e7 --- /dev/null +++ b/x-pack/packages/security-solution/upselling/pages/attack_discovery/index.tsx @@ -0,0 +1,48 @@ +/* + * 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 { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template'; +import { EuiPageHeader, EuiSpacer } from '@elastic/eui'; +import React, { useMemo } from 'react'; + +import { PageTitle } from './page_title'; +import { AttackDiscoveryUpsellingSection } from '../../sections/attack_discovery'; + +interface Props { + actions?: React.ReactNode; + availabilityMessage: string; + upgradeMessage: string; +} + +/** + * This component handles the styling of the _page_ that hosts the `AttackDiscoveryUpsellingSection` + */ +const AttackDiscoveryUpsellingPageComponent: React.FC = ({ + actions, + availabilityMessage, + upgradeMessage, +}) => { + const pageTitle = useMemo(() => , []); + + return ( + + + + + + + + ); +}; + +AttackDiscoveryUpsellingPageComponent.displayName = 'AttackDiscoveryUpsellingPage'; + +export const AttackDiscoveryUpsellingPage = React.memo(AttackDiscoveryUpsellingPageComponent); diff --git a/x-pack/packages/security-solution/upselling/pages/attack_discovery/page_title/index.test.tsx b/x-pack/packages/security-solution/upselling/pages/attack_discovery/page_title/index.test.tsx new file mode 100644 index 0000000000000..630000cecd5be --- /dev/null +++ b/x-pack/packages/security-solution/upselling/pages/attack_discovery/page_title/index.test.tsx @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { render, screen } from '@testing-library/react'; +import React from 'react'; + +import { PageTitle } from '.'; +import { ATTACK_DISCOVERY_PAGE_TITLE } from './translations'; + +describe('PageTitle', () => { + beforeEach(() => { + render(); + }); + + it('renders the expected title', () => { + const attackDiscoveryPageTitle = screen.getByTestId('attackDiscoveryPageTitle'); + + expect(attackDiscoveryPageTitle).toHaveTextContent(ATTACK_DISCOVERY_PAGE_TITLE); + }); + + it('renders the beta badge icon', () => { + const betaBadge = screen.getByTestId('betaBadge'); + + expect(betaBadge).toBeInTheDocument(); + }); +}); diff --git a/x-pack/packages/security-solution/upselling/pages/attack_discovery/page_title/index.tsx b/x-pack/packages/security-solution/upselling/pages/attack_discovery/page_title/index.tsx new file mode 100644 index 0000000000000..564488a0264ee --- /dev/null +++ b/x-pack/packages/security-solution/upselling/pages/attack_discovery/page_title/index.tsx @@ -0,0 +1,52 @@ +/* + * 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 { EuiBetaBadge, EuiFlexGroup, EuiFlexItem, EuiTitle, useEuiTheme } from '@elastic/eui'; +import { css } from '@emotion/react'; +import React from 'react'; + +import * as i18n from './translations'; + +const PageTitleComponent: React.FC = () => { + const { euiTheme } = useEuiTheme(); + + return ( + + + +

{i18n.ATTACK_DISCOVERY_PAGE_TITLE}

+
+
+ + + + +
+ ); +}; + +PageTitleComponent.displayName = 'PageTitle'; + +export const PageTitle = React.memo(PageTitleComponent); diff --git a/x-pack/packages/security-solution/upselling/pages/attack_discovery/page_title/translations.ts b/x-pack/packages/security-solution/upselling/pages/attack_discovery/page_title/translations.ts new file mode 100644 index 0000000000000..7b3f3400e0443 --- /dev/null +++ b/x-pack/packages/security-solution/upselling/pages/attack_discovery/page_title/translations.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const ATTACK_DISCOVERY_PAGE_TITLE = i18n.translate( + 'securitySolutionPackages.upselling.pages.attackDiscovery.pageTitle.pageTitle', + { + defaultMessage: 'Attack discovery', + } +); + +export const BETA = i18n.translate( + 'securitySolutionPackages.upselling.pages.attackDiscovery.pageTitle.betaBadge', + { + defaultMessage: 'Technical preview', + } +); + +export const BETA_TOOLTIP = i18n.translate( + 'securitySolutionPackages.upselling.pages.attackDiscovery.pageTitle.betaTooltip', + { + defaultMessage: + 'This functionality is in technical preview and is subject to change. Please use Attack Discovery with caution in production environments.', + } +); diff --git a/x-pack/packages/security-solution/upselling/sections/attack_discovery/assistant_avatar/assistant_avatar.tsx b/x-pack/packages/security-solution/upselling/sections/attack_discovery/assistant_avatar/assistant_avatar.tsx new file mode 100644 index 0000000000000..626c3a4126334 --- /dev/null +++ b/x-pack/packages/security-solution/upselling/sections/attack_discovery/assistant_avatar/assistant_avatar.tsx @@ -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 React from 'react'; + +interface Props { + className?: string; + size?: keyof typeof sizeMap; +} + +export const sizeMap = { + xl: 64, + l: 48, + m: 32, + s: 24, + xs: 16, + xxs: 12, +}; + +/** + * Default Elastic AI Assistant logo + * + * TODO: This may be removed when the logo is added to EUI + */ +export const AssistantAvatar = ({ className, size = 's' }: Props) => ( + + + + + + +); diff --git a/x-pack/packages/security-solution/upselling/sections/attack_discovery/index.test.tsx b/x-pack/packages/security-solution/upselling/sections/attack_discovery/index.test.tsx new file mode 100644 index 0000000000000..85795ae1195e1 --- /dev/null +++ b/x-pack/packages/security-solution/upselling/sections/attack_discovery/index.test.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 { render, screen } from '@testing-library/react'; +import React from 'react'; + +import { AttackDiscoveryUpsellingSection } from '.'; +import { FIND_POTENTIAL_ATTACKS_WITH_AI } from './translations'; + +const availabilityMessage = 'Serverless or self-managed-specific availability message'; +const upgradeMessage = 'Serverless or self-managed-specific upgrade message'; + +const mockActions = ( +
{'typically call to action buttons or links'}
+); + +describe('AttackDiscoveryUpsellingSection', () => { + beforeEach(() => { + render( + + ); + }); + + it('renders the assistant avatar', () => { + const assistantAvatar = screen.getByTestId('assistantAvatar'); + + expect(assistantAvatar).toBeInTheDocument(); + }); + + it('renders the expected upgrade title', () => { + const upgradeTitle = screen.getByTestId('upgradeTitle'); + + expect(upgradeTitle).toHaveTextContent(FIND_POTENTIAL_ATTACKS_WITH_AI); + }); + + it('renders the expected availability message', () => { + const attackDiscoveryIsAvailable = screen.getByTestId('availabilityMessage'); + + expect(attackDiscoveryIsAvailable).toHaveTextContent(availabilityMessage); + }); + + it('renders the expected upgrade message', () => { + const pleaseUpgrade = screen.getByTestId('upgradeMessage'); + + expect(pleaseUpgrade).toHaveTextContent(upgradeMessage); + }); + + it('renders the actions', () => { + const actions = screen.getByTestId('mockActions'); + + expect(actions).toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/attack_discovery/pages/upgrade/index.tsx b/x-pack/packages/security-solution/upselling/sections/attack_discovery/index.tsx similarity index 63% rename from x-pack/plugins/security_solution/public/attack_discovery/pages/upgrade/index.tsx rename to x-pack/packages/security-solution/upselling/sections/attack_discovery/index.tsx index bf2cd241408a1..0c6a12c88d7fd 100644 --- a/x-pack/plugins/security_solution/public/attack_discovery/pages/upgrade/index.tsx +++ b/x-pack/packages/security-solution/upselling/sections/attack_discovery/index.tsx @@ -5,15 +5,27 @@ * 2.0. */ -import { AssistantAvatar, UpgradeButtons, useAssistantContext } from '@kbn/elastic-assistant'; import { EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText } from '@elastic/eui'; import React, { useMemo } from 'react'; +import { AssistantAvatar } from './assistant_avatar/assistant_avatar'; import * as i18n from './translations'; -const UpgradeComponent: React.FC = () => { - const { http } = useAssistantContext(); +interface Props { + actions?: React.ReactNode; + availabilityMessage: string; + upgradeMessage: string; +} +/** + * This `section` component handles (just) the styling of the upselling message + * (by itself, without the page wrapper) + */ +const AttackDiscoveryUpsellingSectionComponent: React.FC = ({ + actions, + availabilityMessage, + upgradeMessage, +}) => { const title = useMemo( () => ( @@ -38,33 +50,24 @@ const UpgradeComponent: React.FC = () => { () => ( - - {i18n.ATTACK_DISCOVERY_IS_AVAILABLE} + + {availabilityMessage} - - {i18n.PLEASE_UPGRADE} + + {upgradeMessage} ), - [] - ); - - const actions = useMemo( - () => ( - - - - - - ), - [http.basePath] + [availabilityMessage, upgradeMessage] ); return ; }; -export const Upgrade = React.memo(UpgradeComponent); +AttackDiscoveryUpsellingSectionComponent.displayName = 'AttackDiscoveryUpsellingSection'; + +export const AttackDiscoveryUpsellingSection = React.memo(AttackDiscoveryUpsellingSectionComponent); diff --git a/x-pack/packages/security-solution/upselling/sections/attack_discovery/translations.ts b/x-pack/packages/security-solution/upselling/sections/attack_discovery/translations.ts new file mode 100644 index 0000000000000..776aa19ebf207 --- /dev/null +++ b/x-pack/packages/security-solution/upselling/sections/attack_discovery/translations.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const FIND_POTENTIAL_ATTACKS_WITH_AI = i18n.translate( + 'securitySolutionPackages.upselling.sections.attackDiscovery.findPotentialAttacksWithAiTitle', + { + defaultMessage: 'Find potential attacks with AI', + } +); diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index 7357f48474079..5da9b87a4e267 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -20,6 +20,7 @@ export { SecurityPageName } from '@kbn/security-solution-navigation'; export const APP_ID = 'securitySolution' as const; export const APP_UI_ID = 'securitySolutionUI' as const; export const ASSISTANT_FEATURE_ID = 'securitySolutionAssistant' as const; +export const ATTACK_DISCOVERY_FEATURE_ID = 'securitySolutionAttackDiscovery' as const; export const CASES_FEATURE_ID = 'securitySolutionCases' as const; export const SERVER_APP_ID = 'siem' as const; export const APP_NAME = 'Security' as const; diff --git a/x-pack/plugins/security_solution/common/test/ess_roles.json b/x-pack/plugins/security_solution/common/test/ess_roles.json index 9bf9e1b64aee3..94bd3d57a6d7b 100644 --- a/x-pack/plugins/security_solution/common/test/ess_roles.json +++ b/x-pack/plugins/security_solution/common/test/ess_roles.json @@ -29,6 +29,7 @@ "ml": ["read"], "siem": ["read", "read_alerts"], "securitySolutionAssistant": ["none"], + "securitySolutionAttackDiscovery": ["none"], "securitySolutionCases": ["read"], "actions": ["read"], "builtInAlerts": ["read"] @@ -77,6 +78,7 @@ "ml": ["read"], "siem": ["all", "read_alerts", "crud_alerts"], "securitySolutionAssistant": ["all"], + "securitySolutionAttackDiscovery": ["all"], "securitySolutionCases": ["all"], "actions": ["read"], "builtInAlerts": ["all"] @@ -125,6 +127,7 @@ "ml": ["read"], "siem": ["all", "read_alerts", "crud_alerts"], "securitySolutionAssistant": ["all"], + "securitySolutionAttackDiscovery": ["all"], "securitySolutionCases": ["all"], "builtInAlerts": ["all"] }, diff --git a/x-pack/plugins/security_solution/public/attack_discovery/links.test.ts b/x-pack/plugins/security_solution/public/attack_discovery/links.test.ts new file mode 100644 index 0000000000000..01ccdf773c081 --- /dev/null +++ b/x-pack/plugins/security_solution/public/attack_discovery/links.test.ts @@ -0,0 +1,22 @@ +/* + * 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 { ATTACK_DISCOVERY_FEATURE_ID } from '../../common/constants'; +import { SERVER_APP_ID } from '../../common'; +import { links } from './links'; + +describe('links', () => { + it('for serverless, it specifies capabilities as an AND condition, via a nested array', () => { + expect(links.capabilities).toEqual([ + [`${SERVER_APP_ID}.show`, `${ATTACK_DISCOVERY_FEATURE_ID}.attack-discovery`], + ]); + }); + + it('for self managed, it requires an enterprise license', () => { + expect(links.licenseType).toEqual('enterprise'); + }); +}); diff --git a/x-pack/plugins/security_solution/public/attack_discovery/links.ts b/x-pack/plugins/security_solution/public/attack_discovery/links.ts index dd3e45dfefc8a..531c0a1c48610 100644 --- a/x-pack/plugins/security_solution/public/attack_discovery/links.ts +++ b/x-pack/plugins/security_solution/public/attack_discovery/links.ts @@ -8,11 +8,16 @@ import { i18n } from '@kbn/i18n'; import { ATTACK_DISCOVERY } from '../app/translations'; -import { ATTACK_DISCOVERY_PATH, SecurityPageName, SERVER_APP_ID } from '../../common/constants'; +import { + ATTACK_DISCOVERY_FEATURE_ID, + ATTACK_DISCOVERY_PATH, + SecurityPageName, + SERVER_APP_ID, +} from '../../common/constants'; import type { LinkItem } from '../common/links/types'; export const links: LinkItem = { - capabilities: [`${SERVER_APP_ID}.show`], + capabilities: [[`${SERVER_APP_ID}.show`, `${ATTACK_DISCOVERY_FEATURE_ID}.attack-discovery`]], // This is an AND condition via the nested array globalNavPosition: 4, globalSearchKeywords: [ i18n.translate('xpack.securitySolution.appLinks.attackDiscovery', { @@ -20,6 +25,7 @@ export const links: LinkItem = { }), ], id: SecurityPageName.attackDiscovery, + licenseType: 'enterprise', path: ATTACK_DISCOVERY_PATH, title: ATTACK_DISCOVERY, }; diff --git a/x-pack/plugins/security_solution/public/attack_discovery/pages/empty_prompt/index.test.tsx b/x-pack/plugins/security_solution/public/attack_discovery/pages/empty_prompt/index.test.tsx index 70acc1dbb2ca8..56b2205b28726 100644 --- a/x-pack/plugins/security_solution/public/attack_discovery/pages/empty_prompt/index.test.tsx +++ b/x-pack/plugins/security_solution/public/attack_discovery/pages/empty_prompt/index.test.tsx @@ -68,30 +68,6 @@ describe('EmptyPrompt', () => { }); }); - describe('when the user does NOT have the assistant privilege', () => { - it('disables the generate button when the user does NOT have the assistant privilege', () => { - (useAssistantAvailability as jest.Mock).mockReturnValue({ - hasAssistantPrivilege: false, // <-- the user does NOT have the assistant privilege - isAssistantEnabled: true, - }); - - render( - - - - ); - - const generateButton = screen.getByTestId('generate'); - - expect(generateButton).toBeDisabled(); - }); - }); - describe('when loading is true', () => { const isLoading = true; diff --git a/x-pack/plugins/security_solution/public/attack_discovery/pages/empty_prompt/index.tsx b/x-pack/plugins/security_solution/public/attack_discovery/pages/empty_prompt/index.tsx index 3a616c299ecc5..75c8533efcc92 100644 --- a/x-pack/plugins/security_solution/public/attack_discovery/pages/empty_prompt/index.tsx +++ b/x-pack/plugins/security_solution/public/attack_discovery/pages/empty_prompt/index.tsx @@ -21,7 +21,6 @@ import { import { css } from '@emotion/react'; import React, { useMemo } from 'react'; -import { useAssistantAvailability } from '../../../assistant/use_assistant_availability'; import { AnimatedCounter } from './animated_counter'; import * as i18n from './translations'; @@ -39,7 +38,6 @@ const EmptyPromptComponent: React.FC = ({ onGenerate, }) => { const { euiTheme } = useEuiTheme(); - const { hasAssistantPrivilege } = useAssistantAvailability(); const title = useMemo( () => ( = ({ ); const actions = useMemo(() => { - const disabled = !hasAssistantPrivilege || isLoading || isDisabled; + const disabled = isLoading || isDisabled; return ( = ({ ); - }, [hasAssistantPrivilege, isDisabled, isLoading, onGenerate]); + }, [isDisabled, isLoading, onGenerate]); return ( { expect(onGenerate).toHaveBeenCalled(); }); - it('disables the generate button when the user does NOT have the assistant privilege', () => { - (useAssistantAvailability as jest.Mock).mockReturnValue({ - hasAssistantPrivilege: false, - isAssistantEnabled: true, - }); - - render( - -
- - ); - - const generate = screen.getByTestId('generate'); - - expect(generate).toBeDisabled(); - }); - it('displays the cancel button when loading', () => { const isLoading = true; diff --git a/x-pack/plugins/security_solution/public/attack_discovery/pages/header/index.tsx b/x-pack/plugins/security_solution/public/attack_discovery/pages/header/index.tsx index fa4a9caa3dcb1..583bcc25d0eb6 100644 --- a/x-pack/plugins/security_solution/public/attack_discovery/pages/header/index.tsx +++ b/x-pack/plugins/security_solution/public/attack_discovery/pages/header/index.tsx @@ -14,7 +14,6 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import type { AttackDiscoveryStats } from '@kbn/elastic-assistant-common'; import { StatusBell } from './status_bell'; -import { useAssistantAvailability } from '../../../assistant/use_assistant_availability'; import * as i18n from './translations'; interface Props { @@ -38,9 +37,8 @@ const HeaderComponent: React.FC = ({ onCancel, stats, }) => { - const { hasAssistantPrivilege } = useAssistantAvailability(); const { euiTheme } = useEuiTheme(); - const disabled = !hasAssistantPrivilege || connectorId == null; + const disabled = connectorId == null; const [didCancel, setDidCancel] = useState(false); diff --git a/x-pack/plugins/security_solution/public/attack_discovery/pages/index.test.tsx b/x-pack/plugins/security_solution/public/attack_discovery/pages/index.test.tsx index f27f8d448bbd3..97f98b81dc153 100644 --- a/x-pack/plugins/security_solution/public/attack_discovery/pages/index.test.tsx +++ b/x-pack/plugins/security_solution/public/attack_discovery/pages/index.test.tsx @@ -9,7 +9,6 @@ import { mockCasesContext } from '@kbn/cases-plugin/public/mocks/mock_cases_cont import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; import { createFilterManagerMock } from '@kbn/data-plugin/public/query/filter_manager/filter_manager.mock'; import { createStubDataView } from '@kbn/data-views-plugin/common/data_view.stub'; -import type { AssistantAvailability } from '@kbn/elastic-assistant'; import { UpsellingService } from '@kbn/security-solution-upselling/service'; import { Router } from '@kbn/shared-ux-router'; import { render, screen } from '@testing-library/react'; @@ -17,7 +16,6 @@ import React from 'react'; import { useLocalStorage } from 'react-use'; import { TestProviders } from '../../common/mock'; -import { MockAssistantProvider } from '../../common/mock/mock_assistant_provider'; import { ATTACK_DISCOVERY_PATH } from '../../../common/constants'; import { mockHistory } from '../../common/utils/route/mocks'; import { AttackDiscoveryPage } from '.'; @@ -551,52 +549,4 @@ describe('AttackDiscovery', () => { expect(screen.queryByTestId('upgrade')).toBeNull(); }); }); - - describe('when the user does not have an Enterprise license', () => { - const assistantUnavailable: AssistantAvailability = { - hasAssistantPrivilege: false, - hasConnectorsAllPrivilege: true, - hasConnectorsReadPrivilege: true, - hasUpdateAIAssistantAnonymization: false, - isAssistantEnabled: false, // <-- non-Enterprise license - }; - - beforeEach(() => { - render( - - - - - - - - - - ); - }); - - it('does NOT render the animated logo', () => { - expect(screen.queryByTestId('animatedLogo')).toBeNull(); - }); - - it('does NOT render the header', () => { - expect(screen.queryByTestId('header')).toBeNull(); - }); - - it('does NOT render the summary', () => { - expect(screen.queryByTestId('summary')).toBeNull(); - }); - - it('does NOT render attack discoveries', () => { - expect(screen.queryAllByTestId('attackDiscovery')).toHaveLength(0); - }); - - it('does NOT render the loading callout', () => { - expect(screen.queryByTestId('loadingCallout')).toBeNull(); - }); - - it('renders the upgrade call to action', () => { - expect(screen.getByTestId('upgrade')).toBeInTheDocument(); - }); - }); }); diff --git a/x-pack/plugins/security_solution/public/attack_discovery/pages/index.tsx b/x-pack/plugins/security_solution/public/attack_discovery/pages/index.tsx index 9a5c311b5494f..f3981696b3e80 100644 --- a/x-pack/plugins/security_solution/public/attack_discovery/pages/index.tsx +++ b/x-pack/plugins/security_solution/public/attack_discovery/pages/index.tsx @@ -18,7 +18,6 @@ import { uniq } from 'lodash/fp'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useLocalStorage } from 'react-use'; -import { SecurityRoutePageWrapper } from '../../common/components/security_route_page_wrapper'; import { SecurityPageName } from '../../../common/constants'; import { HeaderPage } from '../../common/components/header_page'; import { useSpaceId } from '../../common/hooks/use_space_id'; @@ -35,17 +34,12 @@ import { EmptyStates } from './empty_states'; import { LoadingCallout } from './loading_callout'; import { PageTitle } from './page_title'; import { Summary } from './summary'; -import { Upgrade } from './upgrade'; import { useAttackDiscovery } from '../use_attack_discovery'; const AttackDiscoveryPageComponent: React.FC = () => { const spaceId = useSpaceId() ?? 'default'; - const { - assistantAvailability: { isAssistantEnabled }, - http, - knowledgeBase, - } = useAssistantContext(); + const { http, knowledgeBase } = useAssistantContext(); const { data: aiConnectors } = useLoadConnectors({ http, }); @@ -144,18 +138,10 @@ const AttackDiscoveryPageComponent: React.FC = () => { }, [aiConnectors]); const animatedLogo = useMemo(() => , []); + const connectorsAreConfigured = aiConnectors != null && aiConnectors.length > 0; const attackDiscoveriesCount = selectedConnectorAttackDiscoveries.length; - if (!isAssistantEnabled) { - return ( - <> - - - - ); - } - return (
{ `} data-test-subj="fullHeightContainer" > - +
{ )} - +
); }; diff --git a/x-pack/plugins/security_solution/public/attack_discovery/pages/page_title/index.tsx b/x-pack/plugins/security_solution/public/attack_discovery/pages/page_title/index.tsx index f5bab1cbdf87a..65e07aa726d11 100644 --- a/x-pack/plugins/security_solution/public/attack_discovery/pages/page_title/index.tsx +++ b/x-pack/plugins/security_solution/public/attack_discovery/pages/page_title/index.tsx @@ -15,7 +15,13 @@ const PageTitleComponent: React.FC = () => { const { euiTheme } = useEuiTheme(); return ( - +

{i18n.ATTACK_DISCOVERY_PAGE_TITLE}

@@ -23,14 +29,10 @@ const PageTitleComponent: React.FC = () => {
{ size="m" color="hollow" css={css` - margin-bottom: ${euiTheme.size.s}; + .euiBetaBadge__icon { + position: relative; + top: 5px; + } `} /> diff --git a/x-pack/plugins/security_solution/public/attack_discovery/pages/upgrade/index.test.tsx b/x-pack/plugins/security_solution/public/attack_discovery/pages/upgrade/index.test.tsx deleted file mode 100644 index e72f53e9062d7..0000000000000 --- a/x-pack/plugins/security_solution/public/attack_discovery/pages/upgrade/index.test.tsx +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { render, screen } from '@testing-library/react'; -import React from 'react'; - -import { Upgrade } from '.'; -import { TestProviders } from '../../../common/mock'; -import { - ATTACK_DISCOVERY_IS_AVAILABLE, - FIND_POTENTIAL_ATTACKS_WITH_AI, - PLEASE_UPGRADE, -} from './translations'; - -describe('Upgrade', () => { - beforeEach(() => { - render( - - - - ); - }); - - it('renders the assistant avatar', () => { - const assistantAvatar = screen.getByTestId('assistantAvatar'); - - expect(assistantAvatar).toBeInTheDocument(); - }); - - it('renders the expected upgrade title', () => { - const upgradeTitle = screen.getByTestId('upgradeTitle'); - - expect(upgradeTitle).toHaveTextContent(FIND_POTENTIAL_ATTACKS_WITH_AI); - }); - - it('renders the attack discovery availability text', () => { - const attackDiscoveryIsAvailable = screen.getByTestId('attackDiscoveryIsAvailable'); - - expect(attackDiscoveryIsAvailable).toHaveTextContent(ATTACK_DISCOVERY_IS_AVAILABLE); - }); - - it('renders the please upgrade text', () => { - const pleaseUpgrade = screen.getByTestId('pleaseUpgrade'); - - expect(pleaseUpgrade).toHaveTextContent(PLEASE_UPGRADE); - }); - - it('renders the upgrade subscription plans (docs) link', () => { - const upgradeDocs = screen.getByRole('link', { name: 'Subscription plans' }); - - expect(upgradeDocs).toBeInTheDocument(); - }); - - it('renders the upgrade Manage license call to action', () => { - const upgradeCta = screen.getByRole('link', { name: 'Manage license' }); - - expect(upgradeCta).toBeInTheDocument(); - }); -}); diff --git a/x-pack/plugins/security_solution/public/attack_discovery/pages/upgrade/translations.ts b/x-pack/plugins/security_solution/public/attack_discovery/pages/upgrade/translations.ts deleted file mode 100644 index eece0fb5a6870..0000000000000 --- a/x-pack/plugins/security_solution/public/attack_discovery/pages/upgrade/translations.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; - -export const FIND_POTENTIAL_ATTACKS_WITH_AI = i18n.translate( - 'xpack.securitySolution.attackDiscovery.upgrade.findPotentialAttacksWithAiTitle', - { - defaultMessage: 'Find potential attacks with AI', - } -); - -export const ATTACK_DISCOVERY_IS_AVAILABLE = i18n.translate( - 'xpack.securitySolution.attackDiscovery.upgrade.attackDiscoveryIsAvailable', - { - defaultMessage: 'Your license does not support Attack discovery.', - } -); - -export const PLEASE_UPGRADE = i18n.translate( - 'xpack.securitySolution.attackDiscovery.upgrade.pleaseUpgradeMessage', - { - defaultMessage: 'Please upgrade your license to use this feature.', - } -); - -export const UPGRADE = i18n.translate( - 'xpack.securitySolution.attackDiscovery.upgrade.upgradeButton', - { - defaultMessage: 'Upgrade', - } -); diff --git a/x-pack/plugins/security_solution/public/attack_discovery/routes.tsx b/x-pack/plugins/security_solution/public/attack_discovery/routes.tsx index 10c9a70e4217f..f91fda6cd6304 100644 --- a/x-pack/plugins/security_solution/public/attack_discovery/routes.tsx +++ b/x-pack/plugins/security_solution/public/attack_discovery/routes.tsx @@ -6,7 +6,6 @@ */ import React from 'react'; -import { TrackApplicationView } from '@kbn/usage-collection-plugin/public'; import { AttackDiscoveryPage } from './pages'; import type { SecuritySubPluginRoutes } from '../app/types'; @@ -17,11 +16,9 @@ import { SecurityRoutePageWrapper } from '../common/components/security_route_pa export const AttackDiscoveryRoutes = () => ( - - - - - + + + ); diff --git a/x-pack/plugins/security_solution/public/attack_discovery/use_attack_discovery/index.test.tsx b/x-pack/plugins/security_solution/public/attack_discovery/use_attack_discovery/index.test.tsx index d21630bb4c835..6329ce5ca699a 100644 --- a/x-pack/plugins/security_solution/public/attack_discovery/use_attack_discovery/index.test.tsx +++ b/x-pack/plugins/security_solution/public/attack_discovery/use_attack_discovery/index.test.tsx @@ -5,15 +5,24 @@ * 2.0. */ -import React from 'react'; +import { useLoadConnectors } from '@kbn/elastic-assistant'; +import { useFetchAnonymizationFields } from '@kbn/elastic-assistant/impl/assistant/api/anonymization_fields/use_fetch_anonymization_fields'; import { renderHook, act } from '@testing-library/react-hooks'; +import React from 'react'; + import { useKibana } from '../../common/lib/kibana'; -import { useFetchAnonymizationFields } from '@kbn/elastic-assistant/impl/assistant/api/anonymization_fields/use_fetch_anonymization_fields'; import { usePollApi } from '../hooks/use_poll_api'; import { useAttackDiscovery } from '.'; import { ERROR_GENERATING_ATTACK_DISCOVERIES } from '../pages/translations'; import { useKibana as mockUseKibana } from '../../common/lib/kibana/__mocks__'; +jest.mock('../../assistant/use_assistant_availability', () => ({ + useAssistantAvailability: jest.fn(() => ({ + hasAssistantPrivilege: true, + isAssistantEnabled: true, + })), +})); + jest.mock( '@kbn/elastic-assistant/impl/assistant/api/anonymization_fields/use_fetch_anonymization_fields' ); @@ -40,10 +49,10 @@ jest.mock('@kbn/elastic-assistant', () => ({ latestAlerts: 20, }, }), - useLoadConnectors: () => ({ + useLoadConnectors: jest.fn(() => ({ isFetched: true, data: mockConnectors, - }), + })), })); const mockAttackDiscoveryPost = { timestamp: '2024-06-13T17:50:59.409Z', @@ -224,4 +233,26 @@ describe('useAttackDiscovery', () => { expect(result.current.isLoading).toBe(false); expect(result.current.lastUpdated).toEqual(null); }); + + describe('when zero connectors are configured', () => { + beforeEach(() => { + (useLoadConnectors as jest.Mock).mockReturnValue({ + isFetched: true, + data: [], // <-- zero connectors configured + }); + + renderHook(() => useAttackDiscovery({ connectorId: 'test-id', setLoadingConnectorId })); + }); + + afterEach(() => { + (useLoadConnectors as jest.Mock).mockReturnValue({ + isFetched: true, + data: mockConnectors, + }); + }); + + it('does NOT call pollApi when zero connectors are configured', () => { + expect(mockPollApi.pollApi).not.toHaveBeenCalled(); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/attack_discovery/use_attack_discovery/index.tsx b/x-pack/plugins/security_solution/public/attack_discovery/use_attack_discovery/index.tsx index 87f0f4d9a5089..deb1c556bdb43 100644 --- a/x-pack/plugins/security_solution/public/attack_discovery/use_attack_discovery/index.tsx +++ b/x-pack/plugins/security_solution/public/attack_discovery/use_attack_discovery/index.tsx @@ -109,7 +109,12 @@ export const useAttackDiscovery = ({ ]); useEffect(() => { - if (connectorId != null && connectorId !== '') { + if ( + connectorId != null && + connectorId !== '' && + aiConnectors != null && + aiConnectors.length > 0 + ) { pollApi(); setLoadingConnectorId?.(connectorId); setAlertsContextCount(null); @@ -120,7 +125,7 @@ export const useAttackDiscovery = ({ setGenerationIntervals([]); setPollStatus(null); } - }, [pollApi, connectorId, setLoadingConnectorId, setPollStatus]); + }, [aiConnectors, connectorId, pollApi, setLoadingConnectorId, setPollStatus]); useEffect(() => { if (pollStatus === 'running') { diff --git a/x-pack/plugins/security_solution/scripts/endpoint/common/roles_users/serverless/es_serverless_resources/roles.yml b/x-pack/plugins/security_solution/scripts/endpoint/common/roles_users/serverless/es_serverless_resources/roles.yml index 12214295817a5..3fd3bd2e3233e 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/common/roles_users/serverless/es_serverless_resources/roles.yml +++ b/x-pack/plugins/security_solution/scripts/endpoint/common/roles_users/serverless/es_serverless_resources/roles.yml @@ -65,6 +65,7 @@ viewer: - feature_siem.endpoint_list_read - feature_securitySolutionCases.read - feature_securitySolutionAssistant.all + - feature_securitySolutionAttackDiscovery.all - feature_actions.read - feature_builtInAlerts.read - feature_osquery.read @@ -143,6 +144,7 @@ editor: - feature_siem.file_operations_all - feature_securitySolutionCases.all - feature_securitySolutionAssistant.all + - feature_securitySolutionAttackDiscovery.all - feature_actions.read - feature_builtInAlerts.all - feature_osquery.all @@ -191,6 +193,7 @@ t1_analyst: - feature_siem.endpoint_list_read - feature_securitySolutionCases.read - feature_securitySolutionAssistant.all + - feature_securitySolutionAttackDiscovery.all - feature_actions.read - feature_builtInAlerts.read - feature_osquery.read @@ -245,6 +248,7 @@ t2_analyst: - feature_siem.endpoint_list_read - feature_securitySolutionCases.all - feature_securitySolutionAssistant.all + - feature_securitySolutionAttackDiscovery.all - feature_actions.read - feature_builtInAlerts.read - feature_osquery.read @@ -314,6 +318,7 @@ t3_analyst: - feature_siem.scan_operations_all - feature_securitySolutionCases.all - feature_securitySolutionAssistant.all + - feature_securitySolutionAttackDiscovery.all - feature_actions.read - feature_builtInAlerts.all - feature_osquery.all @@ -370,6 +375,7 @@ threat_intelligence_analyst: - feature_siem.blocklist_all - feature_securitySolutionCases.all - feature_securitySolutionAssistant.all + - feature_securitySolutionAttackDiscovery.all - feature_actions.read - feature_builtInAlerts.read - feature_osquery.all @@ -437,6 +443,7 @@ rule_author: - feature_siem.actions_log_management_read - feature_securitySolutionCases.all - feature_securitySolutionAssistant.all + - feature_securitySolutionAttackDiscovery.all - feature_actions.read - feature_builtInAlerts.all - feature_osquery.all @@ -508,6 +515,7 @@ soc_manager: - feature_siem.scan_operations_all - feature_securitySolutionCases.all - feature_securitySolutionAssistant.all + - feature_securitySolutionAttackDiscovery.all - feature_actions.all - feature_builtInAlerts.all - feature_osquery.all @@ -567,6 +575,7 @@ detections_admin: - feature_siem.crud_alerts - feature_securitySolutionCases.all - feature_securitySolutionAssistant.all + - feature_securitySolutionAttackDiscovery.all - feature_actions.all - feature_builtInAlerts.all - feature_dev_tools.all @@ -625,6 +634,7 @@ platform_engineer: - feature_siem.actions_log_management_read - feature_securitySolutionCases.all - feature_securitySolutionAssistant.all + - feature_securitySolutionAttackDiscovery.all - feature_actions.all - feature_builtInAlerts.all - feature_fleet.all @@ -697,6 +707,7 @@ endpoint_operations_analyst: - feature_siem.scan_operations_all - feature_securitySolutionCases.all - feature_securitySolutionAssistant.all + - feature_securitySolutionAttackDiscovery.all - feature_actions.all - feature_builtInAlerts.all - feature_osquery.all @@ -763,6 +774,7 @@ endpoint_policy_manager: - feature_siem.blocklist_all # Elastic Defend Policy Management - feature_securitySolutionCases.all - feature_securitySolutionAssistant.all + - feature_securitySolutionAttackDiscovery.all - feature_actions.all - feature_builtInAlerts.all - feature_osquery.all diff --git a/x-pack/plugins/security_solution/server/lib/product_features_service/mocks.ts b/x-pack/plugins/security_solution/server/lib/product_features_service/mocks.ts index dfc86518e124f..c2275ebbcee5f 100644 --- a/x-pack/plugins/security_solution/server/lib/product_features_service/mocks.ts +++ b/x-pack/plugins/security_solution/server/lib/product_features_service/mocks.ts @@ -31,6 +31,11 @@ jest.mock('@kbn/security-solution-features/product_features', () => ({ baseKibanaSubFeatureIds: [], subFeaturesMap: new Map(), })), + getAttackDiscoveryFeature: jest.fn(() => ({ + baseKibanaFeature: {}, + baseKibanaSubFeatureIds: [], + subFeaturesMap: new Map(), + })), })); export const createProductFeaturesServiceMock = ( @@ -103,6 +108,25 @@ export const createProductFeaturesServiceMock = ( ]) ) ), + attackDiscovery: jest.fn().mockReturnValue( + new Map( + enabledFeatureKeys.map((key) => [ + key, + { + privileges: { + all: { + ui: ['entity-analytics'], + api: [`test-entity-analytics`], + }, + read: { + ui: ['entity-analytics'], + api: [`test-entity-analytics`], + }, + }, + }, + ]) + ) + ), }); } diff --git a/x-pack/plugins/security_solution/server/lib/product_features_service/product_features_service.test.ts b/x-pack/plugins/security_solution/server/lib/product_features_service/product_features_service.test.ts index b59dafb2f0eb7..27205a30be785 100644 --- a/x-pack/plugins/security_solution/server/lib/product_features_service/product_features_service.test.ts +++ b/x-pack/plugins/security_solution/server/lib/product_features_service/product_features_service.test.ts @@ -40,6 +40,7 @@ const productFeature = { }; const mockGetFeature = jest.fn().mockReturnValue(productFeature); jest.mock('@kbn/security-solution-features/product_features', () => ({ + getAttackDiscoveryFeature: () => mockGetFeature(), getAssistantFeature: () => mockGetFeature(), getCasesFeature: () => mockGetFeature(), getSecurityFeature: () => mockGetFeature(), @@ -54,8 +55,8 @@ describe('ProductFeaturesService', () => { const experimentalFeatures = {} as ExperimentalFeatures; new ProductFeaturesService(loggerMock.create(), experimentalFeatures); - expect(mockGetFeature).toHaveBeenCalledTimes(3); - expect(MockedProductFeatures).toHaveBeenCalledTimes(3); + expect(mockGetFeature).toHaveBeenCalledTimes(4); + expect(MockedProductFeatures).toHaveBeenCalledTimes(4); }); it('should init all ProductFeatures when initialized', () => { @@ -86,8 +87,10 @@ describe('ProductFeaturesService', () => { const mockSecurityConfig = new Map() as ProductFeaturesConfig; const mockCasesConfig = new Map() as ProductFeaturesConfig; const mockAssistantConfig = new Map() as ProductFeaturesConfig; + const mockAttackDiscoveryConfig = new Map() as ProductFeaturesConfig; const configurator: ProductFeaturesConfigurator = { + attackDiscovery: jest.fn(() => mockAttackDiscoveryConfig), security: jest.fn(() => mockSecurityConfig), cases: jest.fn(() => mockCasesConfig), securityAssistant: jest.fn(() => mockAssistantConfig), @@ -97,6 +100,7 @@ describe('ProductFeaturesService', () => { expect(configurator.security).toHaveBeenCalled(); expect(configurator.cases).toHaveBeenCalled(); expect(configurator.securityAssistant).toHaveBeenCalled(); + expect(configurator.attackDiscovery).toHaveBeenCalled(); expect(MockedProductFeatures.mock.instances[0].setConfig).toHaveBeenCalledWith( mockSecurityConfig @@ -105,6 +109,9 @@ describe('ProductFeaturesService', () => { expect(MockedProductFeatures.mock.instances[2].setConfig).toHaveBeenCalledWith( mockAssistantConfig ); + expect(MockedProductFeatures.mock.instances[3].setConfig).toHaveBeenCalledWith( + mockAttackDiscoveryConfig + ); }); it('should return isEnabled for enabled features', () => { @@ -127,8 +134,12 @@ describe('ProductFeaturesService', () => { const mockAssistantConfig = new Map([ [ProductFeatureKey.assistant, {}], ]) as ProductFeaturesConfig; + const mockAttackDiscoveryConfig = new Map([ + [ProductFeatureKey.attackDiscovery, {}], + ]) as ProductFeaturesConfig; const configurator: ProductFeaturesConfigurator = { + attackDiscovery: jest.fn(() => mockAttackDiscoveryConfig), security: jest.fn(() => mockSecurityConfig), cases: jest.fn(() => mockCasesConfig), securityAssistant: jest.fn(() => mockAssistantConfig), @@ -139,6 +150,7 @@ describe('ProductFeaturesService', () => { expect(productFeaturesService.isEnabled(ProductFeatureKey.endpointExceptions)).toEqual(true); expect(productFeaturesService.isEnabled(ProductFeatureKey.casesConnectors)).toEqual(true); expect(productFeaturesService.isEnabled(ProductFeatureKey.assistant)).toEqual(true); + expect(productFeaturesService.isEnabled(ProductFeatureKey.attackDiscovery)).toEqual(true); expect(productFeaturesService.isEnabled(ProductFeatureKey.externalRuleActions)).toEqual(false); }); diff --git a/x-pack/plugins/security_solution/server/lib/product_features_service/product_features_service.ts b/x-pack/plugins/security_solution/server/lib/product_features_service/product_features_service.ts index 8714c2e4ab6ab..e30c067a0d4a4 100644 --- a/x-pack/plugins/security_solution/server/lib/product_features_service/product_features_service.ts +++ b/x-pack/plugins/security_solution/server/lib/product_features_service/product_features_service.ts @@ -17,6 +17,7 @@ import type { FeaturesPluginSetup } from '@kbn/features-plugin/server'; import type { ProductFeatureKeyType } from '@kbn/security-solution-features'; import { getAssistantFeature, + getAttackDiscoveryFeature, getCasesFeature, getSecurityFeature, } from '@kbn/security-solution-features/product_features'; @@ -31,6 +32,7 @@ export class ProductFeaturesService { private securityProductFeatures: ProductFeatures; private casesProductFeatures: ProductFeatures; private securityAssistantProductFeatures: ProductFeatures; + private attackDiscoveryProductFeatures: ProductFeatures; private productFeatures?: Set; constructor( @@ -67,12 +69,21 @@ export class ProductFeaturesService { assistantFeature.baseKibanaFeature, assistantFeature.baseKibanaSubFeatureIds ); + + const attackDiscoveryFeature = getAttackDiscoveryFeature(); + this.attackDiscoveryProductFeatures = new ProductFeatures( + this.logger, + attackDiscoveryFeature.subFeaturesMap, + attackDiscoveryFeature.baseKibanaFeature, + attackDiscoveryFeature.baseKibanaSubFeatureIds + ); } public init(featuresSetup: FeaturesPluginSetup) { this.securityProductFeatures.init(featuresSetup); this.casesProductFeatures.init(featuresSetup); this.securityAssistantProductFeatures.init(featuresSetup); + this.attackDiscoveryProductFeatures.init(featuresSetup); } public setProductFeaturesConfigurator(configurator: ProductFeaturesConfigurator) { @@ -85,11 +96,15 @@ export class ProductFeaturesService { const securityAssistantProductFeaturesConfig = configurator.securityAssistant(); this.securityAssistantProductFeatures.setConfig(securityAssistantProductFeaturesConfig); + const attackDiscoveryProductFeaturesConfig = configurator.attackDiscovery(); + this.attackDiscoveryProductFeatures.setConfig(attackDiscoveryProductFeaturesConfig); + this.productFeatures = new Set( Object.freeze([ ...securityProductFeaturesConfig.keys(), ...casesProductFeaturesConfig.keys(), ...securityAssistantProductFeaturesConfig.keys(), + ...attackDiscoveryProductFeaturesConfig.keys(), ]) as readonly ProductFeatureKeyType[] ); } @@ -107,7 +122,8 @@ export class ProductFeaturesService { return ( this.securityProductFeatures.isActionRegistered(action) || this.casesProductFeatures.isActionRegistered(action) || - this.securityAssistantProductFeatures.isActionRegistered(action) + this.securityAssistantProductFeatures.isActionRegistered(action) || + this.attackDiscoveryProductFeatures.isActionRegistered(action) ); } diff --git a/x-pack/plugins/security_solution/server/lib/product_features_service/types.ts b/x-pack/plugins/security_solution/server/lib/product_features_service/types.ts index 27bf020fad4ae..9c7b20cfba960 100644 --- a/x-pack/plugins/security_solution/server/lib/product_features_service/types.ts +++ b/x-pack/plugins/security_solution/server/lib/product_features_service/types.ts @@ -13,6 +13,7 @@ import type { } from '@kbn/security-solution-features/keys'; export interface ProductFeaturesConfigurator { + attackDiscovery: () => ProductFeaturesConfig; security: () => ProductFeaturesConfig; cases: () => ProductFeaturesConfig; securityAssistant: () => ProductFeaturesConfig; diff --git a/x-pack/plugins/security_solution_ess/public/upselling/lazy_upselling.tsx b/x-pack/plugins/security_solution_ess/public/upselling/lazy_upselling.tsx index 70f39b2ae70e6..56775b6c4433e 100644 --- a/x-pack/plugins/security_solution_ess/public/upselling/lazy_upselling.tsx +++ b/x-pack/plugins/security_solution_ess/public/upselling/lazy_upselling.tsx @@ -24,3 +24,9 @@ export const EntityAnalyticsUpsellingPageLazy = lazy(() => default: EntityAnalyticsUpsellingPageESS, })) ); + +export const AttackDiscoveryUpsellingPageLazy = lazy(() => + import('./pages/attack_discovery').then(({ AttackDiscoveryUpsellingPageESS }) => ({ + default: AttackDiscoveryUpsellingPageESS, + })) +); diff --git a/x-pack/plugins/security_solution_ess/public/upselling/pages/attack_discovery/index.test.tsx b/x-pack/plugins/security_solution_ess/public/upselling/pages/attack_discovery/index.test.tsx new file mode 100644 index 0000000000000..ee84089e5624a --- /dev/null +++ b/x-pack/plugins/security_solution_ess/public/upselling/pages/attack_discovery/index.test.tsx @@ -0,0 +1,56 @@ +/* + * 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 { render, screen } from '@testing-library/react'; +import React from 'react'; + +import * as i18n from './translations'; + +jest.mock('../../../common/services', () => ({ + useKibana: jest.fn(() => ({ + services: { + application: { + getUrlForApp: jest + .fn() + .mockReturnValue('http://localhost:5601/app/management/stack/license_management'), + }, + http: { + basePath: { + get: () => 'some-base-path', + }, + }, + }, + })), +})); + +import { AttackDiscoveryUpsellingPageESS } from '.'; + +describe('AttackDiscoveryUpsellingPageESS', () => { + beforeEach(() => { + jest.clearAllMocks(); + + render(); + }); + + it('renders the expected ESS-specific availability message', () => { + const attackDiscoveryIsAvailable = screen.getByTestId('availabilityMessage'); + + expect(attackDiscoveryIsAvailable).toHaveTextContent(i18n.AVAILABILITY_MESSAGE); + }); + + it('renders the expected ESS-specific upgrade message', () => { + const pleaseUpgrade = screen.getByTestId('upgradeMessage'); + + expect(pleaseUpgrade).toHaveTextContent(i18n.UPGRADE_MESSAGE); + }); + + it('renders the ESS-specific actions', () => { + const actions = screen.getByTestId('essActions'); + + expect(actions).toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/security_solution_ess/public/upselling/pages/attack_discovery/index.tsx b/x-pack/plugins/security_solution_ess/public/upselling/pages/attack_discovery/index.tsx new file mode 100644 index 0000000000000..12486b0a7be8c --- /dev/null +++ b/x-pack/plugins/security_solution_ess/public/upselling/pages/attack_discovery/index.tsx @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { AttackDiscoveryUpsellingPage } from '@kbn/security-solution-upselling/pages/attack_discovery'; +import React, { useMemo } from 'react'; + +import { UpgradeActions } from './upgrade_actions'; +import * as i18n from './translations'; + +/** + * This component passes self-managed-specific upgrade actions and `i18n` to + * the platform agnostic `AttackDiscoveryUpsellingPage` component. + */ +const AttackDiscoveryUpsellingPageESSComponent: React.FC = () => { + const actions = useMemo( + () => ( + + + + + + ), + [] + ); + + return ( + + ); +}; + +AttackDiscoveryUpsellingPageESSComponent.displayName = 'AttackDiscoveryUpsellingPageESS'; + +export const AttackDiscoveryUpsellingPageESS = React.memo(AttackDiscoveryUpsellingPageESSComponent); diff --git a/x-pack/plugins/security_solution_ess/public/upselling/pages/attack_discovery/translations.ts b/x-pack/plugins/security_solution_ess/public/upselling/pages/attack_discovery/translations.ts new file mode 100644 index 0000000000000..de393ffcf26a1 --- /dev/null +++ b/x-pack/plugins/security_solution_ess/public/upselling/pages/attack_discovery/translations.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const AVAILABILITY_MESSAGE = i18n.translate( + 'xpack.securitySolutionEss.upselling.pages.attackDiscovery.availabilityMessage', + { + defaultMessage: 'Your license does not support Attack discovery.', + } +); + +export const UPGRADE_MESSAGE = i18n.translate( + 'xpack.securitySolutionEss.upselling.pages.attackDiscovery.upgradeMessage', + { + defaultMessage: 'Please upgrade your license to use this feature.', + } +); diff --git a/x-pack/plugins/security_solution_ess/public/upselling/pages/attack_discovery/upgrade_actions/index.test.tsx b/x-pack/plugins/security_solution_ess/public/upselling/pages/attack_discovery/upgrade_actions/index.test.tsx new file mode 100644 index 0000000000000..27f19e36f8543 --- /dev/null +++ b/x-pack/plugins/security_solution_ess/public/upselling/pages/attack_discovery/upgrade_actions/index.test.tsx @@ -0,0 +1,57 @@ +/* + * 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 { render, screen } from '@testing-library/react'; +import React from 'react'; + +import * as i18n from './translations'; +import { UpgradeActions } from '.'; + +jest.mock('../../../../common/services', () => ({ + useKibana: jest.fn().mockReturnValue({ + services: { + application: { + getUrlForApp: jest + .fn() + .mockReturnValue('http://localhost:5601/app/management/stack/license_management'), + }, + }, + }), +})); + +describe('UpgradeActions', () => { + beforeEach(() => { + render(); + }); + + describe('upgrade docs button', () => { + it('renders the expected button text', () => { + expect(screen.getByTestId('upgradeDocs')).toHaveTextContent(i18n.UPGRADE_DOCS); + }); + + it('opens the link in a new tab', () => { + expect(screen.getByTestId('upgradeDocs')).toHaveAttribute('target', '_blank'); + }); + }); + + describe('upgrade call to action button', () => { + it('renders the expected button text', () => { + expect(screen.getByTestId('upgradeCta')).toHaveTextContent(i18n.UPGRADE_CTA); + }); + + it('opens the license management page in a new tab', () => { + expect(screen.getByTestId('upgradeCta')).toHaveAttribute('target', '_blank'); + }); + + it('links to the license management page', () => { + expect(screen.getByTestId('upgradeCta')).toHaveAttribute( + 'href', + 'http://localhost:5601/app/management/stack/license_management' + ); + }); + }); +}); diff --git a/x-pack/plugins/security_solution_ess/public/upselling/pages/attack_discovery/upgrade_actions/index.tsx b/x-pack/plugins/security_solution_ess/public/upselling/pages/attack_discovery/upgrade_actions/index.tsx new file mode 100644 index 0000000000000..b0849e8d9526c --- /dev/null +++ b/x-pack/plugins/security_solution_ess/public/upselling/pages/attack_discovery/upgrade_actions/index.tsx @@ -0,0 +1,52 @@ +/* + * 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 { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import React from 'react'; + +import { useKibana } from '../../../../common/services'; +import * as i18n from './translations'; + +const UpgradeActionsComponent = () => { + const { services } = useKibana(); + + return ( + + + + {i18n.UPGRADE_DOCS} + + + + + + {i18n.UPGRADE_CTA} + + + + ); +}; + +export const UpgradeActions = React.memo(UpgradeActionsComponent); diff --git a/x-pack/plugins/security_solution_ess/public/upselling/pages/attack_discovery/upgrade_actions/translations.ts b/x-pack/plugins/security_solution_ess/public/upselling/pages/attack_discovery/upgrade_actions/translations.ts new file mode 100644 index 0000000000000..185d89e188c84 --- /dev/null +++ b/x-pack/plugins/security_solution_ess/public/upselling/pages/attack_discovery/upgrade_actions/translations.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const UPGRADE_CTA = i18n.translate( + 'xpack.securitySolutionEss.upselling.pages.attackDiscovery.upgrade.upgradeTitle', + { + defaultMessage: 'Manage license', + } +); + +export const UPGRADE_DOCS = i18n.translate( + 'xpack.securitySolutionEss.upselling.pages.attackDiscovery.upgrade.upgradeButtonLabel', + { + defaultMessage: 'Subscription plans', + } +); diff --git a/x-pack/plugins/security_solution_ess/public/upselling/register_upsellings.test.tsx b/x-pack/plugins/security_solution_ess/public/upselling/register_upsellings.test.tsx new file mode 100644 index 0000000000000..e7fdcf39e4e72 --- /dev/null +++ b/x-pack/plugins/security_solution_ess/public/upselling/register_upsellings.test.tsx @@ -0,0 +1,20 @@ +/* + * 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 { SecurityPageName } from '@kbn/security-solution-plugin/common'; + +import { upsellingPages } from './register_upsellings'; + +describe('upsellingPages', () => { + it('registers the Attack discovery page with the expected minimum license for self managed', () => { + const attackDiscoveryPage = upsellingPages.find( + ({ pageName }) => pageName === SecurityPageName.attackDiscovery + ); + + expect(attackDiscoveryPage?.minimumLicenseRequired).toEqual('enterprise'); + }); +}); diff --git a/x-pack/plugins/security_solution_ess/public/upselling/register_upsellings.tsx b/x-pack/plugins/security_solution_ess/public/upselling/register_upsellings.tsx index 2f86c68821ef3..b7fbdab3b5982 100644 --- a/x-pack/plugins/security_solution_ess/public/upselling/register_upsellings.tsx +++ b/x-pack/plugins/security_solution_ess/public/upselling/register_upsellings.tsx @@ -25,6 +25,7 @@ import type React from 'react'; import type { Services } from '../common/services'; import { withServicesProvider } from '../common/services'; import { + AttackDiscoveryUpsellingPageLazy, EntityAnalyticsUpsellingPageLazy, EntityAnalyticsUpsellingSectionLazy, } from './lazy_upselling'; @@ -92,6 +93,11 @@ export const upsellingPages: UpsellingPages = [ minimumLicenseRequired: 'platinum', component: EntityAnalyticsUpsellingPageLazy, }, + { + pageName: SecurityPageName.attackDiscovery, + minimumLicenseRequired: 'enterprise', + component: AttackDiscoveryUpsellingPageLazy, + }, ]; // Upsellings for sections, linked by arbitrary ids diff --git a/x-pack/plugins/security_solution_ess/server/product_features/attack_discovery_product_features_config.ts b/x-pack/plugins/security_solution_ess/server/product_features/attack_discovery_product_features_config.ts new file mode 100644 index 0000000000000..9e575e805e203 --- /dev/null +++ b/x-pack/plugins/security_solution_ess/server/product_features/attack_discovery_product_features_config.ts @@ -0,0 +1,41 @@ +/* + * 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 { + ProductFeatureKeys, + ProductFeatureKibanaConfig, + ProductFeaturesAttackDiscoveryConfig, +} from '@kbn/security-solution-features'; +import { + attackDiscoveryDefaultProductFeaturesConfig, + createEnabledProductFeaturesConfigMap, +} from '@kbn/security-solution-features/config'; +import type { ProductFeatureAttackDiscoveryKey } from '@kbn/security-solution-features/keys'; + +/** + * Maps the ProductFeatures keys to Kibana privileges that will be merged + * into the base privileges config for the Security app. + * + * Privileges can be added in different ways: + * - `privileges`: the privileges that will be added directly into the main Attack discovery feature. + * - `subFeatureIds`: the ids of the sub-features that will be added into the Attack discovery subFeatures entry. + * - `subFeaturesPrivileges`: the privileges that will be added into the existing Attack discovery subFeature with the privilege `id` specified. + */ +const attackDiscoveryProductFeaturesConfig: Record< + ProductFeatureAttackDiscoveryKey, + ProductFeatureKibanaConfig +> = { + ...attackDiscoveryDefaultProductFeaturesConfig, + // ess-specific app features configs here +}; + +export const getAttackDiscoveryProductFeaturesConfigurator = + (enabledProductFeatureKeys: ProductFeatureKeys) => (): ProductFeaturesAttackDiscoveryConfig => + createEnabledProductFeaturesConfigMap( + attackDiscoveryProductFeaturesConfig, + enabledProductFeatureKeys + ); diff --git a/x-pack/plugins/security_solution_ess/server/product_features/index.ts b/x-pack/plugins/security_solution_ess/server/product_features/index.ts index dc84442075fc0..ed85c32f12284 100644 --- a/x-pack/plugins/security_solution_ess/server/product_features/index.ts +++ b/x-pack/plugins/security_solution_ess/server/product_features/index.ts @@ -10,11 +10,13 @@ import type { ProductFeaturesConfigurator } from '@kbn/security-solution-plugin/ import { getCasesProductFeaturesConfigurator } from './cases_product_features_config'; import { getSecurityProductFeaturesConfigurator } from './security_product_features_config'; import { getSecurityAssistantProductFeaturesConfigurator } from './assistant_product_features_config'; +import { getAttackDiscoveryProductFeaturesConfigurator } from './attack_discovery_product_features_config'; export const getProductProductFeaturesConfigurator = ( enabledProductFeatureKeys: ProductFeatureKeys ): ProductFeaturesConfigurator => { return { + attackDiscovery: getAttackDiscoveryProductFeaturesConfigurator(enabledProductFeatureKeys), security: getSecurityProductFeaturesConfigurator(enabledProductFeatureKeys), cases: getCasesProductFeaturesConfigurator(enabledProductFeatureKeys), securityAssistant: getSecurityAssistantProductFeaturesConfigurator(enabledProductFeatureKeys), diff --git a/x-pack/plugins/security_solution_serverless/common/pli/pli_config.ts b/x-pack/plugins/security_solution_serverless/common/pli/pli_config.ts index 9a6eb9ab743ca..42cdf7589fb35 100644 --- a/x-pack/plugins/security_solution_serverless/common/pli/pli_config.ts +++ b/x-pack/plugins/security_solution_serverless/common/pli/pli_config.ts @@ -22,6 +22,7 @@ export const PLI_PRODUCT_FEATURES: PliProductFeatures = { complete: [ ProductFeatureKey.advancedInsights, ProductFeatureKey.assistant, + ProductFeatureKey.attackDiscovery, ProductFeatureKey.investigationGuide, ProductFeatureKey.investigationGuideInteractions, ProductFeatureKey.threatIntelligence, diff --git a/x-pack/plugins/security_solution_serverless/public/upselling/lazy_upselling.tsx b/x-pack/plugins/security_solution_serverless/public/upselling/lazy_upselling.tsx index b7e4640608928..526654a6f4509 100644 --- a/x-pack/plugins/security_solution_serverless/public/upselling/lazy_upselling.tsx +++ b/x-pack/plugins/security_solution_serverless/public/upselling/lazy_upselling.tsx @@ -40,3 +40,11 @@ export const EntityAnalyticsUpsellingSectionLazy = withSuspenseUpsell( ) ) ); + +export const AttackDiscoveryUpsellingPageLazy = withSuspenseUpsell( + lazy(() => + import('./pages/attack_discovery').then(({ AttackDiscoveryUpsellingPageServerless }) => ({ + default: AttackDiscoveryUpsellingPageServerless, + })) + ) +); diff --git a/x-pack/plugins/security_solution_serverless/public/upselling/pages/attack_discovery/index.test.tsx b/x-pack/plugins/security_solution_serverless/public/upselling/pages/attack_discovery/index.test.tsx new file mode 100644 index 0000000000000..249e5e369f9c0 --- /dev/null +++ b/x-pack/plugins/security_solution_serverless/public/upselling/pages/attack_discovery/index.test.tsx @@ -0,0 +1,45 @@ +/* + * 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 { render, screen } from '@testing-library/react'; +import React from 'react'; + +import * as i18n from './translations'; + +jest.mock('../../../common/services', () => ({ + useKibana: jest.fn(() => ({ + services: { + http: { + basePath: { + get: () => 'some-base-path', + }, + }, + }, + })), +})); + +import { AttackDiscoveryUpsellingPageServerless } from '.'; + +describe('AttackDiscoveryUpsellingPageServerless', () => { + beforeEach(() => { + jest.clearAllMocks(); + + render(); + }); + + it('renders the expected serverless-specific availability message', () => { + const attackDiscoveryIsAvailable = screen.getByTestId('availabilityMessage'); + + expect(attackDiscoveryIsAvailable).toHaveTextContent(i18n.AVAILABILITY_MESSAGE); + }); + + it('renders the expected serverless-specific upgrade message', () => { + const pleaseUpgrade = screen.getByTestId('upgradeMessage'); + + expect(pleaseUpgrade).toHaveTextContent(i18n.UPGRADE_MESSAGE); + }); +}); diff --git a/x-pack/plugins/security_solution_serverless/public/upselling/pages/attack_discovery/index.tsx b/x-pack/plugins/security_solution_serverless/public/upselling/pages/attack_discovery/index.tsx new file mode 100644 index 0000000000000..cedbbae3ff335 --- /dev/null +++ b/x-pack/plugins/security_solution_serverless/public/upselling/pages/attack_discovery/index.tsx @@ -0,0 +1,31 @@ +/* + * 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 { AttackDiscoveryUpsellingPage } from '@kbn/security-solution-upselling/pages/attack_discovery'; +import React from 'react'; + +import * as i18n from './translations'; + +/** + * This component passes serverless-specific `i18n` to the platform agnostic + * `AttackDiscoveryUpsellingPage` component. + */ +const AttackDiscoveryUpsellingPageServerlessComponent: React.FC = () => { + return ( + + ); +}; + +AttackDiscoveryUpsellingPageServerlessComponent.displayName = + 'AttackDiscoveryUpsellingPageServerless'; + +export const AttackDiscoveryUpsellingPageServerless = React.memo( + AttackDiscoveryUpsellingPageServerlessComponent +); diff --git a/x-pack/plugins/security_solution_serverless/public/upselling/pages/attack_discovery/translations.ts b/x-pack/plugins/security_solution_serverless/public/upselling/pages/attack_discovery/translations.ts new file mode 100644 index 0000000000000..4e246986d1e0a --- /dev/null +++ b/x-pack/plugins/security_solution_serverless/public/upselling/pages/attack_discovery/translations.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const AVAILABILITY_MESSAGE = i18n.translate( + 'xpack.securitySolutionServerless.upselling.pages.attackDiscovery.availabilityMessage', + { + defaultMessage: 'Your product tier does not support Attack discovery.', + } +); + +export const UPGRADE_MESSAGE = i18n.translate( + 'xpack.securitySolutionServerless.upselling.pages.attackDiscovery.upgradeMessage', + { + defaultMessage: 'Please upgrade your product tier to use this feature.', + } +); diff --git a/x-pack/plugins/security_solution_serverless/public/upselling/upsellings.test.tsx b/x-pack/plugins/security_solution_serverless/public/upselling/upsellings.test.tsx new file mode 100644 index 0000000000000..4ec14e81ca175 --- /dev/null +++ b/x-pack/plugins/security_solution_serverless/public/upselling/upsellings.test.tsx @@ -0,0 +1,21 @@ +/* + * 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 { SecurityPageName } from '@kbn/security-solution-plugin/common'; +import { ProductFeatureKey } from '@kbn/security-solution-features/keys'; + +import { upsellingPages } from './upsellings'; + +describe('upsellingPages', () => { + it('registers the Attack discovery page with the Attack discovery PLI', () => { + const attackDiscoveryPage = upsellingPages.find( + ({ pageName }) => pageName === SecurityPageName.attackDiscovery + ); + + expect(attackDiscoveryPage?.pli).toEqual(ProductFeatureKey.attackDiscovery); + }); +}); diff --git a/x-pack/plugins/security_solution_serverless/public/upselling/upsellings.tsx b/x-pack/plugins/security_solution_serverless/public/upselling/upsellings.tsx index cb0e1514b1df5..bbc873ef137e7 100644 --- a/x-pack/plugins/security_solution_serverless/public/upselling/upsellings.tsx +++ b/x-pack/plugins/security_solution_serverless/public/upselling/upsellings.tsx @@ -25,6 +25,7 @@ import { } from './sections/endpoint_management'; import { getProductTypeByPLI } from './hooks/use_product_type_by_pli'; import { + AttackDiscoveryUpsellingPageLazy, EndpointExceptionsDetailsUpsellingLazy, EntityAnalyticsUpsellingPageLazy, EntityAnalyticsUpsellingSectionLazy, @@ -76,6 +77,11 @@ export const upsellingPages: UpsellingPages = [ ), }, + { + pageName: SecurityPageName.attackDiscovery, + pli: ProductFeatureKey.attackDiscovery, + component: () => , + }, ]; const entityAnalyticsProductType = getProductTypeByPLI(ProductFeatureKey.advancedInsights) ?? ''; diff --git a/x-pack/plugins/security_solution_serverless/server/plugin.ts b/x-pack/plugins/security_solution_serverless/server/plugin.ts index f81a8e013e290..7161c5b684505 100644 --- a/x-pack/plugins/security_solution_serverless/server/plugin.ts +++ b/x-pack/plugins/security_solution_serverless/server/plugin.ts @@ -63,6 +63,7 @@ export class SecuritySolutionServerlessPlugin // Register product features const enabledProductFeatures = getProductProductFeatures(this.config.productTypes); + registerProductFeatures(pluginsSetup, enabledProductFeatures, this.config); // Register telemetry events diff --git a/x-pack/plugins/security_solution_serverless/server/product_features/attack_discovery_product_features_config.ts b/x-pack/plugins/security_solution_serverless/server/product_features/attack_discovery_product_features_config.ts new file mode 100644 index 0000000000000..406c396edfb72 --- /dev/null +++ b/x-pack/plugins/security_solution_serverless/server/product_features/attack_discovery_product_features_config.ts @@ -0,0 +1,40 @@ +/* + * 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 { + ProductFeatureKeys, + ProductFeatureKibanaConfig, + ProductFeaturesAttackDiscoveryConfig, +} from '@kbn/security-solution-features'; +import { + attackDiscoveryDefaultProductFeaturesConfig, + createEnabledProductFeaturesConfigMap, +} from '@kbn/security-solution-features/config'; +import type { ProductFeatureAttackDiscoveryKey } from '@kbn/security-solution-features/keys'; + +/** + * Maps the ProductFeatures keys to Kibana privileges that will be merged + * into the base privileges config for the app. + * + * Privileges can be added in different ways: + * - `privileges`: the privileges that will be added directly into the main Attack discovery feature. + * - `subFeatureIds`: the ids of the sub-features that will be added into the Attack discovery subFeatures entry. + * - `subFeaturesPrivileges`: the privileges that will be added into the existing Attack discovery subFeature with the privilege `id` specified. + */ +const attackDiscoveryProductFeaturesConfig: Record< + ProductFeatureAttackDiscoveryKey, + ProductFeatureKibanaConfig +> = { + ...attackDiscoveryDefaultProductFeaturesConfig, + // serverless-specific app features configs here +}; + +export const getAttackDiscoveryProductFeaturesConfigurator = + (enabledProductFeatureKeys: ProductFeatureKeys) => (): ProductFeaturesAttackDiscoveryConfig => + createEnabledProductFeaturesConfigMap( + attackDiscoveryProductFeaturesConfig, + enabledProductFeatureKeys + ); diff --git a/x-pack/plugins/security_solution_serverless/server/product_features/index.ts b/x-pack/plugins/security_solution_serverless/server/product_features/index.ts index 6c0b2b9091c66..310ea860787ba 100644 --- a/x-pack/plugins/security_solution_serverless/server/product_features/index.ts +++ b/x-pack/plugins/security_solution_serverless/server/product_features/index.ts @@ -9,6 +9,7 @@ import type { Logger } from '@kbn/logging'; import { ProductFeatureKey } from '@kbn/security-solution-features/keys'; import type { ProductFeatureKeys } from '@kbn/security-solution-features'; +import { getAttackDiscoveryProductFeaturesConfigurator } from './attack_discovery_product_features_config'; import { getCasesProductFeaturesConfigurator } from './cases_product_features_config'; import { getSecurityProductFeaturesConfigurator } from './security_product_features_config'; import { getSecurityAssistantProductFeaturesConfigurator } from './assistant_product_features_config'; @@ -32,6 +33,7 @@ export const registerProductFeatures = ( // register product features for the main security solution product features service pluginsSetup.securitySolution.setProductFeaturesConfigurator({ + attackDiscovery: getAttackDiscoveryProductFeaturesConfigurator(enabledProductFeatureKeys), security: getSecurityProductFeaturesConfigurator( enabledProductFeatureKeys, config.experimentalFeatures diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index b048ed05a577a..bf087405664b8 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -35882,10 +35882,6 @@ "xpack.securitySolution.attackDiscovery.tour.video": "Regardez la vidéo de présentation", "xpack.securitySolution.attackDiscovery.tour.videoStep.desc": "Plongez dans les découvertes d'attaques axées sur les données et rationalisez votre flux de travail grâce à notre technologie d'IA intuitive, conçue pour accroître instantanément votre productivité.", "xpack.securitySolution.attackDiscovery.tour.videoStep.title": "Démarrez la découverte des attaques", - "xpack.securitySolution.attackDiscovery.upgrade.attackDiscoveryIsAvailable": "Votre licence ne prend pas en charge la découverte d'attaques.", - "xpack.securitySolution.attackDiscovery.upgrade.findPotentialAttacksWithAiTitle": "Trouvez les attaques potentielles grâce à l'IA", - "xpack.securitySolution.attackDiscovery.upgrade.pleaseUpgradeMessage": "Veuillez mettre votre licence à niveau pour bénéficier de cette fonctionnalité.", - "xpack.securitySolution.attackDiscovery.upgrade.upgradeButton": "Mettre à niveau", "xpack.securitySolution.auditd.abortedAuditStartupDescription": "démarrage de l'audit abandonné", "xpack.securitySolution.auditd.accessErrorDescription": "erreur d'accès", "xpack.securitySolution.auditd.accessPermissionDescription": "autorisation d'accès", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index a1ed084e1a6fb..8255c814498ba 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -35866,10 +35866,6 @@ "xpack.securitySolution.attackDiscovery.tour.video": "概要動画を視聴", "xpack.securitySolution.attackDiscovery.tour.videoStep.desc": "データ主導のAttack Discoveryを導入し、生産性を即時に高めるために設計されたElasticの直感的なAI技術でワークフローを合理化しましょう。", "xpack.securitySolution.attackDiscovery.tour.videoStep.title": "攻撃の検出を開始", - "xpack.securitySolution.attackDiscovery.upgrade.attackDiscoveryIsAvailable": "ご使用のライセンスはAttack Discoveryをサポートしていません。", - "xpack.securitySolution.attackDiscovery.upgrade.findPotentialAttacksWithAiTitle": "AIを利用して潜在的な攻撃を検出", - "xpack.securitySolution.attackDiscovery.upgrade.pleaseUpgradeMessage": "この機能を使用するには、ライセンスをアップグレードしてください。", - "xpack.securitySolution.attackDiscovery.upgrade.upgradeButton": "アップグレード", "xpack.securitySolution.auditd.abortedAuditStartupDescription": "中断された監査のスタートアップ", "xpack.securitySolution.auditd.accessErrorDescription": "アクセスエラー", "xpack.securitySolution.auditd.accessPermissionDescription": "アクセス権限", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 6b824bc3a21c3..10be509e503b6 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -35907,10 +35907,6 @@ "xpack.securitySolution.attackDiscovery.tour.video": "观看概述视频", "xpack.securitySolution.attackDiscovery.tour.videoStep.desc": "深入了解数据驱动式 Attack Discovery,并利用旨在即时提高生产力的直观式 AI 技术精简您的工作流。", "xpack.securitySolution.attackDiscovery.tour.videoStep.title": "开始发现攻击", - "xpack.securitySolution.attackDiscovery.upgrade.attackDiscoveryIsAvailable": "您的许可证不支持 Attack Discovery。", - "xpack.securitySolution.attackDiscovery.upgrade.findPotentialAttacksWithAiTitle": "利用 AI 发现潜在攻击", - "xpack.securitySolution.attackDiscovery.upgrade.pleaseUpgradeMessage": "请升级许可证以使用此功能。", - "xpack.securitySolution.attackDiscovery.upgrade.upgradeButton": "升级", "xpack.securitySolution.auditd.abortedAuditStartupDescription": "已中止审计启动", "xpack.securitySolution.auditd.accessErrorDescription": "访问错误", "xpack.securitySolution.auditd.accessPermissionDescription": "访问权限", diff --git a/x-pack/test/api_integration/apis/features/features/features.ts b/x-pack/test/api_integration/apis/features/features/features.ts index a494e3e09ee0a..5236ec72d437a 100644 --- a/x-pack/test/api_integration/apis/features/features/features.ts +++ b/x-pack/test/api_integration/apis/features/features/features.ts @@ -128,6 +128,7 @@ export default function ({ getService }: FtrProviderContext) { 'siem', 'slo', 'securitySolutionAssistant', + 'securitySolutionAttackDiscovery', 'securitySolutionCases', 'fleet', 'fleetv2', diff --git a/x-pack/test/api_integration/apis/security/privileges.ts b/x-pack/test/api_integration/apis/security/privileges.ts index 4cbaeca03373c..b4528ad1b1475 100644 --- a/x-pack/test/api_integration/apis/security/privileges.ts +++ b/x-pack/test/api_integration/apis/security/privileges.ts @@ -78,6 +78,7 @@ export default function ({ getService }: FtrProviderContext) { 'minimal_read', 'update_anonymization', ], + securitySolutionAttackDiscovery: ['all', 'read', 'minimal_all', 'minimal_read'], securitySolutionCases: [ 'all', 'read', diff --git a/x-pack/test/api_integration/apis/security/privileges_basic.ts b/x-pack/test/api_integration/apis/security/privileges_basic.ts index 5fb3c715805f1..683fa48a69216 100644 --- a/x-pack/test/api_integration/apis/security/privileges_basic.ts +++ b/x-pack/test/api_integration/apis/security/privileges_basic.ts @@ -44,6 +44,7 @@ export default function ({ getService }: FtrProviderContext) { ml: ['all', 'read', 'minimal_all', 'minimal_read'], siem: ['all', 'read', 'minimal_all', 'minimal_read'], securitySolutionAssistant: ['all', 'read', 'minimal_all', 'minimal_read'], + securitySolutionAttackDiscovery: ['all', 'read', 'minimal_all', 'minimal_read'], securitySolutionCases: ['all', 'read', 'minimal_all', 'minimal_read'], fleetv2: ['all', 'read', 'minimal_all', 'minimal_read'], fleet: ['all', 'read', 'minimal_all', 'minimal_read'], @@ -161,6 +162,7 @@ export default function ({ getService }: FtrProviderContext) { 'minimal_read', 'update_anonymization', ], + securitySolutionAttackDiscovery: ['all', 'read', 'minimal_all', 'minimal_read'], securitySolutionCases: [ 'all', 'read', diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/privileges.ts b/x-pack/test/security_solution_cypress/cypress/tasks/privileges.ts index 04dddd6da4384..bd7c9d9178198 100644 --- a/x-pack/test/security_solution_cypress/cypress/tasks/privileges.ts +++ b/x-pack/test/security_solution_cypress/cypress/tasks/privileges.ts @@ -64,6 +64,7 @@ export const secAll: Role = { feature: { siem: ['all'], securitySolutionAssistant: ['all'], + securitySolutionAttackDiscovery: ['all'], securitySolutionCases: ['all'], actions: ['all'], actionsSimulators: ['all'], @@ -96,6 +97,7 @@ export const secReadCasesAll: Role = { feature: { siem: ['read'], securitySolutionAssistant: ['all'], + securitySolutionAttackDiscovery: ['all'], securitySolutionCases: ['all'], actions: ['all'], actionsSimulators: ['all'], @@ -128,6 +130,7 @@ export const secAllCasesOnlyReadDelete: Role = { feature: { siem: ['all'], securitySolutionAssistant: ['all'], + securitySolutionAttackDiscovery: ['all'], securitySolutionCases: ['cases_read', 'cases_delete'], actions: ['all'], actionsSimulators: ['all'], @@ -160,6 +163,7 @@ export const secAllCasesNoDelete: Role = { feature: { siem: ['all'], securitySolutionAssistant: ['all'], + securitySolutionAttackDiscovery: ['all'], securitySolutionCases: ['minimal_all'], actions: ['all'], actionsSimulators: ['all'], diff --git a/x-pack/test/spaces_api_integration/spaces_only/telemetry/telemetry.ts b/x-pack/test/spaces_api_integration/spaces_only/telemetry/telemetry.ts index 6987186c7d525..49b0794accb5b 100644 --- a/x-pack/test/spaces_api_integration/spaces_only/telemetry/telemetry.ts +++ b/x-pack/test/spaces_api_integration/spaces_only/telemetry/telemetry.ts @@ -82,6 +82,7 @@ export default function ({ getService }: FtrProviderContext) { siem: 0, securitySolutionCases: 0, securitySolutionAssistant: 0, + securitySolutionAttackDiscovery: 0, discover: 0, visualize: 0, dashboard: 0, diff --git a/x-pack/test/ui_capabilities/security_and_spaces/tests/nav_links.ts b/x-pack/test/ui_capabilities/security_and_spaces/tests/nav_links.ts index b2955ade938b1..f51f169b51a35 100644 --- a/x-pack/test/ui_capabilities/security_and_spaces/tests/nav_links.ts +++ b/x-pack/test/ui_capabilities/security_and_spaces/tests/nav_links.ts @@ -83,7 +83,8 @@ export default function navLinksTests({ getService }: FtrProviderContext) { 'appSearch', 'workplaceSearch', 'guidedOnboardingFeature', - 'securitySolutionAssistant' + 'securitySolutionAssistant', + 'securitySolutionAttackDiscovery' ) ); break; diff --git a/x-pack/test_serverless/shared/lib/security/kibana_roles/project_controller_security_roles.yml b/x-pack/test_serverless/shared/lib/security/kibana_roles/project_controller_security_roles.yml index 9e1e542df8e87..0c60ac2aa0427 100644 --- a/x-pack/test_serverless/shared/lib/security/kibana_roles/project_controller_security_roles.yml +++ b/x-pack/test_serverless/shared/lib/security/kibana_roles/project_controller_security_roles.yml @@ -46,6 +46,7 @@ viewer: - feature_siem.endpoint_list_read - feature_securitySolutionCases.read - feature_securitySolutionAssistant.all + - feature_securitySolutionAttackDiscovery.all - feature_actions.read - feature_builtInAlerts.read - feature_osquery.read @@ -124,6 +125,7 @@ editor: - feature_siem.file_operations_all - feature_securitySolutionCases.all - feature_securitySolutionAssistant.all + - feature_securitySolutionAttackDiscovery.all - feature_actions.read - feature_builtInAlerts.all - feature_osquery.all @@ -172,6 +174,7 @@ t1_analyst: - feature_siem.endpoint_list_read - feature_securitySolutionCases.read - feature_securitySolutionAssistant.all + - feature_securitySolutionAttackDiscovery.all - feature_actions.read - feature_builtInAlerts.read - feature_osquery.read @@ -226,6 +229,7 @@ t2_analyst: - feature_siem.endpoint_list_read - feature_securitySolutionCases.all - feature_securitySolutionAssistant.all + - feature_securitySolutionAttackDiscovery.all - feature_actions.read - feature_builtInAlerts.read - feature_osquery.read @@ -295,6 +299,7 @@ t3_analyst: - feature_siem.scan_operations_all - feature_securitySolutionCases.all - feature_securitySolutionAssistant.all + - feature_securitySolutionAttackDiscovery.all - feature_actions.read - feature_builtInAlerts.all - feature_osquery.all @@ -351,6 +356,7 @@ threat_intelligence_analyst: - feature_siem.blocklist_all - feature_securitySolutionCases.all - feature_securitySolutionAssistant.all + - feature_securitySolutionAttackDiscovery.all - feature_actions.read - feature_builtInAlerts.read - feature_osquery.all @@ -418,6 +424,7 @@ rule_author: - feature_siem.actions_log_management_read - feature_securitySolutionCases.all - feature_securitySolutionAssistant.all + - feature_securitySolutionAttackDiscovery.all - feature_actions.read - feature_builtInAlerts.all - feature_osquery.all @@ -489,6 +496,7 @@ soc_manager: - feature_siem.scan_operations_all - feature_securitySolutionCases.all - feature_securitySolutionAssistant.all + - feature_securitySolutionAttackDiscovery.all - feature_actions.all - feature_builtInAlerts.all - feature_osquery.all @@ -548,6 +556,7 @@ detections_admin: - feature_siem.crud_alerts - feature_securitySolutionCases.all - feature_securitySolutionAssistant.all + - feature_securitySolutionAttackDiscovery.all - feature_actions.all - feature_builtInAlerts.all - feature_dev_tools.all @@ -606,6 +615,7 @@ platform_engineer: - feature_siem.actions_log_management_read - feature_securitySolutionCases.all - feature_securitySolutionAssistant.all + - feature_securitySolutionAttackDiscovery.all - feature_actions.all - feature_builtInAlerts.all - feature_fleet.all @@ -678,6 +688,7 @@ endpoint_operations_analyst: - feature_siem.scan_operations_all - feature_securitySolutionCases.all - feature_securitySolutionAssistant.all + - feature_securitySolutionAttackDiscovery.all - feature_actions.all - feature_builtInAlerts.all - feature_osquery.all @@ -744,6 +755,7 @@ endpoint_policy_manager: - feature_siem.blocklist_all # Elastic Defend Policy Management - feature_securitySolutionCases.all - feature_securitySolutionAssistant.all + - feature_securitySolutionAttackDiscovery.all - feature_actions.all - feature_builtInAlerts.all - feature_osquery.all