From 9975c552da13e778b82ffa91d1a6fe1de8cac4a6 Mon Sep 17 00:00:00 2001 From: Marco Antonio Ghiani Date: Tue, 12 Nov 2024 14:25:09 +0100 Subject: [PATCH 1/5] [Logs] Deprecation warning for Logs Explorer and Logs Stream (#199652) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 📓 Summary Closes https://github.com/elastic/observability-dev/issues/4070 - Update the deprecation callouts to suggest that the user use Discover. - Replace the beta badge in Logs Explorer with a deprecation notice. - Mark the advanced setting to enable the log stream to be deprecated. Screenshot 2024-11-11 at 15 22 51 --------- Co-authored-by: Marco Antonio Ghiani Co-authored-by: Mike Birnstiehl <114418652+mdbirnstiehl@users.noreply.github.com> --- .../infra/common/ui_settings.ts | 7 ++++ .../components/logs_deprecation_callout.tsx | 37 +++++++--------- .../common/translations.ts | 24 ++++++++--- .../components/logs_explorer_top_nav_menu.tsx | 42 +++++++++++-------- .../translations/translations/fr-FR.json | 5 --- .../translations/translations/ja-JP.json | 5 --- .../translations/translations/zh-CN.json | 5 --- 7 files changed, 64 insertions(+), 61 deletions(-) diff --git a/x-pack/plugins/observability_solution/infra/common/ui_settings.ts b/x-pack/plugins/observability_solution/infra/common/ui_settings.ts index 95f1ee0a44ba..9b8563076194 100644 --- a/x-pack/plugins/observability_solution/infra/common/ui_settings.ts +++ b/x-pack/plugins/observability_solution/infra/common/ui_settings.ts @@ -23,6 +23,13 @@ export const uiSettings: Record = { description: i18n.translate('xpack.infra.enableLogsStreamDescription', { defaultMessage: 'Enables the legacy Logs Stream application and dashboard panel. ', }), + deprecation: { + message: i18n.translate('xpack.infra.enableLogsStreamDeprecationWarning', { + defaultMessage: + 'Logs Stream is deprecated, and this setting will be removed in Kibana 9.0.', + }), + docLinksKey: 'generalSettings', + }, type: 'boolean', schema: schema.boolean(), requiresPageReload: true, diff --git a/x-pack/plugins/observability_solution/infra/public/components/logs_deprecation_callout.tsx b/x-pack/plugins/observability_solution/infra/public/components/logs_deprecation_callout.tsx index 21e61c08d281..0edb8b9ab292 100644 --- a/x-pack/plugins/observability_solution/infra/public/components/logs_deprecation_callout.tsx +++ b/x-pack/plugins/observability_solution/infra/public/components/logs_deprecation_callout.tsx @@ -9,36 +9,28 @@ import { EuiCallOut } from '@elastic/eui'; import React from 'react'; import { i18n } from '@kbn/i18n'; import { EuiButton } from '@elastic/eui'; -import { - AllDatasetsLocatorParams, - ALL_DATASETS_LOCATOR_ID, - DatasetLocatorParams, -} from '@kbn/deeplinks-observability'; import { getRouterLinkProps } from '@kbn/router-utils'; import useLocalStorage from 'react-use/lib/useLocalStorage'; - import { euiThemeVars } from '@kbn/ui-theme'; import { css } from '@emotion/css'; import { LocatorPublic } from '@kbn/share-plugin/common'; +import { DISCOVER_APP_LOCATOR, DiscoverAppLocatorParams } from '@kbn/discover-plugin/common'; import { useKibanaContextForPlugin } from '../hooks/use_kibana'; const pageConfigurations = { stream: { dismissalStorageKey: 'log_stream_deprecation_callout_dismissed', - message: i18n.translate('xpack.infra.logsDeprecationCallout.p.theNewLogsExplorerLabel', { + message: i18n.translate('xpack.infra.logsDeprecationCallout.stream.exploreWithDiscover', { defaultMessage: - 'The new Logs Explorer makes viewing and inspecting your logs easier with more features, better performance, and more intuitive navigation. We recommend switching to Logs Explorer, as it will replace Logs Stream in a future version.', + 'Logs Stream and Logs Explorer are set to be deprecated. Switch to Discover which now includes their functionality plus more features, better performance, and more intuitive navigation. ', }), }, settings: { dismissalStorageKey: 'log_settings_deprecation_callout_dismissed', - message: i18n.translate( - 'xpack.infra.logsSettingsDeprecationCallout.p.theNewLogsExplorerLabel', - { - defaultMessage: - 'These settings only apply to the legacy Logs Stream app, and we do not recommend configuring them. Instead, use Logs Explorer which makes viewing and inspecting your logs easier with more features, better performance, and more intuitive navigation.', - } - ), + message: i18n.translate('xpack.infra.logsDeprecationCallout.settings.exploreWithDiscover', { + defaultMessage: + 'These settings only apply to the legacy Logs Stream app. Switch to Discover for the same functionality plus more features, better performance, and more intuitive navigation.', + }), }, }; @@ -60,10 +52,9 @@ export const LogsDeprecationCallout = ({ page }: LogsDeprecationCalloutProps) => const [isDismissed, setDismissed] = useLocalStorage(dismissalStorageKey, false); - const allDatasetLocator = - share.url.locators.get(ALL_DATASETS_LOCATOR_ID); + const discoverLocator = share.url.locators.get(DISCOVER_APP_LOCATOR); - if (isDismissed || !(allDatasetLocator && discover?.show && fleet?.read)) { + if (isDismissed || !(discoverLocator && discover?.show && fleet?.read)) { return null; } @@ -81,19 +72,19 @@ export const LogsDeprecationCallout = ({ page }: LogsDeprecationCalloutProps) =>

{message}

- {i18n.translate('xpack.infra.logsDeprecationCallout.tryLogsExplorerButtonLabel', { - defaultMessage: 'Try Logs Explorer', + {i18n.translate('xpack.infra.logsDeprecationCallout.goToDiscoverButtonLabel', { + defaultMessage: 'Go to Discover', })} ); }; -const getLogsExplorerLinkProps = (locator: LocatorPublic) => { +const getDiscoverLinkProps = (locator: LocatorPublic) => { return getRouterLinkProps({ href: locator.getRedirectUrl({}), onClick: () => locator.navigate({}), diff --git a/x-pack/plugins/observability_solution/observability_logs_explorer/common/translations.ts b/x-pack/plugins/observability_solution/observability_logs_explorer/common/translations.ts index f89c6eb47e6c..380bc3c3c5a2 100644 --- a/x-pack/plugins/observability_solution/observability_logs_explorer/common/translations.ts +++ b/x-pack/plugins/observability_solution/observability_logs_explorer/common/translations.ts @@ -22,14 +22,26 @@ export const observabilityAppTitle = i18n.translate( } ); -export const betaBadgeTitle = i18n.translate('xpack.observabilityLogsExplorer.betaBadgeTitle', { - defaultMessage: 'Beta', -}); +export const deprecationBadgeTitle = i18n.translate( + 'xpack.observabilityLogsExplorer.deprecationBadgeTitle', + { + defaultMessage: 'Deprecation notice', + } +); + +export const deprecationBadgeDescription = i18n.translate( + 'xpack.observabilityLogsExplorer.deprecationBadgeDescription', + { + defaultMessage: + 'Logs Stream and Logs Explorer are set to be deprecated. Switch to Discover which now includes their functionality plus more features and better performance.', + } +); -export const betaBadgeDescription = i18n.translate( - 'xpack.observabilityLogsExplorer.betaBadgeDescription', +export const deprecationBadgeGuideline = i18n.translate( + 'xpack.observabilityLogsExplorer.deprecationBadgeGuideline', { - defaultMessage: 'This application is in beta and therefore subject to change.', + defaultMessage: + 'You can temporarily access Logs Stream by re-enabling it in Advanced Settings.', } ); diff --git a/x-pack/plugins/observability_solution/observability_logs_explorer/public/components/logs_explorer_top_nav_menu.tsx b/x-pack/plugins/observability_solution/observability_logs_explorer/public/components/logs_explorer_top_nav_menu.tsx index c3f91b3bf866..0020df30b870 100644 --- a/x-pack/plugins/observability_solution/observability_logs_explorer/public/components/logs_explorer_top_nav_menu.tsx +++ b/x-pack/plugins/observability_solution/observability_logs_explorer/public/components/logs_explorer_top_nav_menu.tsx @@ -21,7 +21,11 @@ import { LogsExplorerTabs } from '@kbn/discover-plugin/public'; import React, { useEffect, useState } from 'react'; import useObservable from 'react-use/lib/useObservable'; import { filter, take } from 'rxjs'; -import { betaBadgeDescription, betaBadgeTitle } from '../../common/translations'; +import { + deprecationBadgeDescription, + deprecationBadgeGuideline, + deprecationBadgeTitle, +} from '../../common/translations'; import { useKibanaContextForPlugin } from '../utils/use_kibana'; import { ConnectedDiscoverLink } from './discover_link'; import { FeedbackLink } from './feedback_link'; @@ -59,13 +63,7 @@ const ProjectTopNav = () => { `} > - + @@ -115,15 +113,6 @@ const ClassicTopNav = () => { margin-left: ${euiThemeVars.euiSizeM}; `} > - - - @@ -145,6 +134,9 @@ const ClassicTopNav = () => { + + + @@ -171,3 +163,19 @@ const ConditionalVerticalRule = ({ Component }: { Component: JSX.Element | null {Component} ); + +const DeprecationNoticeBadge = () => ( + + {deprecationBadgeDescription} +
+
+ {deprecationBadgeGuideline} + + } + alignment="middle" + /> +); diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 301635bbb962..2a66c10a6509 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -24651,8 +24651,6 @@ "xpack.infra.logs.viewInContext.logsFromContainerTitle": "Les logs affichés proviennent du conteneur {container}", "xpack.infra.logs.viewInContext.logsFromFileTitle": "Les logs affichés proviennent du fichier {file} et de l'hôte {host}", "xpack.infra.logsDeprecationCallout.euiCallOut.discoverANewLogLabel": "Il existe une nouvelle (et meilleure) façon d'explorer vos logs !", - "xpack.infra.logsDeprecationCallout.p.theNewLogsExplorerLabel": "Le nouveau Logs Explorer facilite la visualisation et l'inspection de vos journaux et propose davantage de fonctionnalités, des performances supérieures ainsi qu’une navigation plus intuitive. Nous vous recommandons de passer à Logs Explorer, car il remplacera Logs Stream dans une version ultérieure.", - "xpack.infra.logsDeprecationCallout.tryLogsExplorerButtonLabel": "Essayer Logs Explorer", "xpack.infra.logsHeaderAddDataButtonLabel": "Ajouter des données", "xpack.infra.logSourceConfiguration.childFormElementErrorMessage": "L'état d'au moins un champ du formulaire est non valide.", "xpack.infra.logSourceConfiguration.dataViewDescription": "Vue de données contenant les données de log", @@ -24685,7 +24683,6 @@ "xpack.infra.logSourceErrorPage.tryAgainButtonLabel": "Réessayer", "xpack.infra.logsPage.toolbar.kqlSearchFieldPlaceholder": "Recherche d'entrées de log… (par ex. host.name:host-1)", "xpack.infra.logsPage.toolbar.logFilterErrorToastTitle": "Erreur de filtrage du log", - "xpack.infra.logsSettingsDeprecationCallout.p.theNewLogsExplorerLabel": "Ces paramètres s'appliquent uniquement à l'ancienne application Logs Stream et nous vous déconseillons de les configurer. Utilisez plutôt le nouveau Logs Explorer qui facilite la visualisation et l'inspection de vos logs et propose davantage de fonctionnalités, des performances supérieures ainsi qu'une navigation plus intuitive.", "xpack.infra.logsSettingsPage.loadingButtonLabel": "Chargement", "xpack.infra.logsStreamEmbeddable.deprecationWarningDescription": "La maintenance des panneaux de flux de logs n'est plus assurée. Essayez d'utiliser {savedSearchDocsLink} pour une visualisation similaire.", "xpack.infra.logsStreamEmbeddable.deprecationWarningDescription.savedSearchesLinkLabel": "recherches enregistrées", @@ -34658,8 +34655,6 @@ "xpack.observabilityLogsExplorer.alertsPopover.createSLOMenuItem": "Créer un SLO", "xpack.observabilityLogsExplorer.alertsPopover.manageRulesMenuItem": "{canCreateRule, select, true{Gérer} other{Afficher}} les règles", "xpack.observabilityLogsExplorer.appTitle": "Explorateur de logs", - "xpack.observabilityLogsExplorer.betaBadgeDescription": "Il s'agit du stade bêta de l'application, qui est donc susceptible d'évoluer.", - "xpack.observabilityLogsExplorer.betaBadgeTitle": "Bêta", "xpack.observabilityLogsExplorer.createSlo": "Créer un SLO", "xpack.observabilityLogsExplorer.datasetQualityLinkTitle": "Ensembles de données", "xpack.observabilityLogsExplorer.discoverLinkTitle": "Ouvrir dans Discover", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 17fb0b2ff933..6ac1f1e800bf 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -24625,8 +24625,6 @@ "xpack.infra.logs.viewInContext.logsFromContainerTitle": "表示されたログはコンテナー{container}から取得されました", "xpack.infra.logs.viewInContext.logsFromFileTitle": "表示されたログは、ファイル{file}およびホスト{host}から取得されました", "xpack.infra.logsDeprecationCallout.euiCallOut.discoverANewLogLabel": "ログの探索には、新しく、もっと効果的な方法があります。", - "xpack.infra.logsDeprecationCallout.p.theNewLogsExplorerLabel": "新しいログエクスプローラーには、追加機能、パフォーマンスの改善、さらに直感的なナビゲーションが導入され、ログの表示と調査がさらに簡単になりました。将来のバージョンでは、ログストリームに取って代わるため、ログエクスプローラーを使用することをお勧めします。", - "xpack.infra.logsDeprecationCallout.tryLogsExplorerButtonLabel": "ログエクスプローラーを試す", "xpack.infra.logsHeaderAddDataButtonLabel": "データの追加", "xpack.infra.logSourceConfiguration.childFormElementErrorMessage": "1つ以上のフォームフィールドが無効な状態です。", "xpack.infra.logSourceConfiguration.dataViewDescription": "ログデータを含むデータビュー", @@ -24658,7 +24656,6 @@ "xpack.infra.logSourceErrorPage.tryAgainButtonLabel": "再試行", "xpack.infra.logsPage.toolbar.kqlSearchFieldPlaceholder": "ログエントリーを検索中…(例:host.name:host-1)", "xpack.infra.logsPage.toolbar.logFilterErrorToastTitle": "ログフィルターエラー", - "xpack.infra.logsSettingsDeprecationCallout.p.theNewLogsExplorerLabel": "これらの設定はレガシーログストリームアプリにのみ適用されます。これらを構成することはお勧めしません。代わりに、新しいログエクスプローラーを使用してください。追加機能、パフォーマンスの改善、さらに直感的なナビゲーションが導入され、ログの表示と調査がさらに簡単になりました。", "xpack.infra.logsSettingsPage.loadingButtonLabel": "読み込み中", "xpack.infra.logsStreamEmbeddable.deprecationWarningDescription": "ログストリームパネルは管理されていません。{savedSearchDocsLink}を同様の視覚化に活用してください。", "xpack.infra.logsStreamEmbeddable.deprecationWarningDescription.savedSearchesLinkLabel": "保存された検索", @@ -34628,8 +34625,6 @@ "xpack.observabilityLogsExplorer.alertsPopover.createSLOMenuItem": "SLOの作成", "xpack.observabilityLogsExplorer.alertsPopover.manageRulesMenuItem": "ルールを{canCreateRule, select, true{管理} other{表示}}", "xpack.observabilityLogsExplorer.appTitle": "ログエクスプローラー", - "xpack.observabilityLogsExplorer.betaBadgeDescription": "このアプリケーションはベータ版であるため、変更される場合があります。", - "xpack.observabilityLogsExplorer.betaBadgeTitle": "ベータ", "xpack.observabilityLogsExplorer.createSlo": "SLOの作成", "xpack.observabilityLogsExplorer.datasetQualityLinkTitle": "データセット", "xpack.observabilityLogsExplorer.discoverLinkTitle": "Discoverで開く", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 4cb03c51d057..787ddd9a45d4 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -24204,8 +24204,6 @@ "xpack.infra.logs.viewInContext.logsFromContainerTitle": "显示的日志来自容器 {container}", "xpack.infra.logs.viewInContext.logsFromFileTitle": "显示的日志来自文件 {file} 和主机 {host}", "xpack.infra.logsDeprecationCallout.euiCallOut.discoverANewLogLabel": "这是浏览日志的更有效的新方法!", - "xpack.infra.logsDeprecationCallout.p.theNewLogsExplorerLabel": "新的日志浏览器具有更多功能,更优异的性能和更直观的导航,便于您更轻松地查看和检查日志。建议您切换到日志浏览器,因为它将在未来版本中替代日志流。", - "xpack.infra.logsDeprecationCallout.tryLogsExplorerButtonLabel": "试用日志浏览器", "xpack.infra.logsHeaderAddDataButtonLabel": "添加数据", "xpack.infra.logSourceConfiguration.childFormElementErrorMessage": "至少一个表单字段处于无效状态。", "xpack.infra.logSourceConfiguration.dataViewDescription": "包含日志数据的数据视图", @@ -24236,7 +24234,6 @@ "xpack.infra.logSourceErrorPage.tryAgainButtonLabel": "重试", "xpack.infra.logsPage.toolbar.kqlSearchFieldPlaceholder": "搜索日志条目……(例如 host.name:host-1)", "xpack.infra.logsPage.toolbar.logFilterErrorToastTitle": "日志筛选错误", - "xpack.infra.logsSettingsDeprecationCallout.p.theNewLogsExplorerLabel": "这些设置仅适用于旧版日志流应用,不建议配置它们。相反,应使用具有更多功能,更优异的性能和更直观的导航,便于您更轻松地查看和检查日志的日志浏览器。", "xpack.infra.logsSettingsPage.loadingButtonLabel": "正在加载", "xpack.infra.logsStreamEmbeddable.deprecationWarningDescription": "将不再维护日志流面板。尝试将 {savedSearchDocsLink} 用于类似可视化。", "xpack.infra.logsStreamEmbeddable.deprecationWarningDescription.savedSearchesLinkLabel": "已保存的搜索", @@ -34107,8 +34104,6 @@ "xpack.observabilityLogsExplorer.alertsPopover.createSLOMenuItem": "创建 SLO", "xpack.observabilityLogsExplorer.alertsPopover.manageRulesMenuItem": "{canCreateRule, select, true{管理} other{查看}}规则", "xpack.observabilityLogsExplorer.appTitle": "日志浏览器", - "xpack.observabilityLogsExplorer.betaBadgeDescription": "此应用程序为公测版,因此可能会进行更改。", - "xpack.observabilityLogsExplorer.betaBadgeTitle": "公测版", "xpack.observabilityLogsExplorer.createSlo": "创建 SLO", "xpack.observabilityLogsExplorer.datasetQualityLinkTitle": "数据集", "xpack.observabilityLogsExplorer.discoverLinkTitle": "在 Discover 中打开", From 7e06f554363d2bf8fe5656062e34053284005943 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Tue, 12 Nov 2024 08:28:28 -0500 Subject: [PATCH 2/5] [Fleet] Fix install with streaming test (#199725) --- .../apis/epm/install_with_streaming.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/test/fleet_api_integration/apis/epm/install_with_streaming.ts b/x-pack/test/fleet_api_integration/apis/epm/install_with_streaming.ts index 4fa0e485be2b..95968cd51976 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/install_with_streaming.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/install_with_streaming.ts @@ -36,8 +36,7 @@ export default function (providerContext: FtrProviderContext) { return res?._source?.['epm-packages'] as Installation; }; - // Failing: See https://github.com/elastic/kibana/issues/199701 - describe.skip('Installs a package using stream-based approach', () => { + describe('Installs a package using stream-based approach', () => { skipIfNoDockerRegistry(providerContext); before(async () => { @@ -50,7 +49,8 @@ export default function (providerContext: FtrProviderContext) { await uninstallPackage('security_detection_engine', '8.16.0'); }); it('should install security-rule assets from the package', async () => { - await installPackage('security_detection_engine', '8.16.0').expect(200); + // Force install to install an outdatded version + await installPackage('security_detection_engine', '8.16.0', { force: true }).expect(200); const installationSO = await getInstallationSavedObject('security_detection_engine'); expect(installationSO?.installed_kibana).toEqual( expect.arrayContaining([ From f9e8aa07b79e6d81d691a4f166c04f74335fdf7f Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Tue, 12 Nov 2024 08:28:54 -0500 Subject: [PATCH 3/5] [Fleet] Change uninstall tokens space when changing agent policies spaces (#199536) --- .../fleet/server/services/app_context.ts | 1 + .../security/uninstall_token_service/index.ts | 2 +- .../services/spaces/agent_policy.test.ts | 28 ++++++++ .../server/services/spaces/agent_policy.ts | 22 ++++++ .../change_space_agent_policies.ts | 68 ++++++++++++------- 5 files changed, 95 insertions(+), 26 deletions(-) diff --git a/x-pack/plugins/fleet/server/services/app_context.ts b/x-pack/plugins/fleet/server/services/app_context.ts index 7dccb7ba1dfe..da40f8ff7d81 100644 --- a/x-pack/plugins/fleet/server/services/app_context.ts +++ b/x-pack/plugins/fleet/server/services/app_context.ts @@ -240,6 +240,7 @@ class AppContextService { // soClient as kibana internal users, be careful on how you use it, security is not enabled return appContextService.getSavedObjects().getScopedClient(fakeRequest, { excludedExtensions: [SECURITY_EXTENSION_ID, SPACES_EXTENSION_ID], + includedHiddenTypes: [UNINSTALL_TOKENS_SAVED_OBJECT_TYPE], }); } diff --git a/x-pack/plugins/fleet/server/services/security/uninstall_token_service/index.ts b/x-pack/plugins/fleet/server/services/security/uninstall_token_service/index.ts index 0a44a3df0f29..1f58fcafd396 100644 --- a/x-pack/plugins/fleet/server/services/security/uninstall_token_service/index.ts +++ b/x-pack/plugins/fleet/server/services/security/uninstall_token_service/index.ts @@ -46,7 +46,7 @@ import { appContextService } from '../../app_context'; import { agentPolicyService, getAgentPolicySavedObjectType } from '../../agent_policy'; import { isSpaceAwarenessEnabled } from '../../spaces/helpers'; -interface UninstallTokenSOAttributes { +export interface UninstallTokenSOAttributes { policy_id: string; token: string; token_plain: string; diff --git a/x-pack/plugins/fleet/server/services/spaces/agent_policy.test.ts b/x-pack/plugins/fleet/server/services/spaces/agent_policy.test.ts index ab69d4708c43..079148a6ae04 100644 --- a/x-pack/plugins/fleet/server/services/spaces/agent_policy.test.ts +++ b/x-pack/plugins/fleet/server/services/spaces/agent_policy.test.ts @@ -39,6 +39,22 @@ describe('updateAgentPolicySpaces', () => { jest .mocked(appContextService.getInternalUserSOClientWithoutSpaceExtension()) .updateObjectsSpaces.mockResolvedValue({ objects: [] }); + + jest + .mocked(appContextService.getInternalUserSOClientWithoutSpaceExtension()) + .find.mockResolvedValue({ + total: 1, + page: 1, + per_page: 100, + saved_objects: [ + { + id: 'token1', + attributes: { + namespaces: ['default'], + }, + } as any, + ], + }); }); it('does nothings if agent policy already in correct space', async () => { @@ -87,6 +103,18 @@ describe('updateAgentPolicySpaces', () => { ['default'], { namespace: 'default', refresh: 'wait_for' } ); + + expect( + jest.mocked(appContextService.getInternalUserSOClientWithoutSpaceExtension()).bulkUpdate + ).toBeCalledWith([ + { + id: 'token1', + type: 'fleet-uninstall-tokens', + attributes: { + namespaces: ['test'], + }, + }, + ]); }); it('throw when trying to change space to a policy with reusable package policies', async () => { diff --git a/x-pack/plugins/fleet/server/services/spaces/agent_policy.ts b/x-pack/plugins/fleet/server/services/spaces/agent_policy.ts index 2f8d5ff1b14c..e123ca442665 100644 --- a/x-pack/plugins/fleet/server/services/spaces/agent_policy.ts +++ b/x-pack/plugins/fleet/server/services/spaces/agent_policy.ts @@ -13,6 +13,8 @@ import { AGENTS_INDEX, AGENT_POLICY_SAVED_OBJECT_TYPE, PACKAGE_POLICY_SAVED_OBJECT_TYPE, + SO_SEARCH_LIMIT, + UNINSTALL_TOKENS_SAVED_OBJECT_TYPE, } from '../../../common/constants'; import { appContextService } from '../app_context'; @@ -22,6 +24,7 @@ import { packagePolicyService } from '../package_policy'; import { FleetError, HostedAgentPolicyRestrictionRelatedError } from '../../errors'; import { isSpaceAwarenessEnabled } from './helpers'; +import type { UninstallTokenSOAttributes } from '../security/uninstall_token_service'; export async function updateAgentPolicySpaces({ agentPolicyId, @@ -112,6 +115,25 @@ export async function updateAgentPolicySpaces({ } } + // Update uninstall tokens + const uninstallTokensRes = await soClient.find({ + perPage: SO_SEARCH_LIMIT, + type: UNINSTALL_TOKENS_SAVED_OBJECT_TYPE, + filter: `${UNINSTALL_TOKENS_SAVED_OBJECT_TYPE}.attributes.policy_id:"${agentPolicyId}"`, + }); + + if (uninstallTokensRes.total > 0) { + await soClient.bulkUpdate( + uninstallTokensRes.saved_objects.map((so) => ({ + id: so.id, + type: UNINSTALL_TOKENS_SAVED_OBJECT_TYPE, + attributes: { + namespaces: newSpaceIds, + }, + })) + ); + } + // Update fleet server index agents, enrollment api keys await esClient.updateByQuery({ index: ENROLLMENT_API_KEYS_INDEX, diff --git a/x-pack/test/fleet_api_integration/apis/space_awareness/change_space_agent_policies.ts b/x-pack/test/fleet_api_integration/apis/space_awareness/change_space_agent_policies.ts index 4d1d08ac7b35..df3aa6f8f257 100644 --- a/x-pack/test/fleet_api_integration/apis/space_awareness/change_space_agent_policies.ts +++ b/x-pack/test/fleet_api_integration/apis/space_awareness/change_space_agent_policies.ts @@ -109,18 +109,29 @@ export default function (providerContext: FtrProviderContext) { }) .catch(() => {}); }); - async function assertPolicyAvailableInSpace(spaceId?: string) { - await apiClient.getAgentPolicy(defaultSpacePolicy1.item.id, spaceId); + + async function assertPackagePolicyAvailableInSpace(spaceId?: string) { await apiClient.getPackagePolicy(defaultPackagePolicy1.item.id, spaceId); + } + + async function assertPackagePolicyNotAvailableInSpace(spaceId?: string) { + await expectToRejectWithNotFound(() => + apiClient.getPackagePolicy(defaultPackagePolicy1.item.id, spaceId) + ); + } + + async function assertAgentPolicyAvailableInSpace(policyId: string, spaceId?: string) { + await apiClient.getAgentPolicy(policyId, spaceId); const enrollmentApiKeys = await apiClient.getEnrollmentApiKeys(spaceId); - expect( - enrollmentApiKeys.items.find((item) => item.policy_id === defaultSpacePolicy1.item.id) - ).not.to.be(undefined); + expect(enrollmentApiKeys.items.find((item) => item.policy_id === policyId)).not.to.be( + undefined + ); const agents = await apiClient.getAgents(spaceId); - expect( - agents.items.filter((a) => a.policy_id === defaultSpacePolicy1.item.id).length - ).to.be(1); + expect(agents.items.filter((a) => a.policy_id === policyId).length).to.be(1); + + const uninstallTokens = await apiClient.getUninstallTokens(spaceId); + expect(uninstallTokens.items.filter((t) => t.policy_id === policyId).length).to.be(1); } async function assertEnrollemntApiKeysForSpace(spaceId?: string, policyIds?: string[]) { @@ -136,23 +147,19 @@ export default function (providerContext: FtrProviderContext) { expect([...foundPolicyIds].sort()).to.eql(policyIds?.sort()); } - async function assertPolicyNotAvailableInSpace(spaceId?: string) { - await expectToRejectWithNotFound(() => - apiClient.getPackagePolicy(defaultPackagePolicy1.item.id, spaceId) - ); - await expectToRejectWithNotFound(() => - apiClient.getAgentPolicy(defaultSpacePolicy1.item.id, spaceId) - ); + async function assertAgentPolicyNotAvailableInSpace(policyId: string, spaceId?: string) { + await expectToRejectWithNotFound(() => apiClient.getAgentPolicy(policyId, spaceId)); const enrollmentApiKeys = await apiClient.getEnrollmentApiKeys(spaceId); - expect( - enrollmentApiKeys.items.find((item) => item.policy_id === defaultSpacePolicy1.item.id) - ).to.be(undefined); + expect(enrollmentApiKeys.items.find((item) => item.policy_id === policyId)).to.be( + undefined + ); const agents = await apiClient.getAgents(spaceId); - expect( - agents.items.filter((a) => a.policy_id === defaultSpacePolicy1.item.id).length - ).to.be(0); + expect(agents.items.filter((a) => a.policy_id === policyId).length).to.be(0); + + const uninstallTokens = await apiClient.getUninstallTokens(spaceId); + expect(uninstallTokens.items.filter((t) => t.policy_id === policyId).length).to.be(0); } async function assertAgentSpaces(agentId: string, expectedSpaces: string[]) { @@ -173,8 +180,11 @@ export default function (providerContext: FtrProviderContext) { space_ids: ['default', TEST_SPACE_1], }); - await assertPolicyAvailableInSpace(); - await assertPolicyAvailableInSpace(TEST_SPACE_1); + await assertAgentPolicyAvailableInSpace(defaultSpacePolicy1.item.id); + await assertAgentPolicyAvailableInSpace(defaultSpacePolicy1.item.id, TEST_SPACE_1); + + await assertPackagePolicyAvailableInSpace(); + await assertPackagePolicyAvailableInSpace(TEST_SPACE_1); await assertAgentSpaces(policy1AgentId, ['default', TEST_SPACE_1]); await assertAgentSpaces(policy2AgentId, ['default']); @@ -184,6 +194,9 @@ export default function (providerContext: FtrProviderContext) { defaultSpacePolicy2.item.id, ]); await assertEnrollemntApiKeysForSpace(TEST_SPACE_1, [defaultSpacePolicy1.item.id]); + // Ensure no side effect on other policies + await assertAgentPolicyAvailableInSpace(defaultSpacePolicy2.item.id); + await assertAgentPolicyNotAvailableInSpace(defaultSpacePolicy2.item.id, TEST_SPACE_1); }); it('should allow set policy in test space only', async () => { @@ -194,12 +207,17 @@ export default function (providerContext: FtrProviderContext) { space_ids: [TEST_SPACE_1], }); - await assertPolicyNotAvailableInSpace(); - await assertPolicyAvailableInSpace(TEST_SPACE_1); + await assertAgentPolicyNotAvailableInSpace(defaultSpacePolicy1.item.id); + await assertAgentPolicyAvailableInSpace(defaultSpacePolicy1.item.id, TEST_SPACE_1); + await assertPackagePolicyAvailableInSpace(TEST_SPACE_1); + await assertPackagePolicyNotAvailableInSpace(); await assertAgentSpaces(policy1AgentId, [TEST_SPACE_1]); await assertAgentSpaces(policy2AgentId, ['default']); await assertEnrollemntApiKeysForSpace('default', [defaultSpacePolicy2.item.id]); await assertEnrollemntApiKeysForSpace(TEST_SPACE_1, [defaultSpacePolicy1.item.id]); + // Ensure no side effect on other policies + await assertAgentPolicyAvailableInSpace(defaultSpacePolicy2.item.id); + await assertAgentPolicyNotAvailableInSpace(defaultSpacePolicy2.item.id, TEST_SPACE_1); }); it('should not allow add policy to a space where user do not have access', async () => { From 4a2de76333063a1c6e68eeeaf4697753593928a5 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 13 Nov 2024 01:17:12 +1100 Subject: [PATCH 4/5] Unauthorized route migration for routes owned by obs-ux-logs-team (#198349) ### Authz API migration for unauthorized routes This PR migrates unauthorized routes owned by your team to a new security configuration. Please refer to the documentation for more information: [Authorization API](https://docs.elastic.dev/kibana-dev-docs/key-concepts/security-api-authorization) ### **Before migration:** ```ts router.get({ path: '/api/path', ... }, handler); ``` ### **After migration:** ```ts router.get({ path: '/api/path', security: { authz: { enabled: false, reason: 'This route is opted out from authorization because ...', }, }, ... }, handler); ``` ### What to do next? 1. Review the changes in this PR. 2. Elaborate on the reasoning to opt-out of authorization. 3. Routes without a compelling reason to opt-out of authorization should plan to introduce them as soon as possible. 2. You might need to update your tests to reflect the new security configuration: - If you have snapshot tests that include the route definition. ## Any questions? If you have any questions or need help with API authorization, please reach out to the `@elastic/kibana-security` team. Co-authored-by: Elastic Machine Co-authored-by: Marco Antonio Ghiani --- .../server/routes/fields_metadata/find_fields_metadata.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/x-pack/plugins/fields_metadata/server/routes/fields_metadata/find_fields_metadata.ts b/x-pack/plugins/fields_metadata/server/routes/fields_metadata/find_fields_metadata.ts index 5e518618d98d..422c16a72684 100644 --- a/x-pack/plugins/fields_metadata/server/routes/fields_metadata/find_fields_metadata.ts +++ b/x-pack/plugins/fields_metadata/server/routes/fields_metadata/find_fields_metadata.ts @@ -24,6 +24,12 @@ export const initFindFieldsMetadataRoute = ({ .addVersion( { version: '1', + security: { + authz: { + enabled: false, + reason: 'This route is opted out from authorization', + }, + }, validate: { request: { query: createValidationFunction(fieldsMetadataV1.findFieldsMetadataRequestQueryRT), From 06986e4a86a0fa3c3951fcb6b2ba34ebe2769820 Mon Sep 17 00:00:00 2001 From: Maxim Palenov Date: Tue, 12 Nov 2024 15:46:39 +0100 Subject: [PATCH 5/5] [Security Solution] Add Alert Suppression editable component (#198673) **Partially addresses:** https://github.com/elastic/kibana/issues/171520 ## Summary This PR adds is built on top of https://github.com/elastic/kibana/pull/193828 and https://github.com/elastic/kibana/pull/196948 and adds an Alert Suppression editable component for Three Way Diff tab's final edit side of the upgrade prebuilt rule workflow. ## Details https://github.com/elastic/kibana/issues/171520 required adding editable components for each field diffable rule field. Alert Suppression edit component was extracted from Define Rule Step Component into a separate reusable component. To simplify the logic it was split into common Alert Suppression and Threshold Alert Suppression since the latter is a specific use case. ## Caveats Upgrade prebuilt rules workflow is quite different from rule creation and editing. In create and edit rule forms users are capable to change any field at their will. Upgrade prebuilt rules workflow allow to modify only specific fields having diff in the current rule upgrade. There are fields which depend on each other. In particular Alert Suppression isn't supported for EQL sequence though it's addressed in https://github.com/elastic/kibana/pull/189725. - Alert Suppression editable component in Three Way Diff workflow isn't disabled EQL sequence rule queries. Alert suppression support for rules with EQL sequence queries is implemented in https://github.com/elastic/kibana/pull/189725. - Machine learning rule type require running selected machine learning jobs otherwise input could be disabled in case of there are no fields to pick from otherwise a warning message below the combobox is shown. ## How to test The simplest way to test is via patching installed prebuilt rules via Rule Patch API. Please follow steps below - Enable Prebuilt rule customization feature by adding a `prebuiltRulesCustomizationEnabled` feature flag - Run Kibana locally - Install a prebuilt rule, e.g. `Potential Code Execution via Postgresql` with rule_id `2a692072-d78d-42f3-a48a-775677d79c4e` - Patch the installed rule by running a query below ```bash curl -X PATCH --user elastic:changeme -H 'Content-Type: application/json' -H 'kbn-xsrf: 123' -H "elastic-api-version: 2023-10-31" -d '{"rule_id":"2a692072-d78d-42f3-a48a-775677d79c4e","version":1,"alert_suppression":{"group_by":["host.name"]}}' http://localhost:5601/kbn/api/detection_engine/rules ``` - Open `Detection Rules (SIEM)` Page -> `Rule Updates` -> click on `Potential Code Execution via Postgresql` rule -> expand `EQL Query` to see EQL Query -> press `Edit` button ## Screenshots Custom query prebuilt rule (UI looks similar for EQL, Indicator Match, New Terms and ES|QL rule types) ![image](https://github.com/user-attachments/assets/86015d5b-e252-4d0b-9aa3-fc14679a493b) Machine learning prebuilt rule with a diff in alert suppression ![image](https://github.com/user-attachments/assets/210246cd-27fd-4976-befc-dee023101ec9) Threshold prebuilt rule ![image](https://github.com/user-attachments/assets/44b0c1bc-4134-4d58-bd9a-e8e2d4c50802) --- oas_docs/output/kibana.serverless.yaml | 13 +- oas_docs/output/kibana.yaml | 13 +- .../rule_schema/common_attributes.gen.ts | 7 +- .../rule_schema/common_attributes.schema.yaml | 13 +- ...ections_api_2023_10_31.bundled.schema.yaml | 12 +- ...ections_api_2023_10_31.bundled.schema.yaml | 12 +- .../markdown_editor/plugins/index.ts | 4 +- .../plugins/insight/index.test.tsx | 2 +- .../markdown_editor/plugins/insight/index.tsx | 6 +- .../plugins/osquery/plugin.tsx | 2 +- .../plugins/timeline/plugin.tsx | 2 +- .../common/hooks/use_upselling.test.tsx | 4 +- .../public/common/hooks/use_upselling.ts | 4 +- .../public/common/test/eui/combobox.ts | 79 +++++ .../components/alert_suppression_edit.tsx | 64 ++++ .../missing_fields_strategy_selector.tsx | 61 ++++ .../suppression_duration_selector.tsx | 140 +++++++++ .../suppression_fields_selector.tsx | 46 +++ .../components/suppression_info_icon.tsx} | 21 +- .../components/translations.ts | 94 ++++++ .../constants/default_duration.ts | 17 ++ .../constants/fields.ts | 13 + .../alert_suppression_edit/index.ts | 11 + .../alert_suppression_edit/test_helpers.ts | 72 +++++ .../components/duration_input/index.tsx | 82 ++--- .../components/duration_input/translations.ts | 7 + .../related_integrations.test.tsx | 88 +----- .../related_integrations/test_helpers.ts | 47 +++ .../fields.ts | 8 + .../threshold_alert_suppression_edit/index.ts | 9 + .../threshold_alert_suppression_edit.tsx | 63 ++++ .../translations.tsx | 32 ++ ...a_views.ts => use_data_view_list_items.ts} | 2 +- .../data_view_selector_field.test.tsx | 14 +- .../data_view_selector_field.tsx | 4 +- ...a_views.ts => use_data_view_list_items.ts} | 2 +- .../components/description_step/helpers.tsx | 8 +- .../description_step/index.test.tsx | 63 ++-- .../components/description_step/index.tsx | 23 +- .../description_step/translations.ts | 4 +- .../components/multi_select_fields/index.tsx | 18 +- .../components/step_about_rule/index.test.tsx | 25 +- .../step_define_rule/index.test.tsx | 208 +++++-------- .../components/step_define_rule/index.tsx | 283 +++--------------- .../components/step_define_rule/schema.tsx | 67 ++--- .../step_define_rule/translations.tsx | 15 +- .../use_persistent_alert_suppression_state.ts | 77 +++++ .../components/step_define_rule/utils.ts | 11 +- .../rule_creation_ui/pages/form.test.ts | 3 +- .../pages/rule_creation/helpers.test.ts | 29 +- .../pages/rule_creation/helpers.ts | 25 +- .../pages/rule_creation/index.tsx | 14 +- .../pages/rule_editing/index.tsx | 4 +- ...rt_suppression_fields_validator_factory.ts | 25 ++ .../custom_query_rule_field_edit.tsx | 3 + .../final_edit/eql_rule_field_edit.tsx | 3 + .../final_edit/esql_rule_field_edit.tsx | 23 ++ .../fields/alert_suppression/form_schema.ts | 37 +++ .../fields/alert_suppression/index.ts | 8 + .../suppression_edit_adapter.tsx | 81 +++++ .../suppression_edit_form.tsx | 70 +++++ .../fields/alert_suppression/translations.tsx | 38 +++ .../data_source/data_source_edit_form.tsx | 16 +- .../data_source_type_selector_field.tsx | 13 +- .../fields/data_source/index_pattern_edit.tsx | 2 +- .../fields/data_source/translations.tsx | 18 +- .../final_edit/fields/hooks/use_data_view.ts | 59 ++++ .../hooks/use_diffable_rule_data_view.ts | 33 ++ .../fields/kql_query/kql_query_edit.tsx | 54 +--- .../form_schema.ts | 33 ++ .../threshold_alert_suppression/index.ts | 8 + .../suppression_edit_form.tsx | 70 +++++ .../three_way_diff/final_edit/final_edit.tsx | 10 +- .../machine_learning_rule_field_edit.tsx | 23 ++ .../final_edit/new_terms_rule_field_edit.tsx | 3 + .../saved_query_rule_field_edit.tsx | 3 + .../threat_match_rule_field_edit.tsx | 3 + .../final_edit/threshold_rule_field_edit.tsx | 3 + .../components/rules_table/__mocks__/mock.ts | 30 +- .../detection_engine/rules/helpers.test.tsx | 29 +- .../pages/detection_engine/rules/helpers.tsx | 24 +- .../pages/detection_engine/rules/types.ts | 19 +- .../pages/detection_engine/rules/utils.ts | 23 +- .../plugins/security_solution/tsconfig.json | 1 + .../translations/translations/fr-FR.json | 8 - .../translations/translations/ja-JP.json | 8 - .../translations/translations/zh-CN.json | 8 - .../common_flows_supression_ess_basic.cy.ts | 7 +- .../machine_learning_rule_suppression.cy.ts | 4 +- .../rule_edit/esql_rule.cy.ts | 8 +- .../rule_edit/indicator_match_rule.cy.ts | 9 +- .../rule_edit/machine_learning_rule.cy.ts | 15 +- .../rule_edit/new_terms_rule.cy.ts | 9 +- .../rule_edit/threshold_rule.cy.ts | 11 +- .../cypress/screens/create_new_rule.ts | 13 +- .../cypress/tasks/create_new_rule.ts | 7 +- 96 files changed, 1942 insertions(+), 877 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/common/test/eui/combobox.ts create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/alert_suppression_edit/components/alert_suppression_edit.tsx create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/alert_suppression_edit/components/missing_fields_strategy_selector.tsx create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/alert_suppression_edit/components/suppression_duration_selector.tsx create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/alert_suppression_edit/components/suppression_fields_selector.tsx rename x-pack/plugins/security_solution/public/detection_engine/{rule_creation_ui/components/suppression_info_icon/index.tsx => rule_creation/components/alert_suppression_edit/components/suppression_info_icon.tsx} (80%) create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/alert_suppression_edit/components/translations.ts create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/alert_suppression_edit/constants/default_duration.ts create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/alert_suppression_edit/constants/fields.ts create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/alert_suppression_edit/index.ts create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/alert_suppression_edit/test_helpers.ts rename x-pack/plugins/security_solution/public/detection_engine/{rule_creation_ui => rule_creation}/components/duration_input/index.tsx (68%) rename x-pack/plugins/security_solution/public/detection_engine/{rule_creation_ui => rule_creation}/components/duration_input/translations.ts (82%) create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/related_integrations/test_helpers.ts create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/threshold_alert_suppression_edit/fields.ts create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/threshold_alert_suppression_edit/index.ts create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/threshold_alert_suppression_edit/threshold_alert_suppression_edit.tsx create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/threshold_alert_suppression_edit/translations.tsx rename x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/data_view_selector_field/__mocks__/{use_data_views.ts => use_data_view_list_items.ts} (81%) rename x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/data_view_selector_field/{use_data_views.ts => use_data_view_list_items.ts} (95%) create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/use_persistent_alert_suppression_state.ts create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/validators/alert_suppression_fields_validator_factory.ts create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/esql_rule_field_edit.tsx create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/alert_suppression/form_schema.ts create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/alert_suppression/index.ts create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/alert_suppression/suppression_edit_adapter.tsx create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/alert_suppression/suppression_edit_form.tsx create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/alert_suppression/translations.tsx create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/hooks/use_data_view.ts create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/hooks/use_diffable_rule_data_view.ts create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/threshold_alert_suppression/form_schema.ts create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/threshold_alert_suppression/index.ts create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/threshold_alert_suppression/suppression_edit_form.tsx create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/machine_learning_rule_field_edit.tsx diff --git a/oas_docs/output/kibana.serverless.yaml b/oas_docs/output/kibana.serverless.yaml index 5f18154db449..95c201de052c 100644 --- a/oas_docs/output/kibana.serverless.yaml +++ b/oas_docs/output/kibana.serverless.yaml @@ -39976,17 +39976,20 @@ components: type: object properties: unit: - enum: - - s - - m - - h - type: string + $ref: >- + #/components/schemas/Security_Detections_API_AlertSuppressionDurationUnit value: minimum: 1 type: integer required: - value - unit + Security_Detections_API_AlertSuppressionDurationUnit: + enum: + - s + - m + - h + type: string Security_Detections_API_AlertSuppressionGroupBy: items: type: string diff --git a/oas_docs/output/kibana.yaml b/oas_docs/output/kibana.yaml index 133dede5fcd0..8b4953b4149a 100644 --- a/oas_docs/output/kibana.yaml +++ b/oas_docs/output/kibana.yaml @@ -48220,17 +48220,20 @@ components: type: object properties: unit: - enum: - - s - - m - - h - type: string + $ref: >- + #/components/schemas/Security_Detections_API_AlertSuppressionDurationUnit value: minimum: 1 type: integer required: - value - unit + Security_Detections_API_AlertSuppressionDurationUnit: + enum: + - s + - m + - h + type: string Security_Detections_API_AlertSuppressionGroupBy: items: type: string diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/common_attributes.gen.ts b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/common_attributes.gen.ts index 1bf3bfb5e244..2d722e7d5076 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/common_attributes.gen.ts +++ b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/common_attributes.gen.ts @@ -555,10 +555,15 @@ export const RuleExceptionList = z.object({ namespace_type: z.enum(['agnostic', 'single']), }); +export type AlertSuppressionDurationUnit = z.infer; +export const AlertSuppressionDurationUnit = z.enum(['s', 'm', 'h']); +export type AlertSuppressionDurationUnitEnum = typeof AlertSuppressionDurationUnit.enum; +export const AlertSuppressionDurationUnitEnum = AlertSuppressionDurationUnit.enum; + export type AlertSuppressionDuration = z.infer; export const AlertSuppressionDuration = z.object({ value: z.number().int().min(1), - unit: z.enum(['s', 'm', 'h']), + unit: AlertSuppressionDurationUnit, }); /** diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/common_attributes.schema.yaml b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/common_attributes.schema.yaml index 088153cca488..029a71b9e0a1 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/common_attributes.schema.yaml +++ b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/common_attributes.schema.yaml @@ -581,6 +581,13 @@ components: - type - namespace_type + AlertSuppressionDurationUnit: + type: string + enum: + - s + - m + - h + AlertSuppressionDuration: type: object properties: @@ -588,11 +595,7 @@ components: type: integer minimum: 1 unit: - type: string - enum: - - s - - m - - h + $ref: '#/components/schemas/AlertSuppressionDurationUnit' required: - value - unit diff --git a/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_detections_api_2023_10_31.bundled.schema.yaml b/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_detections_api_2023_10_31.bundled.schema.yaml index ebd4c9328009..7e8d7a61bff2 100644 --- a/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_detections_api_2023_10_31.bundled.schema.yaml +++ b/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_detections_api_2023_10_31.bundled.schema.yaml @@ -1560,17 +1560,19 @@ components: type: object properties: unit: - enum: - - s - - m - - h - type: string + $ref: '#/components/schemas/AlertSuppressionDurationUnit' value: minimum: 1 type: integer required: - value - unit + AlertSuppressionDurationUnit: + enum: + - s + - m + - h + type: string AlertSuppressionGroupBy: items: type: string diff --git a/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_detections_api_2023_10_31.bundled.schema.yaml b/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_detections_api_2023_10_31.bundled.schema.yaml index 30a7ae3ea343..58456e71140a 100644 --- a/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_detections_api_2023_10_31.bundled.schema.yaml +++ b/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_detections_api_2023_10_31.bundled.schema.yaml @@ -850,17 +850,19 @@ components: type: object properties: unit: - enum: - - s - - m - - h - type: string + $ref: '#/components/schemas/AlertSuppressionDurationUnit' value: minimum: 1 type: integer required: - value - unit + AlertSuppressionDurationUnit: + enum: + - s + - m + - h + type: string AlertSuppressionGroupBy: items: type: string diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/index.ts b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/index.ts index 607ed6d94959..b6067ac21eb1 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/index.ts +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/index.ts @@ -24,8 +24,8 @@ export const uiPlugins = ({ insightsUpsellingMessage, interactionsUpsellingMessage, }: { - insightsUpsellingMessage: string | null; - interactionsUpsellingMessage: string | null; + insightsUpsellingMessage?: string; + interactionsUpsellingMessage?: string; }) => { const currentPlugins = nonStatefulUiPlugins.map((plugin) => plugin.name); const insightPluginWithLicense = insightMarkdownPlugin.plugin({ diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.test.tsx index 37d4e004edf5..026e070f320d 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.test.tsx @@ -135,7 +135,7 @@ describe('plugin', () => { }); it('show investigate message when insightsUpsellingMessage is not provided', () => { - const result = plugin({ insightsUpsellingMessage: null }); + const result = plugin({ insightsUpsellingMessage: undefined }); expect(result.button.label).toEqual('Investigate'); }); diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.tsx index e8fa43d0a189..794b17a21920 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.tsx @@ -541,11 +541,7 @@ const exampleInsight = `${insightPrefix}{ ] }}`; -export const plugin = ({ - insightsUpsellingMessage, -}: { - insightsUpsellingMessage: string | null; -}) => { +export const plugin = ({ insightsUpsellingMessage }: { insightsUpsellingMessage?: string }) => { return { name: 'insights', button: { diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/osquery/plugin.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/osquery/plugin.tsx index 6a37280f9ef2..67b3f9e2b4f8 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/osquery/plugin.tsx +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/osquery/plugin.tsx @@ -161,7 +161,7 @@ const OsqueryEditor = React.memo(OsqueryEditorComponent); export const plugin = ({ interactionsUpsellingMessage, }: { - interactionsUpsellingMessage: string | null; + interactionsUpsellingMessage?: string; }) => { return { name: 'osquery', diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/plugin.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/plugin.tsx index 40b2fba4b84d..4d5b2e14e0d9 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/plugin.tsx +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/plugin.tsx @@ -78,7 +78,7 @@ const TimelineEditor = memo(TimelineEditorComponent); export const plugin = ({ interactionsUpsellingMessage, }: { - interactionsUpsellingMessage: string | null; + interactionsUpsellingMessage?: string; }): EuiMarkdownEditorUiPlugin => { return { name: ID, diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_upselling.test.tsx b/x-pack/plugins/security_solution/public/common/hooks/use_upselling.test.tsx index ee783bcd19e3..c18a6282eb37 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/use_upselling.test.tsx +++ b/x-pack/plugins/security_solution/public/common/hooks/use_upselling.test.tsx @@ -71,7 +71,7 @@ describe('use_upselling', () => { expect(result.all.length).toBe(1); // assert that it should not cause unnecessary re-renders }); - test('useUpsellingMessage returns null when upsellingMessageId not found', () => { + test('useUpsellingMessage returns undefined when upsellingMessageId not found', () => { const emptyMessages = {}; mockUpselling.setPages(emptyMessages); @@ -81,6 +81,6 @@ describe('use_upselling', () => { wrapper: RenderWrapper, } ); - expect(result.current).toBe(null); + expect(result.current).toBeUndefined(); }); }); diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_upselling.ts b/x-pack/plugins/security_solution/public/common/hooks/use_upselling.ts index 8ef01b5b56a2..9f452bb4f287 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/use_upselling.ts +++ b/x-pack/plugins/security_solution/public/common/hooks/use_upselling.ts @@ -22,11 +22,11 @@ export const useUpsellingComponent = (id: UpsellingSectionId): React.ComponentTy return useMemo(() => upsellingSections?.get(id) ?? null, [id, upsellingSections]); }; -export const useUpsellingMessage = (id: UpsellingMessageId): string | null => { +export const useUpsellingMessage = (id: UpsellingMessageId): string | undefined => { const upselling = useUpsellingService(); const upsellingMessages = useObservable(upselling.messages$, upselling.getMessagesValue()); - return useMemo(() => upsellingMessages?.get(id) ?? null, [id, upsellingMessages]); + return useMemo(() => upsellingMessages?.get(id), [id, upsellingMessages]); }; export const useUpsellingPage = (pageName: SecurityPageName): React.ComponentType | null => { diff --git a/x-pack/plugins/security_solution/public/common/test/eui/combobox.ts b/x-pack/plugins/security_solution/public/common/test/eui/combobox.ts new file mode 100644 index 000000000000..dad99a3c426d --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/test/eui/combobox.ts @@ -0,0 +1,79 @@ +/* + * 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 { act, fireEvent, waitFor } from '@testing-library/react'; + +export function showEuiComboBoxOptions(comboBoxToggleButton: HTMLElement): Promise { + fireEvent.click(comboBoxToggleButton); + + return waitFor(() => { + const listWithOptionsElement = document.querySelector('[role="listbox"]'); + const emptyListElement = document.querySelector('.euiComboBoxOptionsList__empty'); + + expect(listWithOptionsElement || emptyListElement).toBeInTheDocument(); + }); +} + +type SelectEuiComboBoxOptionParameters = + | { + comboBoxToggleButton: HTMLElement; + optionIndex: number; + optionText?: undefined; + } + | { + comboBoxToggleButton: HTMLElement; + optionText: string; + optionIndex?: undefined; + }; + +export function selectEuiComboBoxOption({ + comboBoxToggleButton, + optionIndex, + optionText, +}: SelectEuiComboBoxOptionParameters): Promise { + return act(async () => { + await showEuiComboBoxOptions(comboBoxToggleButton); + + const options = Array.from( + document.querySelectorAll('[data-test-subj*="comboBoxOptionsList"] [role="option"]') + ); + + if (typeof optionText === 'string') { + const optionToSelect = options.find((option) => option.textContent === optionText); + + if (optionToSelect) { + fireEvent.click(optionToSelect); + } else { + throw new Error( + `Could not find option with text "${optionText}". Available options: ${options + .map((option) => option.textContent) + .join(', ')}` + ); + } + } else { + fireEvent.click(options[optionIndex]); + } + }); +} + +export function selectFirstEuiComboBoxOption({ + comboBoxToggleButton, +}: { + comboBoxToggleButton: HTMLElement; +}): Promise { + return selectEuiComboBoxOption({ comboBoxToggleButton, optionIndex: 0 }); +} + +export function clearEuiComboBoxSelection({ + clearButton, +}: { + clearButton: HTMLElement; +}): Promise { + return act(async () => { + fireEvent.click(clearButton); + }); +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/alert_suppression_edit/components/alert_suppression_edit.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/alert_suppression_edit/components/alert_suppression_edit.tsx new file mode 100644 index 000000000000..5c6099529e92 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/alert_suppression_edit/components/alert_suppression_edit.tsx @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo } from 'react'; +import { EuiPanel, EuiText, EuiToolTip } from '@elastic/eui'; +import type { DataViewFieldBase } from '@kbn/es-query'; +import { useFormData } from '../../../../../shared_imports'; +import { MissingFieldsStrategySelector } from './missing_fields_strategy_selector'; +import { SuppressionDurationSelector } from './suppression_duration_selector'; +import { SuppressionFieldsSelector } from './suppression_fields_selector'; +import { ALERT_SUPPRESSION_FIELDS_FIELD_NAME } from '../constants/fields'; + +interface AlertSuppressionEditProps { + suppressibleFields: DataViewFieldBase[]; + labelAppend?: React.ReactNode; + disabled?: boolean; + disabledText?: string; + warningText?: string; +} + +export const AlertSuppressionEdit = memo(function AlertSuppressionEdit({ + suppressibleFields, + labelAppend, + disabled, + disabledText, + warningText, +}: AlertSuppressionEditProps): JSX.Element { + const [{ [ALERT_SUPPRESSION_FIELDS_FIELD_NAME]: suppressionFields }] = useFormData<{ + [ALERT_SUPPRESSION_FIELDS_FIELD_NAME]: string[]; + }>({ + watch: ALERT_SUPPRESSION_FIELDS_FIELD_NAME, + }); + const hasSelectedFields = suppressionFields?.length > 0; + const content = ( + <> + + {warningText && ( + + {warningText} + + )} + + + + + + ); + + return disabled && disabledText ? ( + + {content} + + ) : ( + content + ); +}); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/alert_suppression_edit/components/missing_fields_strategy_selector.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/alert_suppression_edit/components/missing_fields_strategy_selector.tsx new file mode 100644 index 000000000000..a7b38843a61c --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/alert_suppression_edit/components/missing_fields_strategy_selector.tsx @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo } from 'react'; +import type { EuiFormRowProps, EuiRadioGroupOption, EuiRadioGroupProps } from '@elastic/eui'; +import { RadioGroupField } from '@kbn/es-ui-shared-plugin/static/forms/components'; +import { AlertSuppressionMissingFieldsStrategyEnum } from '../../../../../../common/api/detection_engine'; +import { UseField } from '../../../../../shared_imports'; +import { SuppressionInfoIcon } from './suppression_info_icon'; +import { ALERT_SUPPRESSION_MISSING_FIELDS_FIELD_NAME } from '../constants/fields'; +import * as i18n from './translations'; + +interface MissingFieldsStrategySelectorProps { + disabled?: boolean; +} + +export function MissingFieldsStrategySelector({ + disabled, +}: MissingFieldsStrategySelectorProps): JSX.Element { + const radioFieldProps: Partial = useMemo( + () => ({ + options: ALERT_SUPPRESSION_MISSING_FIELDS_STRATEGY_OPTIONS, + 'data-test-subj': 'suppressionMissingFieldsOptions', + disabled, + }), + [disabled] + ); + + return ( + + ); +} + +const EUI_FORM_ROW_PROPS: Partial = { + label: ( + + {i18n.ALERT_SUPPRESSION_MISSING_FIELDS_LABEL} + + ), + 'data-test-subj': 'alertSuppressionMissingFields', +}; + +const ALERT_SUPPRESSION_MISSING_FIELDS_STRATEGY_OPTIONS: EuiRadioGroupOption[] = [ + { + id: AlertSuppressionMissingFieldsStrategyEnum.suppress, + label: i18n.ALERT_SUPPRESSION_MISSING_FIELDS_SUPPRESS_OPTION, + }, + { + id: AlertSuppressionMissingFieldsStrategyEnum.doNotSuppress, + label: i18n.ALERT_SUPPRESSION_MISSING_FIELDS_DO_NOT_SUPPRESS_OPTION, + }, +]; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/alert_suppression_edit/components/suppression_duration_selector.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/alert_suppression_edit/components/suppression_duration_selector.tsx new file mode 100644 index 000000000000..7cf5eeb3018b --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/alert_suppression_edit/components/suppression_duration_selector.tsx @@ -0,0 +1,140 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo, useEffect } from 'react'; +import { EuiFormRow, EuiRadioGroup, EuiToolTip, useEuiTheme } from '@elastic/eui'; +import { css } from '@emotion/css'; +import type { FieldHook } from '../../../../../shared_imports'; +import { UseMultiFields } from '../../../../../shared_imports'; +import { AlertSuppressionDurationType } from '../../../../../detections/pages/detection_engine/rules/types'; +import { DurationInput } from '../../duration_input'; +import { + ALERT_SUPPRESSION_DURATION_FIELD_NAME, + ALERT_SUPPRESSION_DURATION_TYPE_FIELD_NAME, + ALERT_SUPPRESSION_DURATION_UNIT_FIELD_NAME, + ALERT_SUPPRESSION_DURATION_VALUE_FIELD_NAME, +} from '../constants/fields'; +import * as i18n from './translations'; + +interface AlertSuppressionDurationProps { + onlyPerTimePeriod?: boolean; + onlyPerTimePeriodReasonMessage?: string; + disabled?: boolean; +} + +export function SuppressionDurationSelector({ + onlyPerTimePeriod, + onlyPerTimePeriodReasonMessage, + disabled, +}: AlertSuppressionDurationProps): JSX.Element { + return ( + + + fields={{ + suppressionDurationSelector: { + path: ALERT_SUPPRESSION_DURATION_TYPE_FIELD_NAME, + }, + suppressionDurationValue: { + path: `${ALERT_SUPPRESSION_DURATION_FIELD_NAME}.${ALERT_SUPPRESSION_DURATION_VALUE_FIELD_NAME}`, + }, + suppressionDurationUnit: { + path: `${ALERT_SUPPRESSION_DURATION_FIELD_NAME}.${ALERT_SUPPRESSION_DURATION_UNIT_FIELD_NAME}`, + }, + }} + > + {({ suppressionDurationSelector, suppressionDurationValue, suppressionDurationUnit }) => ( + + )} + + + ); +} + +interface SuppressionDurationSelectorFieldsProps { + suppressionDurationSelectorField: FieldHook; + suppressionDurationValueField: FieldHook; + suppressionDurationUnitField: FieldHook; + onlyPerTimePeriod?: boolean; + onlyPerTimePeriodReasonMessage?: string; + disabled?: boolean; +} + +const SuppressionDurationSelectorFields = memo(function SuppressionDurationSelectorFields({ + suppressionDurationSelectorField, + suppressionDurationValueField, + suppressionDurationUnitField, + onlyPerTimePeriod = false, + onlyPerTimePeriodReasonMessage, + disabled, +}: SuppressionDurationSelectorFieldsProps): JSX.Element { + const { euiTheme } = useEuiTheme(); + const { value: durationType, setValue: setDurationType } = suppressionDurationSelectorField; + + useEffect(() => { + if (onlyPerTimePeriod && durationType !== AlertSuppressionDurationType.PerTimePeriod) { + setDurationType(AlertSuppressionDurationType.PerTimePeriod); + } + }, [onlyPerTimePeriod, durationType, setDurationType]); + + return ( + <> + + <> {i18n.ALERT_SUPPRESSION_DURATION_PER_RULE_EXECUTION_OPTION} + + ) : ( + i18n.ALERT_SUPPRESSION_DURATION_PER_RULE_EXECUTION_OPTION + ), + disabled: onlyPerTimePeriod ? true : disabled, + }, + { + id: AlertSuppressionDurationType.PerTimePeriod, + disabled, + label: i18n.ALERT_SUPPRESSION_DURATION_PER_TIME_PERIOD_OPTION, + }, + ]} + onChange={(id) => { + suppressionDurationSelectorField.setValue(id); + }} + data-test-subj="alertSuppressionDurationOptions" + /> +
+ +
+ + ); +}); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/alert_suppression_edit/components/suppression_fields_selector.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/alert_suppression_edit/components/suppression_fields_selector.tsx new file mode 100644 index 000000000000..72eea027288f --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/alert_suppression_edit/components/suppression_fields_selector.tsx @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiFormRow } from '@elastic/eui'; +import type { DataViewFieldBase } from '@kbn/es-query'; +import { UseField } from '../../../../../shared_imports'; +import { MultiSelectFieldsAutocomplete } from '../../../../rule_creation_ui/components/multi_select_fields'; +import { ALERT_SUPPRESSION_FIELDS_FIELD_NAME } from '../constants/fields'; +import * as i18n from './translations'; + +interface SuppressionFieldsSelectorProps { + suppressibleFields: DataViewFieldBase[]; + labelAppend?: React.ReactNode; + disabled?: boolean; +} + +export function SuppressionFieldsSelector({ + suppressibleFields, + labelAppend, + disabled, +}: SuppressionFieldsSelectorProps): JSX.Element { + return ( + + <> + + + + ); +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/suppression_info_icon/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/alert_suppression_edit/components/suppression_info_icon.tsx similarity index 80% rename from x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/suppression_info_icon/index.tsx rename to x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/alert_suppression_edit/components/suppression_info_icon.tsx index bb3b0db1ccda..466600dd394d 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/suppression_info_icon/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/alert_suppression_edit/components/suppression_info_icon.tsx @@ -5,28 +5,25 @@ * 2.0. */ -import React, { useState } from 'react'; +import React from 'react'; import { EuiLink, EuiPopover, EuiText, EuiButtonIcon } from '@elastic/eui'; +import { useBoolean } from '@kbn/react-hooks'; import { FormattedMessage } from '@kbn/i18n-react'; - -import { useKibana } from '../../../../common/lib/kibana'; +import { useKibana } from '../../../../../common/lib/kibana'; const POPOVER_WIDTH = 320; /** * Icon and popover that gives hint to users how suppression for missing fields work */ -const SuppressionInfoIconComponent = () => { - const [isPopoverOpen, setIsPopoverOpen] = useState(false); +export function SuppressionInfoIcon(): JSX.Element { + const [isPopoverOpen, { off: closePopover, toggle: togglePopover }] = useBoolean(false); const { docLinks } = useKibana().services; - const onButtonClick = () => setIsPopoverOpen(!isPopoverOpen); - const closePopover = () => setIsPopoverOpen(false); - const button = ( ); @@ -59,8 +56,4 @@ const SuppressionInfoIconComponent = () => { ); -}; - -export const SuppressionInfoIcon = React.memo(SuppressionInfoIconComponent); - -SuppressionInfoIcon.displayName = 'SuppressionInfoIcon'; +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/alert_suppression_edit/components/translations.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/alert_suppression_edit/components/translations.ts new file mode 100644 index 000000000000..8da7d435adfe --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/alert_suppression_edit/components/translations.ts @@ -0,0 +1,94 @@ +/* + * 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 ALERT_SUPPRESSION_SUPPRESS_BY_FIELD_LABEL = i18n.translate( + 'xpack.securitySolution.ruleManagement.ruleFields.alertSuppression.fieldsSelector.label', + { + defaultMessage: 'Suppress alerts by', + } +); + +export const ALERT_SUPPRESSION_SUPPRESS_BY_FIELD_HELP_TEXT = i18n.translate( + 'xpack.securitySolution.ruleManagement.ruleFields.alertSuppression.suppressByFields.helpText', + { + defaultMessage: 'Select field(s) to use for suppressing extra alerts', + } +); + +export const ALERT_SUPPRESSION_DURATION_PER_RULE_EXECUTION_OPTION = i18n.translate( + 'xpack.securitySolution.ruleManagement.ruleFields.alertSuppression.suppressionDuration.perRuleExecutionOption', + { + defaultMessage: 'Per rule execution', + } +); + +export const ALERT_SUPPRESSION_DURATION_PER_TIME_PERIOD_OPTION = i18n.translate( + 'xpack.securitySolution.ruleManagement.ruleFields.alertSuppression.suppressionDuration.perTimePeriodOption', + { + defaultMessage: 'Per time period', + } +); + +export const ALERT_SUPPRESSION_MISSING_FIELDS_LABEL = i18n.translate( + 'xpack.securitySolution.ruleManagement.ruleFields.alertSuppression.missingFields.label', + { + defaultMessage: 'If a suppression field is missing', + } +); + +export const ALERT_SUPPRESSION_MISSING_FIELDS_SUPPRESS_OPTION = i18n.translate( + 'xpack.securitySolution.ruleManagement.ruleFields.alertSuppression.missingFields.suppressOption', + { + defaultMessage: 'Suppress and group alerts for events with missing fields', + } +); + +export const ALERT_SUPPRESSION_MISSING_FIELDS_DO_NOT_SUPPRESS_OPTION = i18n.translate( + 'xpack.securitySolution.ruleManagement.ruleFields.alertSuppression.missingFields.doNotSuppressOption', + { + defaultMessage: 'Do not suppress alerts for events with missing fields', + } +); + +export const ALERT_SUPPRESSION_NOT_SUPPORTED_FOR_EQL_SEQUENCE = i18n.translate( + 'xpack.securitySolution.ruleManagement.ruleFields.alertSuppression.notSupportedForEqlSequence', + { + defaultMessage: 'Suppression is not supported for EQL sequence queries', + } +); + +export const MACHINE_LEARNING_SUPPRESSION_FIELDS_LOADING = i18n.translate( + 'xpack.securitySolution.ruleManagement.ruleFields.alertSuppression.machineLearningSuppressionFieldsLoading', + { + defaultMessage: 'Machine Learning suppression fields are loading', + } +); + +export const MACHINE_LEARNING_NO_SUPPRESSION_FIELDS = i18n.translate( + 'xpack.securitySolution.ruleManagement.ruleFields.alertSuppression.machineLearningNoSuppressionFields', + { + defaultMessage: + 'Unable to load machine Learning suppression fields, start relevant Machine Learning jobs.', + } +); + +export const ESQL_SUPPRESSION_FIELDS_LOADING = i18n.translate( + 'xpack.securitySolution.ruleManagement.ruleFields.alertSuppression.esqlFieldsLoading', + { + defaultMessage: 'ES|QL suppression fields are loading', + } +); + +export const MACHINE_LEARNING_SUPPRESSION_INCOMPLETE_LABEL = i18n.translate( + 'xpack.securitySolution.ruleManagement.ruleFields.alertSuppression.machineLearningSuppressionIncomplete', + { + defaultMessage: + 'This list of fields might be incomplete as some Machine Learning jobs are not running. Start all relevant jobs for a complete list.', + } +); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/alert_suppression_edit/constants/default_duration.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/alert_suppression_edit/constants/default_duration.ts new file mode 100644 index 000000000000..6e06d0d67031 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/alert_suppression_edit/constants/default_duration.ts @@ -0,0 +1,17 @@ +/* + * 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 { AlertSuppressionDurationUnitEnum } from '../../../../../../common/api/detection_engine'; +import { + ALERT_SUPPRESSION_DURATION_VALUE_FIELD_NAME, + ALERT_SUPPRESSION_DURATION_UNIT_FIELD_NAME, +} from './fields'; + +export const ALERT_SUPPRESSION_DEFAULT_DURATION = { + [ALERT_SUPPRESSION_DURATION_VALUE_FIELD_NAME]: 5, + [ALERT_SUPPRESSION_DURATION_UNIT_FIELD_NAME]: AlertSuppressionDurationUnitEnum.m, +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/alert_suppression_edit/constants/fields.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/alert_suppression_edit/constants/fields.ts new file mode 100644 index 000000000000..42a0583e9051 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/alert_suppression_edit/constants/fields.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const ALERT_SUPPRESSION_FIELDS_FIELD_NAME = 'alertSuppressionFields' as const; +export const ALERT_SUPPRESSION_DURATION_TYPE_FIELD_NAME = 'alertSuppressionDurationType' as const; +export const ALERT_SUPPRESSION_DURATION_FIELD_NAME = 'alertSuppressionDuration' as const; +export const ALERT_SUPPRESSION_DURATION_VALUE_FIELD_NAME = 'value' as const; +export const ALERT_SUPPRESSION_DURATION_UNIT_FIELD_NAME = 'unit' as const; +export const ALERT_SUPPRESSION_MISSING_FIELDS_FIELD_NAME = 'alertSuppressionMissingFields' as const; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/alert_suppression_edit/index.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/alert_suppression_edit/index.ts new file mode 100644 index 000000000000..a97e74726e3c --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/alert_suppression_edit/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './components/alert_suppression_edit'; +export * from './components/suppression_duration_selector'; +export * from './constants/fields'; +export * from './constants/default_duration'; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/alert_suppression_edit/test_helpers.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/alert_suppression_edit/test_helpers.ts new file mode 100644 index 000000000000..b7d6c4003e93 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/alert_suppression_edit/test_helpers.ts @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +// eslint-disable-next-line import/no-extraneous-dependencies +import { act, fireEvent, waitFor, within, screen } from '@testing-library/react'; +import type { AlertSuppressionDurationUnit } from '../../../../../common/api/detection_engine'; +import { selectEuiComboBoxOption } from '../../../../common/test/eui/combobox'; + +const COMBO_BOX_TOGGLE_BUTTON_TEST_ID = 'comboBoxToggleListButton'; + +export async function setSuppressionFields(fieldNames: string[]): Promise { + const getAlertSuppressionFieldsComboBoxToggleButton = () => + within(screen.getByTestId('alertSuppressionInput')).getByTestId( + COMBO_BOX_TOGGLE_BUTTON_TEST_ID + ); + + await waitFor(() => { + expect(getAlertSuppressionFieldsComboBoxToggleButton()).toBeInTheDocument(); + }); + + for (const fieldName of fieldNames) { + await selectEuiComboBoxOption({ + comboBoxToggleButton: getAlertSuppressionFieldsComboBoxToggleButton(), + optionText: fieldName, + }); + } +} + +export function expectSuppressionFields(fieldNames: string[]): void { + for (const fieldName of fieldNames) { + expect( + within(screen.getByTestId('alertSuppressionInput')).getByTitle(fieldName) + ).toBeInTheDocument(); + } +} + +export function setDurationType(value: 'Per rule execution' | 'Per time period'): void { + act(() => { + fireEvent.click(within(screen.getByTestId('alertSuppressionDuration')).getByLabelText(value)); + }); +} + +export function setDuration(value: number, unit: AlertSuppressionDurationUnit): void { + act(() => { + fireEvent.input( + within(screen.getByTestId('alertSuppressionDuration')).getByTestId('interval'), + { + target: { value: value.toString() }, + } + ); + + fireEvent.change( + within(screen.getByTestId('alertSuppressionDuration')).getByTestId('timeType'), + { + target: { value: unit }, + } + ); + }); +} + +export function expectDuration(value: number, unit: AlertSuppressionDurationUnit): void { + expect( + within(screen.getByTestId('alertSuppressionDuration')).getByTestId('interval') + ).toHaveValue(value); + expect( + within(screen.getByTestId('alertSuppressionDuration')).getByTestId('timeType') + ).toHaveValue(unit); +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/duration_input/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/duration_input/index.tsx similarity index 68% rename from x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/duration_input/index.tsx rename to x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/duration_input/index.tsx index 99222756bcf2..b203cdea8f73 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/duration_input/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/duration_input/index.tsx @@ -5,10 +5,9 @@ * 2.0. */ -import { EuiFieldNumber, EuiFormRow, EuiSelect, transparentize } from '@elastic/eui'; -import React, { useCallback } from 'react'; -import styled from 'styled-components'; - +import React, { memo, useCallback } from 'react'; +import { css } from '@emotion/css'; +import { EuiFieldNumber, EuiFormRow, EuiSelect, transparentize, useEuiTheme } from '@elastic/eui'; import type { FieldHook } from '../../../../shared_imports'; import { getFieldValidityAndErrorMessage } from '../../../../shared_imports'; import * as I18n from './translations'; @@ -21,39 +20,10 @@ interface DurationInputProps { durationUnitOptions?: Array<{ value: 's' | 'm' | 'h' | 'd'; text: string }>; } -const getNumberFromUserInput = (input: string, minimumValue = 0): number | undefined => { - const number = parseInt(input, 10); - if (Number.isNaN(number)) { - return minimumValue; - } else { - return Math.max(minimumValue, Math.min(number, Number.MAX_SAFE_INTEGER)); - } -}; - -const StyledEuiFormRow = styled(EuiFormRow)` - max-width: 235px; - - .euiFormControlLayout__append { - padding-inline: 0 !important; - } - - .euiFormControlLayoutIcons { - color: ${({ theme }) => theme.eui.euiColorPrimary}; - } -`; - -const MyEuiSelect = styled(EuiSelect)` - min-width: 106px; // Preserve layout when disabled & dropdown arrow is not rendered - box-shadow: none; - background: ${({ theme }) => - transparentize(theme.eui.euiColorPrimary, 0.1)} !important; // Override focus states etc. - color: ${({ theme }) => theme.eui.euiColorPrimary}; -`; - // This component is similar to the ScheduleItem component, but instead of combining the value // and unit into a single string it keeps them separate. This makes the component simpler and // allows for easier validation of values and units in APIs as well. -const DurationInputComponent: React.FC = ({ +export const DurationInput = memo(function DurationInputComponent({ durationValueField, durationUnitField, minimumValue = 0, @@ -64,7 +34,8 @@ const DurationInputComponent: React.FC = ({ { value: 'h', text: I18n.HOURS }, ], ...props -}: DurationInputProps) => { +}: DurationInputProps): JSX.Element { + const { euiTheme } = useEuiTheme(); const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(durationValueField); const { value: durationValue, setValue: setDurationValue } = durationValueField; const { value: durationUnit, setValue: setDurationUnit } = durationUnitField; @@ -84,17 +55,40 @@ const DurationInputComponent: React.FC = ({ [minimumValue, setDurationValue] ); + const durationFormRowStyle = css` + max-width: 235px; + + .euiFormControlLayout__append { + padding-inline: 0 !important; + } + + .euiFormControlLayoutIcons { + color: ${euiTheme.colors.primary}; + } + `; + const durationUnitSelectStyle = css` + min-width: 106px; // Preserve layout when disabled & dropdown arrow is not rendered + box-shadow: none; + background: ${transparentize( + euiTheme.colors.primary, + 0.1 + )} !important; // Override focus states etc. + color: ${euiTheme.colors.primary}; + `; + // EUI missing some props const rest = { disabled: isDisabled, ...props }; return ( - + @@ -106,8 +100,16 @@ const DurationInputComponent: React.FC = ({ data-test-subj="interval" {...rest} /> - + ); -}; +}); + +function getNumberFromUserInput(input: string, minimumValue = 0): number | undefined { + const number = parseInt(input, 10); -export const DurationInput = React.memo(DurationInputComponent); + if (Number.isNaN(number)) { + return minimumValue; + } else { + return Math.max(minimumValue, Math.min(number, Number.MAX_SAFE_INTEGER)); + } +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/duration_input/translations.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/duration_input/translations.ts similarity index 82% rename from x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/duration_input/translations.ts rename to x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/duration_input/translations.ts index c460d2f7198b..51d659210c52 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/duration_input/translations.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/duration_input/translations.ts @@ -34,3 +34,10 @@ export const DAYS = i18n.translate( defaultMessage: 'Days', } ); + +export const DURATION_UNIT_SELECTOR = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepScheduleRuleForm.durationUnitSelector', + { + defaultMessage: 'Duration unit selector', + } +); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/related_integrations/related_integrations.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/related_integrations/related_integrations.test.tsx index 960df4c7de5b..31e139a335be 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/related_integrations/related_integrations.test.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/related_integrations/related_integrations.test.tsx @@ -6,19 +6,24 @@ */ import React from 'react'; -import { - screen, - render, - act, - fireEvent, - waitFor, - waitForElementToBeRemoved, -} from '@testing-library/react'; +import { screen, render, act, fireEvent, waitFor } from '@testing-library/react'; import type { RelatedIntegration } from '../../../../../common/api/detection_engine'; import { FIELD_TYPES, Form, useForm } from '../../../../shared_imports'; import { createReactQueryWrapper } from '../../../../common/mock'; import { fleetIntegrationsApi } from '../../../fleet_integrations/api/__mocks__'; import { RelatedIntegrations } from './related_integrations'; +import { + clearEuiComboBoxSelection, + selectEuiComboBoxOption, + selectFirstEuiComboBoxOption, + showEuiComboBoxOptions, +} from '../../../../common/test/eui/combobox'; +import { + addRelatedIntegrationRow, + removeLastRelatedIntegrationRow, + setVersion, + waitForIntegrationsToBeLoaded, +} from './test_helpers'; // must match to the import in rules/related_integrations/use_integrations.tsx jest.mock('../../../fleet_integrations/api'); @@ -41,7 +46,6 @@ const COMBO_BOX_TOGGLE_BUTTON_TEST_ID = 'comboBoxToggleListButton'; const COMBO_BOX_SELECTION_TEST_ID = 'euiComboBoxPill'; const COMBO_BOX_CLEAR_BUTTON_TEST_ID = 'comboBoxClearButton'; const VERSION_INPUT_TEST_ID = 'relatedIntegrationVersionDependency'; -const REMOVE_INTEGRATION_ROW_BUTTON_TEST_ID = 'relatedIntegrationRemove'; describe('RelatedIntegrations form part', () => { beforeEach(() => { @@ -708,72 +712,6 @@ function TestForm({ initialState, onSubmit }: TestFormProps): JSX.Element { ); } -function waitForIntegrationsToBeLoaded(): Promise { - return waitForElementToBeRemoved(screen.queryAllByRole('progressbar')); -} - -function addRelatedIntegrationRow(): Promise { - return act(async () => { - fireEvent.click(screen.getByText('Add integration')); - }); -} - -function removeLastRelatedIntegrationRow(): Promise { - return act(async () => { - const lastRemoveButton = screen.getAllByTestId(REMOVE_INTEGRATION_ROW_BUTTON_TEST_ID).at(-1); - - if (!lastRemoveButton) { - throw new Error(`There are no "${REMOVE_INTEGRATION_ROW_BUTTON_TEST_ID}" found`); - } - - fireEvent.click(lastRemoveButton); - }); -} - -function showEuiComboBoxOptions(comboBoxToggleButton: HTMLElement): Promise { - fireEvent.click(comboBoxToggleButton); - - return waitFor(() => { - expect(screen.getByRole('listbox')).toBeInTheDocument(); - }); -} - -function selectEuiComboBoxOption({ - comboBoxToggleButton, - optionIndex, -}: { - comboBoxToggleButton: HTMLElement; - optionIndex: number; -}): Promise { - return act(async () => { - await showEuiComboBoxOptions(comboBoxToggleButton); - - fireEvent.click(screen.getAllByRole('option')[optionIndex]); - }); -} - -function clearEuiComboBoxSelection({ clearButton }: { clearButton: HTMLElement }): Promise { - return act(async () => { - fireEvent.click(clearButton); - }); -} - -function selectFirstEuiComboBoxOption({ - comboBoxToggleButton, -}: { - comboBoxToggleButton: HTMLElement; -}): Promise { - return selectEuiComboBoxOption({ comboBoxToggleButton, optionIndex: 0 }); -} - -function setVersion({ input, value }: { input: HTMLInputElement; value: string }): Promise { - return act(async () => { - fireEvent.input(input, { - target: { value }, - }); - }); -} - function submitForm(): Promise { return act(async () => { fireEvent.click(screen.getByText('Submit')); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/related_integrations/test_helpers.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/related_integrations/test_helpers.ts new file mode 100644 index 000000000000..b8c51fd594e1 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/related_integrations/test_helpers.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +// eslint-disable-next-line import/no-extraneous-dependencies +import { act, fireEvent, waitForElementToBeRemoved, screen } from '@testing-library/react'; + +const REMOVE_INTEGRATION_ROW_BUTTON_TEST_ID = 'relatedIntegrationRemove'; + +export function waitForIntegrationsToBeLoaded(): Promise { + return waitForElementToBeRemoved(screen.queryAllByRole('progressbar')); +} + +export function addRelatedIntegrationRow(): Promise { + return act(async () => { + fireEvent.click(screen.getByText('Add integration')); + }); +} + +export function removeLastRelatedIntegrationRow(): Promise { + return act(async () => { + const lastRemoveButton = screen.getAllByTestId(REMOVE_INTEGRATION_ROW_BUTTON_TEST_ID).at(-1); + + if (!lastRemoveButton) { + throw new Error(`There are no "${REMOVE_INTEGRATION_ROW_BUTTON_TEST_ID}" found`); + } + + fireEvent.click(lastRemoveButton); + }); +} + +export function setVersion({ + input, + value, +}: { + input: HTMLInputElement; + value: string; +}): Promise { + return act(async () => { + fireEvent.input(input, { + target: { value }, + }); + }); +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/threshold_alert_suppression_edit/fields.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/threshold_alert_suppression_edit/fields.ts new file mode 100644 index 000000000000..4956a2555bc9 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/threshold_alert_suppression_edit/fields.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const THRESHOLD_ALERT_SUPPRESSION_ENABLED = 'thresholdAlertSuppressionEnabled' as const; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/threshold_alert_suppression_edit/index.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/threshold_alert_suppression_edit/index.ts new file mode 100644 index 000000000000..67848fbd5e3b --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/threshold_alert_suppression_edit/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './threshold_alert_suppression_edit'; +export * from './fields'; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/threshold_alert_suppression_edit/threshold_alert_suppression_edit.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/threshold_alert_suppression_edit/threshold_alert_suppression_edit.tsx new file mode 100644 index 000000000000..a832bff648e8 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/threshold_alert_suppression_edit/threshold_alert_suppression_edit.tsx @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo } from 'react'; +import { EuiPanel, EuiToolTip } from '@elastic/eui'; +import { CheckBoxField } from '@kbn/es-ui-shared-plugin/static/forms/components'; +import { UseField, useFormData } from '../../../../shared_imports'; +import { THRESHOLD_ALERT_SUPPRESSION_ENABLED } from './fields'; +import { SuppressionDurationSelector } from '../alert_suppression_edit'; +import * as i18n from './translations'; + +interface ThresholdAlertSuppressionEditProps { + suppressionFieldNames: string[] | undefined; + disabled?: boolean; + disabledText?: string; +} + +export const ThresholdAlertSuppressionEdit = memo(function ThresholdAlertSuppressionEdit({ + suppressionFieldNames, + disabled, + disabledText, +}: ThresholdAlertSuppressionEditProps): JSX.Element { + const [{ [THRESHOLD_ALERT_SUPPRESSION_ENABLED]: suppressionEnabled }] = useFormData({ + watch: THRESHOLD_ALERT_SUPPRESSION_ENABLED, + }); + const content = ( + <> + + + + + + ); + + return disabled && disabledText ? ( + + {content} + + ) : ( + content + ); +}); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/threshold_alert_suppression_edit/translations.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/threshold_alert_suppression_edit/translations.tsx new file mode 100644 index 000000000000..25b7158610b3 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/threshold_alert_suppression_edit/translations.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; + +export const enableSuppressionForFields = (fields: string[]) => ( + {fields.join(', ')} }} + /> +); + +export const SUPPRESS_ALERTS = i18n.translate( + 'xpack.securitySolution.ruleManagement.ruleFields.thresholdAlertSuppression.enable', + { + defaultMessage: 'Suppress alerts', + } +); + +export const THRESHOLD_SUPPRESSION_PER_RULE_EXECUTION_WARNING = i18n.translate( + 'xpack.securitySolution.ruleManagement.ruleFields.thresholdAlertSuppression.perRuleExecutionWarning', + { + defaultMessage: 'Per rule execution option is not available for Threshold rule type', + } +); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/data_view_selector_field/__mocks__/use_data_views.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/data_view_selector_field/__mocks__/use_data_view_list_items.ts similarity index 81% rename from x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/data_view_selector_field/__mocks__/use_data_views.ts rename to x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/data_view_selector_field/__mocks__/use_data_view_list_items.ts index 248729f1f46e..3d2ba5d1c372 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/data_view_selector_field/__mocks__/use_data_views.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/data_view_selector_field/__mocks__/use_data_view_list_items.ts @@ -5,7 +5,7 @@ * 2.0. */ -export const useDataViews = jest.fn().mockReturnValue({ +export const useDataViewListItems = jest.fn().mockReturnValue({ data: [], isFetching: false, }); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/data_view_selector_field/data_view_selector_field.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/data_view_selector_field/data_view_selector_field.test.tsx index 6cfdf060434b..8648ade5164e 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/data_view_selector_field/data_view_selector_field.test.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/data_view_selector_field/data_view_selector_field.test.tsx @@ -9,14 +9,14 @@ import React from 'react'; import { screen, render } from '@testing-library/react'; import { TestProviders, useFormFieldMock } from '../../../../common/mock'; import { DataViewSelectorField } from './data_view_selector_field'; -import { useDataViews } from './use_data_views'; +import { useDataViewListItems } from './use_data_view_list_items'; jest.mock('../../../../common/lib/kibana'); -jest.mock('./use_data_views'); +jest.mock('./use_data_view_list_items'); describe('data_view_selector', () => { it('renders correctly', () => { - (useDataViews as jest.Mock).mockReturnValue({ data: [], isFetching: false }); + (useDataViewListItems as jest.Mock).mockReturnValue({ data: [], isFetching: false }); render( { }); it('disables the combobox while data views are fetching', () => { - (useDataViews as jest.Mock).mockReturnValue({ data: [], isFetching: true }); + (useDataViewListItems as jest.Mock).mockReturnValue({ data: [], isFetching: true }); render( { title: 'logs-*', }, ]; - (useDataViews as jest.Mock).mockReturnValue({ data: dataViews, isFetching: false }); + (useDataViewListItems as jest.Mock).mockReturnValue({ data: dataViews, isFetching: false }); render( { title: 'logs-*', }, ]; - (useDataViews as jest.Mock).mockReturnValue({ data: dataViews, isFetching: false }); + (useDataViewListItems as jest.Mock).mockReturnValue({ data: dataViews, isFetching: false }); render( { }); it('displays warning on missing data view', () => { - (useDataViews as jest.Mock).mockReturnValue({ data: [], isFetching: false }); + (useDataViewListItems as jest.Mock).mockReturnValue({ data: [], isFetching: false }); render( { @@ -615,11 +615,11 @@ export const buildAlertSuppressionDescription = ( export const buildAlertSuppressionWindowDescription = ( label: string, value: Duration, - groupByRadioSelection: GroupByOptions, + alertSuppressionDuration: AlertSuppressionDurationType, ruleType: Type ): ListItems[] => { const description = - groupByRadioSelection === GroupByOptions.PerTimePeriod + alertSuppressionDuration === AlertSuppressionDurationType.PerTimePeriod ? `${value.value}${value.unit}` : i18n.ALERT_SUPPRESSION_PER_RULE_EXECUTION; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/description_step/index.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/description_step/index.test.tsx index f5a7e3963435..de46d09065f4 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/description_step/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/description_step/index.test.tsx @@ -30,6 +30,15 @@ import { schema } from '../step_about_rule/schema'; import type { ListItems } from './types'; import type { AboutStepRule } from '../../../../detections/pages/detection_engine/rules/types'; import { createLicenseServiceMock } from '../../../../../common/license/mocks'; +import { + ALERT_SUPPRESSION_DURATION_FIELD_NAME, + ALERT_SUPPRESSION_DURATION_TYPE_FIELD_NAME, + ALERT_SUPPRESSION_DURATION_UNIT_FIELD_NAME, + ALERT_SUPPRESSION_DURATION_VALUE_FIELD_NAME, + ALERT_SUPPRESSION_FIELDS_FIELD_NAME, + ALERT_SUPPRESSION_MISSING_FIELDS_FIELD_NAME, +} from '../../../rule_creation/components/alert_suppression_edit'; +import { THRESHOLD_ALERT_SUPPRESSION_ENABLED } from '../../../rule_creation/components/threshold_alert_suppression_edit'; jest.mock('../../../../common/lib/kibana'); @@ -575,25 +584,25 @@ describe('description_step', () => { describe('alert suppression', () => { const suppressionFields = { - groupByDuration: { - unit: 'm', - value: 50, + [ALERT_SUPPRESSION_DURATION_FIELD_NAME]: { + [ALERT_SUPPRESSION_DURATION_VALUE_FIELD_NAME]: 50, + [ALERT_SUPPRESSION_DURATION_UNIT_FIELD_NAME]: 'm', }, - groupByRadioSelection: 'per-time-period', - enableThresholdSuppression: true, - groupByFields: ['agent.name'], - suppressionMissingFields: 'suppress', + [ALERT_SUPPRESSION_DURATION_TYPE_FIELD_NAME]: 'per-time-period', + [THRESHOLD_ALERT_SUPPRESSION_ENABLED]: true, + [ALERT_SUPPRESSION_FIELDS_FIELD_NAME]: ['agent.name'], + [ALERT_SUPPRESSION_MISSING_FIELDS_FIELD_NAME]: 'suppress', }; - describe('groupByDuration', () => { + describe(ALERT_SUPPRESSION_DURATION_FIELD_NAME, () => { ['query', 'saved_query'].forEach((ruleType) => { - test(`should be empty if groupByFields empty for ${ruleType} rule`, () => { + test(`should be empty if ${ALERT_SUPPRESSION_FIELDS_FIELD_NAME} empty for ${ruleType} rule`, () => { const result: ListItems[] = getDescriptionItem( - 'groupByDuration', + ALERT_SUPPRESSION_DURATION_FIELD_NAME, 'label', { ruleType: 'query', ...suppressionFields, - groupByFields: [], + [ALERT_SUPPRESSION_FIELDS_FIELD_NAME]: [], }, mockFilterManager, mockLicenseService @@ -604,7 +613,7 @@ describe('description_step', () => { test(`should return item for ${ruleType} rule`, () => { const result: ListItems[] = getDescriptionItem( - 'groupByDuration', + ALERT_SUPPRESSION_DURATION_FIELD_NAME, 'label', { ruleType: 'query', @@ -620,7 +629,7 @@ describe('description_step', () => { test('should return item for threshold rule', () => { const result: ListItems[] = getDescriptionItem( - 'groupByDuration', + ALERT_SUPPRESSION_DURATION_FIELD_NAME, 'label', { ruleType: 'threshold', @@ -633,14 +642,14 @@ describe('description_step', () => { expect(result[0].description).toBe('50m'); }); - test('should return item for threshold rule if groupByFields empty', () => { + test(`should return item for threshold rule if ${ALERT_SUPPRESSION_FIELDS_FIELD_NAME} empty`, () => { const result: ListItems[] = getDescriptionItem( - 'groupByDuration', + ALERT_SUPPRESSION_DURATION_FIELD_NAME, 'label', { ruleType: 'threshold', ...suppressionFields, - groupByFields: [], + [ALERT_SUPPRESSION_FIELDS_FIELD_NAME]: [], }, mockFilterManager, mockLicenseService @@ -651,12 +660,12 @@ describe('description_step', () => { test('should be empty for threshold rule if suppression not enabled', () => { const result: ListItems[] = getDescriptionItem( - 'groupByDuration', + ALERT_SUPPRESSION_DURATION_FIELD_NAME, 'label', { ruleType: 'threshold', ...suppressionFields, - enableThresholdSuppression: false, + [THRESHOLD_ALERT_SUPPRESSION_ENABLED]: false, }, mockFilterManager, mockLicenseService @@ -666,10 +675,10 @@ describe('description_step', () => { }); }); - describe('groupByFields', () => { + describe(ALERT_SUPPRESSION_FIELDS_FIELD_NAME, () => { test(`should be empty if rule type is 'threshold'`, () => { const result: ListItems[] = getDescriptionItem( - 'groupByFields', + ALERT_SUPPRESSION_FIELDS_FIELD_NAME, 'label', { ruleType: 'threshold', @@ -685,7 +694,7 @@ describe('description_step', () => { ['query', 'saved_query'].forEach((ruleType) => { test(`should return item for ${ruleType} rule`, () => { const result: ListItems[] = getDescriptionItem( - 'groupByFields', + ALERT_SUPPRESSION_FIELDS_FIELD_NAME, 'label', { ruleType, @@ -699,10 +708,10 @@ describe('description_step', () => { }); }); - describe('suppressionMissingFields', () => { + describe(ALERT_SUPPRESSION_MISSING_FIELDS_FIELD_NAME, () => { test(`should be empty if rule type is 'threshold'`, () => { const result: ListItems[] = getDescriptionItem( - 'suppressionMissingFields', + ALERT_SUPPRESSION_MISSING_FIELDS_FIELD_NAME, 'label', { ruleType: 'threshold', @@ -718,7 +727,7 @@ describe('description_step', () => { ['query', 'saved_query'].forEach((ruleType) => { test(`should return item for ${ruleType} rule`, () => { const result: ListItems[] = getDescriptionItem( - 'suppressionMissingFields', + ALERT_SUPPRESSION_MISSING_FIELDS_FIELD_NAME, 'label', { ruleType, @@ -730,14 +739,14 @@ describe('description_step', () => { expect(result[0].description).toContain('Suppress'); }); - test(`should be empty if groupByFields empty for ${ruleType} rule`, () => { + test(`should be empty if ${ALERT_SUPPRESSION_FIELDS_FIELD_NAME} empty for ${ruleType} rule`, () => { const result: ListItems[] = getDescriptionItem( - 'suppressionMissingFields', + ALERT_SUPPRESSION_MISSING_FIELDS_FIELD_NAME, 'label', { ruleType: 'query', ...suppressionFields, - groupByFields: [], + [ALERT_SUPPRESSION_FIELDS_FIELD_NAME]: [], }, mockFilterManager, mockLicenseService diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/description_step/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/description_step/index.tsx index 4676f065f4af..657f592fe47c 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/description_step/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/description_step/index.tsx @@ -65,6 +65,13 @@ import { isSuppressionRuleConfiguredWithGroupBy, isSuppressionRuleConfiguredWithDuration, } from '../../../../../common/detection_engine/utils'; +import { + ALERT_SUPPRESSION_DURATION_FIELD_NAME, + ALERT_SUPPRESSION_DURATION_TYPE_FIELD_NAME, + ALERT_SUPPRESSION_FIELDS_FIELD_NAME, + ALERT_SUPPRESSION_MISSING_FIELDS_FIELD_NAME, +} from '../../../rule_creation/components/alert_suppression_edit'; +import { THRESHOLD_ALERT_SUPPRESSION_ENABLED } from '../../../rule_creation/components/threshold_alert_suppression_edit'; const DescriptionListContainer = styled(EuiDescriptionList)` max-width: 600px; @@ -217,7 +224,7 @@ export const getDescriptionItem = ( }); } else if (field === 'responseActions') { return []; - } else if (field === 'groupByFields') { + } else if (field === ALERT_SUPPRESSION_FIELDS_FIELD_NAME) { const ruleType: Type = get('ruleType', data); const ruleCanHaveGroupByFields = isSuppressionRuleConfiguredWithGroupBy(ruleType); @@ -226,9 +233,9 @@ export const getDescriptionItem = ( } const values: string[] = get(field, data); return buildAlertSuppressionDescription(label, values, ruleType); - } else if (field === 'groupByRadioSelection') { + } else if (field === ALERT_SUPPRESSION_DURATION_TYPE_FIELD_NAME) { return []; - } else if (field === 'groupByDuration') { + } else if (field === ALERT_SUPPRESSION_DURATION_FIELD_NAME) { const ruleType: Type = get('ruleType', data); const ruleCanHaveDuration = isSuppressionRuleConfiguredWithDuration(ruleType); @@ -239,21 +246,21 @@ export const getDescriptionItem = ( // threshold rule has suppression duration without grouping fields, but suppression should be explicitly enabled by user // query rule have suppression duration only if group by fields selected const showDuration = isThresholdRule(ruleType) - ? get('enableThresholdSuppression', data) === true - : get('groupByFields', data).length > 0; + ? get(THRESHOLD_ALERT_SUPPRESSION_ENABLED, data) === true + : get(ALERT_SUPPRESSION_FIELDS_FIELD_NAME, data).length > 0; if (showDuration) { const value: Duration = get(field, data); return buildAlertSuppressionWindowDescription( label, value, - get('groupByRadioSelection', data), + get(ALERT_SUPPRESSION_DURATION_TYPE_FIELD_NAME, data), ruleType ); } else { return []; } - } else if (field === 'suppressionMissingFields') { + } else if (field === ALERT_SUPPRESSION_MISSING_FIELDS_FIELD_NAME) { const ruleType: Type = get('ruleType', data); const ruleCanHaveSuppressionMissingFields = isSuppressionRuleConfiguredWithMissingFields(ruleType); @@ -261,7 +268,7 @@ export const getDescriptionItem = ( if (!ruleCanHaveSuppressionMissingFields) { return []; } - if (get('groupByFields', data).length > 0) { + if (get(ALERT_SUPPRESSION_FIELDS_FIELD_NAME, data).length > 0) { const value = get(field, data); return buildAlertSuppressionMissingFieldsDescription(label, value, ruleType); } else { diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/description_step/translations.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/description_step/translations.ts index 27dfec9818eb..5c43b9181adc 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/description_step/translations.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/description_step/translations.ts @@ -182,8 +182,8 @@ export const BUILDING_BLOCK_DESCRIPTION = i18n.translate( } ); -export const GROUP_BY_LABEL = i18n.translate( - 'xpack.securitySolution.detectionEngine.ruleDescription.groupByFieldsLabel', +export const ALERT_SUPPRESSION_LABEL = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleDescription.alertSuppressionFieldsLabel', { defaultMessage: 'Suppress alerts by', } diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/multi_select_fields/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/multi_select_fields/index.tsx index d38af219fe85..8a27d2f66809 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/multi_select_fields/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/multi_select_fields/index.tsx @@ -6,11 +6,9 @@ */ import React, { useMemo } from 'react'; - -import { EuiToolTip } from '@elastic/eui'; import type { DataViewFieldBase } from '@kbn/es-query'; +import { ComboBoxField } from '@kbn/es-ui-shared-plugin/static/forms/components'; import type { FieldHook } from '../../../../shared_imports'; -import { Field } from '../../../../shared_imports'; import { FIELD_PLACEHOLDER } from './translations'; interface MultiSelectAutocompleteProps { @@ -18,7 +16,6 @@ interface MultiSelectAutocompleteProps { isDisabled: boolean; field: FieldHook; fullWidth?: boolean; - disabledText?: string; dataTestSubj?: string; } @@ -28,7 +25,6 @@ const fieldDescribedByIds = 'detectionEngineMultiSelectAutocompleteField'; export const MultiSelectAutocompleteComponent: React.FC = ({ browserFields, - disabledText, isDisabled, field, fullWidth = false, @@ -46,21 +42,15 @@ export const MultiSelectAutocompleteComponent: React.FC ); - return isDisabled ? ( - - {fieldComponent} - - ) : ( - fieldComponent - ); }; export const MultiSelectFieldsAutocomplete = React.memo(MultiSelectAutocompleteComponent); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_about_rule/index.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_about_rule/index.test.tsx index bdbc01ada58f..cc303731b26e 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_about_rule/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_about_rule/index.test.tsx @@ -23,7 +23,7 @@ import type { } from '../../../../detections/pages/detection_engine/rules/types'; import { DataSourceType, - GroupByOptions, + AlertSuppressionDurationType, } from '../../../../detections/pages/detection_engine/rules/types'; import { fillEmptySeverityMappings } from '../../../../detections/pages/detection_engine/rules/helpers'; import { TestProviders } from '../../../../common/mock'; @@ -36,6 +36,16 @@ import { import type { FormHook } from '../../../../shared_imports'; import { useKibana as mockUseKibana } from '../../../../common/lib/kibana/__mocks__'; import { useKibana } from '../../../../common/lib/kibana'; +import { + ALERT_SUPPRESSION_DURATION_FIELD_NAME, + ALERT_SUPPRESSION_DURATION_TYPE_FIELD_NAME, + ALERT_SUPPRESSION_DURATION_UNIT_FIELD_NAME, + ALERT_SUPPRESSION_DURATION_VALUE_FIELD_NAME, + ALERT_SUPPRESSION_FIELDS_FIELD_NAME, + ALERT_SUPPRESSION_MISSING_FIELDS_FIELD_NAME, +} from '../../../rule_creation/components/alert_suppression_edit'; +import { THRESHOLD_ALERT_SUPPRESSION_ENABLED } from '../../../rule_creation/components/threshold_alert_suppression_edit'; +import { AlertSuppressionMissingFieldsStrategyEnum } from '../../../../../common/api/detection_engine'; jest.mock('../../../../common/lib/kibana'); jest.mock('../../../../common/containers/source'); @@ -69,16 +79,17 @@ export const stepDefineStepMLRule: DefineStepRule = { timeline: { id: null, title: null }, eqlOptions: {}, dataSourceType: DataSourceType.IndexPatterns, - groupByFields: ['host.name'], - groupByRadioSelection: GroupByOptions.PerRuleExecution, - groupByDuration: { - unit: 'm', - value: 5, + [ALERT_SUPPRESSION_FIELDS_FIELD_NAME]: ['host.name'], + [ALERT_SUPPRESSION_DURATION_TYPE_FIELD_NAME]: AlertSuppressionDurationType.PerRuleExecution, + [ALERT_SUPPRESSION_DURATION_FIELD_NAME]: { + [ALERT_SUPPRESSION_DURATION_VALUE_FIELD_NAME]: 5, + [ALERT_SUPPRESSION_DURATION_UNIT_FIELD_NAME]: 'm', }, + [ALERT_SUPPRESSION_MISSING_FIELDS_FIELD_NAME]: AlertSuppressionMissingFieldsStrategyEnum.suppress, + [THRESHOLD_ALERT_SUPPRESSION_ENABLED]: false, newTermsFields: ['host.ip'], historyWindowSize: '7d', shouldLoadQueryDynamically: false, - enableThresholdSuppression: false, }; describe('StepAboutRuleComponent', () => { diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.test.tsx index f1dcfc74e792..50264fffabfb 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.test.tsx @@ -9,7 +9,8 @@ import React, { useEffect, useState } from 'react'; import { screen, fireEvent, render, within, act, waitFor } from '@testing-library/react'; import type { Type as RuleType } from '@kbn/securitysolution-io-ts-alerting-types'; import type { DataViewBase } from '@kbn/es-query'; -import { StepDefineRule, aggregatableFields } from '.'; +import type { FieldSpec } from '@kbn/data-plugin/common'; +import { StepDefineRule } from '.'; import type { StepDefineRuleProps } from '.'; import { mockBrowserFields } from '../../../../common/containers/source/mock'; import { useRuleFromTimeline } from '../../../../detections/containers/detection_engine/rules/use_rule_from_timeline'; @@ -25,6 +26,22 @@ import { createIndexPatternField, getSelectToggleButtonForName, } from '../../../rule_creation/components/required_fields/required_fields.test'; +import { ALERT_SUPPRESSION_FIELDS_FIELD_NAME } from '../../../rule_creation/components/alert_suppression_edit'; +import { + expectDuration, + expectSuppressionFields, + setDuration, + setDurationType, + setSuppressionFields, +} from '../../../rule_creation/components/alert_suppression_edit/test_helpers'; +import { + selectEuiComboBoxOption, + selectFirstEuiComboBoxOption, +} from '../../../../common/test/eui/combobox'; +import { + addRelatedIntegrationRow, + setVersion, +} from '../../../rule_creation/components/related_integrations/test_helpers'; // Mocks integrations jest.mock('../../../fleet_integrations/api'); @@ -48,7 +65,13 @@ jest.mock('../ai_assistant', () => { }; }); -jest.mock('../data_view_selector_field/use_data_views'); +jest.mock('../data_view_selector_field/use_data_view_list_items'); + +jest.mock('../../../../common/hooks/use_license', () => ({ + useLicense: jest.fn().mockReturnValue({ + isAtLeast: jest.fn().mockReturnValue(true), + }), +})); const mockRedirectLegacyUrl = jest.fn(); const mockGetLegacyUrlConflict = jest.fn(); @@ -149,53 +172,6 @@ jest.mock('react-redux', () => { jest.mock('../../../../detections/containers/detection_engine/rules/use_rule_from_timeline'); -test('aggregatableFields', function () { - expect( - aggregatableFields([ - { - name: 'error.message', - type: 'string', - esTypes: ['text'], - searchable: true, - aggregatable: false, - readFromDocValues: false, - }, - ]) - ).toEqual([]); -}); - -test('aggregatableFields with aggregatable: true', function () { - expect( - aggregatableFields([ - { - name: 'error.message', - type: 'string', - esTypes: ['text'], - searchable: true, - aggregatable: false, - readFromDocValues: false, - }, - { - name: 'file.path', - type: 'string', - esTypes: ['keyword'], - searchable: true, - aggregatable: true, - readFromDocValues: false, - }, - ]) - ).toEqual([ - { - name: 'file.path', - type: 'string', - esTypes: ['keyword'], - searchable: true, - aggregatable: true, - readFromDocValues: false, - }, - ]); -}); - const mockUseRuleFromTimeline = useRuleFromTimeline as jest.Mock; const onOpenTimeline = jest.fn(); @@ -218,6 +194,62 @@ describe.skip('StepDefineRule', () => { expect(screen.getByTestId('stepDefineRule')).toBeDefined(); }); + describe('alert suppression', () => { + it('persists state when switching between custom query and threshold rule types', async () => { + const mockFields: FieldSpec[] = [ + { + name: 'test-field', + type: 'string', + searchable: false, + aggregatable: true, + }, + ]; + + const { rerender } = render( + , + { + wrapper: TestProviders, + } + ); + + await setSuppressionFields(['test-field']); + setDurationType('Per time period'); + setDuration(10, 'h'); + + // switch to threshold rule type + rerender( + + ); + + expectDuration(10, 'h'); + + // switch back to custom query rule type + rerender( + + ); + + expectSuppressionFields(['test-field']); + expectDuration(10, 'h'); + }); + }); + describe('related integrations', () => { beforeEach(() => { fleetIntegrationsApi.fetchAllIntegrations.mockResolvedValue({ @@ -631,13 +663,12 @@ function TestForm({ ruleType={ruleType} index={stepDefineDefaultValue.index} threatIndex={stepDefineDefaultValue.threatIndex} - groupByFields={stepDefineDefaultValue.groupByFields} + alertSuppressionFields={stepDefineDefaultValue[ALERT_SUPPRESSION_FIELDS_FIELD_NAME]} dataSourceType={stepDefineDefaultValue.dataSourceType} shouldLoadQueryDynamically={stepDefineDefaultValue.shouldLoadQueryDynamically} queryBarTitle="" queryBarSavedId="" thresholdFields={[]} - enableThresholdSuppression={false} {...formProps} />