diff --git a/x-pack/platform/plugins/private/translations/translations/fr-FR.json b/x-pack/platform/plugins/private/translations/translations/fr-FR.json index 7ba459bfd2e63..a8bcc64096dcd 100644 --- a/x-pack/platform/plugins/private/translations/translations/fr-FR.json +++ b/x-pack/platform/plugins/private/translations/translations/fr-FR.json @@ -43357,7 +43357,6 @@ "xpack.securitySolution.siemMigrations.rules.panel.expand": "Développer la migration de règles", "xpack.securitySolution.siemMigrations.rules.panel.help.readDocs": "Lire les documents d'IA", "xpack.securitySolution.siemMigrations.rules.panel.help.readMore": "Pour en savoir plus sur nos traductions assistées par IA et les autres fonctionnalités. {readMore}", - "xpack.securitySolution.siemMigrations.rules.panel.migrationTitle": "Migration de règles SIEM #{number}", "xpack.securitySolution.siemMigrations.rules.panel.progress.badge": "Traduction en cours", "xpack.securitySolution.siemMigrations.rules.panel.progress.description": "Traitement de la migration de {totalRules} règles.", "xpack.securitySolution.siemMigrations.rules.panel.progress.preparing": "Préparation de l'environnement pour la traduction alimentée par l'IA.", @@ -43375,7 +43374,6 @@ "xpack.securitySolution.siemMigrations.rules.panel.uploadMissingResources": "Chargez les macros et les consultations manquantes.", "xpack.securitySolution.siemMigrations.rules.retryFailedRulesFailDescription": "Échec du re-traitement des règles de migration", "xpack.securitySolution.siemMigrations.rules.selectionOption.arealLabel": "Sélectionnez une migration", - "xpack.securitySolution.siemMigrations.rules.selectionOption.title": "Migration de règle SIEM {optionIndex}", "xpack.securitySolution.siemMigrations.rules.service.createRuleError": "Échec du chargement du fichier de règles", "xpack.securitySolution.siemMigrations.rules.service.createRuleSuccess.description": "{rules} règles téléchargées", "xpack.securitySolution.siemMigrations.rules.service.createRuleSuccess.title": "Migration de règle créée avec succès", @@ -43489,7 +43487,6 @@ "xpack.securitySolution.siemMigrations.rulesService.noConnector.text": "Aucun connecteur par IA configuré. Sélectionnez un connecteur par IA pour lancer les traductions de règles.", "xpack.securitySolution.siemMigrations.rulesService.noConnector.title": "Aucun connecteur configuré.", "xpack.securitySolution.siemMigrations.rulesService.polling.successLinkText": "Rendez-vous aux règles traduites", - "xpack.securitySolution.siemMigrations.rulesService.polling.successText": "La traduction de la migration de règles SIEM #{number} est terminée. Les résultats ont été ajoutés à une page dédiée.", "xpack.securitySolution.siemMigrations.rulesService.polling.successTitle": "Traduction de règles terminée.", "xpack.securitySolution.siemMigrations.rulesService.pollingError": "Erreur lors de la récupération des migrations de règles", "xpack.securitySolution.siemMigrations.service.capabilities.connectorsRead": "Gestion > Actions & connecteurs : Lire", diff --git a/x-pack/platform/plugins/private/translations/translations/ja-JP.json b/x-pack/platform/plugins/private/translations/translations/ja-JP.json index 951fcf25a3379..0d067ef9f91b5 100644 --- a/x-pack/platform/plugins/private/translations/translations/ja-JP.json +++ b/x-pack/platform/plugins/private/translations/translations/ja-JP.json @@ -43328,7 +43328,6 @@ "xpack.securitySolution.siemMigrations.rules.panel.expand": "ルール移行を展開", "xpack.securitySolution.siemMigrations.rules.panel.help.readDocs": "AIドキュメントを読む", "xpack.securitySolution.siemMigrations.rules.panel.help.readMore": "AIを活用した翻訳やその他の機能の詳細についてお読みください。{readMore}", - "xpack.securitySolution.siemMigrations.rules.panel.migrationTitle": "SIEMルール移行#{number}", "xpack.securitySolution.siemMigrations.rules.panel.progress.badge": "変換中", "xpack.securitySolution.siemMigrations.rules.panel.progress.description": "{totalRules}ルールの移行を処理しています。", "xpack.securitySolution.siemMigrations.rules.panel.progress.preparing": "AIを活用した変換の環境を準備しています。", @@ -43346,7 +43345,6 @@ "xpack.securitySolution.siemMigrations.rules.panel.uploadMissingResources": "欠落しているマクロとルックアップをアップロード", "xpack.securitySolution.siemMigrations.rules.retryFailedRulesFailDescription": "移行ルールを再処理できませんでした", "xpack.securitySolution.siemMigrations.rules.selectionOption.arealLabel": "移行を選択", - "xpack.securitySolution.siemMigrations.rules.selectionOption.title": "SIEMルール移行{optionIndex}", "xpack.securitySolution.siemMigrations.rules.service.createRuleError": "ルールファイルをアップロードできませんでした", "xpack.securitySolution.siemMigrations.rules.service.createRuleSuccess.description": "{rules}ルールがアップロードされました", "xpack.securitySolution.siemMigrations.rules.service.createRuleSuccess.title": "ルール移行が正常に作成されました", @@ -43460,7 +43458,6 @@ "xpack.securitySolution.siemMigrations.rulesService.noConnector.text": "AIコネクターが構成されていませんルール変換を開始するには、AIコネクターを選択してください。", "xpack.securitySolution.siemMigrations.rulesService.noConnector.title": "コネクターが構成されていません。", "xpack.securitySolution.siemMigrations.rulesService.polling.successLinkText": "変換されたルールに移動", - "xpack.securitySolution.siemMigrations.rulesService.polling.successText": "SIEMルール移行#{number}の変換が完了しました。結果が専用ページに追加されました。", "xpack.securitySolution.siemMigrations.rulesService.polling.successTitle": "ルール変換が完了しました。", "xpack.securitySolution.siemMigrations.rulesService.pollingError": "ルール移行の取得エラー", "xpack.securitySolution.siemMigrations.service.capabilities.connectorsRead": "管理 > アクションとコネクター:読み取り", diff --git a/x-pack/platform/plugins/private/translations/translations/zh-CN.json b/x-pack/platform/plugins/private/translations/translations/zh-CN.json index c8c01be3d3870..29436b3cd3d38 100644 --- a/x-pack/platform/plugins/private/translations/translations/zh-CN.json +++ b/x-pack/platform/plugins/private/translations/translations/zh-CN.json @@ -43399,7 +43399,6 @@ "xpack.securitySolution.siemMigrations.rules.panel.expand": "展开规则迁移", "xpack.securitySolution.siemMigrations.rules.panel.help.readDocs": "阅读 AI 文档", "xpack.securitySolution.siemMigrations.rules.panel.help.readMore": "阅读有关我们的 AI 驱动式转换和其他功能的更多信息。{readMore}", - "xpack.securitySolution.siemMigrations.rules.panel.migrationTitle": "SIEM 规则迁移 #{number}", "xpack.securitySolution.siemMigrations.rules.panel.progress.badge": "正在进行转换", "xpack.securitySolution.siemMigrations.rules.panel.progress.description": "正在处理 {totalRules} 个规则的迁移。", "xpack.securitySolution.siemMigrations.rules.panel.progress.preparing": "正在准备环境以进行 AI 驱动式转换。", @@ -43417,7 +43416,6 @@ "xpack.securitySolution.siemMigrations.rules.panel.uploadMissingResources": "上传缺失的宏和查找。", "xpack.securitySolution.siemMigrations.rules.retryFailedRulesFailDescription": "无法重新处理迁移规则", "xpack.securitySolution.siemMigrations.rules.selectionOption.arealLabel": "选择迁移", - "xpack.securitySolution.siemMigrations.rules.selectionOption.title": "SIEM 规则迁移 {optionIndex}", "xpack.securitySolution.siemMigrations.rules.service.createRuleError": "无法上传规则文件", "xpack.securitySolution.siemMigrations.rules.service.createRuleSuccess.description": "{rules} 个规则已上传", "xpack.securitySolution.siemMigrations.rules.service.createRuleSuccess.title": "已成功创建规则迁移", @@ -43531,7 +43529,6 @@ "xpack.securitySolution.siemMigrations.rulesService.noConnector.text": "未配置 AI 连接器。选择 AI 连接器以开始规则转换。", "xpack.securitySolution.siemMigrations.rulesService.noConnector.title": "未配置连接器。", "xpack.securitySolution.siemMigrations.rulesService.polling.successLinkText": "前往已转换规则", - "xpack.securitySolution.siemMigrations.rulesService.polling.successText": "SIEM 规则迁移 #{number} 已完成转换。结果已添加到专用页面。", "xpack.securitySolution.siemMigrations.rulesService.polling.successTitle": "规则转换完成。", "xpack.securitySolution.siemMigrations.rulesService.pollingError": "获取规则迁移时出错", "xpack.securitySolution.siemMigrations.service.capabilities.connectorsRead": "管理 > 操作和连接器:读取", diff --git a/x-pack/solutions/security/plugins/security_solution/common/api/quickstart_client.gen.ts b/x-pack/solutions/security/plugins/security_solution/common/api/quickstart_client.gen.ts index 0d5058fd852ba..cfc5ea12a5547 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/api/quickstart_client.gen.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/api/quickstart_client.gen.ts @@ -380,6 +380,7 @@ import type { ResolveTimelineResponse, } from './timeline/resolve_timeline/resolve_timeline_route.gen'; import type { + CreateRuleMigrationRequestBodyInput, CreateRuleMigrationResponse, CreateRuleMigrationRulesRequestParamsInput, CreateRuleMigrationRulesRequestBodyInput, @@ -413,7 +414,7 @@ import type { StopRuleMigrationRequestParamsInput, StopRuleMigrationResponse, UpdateRuleMigrationRequestParamsInput, - UpdateRuleMigrationResponse, + UpdateRuleMigrationRequestBodyInput, UpdateRuleMigrationRulesRequestParamsInput, UpdateRuleMigrationRulesRequestBodyInput, UpdateRuleMigrationRulesResponse, @@ -815,7 +816,7 @@ For detailed information on Kibana actions and alerting, and additional API call /** * Creates a new rule migration and returns the corresponding migration_id */ - async createRuleMigration() { + async createRuleMigration(props: CreateRuleMigrationProps) { this.log.info(`${new Date().toISOString()} Calling API CreateRuleMigration`); return this.kbnClient .request({ @@ -824,6 +825,7 @@ For detailed information on Kibana actions and alerting, and additional API call [ELASTIC_HTTP_VERSION_HEADER]: '1', }, method: 'PUT', + body: props.body, }) .catch(catchAxiosErrorFormatAndThrow); } @@ -2560,12 +2562,13 @@ The difference between the `id` and `rule_id` is that the `id` is a unique rule async updateRuleMigration(props: UpdateRuleMigrationProps) { this.log.info(`${new Date().toISOString()} Calling API UpdateRuleMigration`); return this.kbnClient - .request({ + .request({ path: replaceParams('/internal/siem_migrations/rules/{migration_id}', props.params), headers: { [ELASTIC_HTTP_VERSION_HEADER]: '1', }, method: 'PATCH', + body: props.body, }) .catch(catchAxiosErrorFormatAndThrow); } @@ -2671,6 +2674,9 @@ export interface CreateAssetCriticalityRecordProps { export interface CreateRuleProps { body: CreateRuleRequestBodyInput; } +export interface CreateRuleMigrationProps { + body: CreateRuleMigrationRequestBodyInput; +} export interface CreateRuleMigrationRulesProps { params: CreateRuleMigrationRulesRequestParamsInput; body: CreateRuleMigrationRulesRequestBodyInput; @@ -2941,6 +2947,7 @@ export interface UpdateRuleProps { } export interface UpdateRuleMigrationProps { params: UpdateRuleMigrationRequestParamsInput; + body: UpdateRuleMigrationRequestBodyInput; } export interface UpdateRuleMigrationRulesProps { params: UpdateRuleMigrationRulesRequestParamsInput; diff --git a/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.gen.ts b/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.gen.ts index 3e2555d891476..64e8a1c70fdfc 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.gen.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.gen.ts @@ -37,6 +37,15 @@ import { RelatedIntegration } from '../../../../api/detection_engine/model/rule_ import { NonEmptyString } from '../../../../api/model/primitives.gen'; import { LangSmithOptions } from '../../common.gen'; +export type CreateRuleMigrationRequestBody = z.infer; +export const CreateRuleMigrationRequestBody = z.object({ + /** + * The rule migration name + */ + name: NonEmptyString, +}); +export type CreateRuleMigrationRequestBodyInput = z.input; + export type CreateRuleMigrationResponse = z.infer; export const CreateRuleMigrationResponse = z.object({ /** @@ -313,8 +322,14 @@ export type UpdateRuleMigrationRequestParamsInput = z.input< typeof UpdateRuleMigrationRequestParams >; -export type UpdateRuleMigrationResponse = z.infer; -export const UpdateRuleMigrationResponse = RuleMigration; +export type UpdateRuleMigrationRequestBody = z.infer; +export const UpdateRuleMigrationRequestBody = z.object({ + /** + * The rule migration name + */ + name: NonEmptyString, +}); +export type UpdateRuleMigrationRequestBodyInput = z.input; export type UpdateRuleMigrationRulesRequestParams = z.infer< typeof UpdateRuleMigrationRulesRequestParams diff --git a/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.schema.yaml b/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.schema.yaml index e573e56f8eaf8..12511b940f957 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.schema.yaml +++ b/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.schema.yaml @@ -64,15 +64,27 @@ paths: /internal/siem_migrations/rules: put: summary: Creates a new rule migration - operationId: "CreateRuleMigration" + operationId: 'CreateRuleMigration' x-codegen-enabled: true x-internal: true description: Creates a new rule migration and returns the corresponding migration_id tags: - SIEM Rule Migrations + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - name + properties: + name: + description: The rule migration name + $ref: '../../../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' responses: 200: - description: The migration was created successfully and migrationId is returned + description: The migration was created successfully and migrationId, name is returned content: application/json: schema: @@ -80,9 +92,9 @@ paths: required: - migration_id properties: - migration_id: - description: The migration id created. - $ref: '../../../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' + migration_id: + description: The migration id created. + $ref: '../../../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' ## Specific rule migration APIs /internal/siem_migrations/rules/{migration_id}: @@ -101,13 +113,21 @@ paths: schema: description: The migration id to start $ref: '../../../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - name + properties: + name: + description: The rule migration name + $ref: '../../../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' responses: 200: description: Indicates rules migrations have been updated correctly. - content: - application/json: - schema: - $ref: '../../rule_migration.schema.yaml#/components/schemas/RuleMigration' 404: description: Indicates the migration id was not found. get: diff --git a/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/rule_migration.gen.ts b/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/rule_migration.gen.ts index f68893dcdff94..67be43f100e0e 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/rule_migration.gen.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/rule_migration.gen.ts @@ -201,6 +201,10 @@ export const RuleMigration = z * The rule migration id */ id: NonEmptyString, + /** + * The rule migration name + */ + name: NonEmptyString, }) .merge(RuleMigrationData); @@ -328,6 +332,10 @@ export const RuleMigrationTaskStats = z.object({ * The migration id */ id: NonEmptyString, + /** + * The migration name + */ + name: NonEmptyString, /** * Indicates if the migration task status. */ diff --git a/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/rule_migration.schema.yaml b/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/rule_migration.schema.yaml index 42538a0e5d1c6..92d1d322cc61d 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/rule_migration.schema.yaml +++ b/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/rule_migration.schema.yaml @@ -124,10 +124,14 @@ components: - type: object required: - id + - name properties: id: description: The rule migration id $ref: '../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' + name: + description: The rule migration name + $ref: '../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' - $ref: '#/components/schemas/RuleMigrationData' RuleMigrationData: @@ -207,6 +211,7 @@ components: description: The rule migration task stats object. required: - id + - name - status - rules - created_at @@ -215,6 +220,9 @@ components: id: description: The migration id $ref: '../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' + name: + description: The migration name + $ref: '../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' status: description: Indicates if the migration task status. $ref: '#/components/schemas/RuleMigrationTaskStatus' diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/hooks/use_is_open_state/index.ts b/x-pack/solutions/security/plugins/security_solution/public/common/hooks/use_is_open_state/index.ts new file mode 100644 index 0000000000000..a19281aeb3b1d --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/common/hooks/use_is_open_state/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './use_is_open_state'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/hooks/use_is_open_state/use_is_open_state.test.ts b/x-pack/solutions/security/plugins/security_solution/public/common/hooks/use_is_open_state/use_is_open_state.test.ts new file mode 100644 index 0000000000000..48fed9fad13ed --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/common/hooks/use_is_open_state/use_is_open_state.test.ts @@ -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 { renderHook, act } from '@testing-library/react'; +import { useIsOpenState } from './use_is_open_state'; + +describe('useIsOpenState', () => { + let initialState: boolean; + let onOpen: jest.Mock; + let onClose: jest.Mock; + let onToggle: jest.Mock; + + beforeEach(() => { + initialState = false; + onOpen = jest.fn(); + onClose = jest.fn(); + onToggle = jest.fn(); + }); + + it('should initialize with the correct state', () => { + const { result } = renderHook(() => useIsOpenState(initialState)); + expect(result.current.isOpen).toBe(initialState); + }); + + it('should call onOpen when opening', () => { + const { result } = renderHook(() => useIsOpenState(initialState, { onOpen })); + act(() => { + result.current.open(); + }); + expect(result.current.isOpen).toBe(true); + expect(onOpen).toHaveBeenCalled(); + }); + + it('should call onClose when closing', () => { + const { result } = renderHook(() => useIsOpenState(true, { onClose })); + act(() => { + result.current.close(); + }); + expect(result.current.isOpen).toBe(false); + expect(onClose).toHaveBeenCalled(); + }); + + it('should call onToggle when toggling', () => { + const { result } = renderHook(() => useIsOpenState(true, { onToggle })); + act(() => { + result.current.toggle(); + }); + expect(result.current.isOpen).toBe(false); + expect(onToggle).toHaveBeenCalledTimes(1); + + act(() => { + result.current.toggle(); + }); + expect(result.current.isOpen).toBe(true); + expect(onToggle).toHaveBeenCalledTimes(2); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/hooks/use_is_open_state/use_is_open_state.ts b/x-pack/solutions/security/plugins/security_solution/public/common/hooks/use_is_open_state/use_is_open_state.ts new file mode 100644 index 0000000000000..1d45ade2c49b5 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/common/hooks/use_is_open_state/use_is_open_state.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { useState, useCallback } from 'react'; + +export type UseIsOpen = ( + defaultValue: boolean, + callbacks?: { onOpen?: () => void; onClose?: () => void; onToggle?: () => void } +) => { isOpen: boolean; open: () => void; close: () => void; toggle: () => void }; + +export const useIsOpenState: UseIsOpen = (defaultValue, { onOpen, onClose, onToggle } = {}) => { + const [isOpen, setIsOpen] = useState(defaultValue); + + const open = useCallback(() => { + setIsOpen(true); + onOpen?.(); + }, [onOpen]); + + const close = useCallback(() => { + setIsOpen(false); + onClose?.(); + }, [onClose]); + + const toggle = useCallback(() => { + setIsOpen((prev) => !prev); + onToggle?.(); + }, [onToggle]); + + return { isOpen, open, close, toggle }; +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/hooks/use_visibility/index.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/hooks/use_visibility/index.test.tsx deleted file mode 100644 index b53e26738f04b..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution/public/common/hooks/use_visibility/index.test.tsx +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { renderHook, act } from '@testing-library/react'; -import { useVisibility } from '.'; - -describe('useVisibility', () => { - let initialState: boolean; - let onOpen: jest.Mock; - let onClose: jest.Mock; - - beforeEach(() => { - initialState = false; - onOpen = jest.fn(); - onClose = jest.fn(); - }); - - it('should initialize with the correct state', () => { - const { result } = renderHook(() => useVisibility(initialState, { onOpen, onClose })); - expect(result.current[0]).toBe(initialState); - }); - - it('should call onOpen when opening visibility', () => { - const { result } = renderHook(() => useVisibility(initialState, { onOpen, onClose })); - act(() => { - result.current[1](); // Call open - }); - expect(result.current[0]).toBe(true); - expect(onOpen).toHaveBeenCalled(); - }); - - it('should call onClose when closing visibility', () => { - const { result } = renderHook(() => useVisibility(true, { onOpen, onClose })); - act(() => { - result.current[2](); // Call close - }); - expect(result.current[0]).toBe(false); - expect(onClose).toHaveBeenCalled(); - }); -}); diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/hooks/use_visibility/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/hooks/use_visibility/index.tsx deleted file mode 100644 index 61adf025e3bc3..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution/public/common/hooks/use_visibility/index.tsx +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { useState, useCallback } from 'react'; - -type UseVisibility = ( - initialState: boolean, - callbacks?: { onOpen?: () => void; onClose?: () => void } -) => [isVisible: boolean, open: () => void, close: () => void]; - -export const useVisibility: UseVisibility = (initialState, { onOpen, onClose } = {}) => { - const [isVisible, setIsVisible] = useState(initialState); - - const open = useCallback(() => { - setIsVisible(true); - onOpen?.(); - }, [onOpen]); - - const close = useCallback(() => { - setIsVisible(false); - onClose?.(); - }, [onClose]); - - return [isVisible, open, close]; -}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/common/mocks/migration_result.data.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/common/mocks/migration_result.data.ts index f903b914d1ead..ac2ad35d71b24 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/common/mocks/migration_result.data.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/common/mocks/migration_result.data.ts @@ -63,7 +63,6 @@ const getMockMigrationResultRule = ({ export const mockedMigrationLatestStatsData: RuleMigrationStats[] = [ { id: '1', - number: 1, status: SiemMigrationTaskStatus.FINISHED, rules: { total: 1, @@ -74,10 +73,10 @@ export const mockedMigrationLatestStatsData: RuleMigrationStats[] = [ }, last_updated_at: '2025-03-06T15:01:37.321Z', created_at: '2025-03-06T15:01:37.321Z', + name: 'test', }, { id: '2', - number: 2, status: SiemMigrationTaskStatus.FINISHED, rules: { total: 2, @@ -86,7 +85,7 @@ export const mockedMigrationLatestStatsData: RuleMigrationStats[] = [ completed: 2, failed: 0, }, - + name: 'test', created_at: '2025-03-06T15:01:37.321Z', last_updated_at: '2025-03-06T15:01:37.321Z', }, diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/api/index.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/api/index.ts index 49c0f2fec30a4..a88054a40a052 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/api/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/api/index.ts @@ -27,28 +27,29 @@ import { SIEM_RULE_MIGRATION_MISSING_PRIVILEGES_PATH, SIEM_RULE_MIGRATION_RULES_PATH, SIEM_RULE_MIGRATIONS_INTEGRATIONS_STATS_PATH, + SIEM_RULE_MIGRATION_PATH, SIEM_RULE_MIGRATION_STOP_PATH, } from '../../../../common/siem_migrations/constants'; import type { CreateRuleMigrationResponse, - GetAllStatsRuleMigrationResponse, GetRuleMigrationTranslationStatsResponse, InstallMigrationRulesResponse, StartRuleMigrationRequestBody, - GetRuleMigrationStatsResponse, GetRuleMigrationResourcesMissingResponse, UpsertRuleMigrationResourcesRequestBody, UpsertRuleMigrationResourcesResponse, GetRuleMigrationPrebuiltRulesResponse, - UpdateRuleMigrationResponse, StartRuleMigrationResponse, GetRuleMigrationIntegrationsResponse, GetRuleMigrationPrivilegesResponse, GetRuleMigrationRulesResponse, CreateRuleMigrationRulesRequestBody, GetRuleMigrationIntegrationsStatsResponse, + UpdateRuleMigrationRulesResponse, + UpdateRuleMigrationRequestBody, StopRuleMigrationResponse, } from '../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; +import type { RuleMigrationStats } from '../types'; export interface GetRuleMigrationStatsParams { /** `id` of the migration to get stats for */ @@ -60,8 +61,9 @@ export interface GetRuleMigrationStatsParams { export const getRuleMigrationStats = async ({ migrationId, signal, -}: GetRuleMigrationStatsParams): Promise => { - return KibanaServices.get().http.get( +}: GetRuleMigrationStatsParams): Promise => { + // Typed with `RuleMigrationStats` instead of `GetRuleMigrationStatsResponse` to use native enums instead of the zod enum + return KibanaServices.get().http.get( replaceParams(SIEM_RULE_MIGRATION_STATS_PATH, { migration_id: migrationId }), { version: '1', signal } ); @@ -74,24 +76,29 @@ export interface GetRuleMigrationsStatsAllParams { /** Retrieves the stats for all the existing migrations, aggregated by `migration_id`. */ export const getRuleMigrationsStatsAll = async ({ signal, -}: GetRuleMigrationsStatsAllParams = {}): Promise => { - return KibanaServices.get().http.get( - SIEM_RULE_MIGRATIONS_ALL_STATS_PATH, - { version: '1', signal } - ); +}: GetRuleMigrationsStatsAllParams = {}): Promise => { + // Typed with `RuleMigrationStats` instead of `GetAllStatsRuleMigrationResponse` to use native enums instead of the zod enum + return KibanaServices.get().http.get(SIEM_RULE_MIGRATIONS_ALL_STATS_PATH, { + version: '1', + signal, + }); }; export interface CreateRuleMigrationParams { /** Optional AbortSignal for cancelling request */ signal?: AbortSignal; + /** The name of the migration */ + name: string; } /** Starts a new migration with the provided rules. */ export const createRuleMigration = async ({ signal, + name, }: CreateRuleMigrationParams): Promise => { return KibanaServices.get().http.put(SIEM_RULE_MIGRATIONS_PATH, { version: '1', signal, + body: JSON.stringify({ name }), }); }; @@ -371,9 +378,28 @@ export const updateMigrationRules = async ({ migrationId, rulesToUpdate, signal, -}: UpdateRulesParams): Promise => { - return KibanaServices.get().http.patch( +}: UpdateRulesParams): Promise => { + return KibanaServices.get().http.patch( replaceParams(SIEM_RULE_MIGRATION_RULES_PATH, { migration_id: migrationId }), { version: '1', body: JSON.stringify(rulesToUpdate), signal } ); }; + +export interface UpdateMigrationParams { + /** `id` of the migration to update the name for */ + migrationId: string; + /** The migration fields to update */ + body: UpdateRuleMigrationRequestBody; + /** Optional AbortSignal for cancelling request */ + signal?: AbortSignal; +} +export const updateMigration = async ({ + migrationId, + signal, + body, +}: UpdateMigrationParams): Promise => { + return KibanaServices.get().http.patch( + replaceParams(SIEM_RULE_MIGRATION_PATH, { migration_id: migrationId }), + { version: '1', body: JSON.stringify(body), signal } + ); +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/rules_data_input.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/rules_data_input.tsx index 3c9d4a270b195..b8bcbf8e75fe3 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/rules_data_input.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/rules_data_input.tsx @@ -18,6 +18,7 @@ import { SubSteps } from '../common/sub_step'; import { useCopyExportQueryStep } from './sub_steps/copy_export_query'; import { useRulesFileUploadStep } from './sub_steps/rules_file_upload'; import { useCheckResourcesStep } from './sub_steps/check_resources'; +import { useMigrationNameStep } from './sub_steps/migration_name'; interface RulesDataInputSubStepsProps { migrationStats?: RuleMigrationTaskStats; @@ -70,32 +71,58 @@ export const RulesDataInput = React.memo( RulesDataInput.displayName = 'RulesDataInput'; const END = 10 as const; -type SubStep = 1 | 2 | 3 | typeof END; +type SubStep = 1 | 2 | 3 | 4 | typeof END; export const RulesDataInputSubSteps = React.memo( ({ migrationStats, onMigrationCreated, onMissingResourcesFetched }) => { const { telemetry } = useKibana().services.siemMigrations.rules; - const [subStep, setSubStep] = useState(migrationStats ? 3 : 1); + const [subStep, setSubStep] = useState(migrationStats ? 4 : 1); + + const [migrationName, setMigrationName] = useState(migrationStats?.name); + const [isRulesFileReady, setIsRuleFileReady] = useState(false); + + // Migration name step + const setName = useCallback( + (name: string) => { + setMigrationName(name); + if (name) { + setSubStep(isRulesFileReady ? 3 : 2); + } else { + setSubStep(1); + } + }, + [isRulesFileReady] + ); + const nameStep = useMigrationNameStep({ + status: getStatus(1, subStep), + setMigrationName: setName, + migrationName, + }); // Copy query step const onCopied = useCallback(() => { - setSubStep(2); + setSubStep((currentSubStep) => (currentSubStep !== 1 ? 3 : currentSubStep)); // Move to the next step only if step 1 was completed telemetry.reportSetupRulesQueryCopied({ migrationId: migrationStats?.id }); }, [telemetry, migrationStats?.id]); - - const copyStep = useCopyExportQueryStep({ status: getStatus(1, subStep), onCopied }); + const copyStep = useCopyExportQueryStep({ status: getStatus(2, subStep), onCopied }); // Upload rules step const onMigrationCreatedStep = useCallback( (stats) => { onMigrationCreated(stats); - setSubStep(3); + setSubStep(4); }, [onMigrationCreated] ); + const onRulesFileChanged = useCallback((files: FileList | null) => { + setIsRuleFileReady(!!files?.length); + setSubStep(3); + }, []); const uploadStep = useRulesFileUploadStep({ - status: getStatus(2, subStep), + status: getStatus(3, subStep), migrationStats, + onRulesFileChanged, onMigrationCreated: onMigrationCreatedStep, + migrationName, }); // Check missing resources step @@ -107,14 +134,14 @@ export const RulesDataInputSubSteps = React.memo( [onMissingResourcesFetched] ); const resourcesStep = useCheckResourcesStep({ - status: getStatus(3, subStep), + status: getStatus(4, subStep), migrationStats, onMissingResourcesFetched: onMissingResourcesFetchedStep, }); const steps = useMemo( - () => [copyStep, uploadStep, resourcesStep], - [copyStep, uploadStep, resourcesStep] + () => [nameStep, copyStep, uploadStep, resourcesStep], + [nameStep, copyStep, uploadStep, resourcesStep] ); return ; diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/migration_name/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/migration_name/index.tsx new file mode 100644 index 0000000000000..07afea42e6add --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/migration_name/index.tsx @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo } from 'react'; +import moment from 'moment'; +import type { EuiStepProps, EuiStepStatus } from '@elastic/eui'; +import { MigrationNameInput } from './migration_name_input'; +import * as i18n from './translations'; +import { useGetCurrentUserProfile } from '../../../../../../../../common/components/user_profiles/use_get_current_user_profile'; + +export interface MigrationNameStepProps { + status: EuiStepStatus; + setMigrationName: (migrationName: string) => void; + migrationName?: string; +} +export const useMigrationNameStep = ({ + status, + setMigrationName, + migrationName: storedMigrationName, +}: MigrationNameStepProps): EuiStepProps => { + const { data: currentUserProfile } = useGetCurrentUserProfile(); + + const migrationName = useMemo(() => { + if (storedMigrationName) { + return storedMigrationName; + } + if (currentUserProfile?.user.username) { + const datetime = moment(Date.now()).format('llll'); // localized date and time (e.g., "Wed, 01 Jan 2025 12:00 PM") + return `${currentUserProfile.user.username}'s migration on ${datetime}`; + } + return undefined; // profile loading + }, [storedMigrationName, currentUserProfile?.user.username]); + + return { + title: i18n.MIGRATION_NAME_INPUT_TITLE, + status, + children: migrationName ? ( + + ) : null, + }; +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/migration_name/migration_name_input.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/migration_name/migration_name_input.test.tsx new file mode 100644 index 0000000000000..dc86266ae8ac9 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/migration_name/migration_name_input.test.tsx @@ -0,0 +1,76 @@ +/* + * 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 { render, screen, fireEvent } from '@testing-library/react'; +import type { MigrationNameInputProps } from './migration_name_input'; +import { MigrationNameInput } from './migration_name_input'; +import * as i18n from './translations'; + +const mockSetMigrationName = jest.fn(); + +const defaultProps: MigrationNameInputProps = { + migrationName: 'Default Name', + setMigrationName: mockSetMigrationName, +}; + +describe('MigrationNameInput', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders with default name', () => { + render(); + expect(screen.getByDisplayValue('Default Name')).toBeInTheDocument(); + }); + + it('updates name on input change', () => { + render(); + const input = screen.getByDisplayValue('Default Name'); + + fireEvent.change(input, { target: { value: 'New Name' } }); + expect(input).toHaveValue('New Name'); + }); + + it('saves name on blur', () => { + render(); + const input = screen.getByDisplayValue('Default Name'); + + fireEvent.change(input, { target: { value: 'New Name' } }); + fireEvent.blur(input); + + expect(mockSetMigrationName).toHaveBeenCalledWith('New Name'); + }); + + it('saves name on enter key', () => { + render(); + const input = screen.getByDisplayValue('Default Name'); + + fireEvent.change(input, { target: { value: 'New Name' } }); + fireEvent.keyDown(input, { key: 'Enter' }); + + expect(mockSetMigrationName).toHaveBeenCalledWith('New Name'); + }); + + it('shows error when empty name is submitted', () => { + render(); + const input = screen.getByDisplayValue('Default Name'); + + fireEvent.change(input, { target: { value: '' } }); + fireEvent.blur(input); + + expect(screen.getByText(i18n.MIGRATION_NAME_INPUT_ERROR)).toBeInTheDocument(); + expect(mockSetMigrationName).toHaveBeenCalled(); + }); + + it('focuses input on mount', () => { + const { container } = render(); + const input = container.querySelector('input'); + + expect(document.activeElement).toBe(input); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/migration_name/migration_name_input.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/migration_name/migration_name_input.tsx new file mode 100644 index 0000000000000..f7f7902038f90 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/migration_name/migration_name_input.tsx @@ -0,0 +1,75 @@ +/* + * 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, { useState, useCallback, useMemo } from 'react'; +import { EuiFieldText, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiForm } from '@elastic/eui'; +import * as i18n from './translations'; + +export interface MigrationNameInputProps { + migrationName: string; + setMigrationName: (migrationName: string) => void; +} + +export const MigrationNameInput = React.memo( + ({ migrationName, setMigrationName }) => { + const [name, setName] = useState(migrationName); + + const handleNameChange = useCallback((e: React.ChangeEvent) => { + setName(e.target.value); + }, []); + + const handleNameSave = useCallback(() => { + setMigrationName(name); + }, [name, setMigrationName]); + + const isInvalid = name.length === 0; + const errors = useMemo(() => { + if (isInvalid) { + return [i18n.MIGRATION_NAME_INPUT_ERROR]; + } + return []; + }, [isInvalid]); + + const onEnter = useCallback( + (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + e.preventDefault(); + (e.target as HTMLInputElement).blur(); + handleNameSave(); + } + }, + [handleNameSave] + ); + + const onBlur = useCallback(() => { + handleNameSave(); + }, [handleNameSave]); + + return ( + + + + + + + + + + ); + } +); + +MigrationNameInput.displayName = 'MigrationNameInput'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/migration_name/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/migration_name/translations.ts new file mode 100644 index 0000000000000..28b608e4b7488 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/migration_name/translations.ts @@ -0,0 +1,23 @@ +/* + * 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 MIGRATION_NAME_INPUT_TITLE = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.migrationName.title', + { defaultMessage: 'Migration name' } +); + +export const MIGRATION_NAME_INPUT_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.migrationName.description', + { defaultMessage: 'Name your migration' } +); + +export const MIGRATION_NAME_INPUT_ERROR = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.migrationName.error', + { defaultMessage: 'Migration name is required' } +); diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/index.tsx index 97dfd903e499e..05936035fcbb3 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/index.tsx @@ -18,13 +18,17 @@ import * as i18n from './translations'; export interface RulesFileUploadStepProps { status: EuiStepStatus; - migrationStats?: RuleMigrationTaskStats; + migrationStats: RuleMigrationTaskStats | undefined; + migrationName: string | undefined; onMigrationCreated: OnMigrationCreated; + onRulesFileChanged: (files: FileList | null) => void; } export const useRulesFileUploadStep = ({ status, migrationStats, + migrationName, onMigrationCreated, + onRulesFileChanged, }: RulesFileUploadStepProps): EuiStepProps => { const [isCreated, setIsCreated] = useState(!!migrationStats); const onSuccess = useCallback( @@ -52,9 +56,11 @@ export const useRulesFileUploadStep = ({ children: ( ), }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/rules_file_upload.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/rules_file_upload.test.tsx index ca8a331b5f8cc..309c441c56016 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/rules_file_upload.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/rules_file_upload.test.tsx @@ -20,13 +20,17 @@ import { splunkTestRules } from './splunk_rules.test.data'; import type { OriginalRule } from '../../../../../../../../../common/siem_migrations/model/rule_migration.gen'; const mockCreateMigration: CreateMigration = jest.fn(); +const mockOnRulesFileChanged = jest.fn(); const mockApiError = 'Some Mock API Error'; +const migrationName = 'test migration name'; const defaultProps: RulesFileUploadProps = { createMigration: mockCreateMigration, + onRulesFileChanged: mockOnRulesFileChanged, apiError: undefined, isLoading: false, isCreated: false, + migrationName, }; const renderTestComponent = (props: Partial = {}) => { @@ -84,6 +88,8 @@ describe('RulesFileUpload', () => { }); }); + expect(mockOnRulesFileChanged).toHaveBeenCalledWith([testFile]); + await waitFor(() => { expect(filePicker).toHaveAttribute('data-loading', 'true'); }); @@ -110,7 +116,7 @@ describe('RulesFileUpload', () => { severity: rule['alert.severity'] as OriginalRule['severity'], })); - expect(mockCreateMigration).toHaveBeenNthCalledWith(1, rulesToExpect); + expect(mockCreateMigration).toHaveBeenNthCalledWith(1, migrationName, rulesToExpect); }); describe('Error Handling', () => { diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/rules_file_upload.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/rules_file_upload.tsx index 296accc221bbe..c02076c929156 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/rules_file_upload.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/rules_file_upload.tsx @@ -12,9 +12,9 @@ import type { EuiFilePickerClass, EuiFilePickerProps, } from '@elastic/eui/src/components/form/file_picker/file_picker'; +import type { CreateMigration } from '../../../../../../service/hooks/use_create_migration'; import type { CreateRuleMigrationRulesRequestBody } from '../../../../../../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; import type { OriginalRule } from '../../../../../../../../../common/siem_migrations/model/rule_migration.gen'; -import type { CreateMigration } from '../../../../../../service/hooks/use_create_migration'; import { FILE_UPLOAD_ERROR } from '../../../../translations'; import { useParseFileInput, type SplunkRow } from '../../../common/use_parse_file_input'; import type { SPLUNK_RULES_COLUMNS } from '../../../../constants'; @@ -25,19 +25,23 @@ type SplunkRulesResult = Partial void; + migrationName: string | undefined; + apiError: string | undefined; } export const RulesFileUpload = React.memo( - ({ createMigration, apiError, isLoading, isCreated }) => { + ({ createMigration, migrationName, apiError, isLoading, isCreated, onRulesFileChanged }) => { const [rulesToUpload, setRulesToUpload] = useState([]); const filePickerRef = useRef(null); const createRules = useCallback(() => { - filePickerRef.current?.removeFiles(); - createMigration(rulesToUpload); - }, [createMigration, rulesToUpload]); + if (migrationName) { + filePickerRef.current?.removeFiles(); + createMigration(migrationName, rulesToUpload); + } + }, [createMigration, migrationName, rulesToUpload]); const onFileParsed = useCallback((content: Array>) => { const rules = content.map(formatRuleRow); @@ -49,9 +53,10 @@ export const RulesFileUpload = React.memo( const onFileChange = useCallback( (files: FileList | null) => { setRulesToUpload([]); + onRulesFileChanged(files); parseFile(files); }, - [parseFile] + [parseFile, onRulesFileChanged] ); const error = useMemo(() => { @@ -62,7 +67,7 @@ export const RulesFileUpload = React.memo( }, [apiError, fileError]); const showLoader = isParsing || isLoading; - const isDisabled = showLoader || isCreated; + const isDisabled = !migrationName || showLoader || isCreated; const isButtonDisabled = isDisabled || rulesToUpload.length === 0; return ( diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/header_buttons/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/header_buttons/index.tsx index d0bda562aabfa..13ebadfde8620 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/header_buttons/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/header_buttons/index.tsx @@ -14,48 +14,32 @@ import type { RuleMigrationStats } from '../../types'; export const SIEM_MIGRATIONS_SELECT_MIGRATION_BUTTON_ID = 'siemMigrationsSelectMigrationButton'; +const migrationStatsToComboBoxOption = ( + stats: RuleMigrationStats +): EuiComboBoxOptionOption => ({ + value: stats.id, + label: stats.name, + 'data-test-subj': `migrationSelectionOption-${stats.id}`, +}); + export interface HeaderButtonsProps { - /** - * Available rule migrations stats - */ + /** Available rule migrations stats */ ruleMigrationsStats: RuleMigrationStats[]; - - /** - * Selected rule migration id - */ + /** Selected rule migration id */ selectedMigrationId: string | undefined; - - /** - * Handles migration selection changes - * @param selectedId Selected migration id - * @returns - */ + /** Handles migration selection changes */ onMigrationIdChange: (selectedId?: string) => void; } - export const HeaderButtons: React.FC = React.memo( ({ ruleMigrationsStats, selectedMigrationId, onMigrationIdChange }) => { - const migrationOptions = useMemo(() => { - const options: Array> = ruleMigrationsStats.map( - ({ id, number }) => ({ - value: id, - 'data-test-subj': `migrationSelectionOption-${number}`, - label: i18n.SIEM_MIGRATIONS_OPTION_LABEL(number), - }) - ); - return options; - }, [ruleMigrationsStats]); + const migrationOptions = useMemo>>( + () => ruleMigrationsStats.map(migrationStatsToComboBoxOption), + [ruleMigrationsStats] + ); + const selectedMigrationOption = useMemo>>(() => { const stats = ruleMigrationsStats.find(({ id }) => id === selectedMigrationId); - return stats - ? [ - { - value: selectedMigrationId, - 'data-test-subj': `migrationSelectionOption-${stats.number}`, - label: i18n.SIEM_MIGRATIONS_OPTION_LABEL(stats.number), - }, - ] - : []; + return stats ? [migrationStatsToComboBoxOption(stats)] : []; }, [ruleMigrationsStats, selectedMigrationId]); const onChange = (selected: Array>) => { @@ -67,7 +51,7 @@ export const HeaderButtons: React.FC = React.memo( } return ( - +
{i18n.SIEM_MIGRATIONS_OPTION_TITLE}
@@ -81,6 +65,7 @@ export const HeaderButtons: React.FC = React.memo( selectedOptions={selectedMigrationOption} singleSelection={{ asPlainText: true }} isClearable={false} + css={{ width: '500rem' }} />
diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/header_buttons/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/header_buttons/translations.ts index 4ef281168224a..49116b2294100 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/header_buttons/translations.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/header_buttons/translations.ts @@ -20,11 +20,3 @@ export const SIEM_MIGRATIONS_OPTION_AREAL_LABEL = i18n.translate( defaultMessage: 'Select a migration', } ); - -export const SIEM_MIGRATIONS_OPTION_LABEL = (optionIndex: number) => - i18n.translate('xpack.securitySolution.siemMigrations.rules.selectionOption.title', { - defaultMessage: 'SIEM rule migration {optionIndex}', - values: { - optionIndex, - }, - }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/migration_panel_title.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/migration_panel_title.tsx new file mode 100644 index 0000000000000..3a8fed417a345 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/migration_panel_title.tsx @@ -0,0 +1,123 @@ +/* + * 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, { useState, useCallback, useMemo } from 'react'; +import { + EuiButtonIcon, + EuiContextMenuItem, + EuiContextMenuPanel, + EuiFlexGroup, + EuiFlexItem, + EuiInlineEditText, + EuiPopover, +} from '@elastic/eui'; +import { useIsOpenState } from '../../../../common/hooks/use_is_open_state'; +import { PanelText } from '../../../../common/components/panel_text'; +import { useUpdateMigration } from '../../logic/use_update_migration'; +import type { RuleMigrationStats } from '../../types'; +import * as i18n from './translations'; + +interface MigrationPanelTitleProps { + migrationStats: RuleMigrationStats; +} +export const MigrationPanelTitle = React.memo(({ migrationStats }) => { + const [name, setName] = useState(migrationStats.name); + const [isEditing, setIsEditing] = useState(false); + const { + isOpen: isPopoverOpen, + close: closePopover, + toggle: togglePopover, + } = useIsOpenState(false); + + const onRenameError = useCallback(() => { + setName(migrationStats.name); // revert to original name on error. Error toast will be shown by the useUpdateMigration hook + }, [migrationStats.name]); + + const { mutate: updateMigration, isLoading: isUpdatingMigrationName } = useUpdateMigration( + migrationStats.id, + { onError: onRenameError } + ); + + const cancelEdit = useCallback(() => { + setIsEditing(false); + }, []); + + const saveName = useCallback( + (value: string) => { + setName(value); + updateMigration({ name: value }); + setIsEditing(false); + }, + [updateMigration] + ); + + const items = useMemo( + () => [ + { + closePopover(); + setIsEditing(true); + }} + icon="pencil" + data-test-subj="renameMigrationItem" + > + {i18n.RENAME_MIGRATION_BUTTON} + , + ], + [closePopover, setIsEditing] + ); + + const stopPropagation = useCallback((e: React.MouseEvent) => { + e.stopPropagation(); // prevent click events from bubbling up and toggle the collapsible panel + }, []); + + return ( + + {isEditing ? ( + + + + ) : ( + <> + + +

{name}

+
+
+ + + } + isOpen={isPopoverOpen} + closePopover={closePopover} + panelPaddingSize="none" + anchorPosition="downCenter" + > + + + + + )} +
+ ); +}); +MigrationPanelTitle.displayName = 'MigrationPanelTitle'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/migration_progress_panel.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/migration_progress_panel.test.tsx index 3db7b08cb6240..dcbbf36dd4b9b 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/migration_progress_panel.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/migration_progress_panel.test.tsx @@ -22,10 +22,10 @@ const mockStopMigration = jest.fn(); const inProgressMigrationStats: RuleMigrationStats = { status: SiemMigrationTaskStatus.RUNNING, id: 'c44d2c7d-0de1-4231-8b82-0dcfd67a9fe3', + name: 'test migration', rules: { total: 26, pending: 6, processing: 10, completed: 9, failed: 1 }, created_at: '2025-05-27T12:12:17.563Z', last_updated_at: '2025-05-27T12:12:17.563Z', - number: 1, }; const preparingMigrationStats: RuleMigrationStats = { ...inProgressMigrationStats, diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/migration_progress_panel.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/migration_progress_panel.tsx index d1311247002fd..46df0cdf6cc3e 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/migration_progress_panel.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/migration_progress_panel.tsx @@ -21,10 +21,11 @@ import { } from '@elastic/eui'; import { AssistantIcon } from '@kbn/ai-assistant-icon'; import { PanelText } from '../../../../common/components/panel_text'; +import { useStopMigration } from '../../service/hooks/use_stop_migration'; import type { RuleMigrationStats } from '../../types'; import * as i18n from './translations'; import { RuleMigrationsReadMore } from './read_more'; -import { useStopMigration } from '../../service/hooks/use_stop_migration'; +import { MigrationPanelTitle } from './migration_panel_title'; export interface MigrationProgressPanelProps { migrationStats: RuleMigrationStats; @@ -49,9 +50,7 @@ export const MigrationProgressPanel = React.memo( - -

{i18n.RULE_MIGRATION_TITLE(migrationStats.number)}

-
+
diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/migration_ready_panel.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/migration_ready_panel.test.tsx index ac18360e2be77..e52538a1b831e 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/migration_ready_panel.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/migration_ready_panel.test.tsx @@ -12,6 +12,8 @@ import { useGetMissingResources } from '../../service/hooks/use_get_missing_reso import { useStartMigration } from '../../service/hooks/use_start_migration'; import { SiemMigrationTaskStatus } from '../../../../../common/siem_migrations/constants'; import type { RuleMigrationResourceBase } from '../../../../../common/siem_migrations/model/rule_migration.gen'; +import { TestProviders } from '../../../../common/mock'; +import type { RuleMigrationStats } from '../../types'; jest.mock('../../../../common/lib/kibana/use_kibana'); @@ -32,28 +34,28 @@ const mockMigrationStateWithError = { 'Failed to populate ELSER indices. Make sure the ELSER model is deployed and running at Machine Learning > Trained Models. Error: Exception when running inference id [.elser-2-elasticsearch] on field [elser_embedding]', }, id: 'c44d2c7d-0de1-4231-8b82-0dcfd67a9fe3', + name: 'Migration 1', rules: { total: 6, pending: 6, processing: 0, completed: 0, failed: 0 }, created_at: '2025-05-27T12:12:17.563Z', last_updated_at: '2025-05-27T12:12:17.563Z', - number: 1, }; const mockMigrationStatsStopped = { status: SiemMigrationTaskStatus.STOPPED, id: 'c44d2c7d-0de1-4231-8b82-0dcfd67a9fe3', + name: 'Migration 1', rules: { total: 6, pending: 6, processing: 0, completed: 0, failed: 0 }, created_at: '2025-05-27T12:12:17.563Z', last_updated_at: '2025-05-27T12:12:17.563Z', - number: 1, }; -const mockMigrationStatsReady = { +const mockMigrationStatsReady: RuleMigrationStats = { status: SiemMigrationTaskStatus.READY, id: 'c44d2c7d-0de1-4231-8b82-0dcfd67a9fe3', + name: 'Migration 1', rules: { total: 6, pending: 6, processing: 0, completed: 0, failed: 0 }, created_at: '2025-05-27T12:12:17.563Z', last_updated_at: '2025-05-27T12:12:17.563Z', - number: 1, }; const missingMacro: RuleMigrationResourceBase = { @@ -68,6 +70,12 @@ const missingLookup: RuleMigrationResourceBase = { jest.mock('../../service/hooks/use_get_missing_resources'); const useGetMissingResourcesMock = useGetMissingResources as jest.Mock; +const renderReadyPanel = (migrationStats: RuleMigrationStats) => { + return render(, { + wrapper: TestProviders, + }); +}; + describe('MigrationReadyPanel', () => { beforeEach(() => { useGetMissingResourcesMock.mockReturnValue({ @@ -84,14 +92,14 @@ describe('MigrationReadyPanel', () => { describe('Ready Migration', () => { it('should render description text correctly', () => { - render(); + renderReadyPanel(mockMigrationStatsReady); expect(screen.getByTestId('ruleMigrationDescription')).toHaveTextContent( `Migration of 6 rules is created and ready to start.` ); }); it('should render start migration button', () => { - render(); + renderReadyPanel(mockMigrationStatsReady); expect(screen.getByTestId('startMigrationButton')).toBeVisible(); expect(screen.getByTestId('startMigrationButton')).toHaveTextContent('Start'); }); @@ -101,7 +109,9 @@ describe('MigrationReadyPanel', () => { startMigration: mockStartMigration, isLoading: true, }); - render(); + render(, { + wrapper: TestProviders, + }); expect(screen.getByTestId('startMigrationButton')).toBeVisible(); expect(screen.getByTestId('startMigrationButton')).toHaveTextContent('Starting'); }); @@ -109,7 +119,7 @@ describe('MigrationReadyPanel', () => { describe('Migration with Error', () => { it('should render error message when migration has an error', () => { - render(); + renderReadyPanel(mockMigrationStateWithError); expect(screen.getByTestId('ruleMigrationDescription')).toHaveTextContent( 'Migration of 6 rules failed. Please correct the below error and try again.' ); @@ -119,33 +129,23 @@ describe('MigrationReadyPanel', () => { }); it('should render start migration button when there is an error', () => { - render(); + renderReadyPanel(mockMigrationStateWithError); expect(screen.queryByTestId('startMigrationButton')).toHaveTextContent('Start'); }); }); describe('Stopped Migration', () => { it('should render aborted migration message', () => { - render(); + renderReadyPanel(mockMigrationStatsStopped); expect(screen.getByTestId('ruleMigrationDescription')).toHaveTextContent( 'Migration of 6 rules was stopped, you can resume it any time.' ); }); it('should render correct start migration button for aborted migration', () => { - render(); + renderReadyPanel(mockMigrationStatsStopped); expect(screen.getByTestId('startMigrationButton')).toHaveTextContent('Resume'); }); - - it('should render resuming migration button while loading', () => { - useStartMigrationMock.mockReturnValue({ - startMigration: mockStartMigration, - isLoading: true, - }); - render(); - expect(screen.getByTestId('startMigrationButton')).toBeVisible(); - expect(screen.getByTestId('startMigrationButton')).toHaveTextContent('Resuming'); - }); }); describe('Missing Resources', () => { @@ -163,7 +163,7 @@ describe('MigrationReadyPanel', () => { }); it('should render missing resources warning when there are missing resources', async () => { - render(); + renderReadyPanel(mockMigrationStatsReady); await waitFor(() => { expect(screen.getByTestId('ruleMigrationDescription')).toHaveTextContent( 'Migration of 6 rules is created and ready to start. You can also upload the missing macros & lookups for more accurate results.' @@ -172,7 +172,7 @@ describe('MigrationReadyPanel', () => { }); it('should render missing resources button', async () => { - render(); + renderReadyPanel(mockMigrationStatsReady); expect(screen.getByTestId('ruleMigrationMissingResourcesButton')).toBeVisible(); }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/migration_ready_panel.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/migration_ready_panel.tsx index b33892296a9b5..5569f3a098293 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/migration_ready_panel.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/migration_ready_panel.tsx @@ -18,17 +18,19 @@ import { SiemMigrationTaskStatus } from '../../../../../common/siem_migrations/c import { CenteredLoadingSpinner } from '../../../../common/components/centered_loading_spinner'; import { useKibana } from '../../../../common/lib/kibana/use_kibana'; import type { RuleMigrationResourceBase } from '../../../../../common/siem_migrations/model/rule_migration.gen'; -import { PanelText } from '../../../../common/components/panel_text'; import { useStartMigration } from '../../service/hooks/use_start_migration'; import type { RuleMigrationStats } from '../../types'; import { useRuleMigrationDataInputContext } from '../data_input_flyout/context'; import * as i18n from './translations'; import { useGetMissingResources } from '../../service/hooks/use_get_missing_resources'; import { RuleMigrationsLastError } from './last_error'; +import { MigrationPanelTitle } from './migration_panel_title'; +import { PanelText } from '../../../../common/components/panel_text'; export interface MigrationReadyPanelProps { migrationStats: RuleMigrationStats; } + export const MigrationReadyPanel = React.memo(({ migrationStats }) => { const { openFlyout } = useRuleMigrationDataInputContext(); const { telemetry } = useKibana().services.siemMigrations.rules; @@ -69,9 +71,7 @@ export const MigrationReadyPanel = React.memo(({ migra - -

{i18n.RULE_MIGRATION_TITLE(migrationStats.number)}

-
+
diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/migration_result_panel.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/migration_result_panel.tsx index 13ebbf548c769..f20192fae7d31 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/migration_result_panel.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/migration_result_panel.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useMemo } from 'react'; +import React, { useCallback, useMemo } from 'react'; import moment from 'moment'; import { EuiFlexGroup, @@ -43,6 +43,7 @@ import { RuleTranslationResult } from '../../../../../common/siem_migrations/con import * as i18n from './translations'; import { RuleMigrationsUploadMissingPanel } from './upload_missing_panel'; import { RuleMigrationsLastError } from './last_error'; +import { MigrationPanelTitle } from './migration_panel_title'; const headerStyle = css` &:hover { @@ -76,16 +77,18 @@ export const MigrationResultPanel = React.memo( const completeBadgeStyles = useCompleteBadgeStyles(); + const toggleCollapsed = useCallback(() => { + onToggleCollapsed(!isCollapsed); + }, [isCollapsed, onToggleCollapsed]); + return ( - onToggleCollapsed(!isCollapsed)} css={headerStyle}> + - -

{i18n.RULE_MIGRATION_TITLE(migrationStats.number)}

-
+
@@ -105,7 +108,7 @@ export const MigrationResultPanel = React.memo( onToggleCollapsed(!isCollapsed)} + onClick={toggleCollapsed} aria-label={isCollapsed ? i18n.RULE_MIGRATION_EXPAND : i18n.RULE_MIGRATION_COLLAPSE} /> diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/translations.ts index 4c84dff2a89dc..6d6028b6926ef 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/translations.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/translations.ts @@ -58,12 +58,6 @@ export const RULE_MIGRATION_RESUMING_TRANSLATION_BUTTON = i18n.translate( { defaultMessage: 'Resuming' } ); -export const RULE_MIGRATION_TITLE = (number: number) => - i18n.translate('xpack.securitySolution.siemMigrations.rules.panel.migrationTitle', { - defaultMessage: 'SIEM rules migration #{number}', - values: { number }, - }); - export const RULE_MIGRATION_PROGRESS_DESCRIPTION = (totalRules: number) => i18n.translate('xpack.securitySolution.siemMigrations.rules.panel.progress.description', { defaultMessage: `Processing migration of {totalRules} rules.`, @@ -151,3 +145,12 @@ export const RULE_MIGRATION_ERROR_TITLE = i18n.translate( 'xpack.securitySolution.siemMigrations.rules.panel.error', { defaultMessage: 'The last execution of this migration failed with the following message:' } ); + +export const OPEN_MIGRATION_OPTIONS_BUTTON = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.panel.openMigrationOptionsButton', + { defaultMessage: 'Open migration options' } +); +export const RENAME_MIGRATION_BUTTON = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.panel.renameMigrationButton', + { defaultMessage: 'Rename' } +); diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table/index.tsx index 71fe947428900..fdfd55547aa11 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table/index.tsx @@ -19,7 +19,7 @@ import { import React, { useCallback, useMemo, useState } from 'react'; import type { RuleMigrationFilters } from '../../../../../common/siem_migrations/types'; -import { useVisibility } from '../../../../common/hooks/use_visibility'; +import { useIsOpenState } from '../../../../common/hooks/use_is_open_state'; import type { RelatedIntegration, RuleResponse } from '../../../../../common/api/detection_engine'; import { isMigrationPrebuiltRule } from '../../../../../common/siem_migrations/rules/utils'; import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; @@ -238,11 +238,11 @@ export const MigrationRulesTable: React.FC = React.mem [migrationStats.last_execution] ); - const [ - isReprocessFailedRulesModalVisible, - showReprocessFailedRulesModal, - closeReprocessFailedRulesModal, - ] = useVisibility(false); + const { + isOpen: isReprocessFailedRulesModalVisible, + open: showReprocessFailedRulesModal, + close: closeReprocessFailedRulesModal, + } = useIsOpenState(false); const isRulesLoading = isPrebuiltRulesLoading || isDataLoading || isTableLoading || isRetryLoading; diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/logic/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/logic/translations.ts index 777f2a01b3081..9234f56bfa4a6 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/logic/translations.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/logic/translations.ts @@ -54,3 +54,17 @@ export const RETRY_FAILED_RULES_FAILURE = i18n.translate( defaultMessage: 'Failed to reprocess migration rules', } ); + +export const UPDATE_MIGRATION_NAME_SUCCESS = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.updateMigrationNameSuccess', + { + defaultMessage: 'Migration name updated', + } +); + +export const UPDATE_MIGRATION_NAME_FAILURE = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.updateMigrationNameFailDescription', + { + defaultMessage: 'Failed to update migration name', + } +); diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/logic/use_update_migration.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/logic/use_update_migration.ts new file mode 100644 index 0000000000000..1d70a47950c7e --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/logic/use_update_migration.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useMutation } from '@tanstack/react-query'; +import type { UpdateRuleMigrationRequestBody } from '../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; +import { useAppToasts } from '../../../common/hooks/use_app_toasts'; +import { updateMigration } from '../api'; +import * as i18n from './translations'; + +export interface UseUpdateMigrationNameProps { + onError?: (error: Error) => void; +} + +export const useUpdateMigration = ( + migrationId: string, + { onError }: UseUpdateMigrationNameProps = {} +) => { + const { addError, addSuccess } = useAppToasts(); + return useMutation({ + mutationFn: (body: UpdateRuleMigrationRequestBody) => updateMigration({ migrationId, body }), + onError: (error: Error) => { + addError(error, { title: i18n.UPDATE_MIGRATION_NAME_FAILURE }); + onError?.(error); + }, + onSuccess: () => { + addSuccess(i18n.UPDATE_MIGRATION_NAME_SUCCESS); + }, + }); +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/logic/use_update_migration_rule.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/logic/use_update_migration_rule.ts index 36b659a6757f2..00cbc2cc07936 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/logic/use_update_migration_rule.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/logic/use_update_migration_rule.ts @@ -12,7 +12,7 @@ import type { UpdateRuleMigrationRule, } from '../../../../common/siem_migrations/model/rule_migration.gen'; import { SIEM_RULE_MIGRATION_RULES_PATH } from '../../../../common/siem_migrations/constants'; -import type { UpdateRuleMigrationResponse } from '../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; +import type { UpdateRuleMigrationRulesResponse } from '../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; import { useAppToasts } from '../../../common/hooks/use_app_toasts'; import { useKibana } from '../../../common/lib/kibana/kibana_react'; import * as i18n from './translations'; @@ -38,7 +38,7 @@ export const useUpdateMigrationRule = (migrationRule: RuleMigrationRule) => { const invalidateGetRuleMigrations = useInvalidateGetMigrationRules(); const invalidateGetMigrationTranslationStats = useInvalidateGetMigrationTranslationStats(); - return useMutation( + return useMutation( (ruleUpdateData) => updateMigrationRules({ migrationId, rulesToUpdate: [ruleUpdateData] }), { mutationKey: UPDATE_MIGRATION_RULE_MUTATION_KEY, diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/hooks/use_create_migration.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/hooks/use_create_migration.ts index d382ce6411d0e..ba2b7c43b15ac 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/hooks/use_create_migration.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/hooks/use_create_migration.ts @@ -26,7 +26,10 @@ export const RULES_DATA_INPUT_CREATE_MIGRATION_ERROR = i18n.translate( { defaultMessage: 'Failed to upload rules file' } ); -export type CreateMigration = (data: CreateRuleMigrationRulesRequestBody) => void; +export type CreateMigration = ( + migrationName: string, + rules: CreateRuleMigrationRulesRequestBody +) => void; export type OnSuccess = (migrationStats: RuleMigrationTaskStats) => void; export const useCreateMigration = (onSuccess: OnSuccess) => { @@ -34,16 +37,16 @@ export const useCreateMigration = (onSuccess: OnSuccess) => { const [state, dispatch] = useReducer(reducer, initialState); const createMigration = useCallback( - (data) => { + (migrationName, rules) => { (async () => { try { dispatch({ type: 'start' }); - const migrationId = await siemMigrations.rules.createRuleMigration(data); + const migrationId = await siemMigrations.rules.createRuleMigration(rules, migrationName); const stats = await siemMigrations.rules.api.getRuleMigrationStats({ migrationId }); notifications.toasts.addSuccess({ title: RULES_DATA_INPUT_CREATE_MIGRATION_SUCCESS_TITLE, - text: RULES_DATA_INPUT_CREATE_MIGRATION_SUCCESS_DESCRIPTION(data.length), + text: RULES_DATA_INPUT_CREATE_MIGRATION_SUCCESS_DESCRIPTION(rules.length), }); onSuccess(stats); dispatch({ type: 'success' }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/notifications/success_notification.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/notifications/success_notification.tsx index f1a59a045b4fc..53122a05fa30c 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/notifications/success_notification.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/notifications/success_notification.tsx @@ -49,8 +49,8 @@ const SuccessToastContent: React.FC<{ migration: RuleMigrationStats }> = ({ migr diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.test.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.test.ts index d60663008c878..1edbe95274266 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.test.ts @@ -147,18 +147,19 @@ describe('SiemRulesMigrationsService', () => { describe('createRuleMigration', () => { it('should throw an error when body is empty', async () => { - await expect(service.createRuleMigration([])).rejects.toThrow(i18n.EMPTY_RULES_ERROR); + await expect(service.createRuleMigration([], 'test')).rejects.toThrow(i18n.EMPTY_RULES_ERROR); }); it('should create migration with a single batch', async () => { const body = [{ id: 'rule1' }] as CreateRuleMigrationRulesRequestBody; + const name = 'test'; (createRuleMigration as jest.Mock).mockResolvedValue({ migration_id: 'mig-1' }); (addRulesToMigration as jest.Mock).mockResolvedValue(undefined); - const migrationId = await service.createRuleMigration(body); + const migrationId = await service.createRuleMigration(body, name); expect(createRuleMigration).toHaveBeenCalledTimes(1); - expect(createRuleMigration).toHaveBeenCalledWith({}); + expect(createRuleMigration).toHaveBeenCalledWith({ name }); expect(addRulesToMigration).toHaveBeenCalledWith({ migrationId: 'mig-1', body }); expect(migrationId).toBe('mig-1'); }); @@ -166,15 +167,16 @@ describe('SiemRulesMigrationsService', () => { it('should create migration in batches if body length exceeds the batch size', async () => { // Create an array of 51 items (the service batches in chunks of 50) const body = new Array(51).fill({ rule: 'rule' }); + const name = 'test'; (createRuleMigration as jest.Mock).mockResolvedValueOnce({ migration_id: 'mig-1' }); (addRulesToMigration as jest.Mock).mockResolvedValue(undefined); - const migrationId = await service.createRuleMigration(body); + const migrationId = await service.createRuleMigration(body, name); expect(createRuleMigration).toHaveBeenCalledTimes(1); expect(addRulesToMigration).toHaveBeenCalledTimes(2); // First call: first 50 items, migrationId undefined - expect(createRuleMigration).toHaveBeenNthCalledWith(1, {}); + expect(createRuleMigration).toHaveBeenNthCalledWith(1, { name }); expect(addRulesToMigration).toHaveBeenNthCalledWith(1, { migrationId: 'mig-1', @@ -316,16 +318,16 @@ describe('SiemRulesMigrationsService', () => { describe('getRuleMigrationsStats', () => { it('should fetch and update latest stats', async () => { const statsArray = [ - { id: 'mig-1', status: SiemMigrationTaskStatus.RUNNING }, - { id: 'mig-2', status: SiemMigrationTaskStatus.FINISHED }, + { id: 'mig-1', status: SiemMigrationTaskStatus.RUNNING, name: 'test 1' }, + { id: 'mig-2', status: SiemMigrationTaskStatus.FINISHED, name: 'test 2' }, ]; mockGetRuleMigrationsStatsAll.mockResolvedValue(statsArray); const result = await service.getRuleMigrationsStats(); expect(getRuleMigrationsStatsAll).toHaveBeenCalled(); expect(result).toHaveLength(2); - expect(result[0].number).toBe(1); - expect(result[1].number).toBe(2); + expect(result[0].name).toBe('test 1'); + expect(result[1].name).toBe('test 2'); const latestStats = await firstValueFrom(service.getLatestStats$()); expect(latestStats).toEqual(result); diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.ts index 1d2c4b5b48e4b..cd3a7463cf38c 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.ts @@ -14,7 +14,6 @@ import { } from '@kbn/elastic-assistant/impl/assistant_context/constants'; import { isEqual } from 'lodash'; import type { TelemetryServiceStart } from '../../../common/lib/telemetry'; -import type { RuleMigrationTaskStats } from '../../../../common/siem_migrations/model/rule_migration.gen'; import type { CreateRuleMigrationRulesRequestBody, StartRuleMigrationResponse, @@ -133,8 +132,11 @@ export class SiemRulesMigrationsService { } } - /** Creates a rule migration and adds the rules to it, returning the migration ID */ - public async createRuleMigration(data: CreateRuleMigrationRulesRequestBody): Promise { + /** Creates a rule migration with a name and adds the rules to it, returning the migration ID */ + public async createRuleMigration( + data: CreateRuleMigrationRulesRequestBody, + migrationName: string + ): Promise { const rulesCount = data.length; if (rulesCount === 0) { throw new Error(i18n.EMPTY_RULES_ERROR); @@ -142,7 +144,9 @@ export class SiemRulesMigrationsService { try { // create the migration - const { migration_id: migrationId } = await api.createRuleMigration({}); + const { migration_id: migrationId } = await api.createRuleMigration({ + name: migrationName, + }); await this.addRulesToMigration(migrationId, data); @@ -265,12 +269,8 @@ export class SiemRulesMigrationsService { params: api.GetRuleMigrationsStatsAllParams = {} ): Promise { const allStats = await this.getRuleMigrationsStatsWithRetry(params); - const results = allStats.map( - // the array order (by creation) is guaranteed by the API - (stats, index) => ({ ...stats, number: index + 1 } as RuleMigrationStats) // needs cast because of the `status` enum override - ); - this.latestStats$.next(results); // Always update the latest stats - return results; + this.latestStats$.next(allStats); // Keep the latest stats observable in sync + return allStats; } private sleep(seconds: number): Promise { @@ -280,7 +280,7 @@ export class SiemRulesMigrationsService { /** Polls the migration task stats until the finish condition is met or the timeout is reached. */ private async migrationTaskPollingUntil( migrationId: string, - finishCondition: (stats: RuleMigrationTaskStats) => boolean, + finishCondition: (stats: RuleMigrationStats) => boolean, { sleepSecs = 1, timeoutSecs = 60 }: { sleepSecs?: number; timeoutSecs?: number } = {} ): Promise { const timeoutId = setTimeout(() => { @@ -305,7 +305,7 @@ export class SiemRulesMigrationsService { private async getRuleMigrationsStatsWithRetry( params: api.GetRuleMigrationsStatsAllParams = {}, sleepSecs?: number - ): Promise { + ): Promise { if (sleepSecs) { await this.sleep(sleepSecs); } diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/types.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/types.ts index b05018ac18cc3..0731647c28d00 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/types.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/types.ts @@ -9,9 +9,7 @@ import type { SiemMigrationTaskStatus } from '../../../common/siem_migrations/co import type { RuleMigrationTaskStats } from '../../../common/siem_migrations/model/rule_migration.gen'; export interface RuleMigrationStats extends RuleMigrationTaskStats { - status: SiemMigrationTaskStatus; - /** The sequential number of the migration */ - number: number; + status: SiemMigrationTaskStatus; // use the native enum instead of the zod enum from the model } export enum AuthorFilter { diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/create.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/create.ts index 2efb532694bd9..db41fd6f80617 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/create.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/create.ts @@ -6,8 +6,12 @@ */ import type { IKibanaResponse, Logger } from '@kbn/core/server'; +import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; import { SIEM_RULE_MIGRATIONS_PATH } from '../../../../../common/siem_migrations/constants'; -import { type CreateRuleMigrationResponse } from '../../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; +import { + type CreateRuleMigrationResponse, + CreateRuleMigrationRequestBody, +} from '../../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; import type { SecuritySolutionPluginRouter } from '../../../../types'; import { SiemMigrationAuditLogger } from './util/audit'; import { authz } from './util/authz'; @@ -26,8 +30,11 @@ export const registerSiemRuleMigrationsCreateRoute = ( .addVersion( { version: '1', - // no request body or params to validate - validate: false, + validate: { + request: { + body: buildRouteValidationWithZod(CreateRuleMigrationRequestBody), + }, + }, }, withLicense( async (context, req, res): Promise> => { @@ -36,8 +43,7 @@ export const registerSiemRuleMigrationsCreateRoute = ( const ctx = await context.resolve(['securitySolution']); const ruleMigrationsClient = ctx.securitySolution.getSiemRuleMigrationsClient(); await siemMigrationAuditLogger.logCreateMigration(); - - const migrationId = await ruleMigrationsClient.data.migrations.create(); + const migrationId = await ruleMigrationsClient.data.migrations.create(req.body.name); return res.ok({ body: { migration_id: migrationId } }); } catch (error) { diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/index.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/index.ts index 1b66f660719c7..5c0169ecf028b 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/index.ts @@ -28,6 +28,7 @@ import { registerSiemRuleMigrationsCreateRulesRoute } from './rules/create'; import { registerSiemRuleMigrationsGetRulesRoute } from './rules/get'; import { registerSiemRuleMigrationsDeleteRoute } from './delete'; import { registerSiemRuleMigrationsIntegrationsStatsRoute } from './integrations_stats'; +import { registerSiemRuleMigrationsUpdateRoute } from './update'; export const registerSiemRuleMigrationsRoutes = ( router: SecuritySolutionPluginRouter, @@ -38,6 +39,7 @@ export const registerSiemRuleMigrationsRoutes = ( registerSiemRuleMigrationsCreateRoute(router, logger); registerSiemRuleMigrationsGetRoute(router, logger); registerSiemRuleMigrationsDeleteRoute(router, logger); + registerSiemRuleMigrationsUpdateRoute(router, logger); /** *******/ /** Rules */ diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/update.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/update.ts new file mode 100644 index 0000000000000..01cda4160bb0a --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/update.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { IKibanaResponse, Logger } from '@kbn/core/server'; +import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; +import { SIEM_RULE_MIGRATION_PATH } from '../../../../../common/siem_migrations/constants'; +import { + UpdateRuleMigrationRequestBody, + UpdateRuleMigrationRequestParams, +} from '../../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; +import type { SecuritySolutionPluginRouter } from '../../../../types'; +import { SiemMigrationAuditLogger } from './util/audit'; +import { authz } from './util/authz'; +import { withLicense } from './util/with_license'; + +export const registerSiemRuleMigrationsUpdateRoute = ( + router: SecuritySolutionPluginRouter, + logger: Logger +) => { + router.versioned + .patch({ + path: SIEM_RULE_MIGRATION_PATH, + access: 'internal', + security: { authz }, + }) + .addVersion( + { + version: '1', + validate: { + request: { + params: buildRouteValidationWithZod(UpdateRuleMigrationRequestParams), + body: buildRouteValidationWithZod(UpdateRuleMigrationRequestBody), + }, + }, + }, + withLicense(async (context, req, res): Promise => { + const siemMigrationAuditLogger = new SiemMigrationAuditLogger(context.securitySolution); + const { migration_id: migrationId } = req.params; + try { + const ctx = await context.resolve(['securitySolution']); + const ruleMigrationsClient = ctx.securitySolution.getSiemRuleMigrationsClient(); + await siemMigrationAuditLogger.logUpdateMigration({ migrationId }); + await ruleMigrationsClient.data.migrations.update(migrationId, req.body); + + return res.ok(); + } catch (error) { + logger.error(error); + await siemMigrationAuditLogger.logUpdateMigration({ migrationId, error }); + return res.badRequest({ body: error.message }); + } + }) + ); +}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/util/audit.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/util/audit.ts index 20d88a0b07501..7ac2f297d61ed 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/util/audit.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/util/audit.ts @@ -11,6 +11,7 @@ import type { SecuritySolutionApiRequestHandlerContext } from '../../../../..'; export enum SiemMigrationsAuditActions { SIEM_MIGRATION_CREATED = 'siem_migration_created', + SIEM_MIGRATION_UPDATED = 'siem_migration_updated', SIEM_MIGRATION_RETRIEVED = 'siem_migration_retrieved', SIEM_MIGRATION_DELETED = 'siem_migration_deleted', SIEM_MIGRATION_ADDED_RULES = 'siem_migration_added_rules', @@ -50,6 +51,7 @@ export const siemMigrationAuditEventType: Record< ArrayElement > = { [SiemMigrationsAuditActions.SIEM_MIGRATION_CREATED]: AUDIT_TYPE.CREATION, + [SiemMigrationsAuditActions.SIEM_MIGRATION_UPDATED]: AUDIT_TYPE.CHANGE, [SiemMigrationsAuditActions.SIEM_MIGRATION_RETRIEVED]: AUDIT_TYPE.ACCESS, [SiemMigrationsAuditActions.SIEM_MIGRATION_UPLOADED_RESOURCES]: AUDIT_TYPE.CREATION, [SiemMigrationsAuditActions.SIEM_MIGRATION_RETRIEVED_RESOURCES]: AUDIT_TYPE.ACCESS, @@ -124,6 +126,16 @@ export class SiemMigrationAuditLogger { }); } + public async logUpdateMigration(params: { migrationId: string; error?: Error }): Promise { + const { migrationId, error } = params; + const message = `User updated the SIEM migration with [id=${migrationId}]`; + return this.log({ + action: SiemMigrationsAuditActions.SIEM_MIGRATION_UPDATED, + message, + error, + }); + } + public async logGetMigration(params: { migrationId: string; error?: Error }): Promise { const { migrationId, error } = params; const message = `User retrieved the SIEM migration with [id=${migrationId}]`; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_migration_client.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_migration_client.test.ts index 3a7432723ca9d..802c6777a017e 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_migration_client.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_migration_client.test.ts @@ -44,8 +44,9 @@ describe('RuleMigrationsDataMigrationClient', () => { describe('create', () => { test('should create a new migration', async () => { const index = '.kibana-siem-rule-migrations'; + const name = 'test name'; - const result = await ruleMigrationsDataMigrationClient.create(); + const result = await ruleMigrationsDataMigrationClient.create(name); expect(result).not.toBeFalsy(); expect(esClient.asInternalUser.create).toHaveBeenCalledWith({ @@ -55,6 +56,7 @@ describe('RuleMigrationsDataMigrationClient', () => { document: { created_by: currentUser.profile_uid, created_at: expect.any(String), + name, }, }); }); @@ -64,7 +66,7 @@ describe('RuleMigrationsDataMigrationClient', () => { esClient.asInternalUser.create as unknown as jest.MockedFn ).mockRejectedValueOnce(new Error('Test error')); - await expect(ruleMigrationsDataMigrationClient.create()).rejects.toThrow('Test error'); + await expect(ruleMigrationsDataMigrationClient.create('test')).rejects.toThrow('Test error'); expect(esClient.asInternalUser.create).toHaveBeenCalled(); expect(logger.error).toHaveBeenCalled(); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_migration_client.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_migration_client.ts index d2c8a8f5dfd5c..b13d12c154b5d 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_migration_client.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_migration_client.ts @@ -14,7 +14,7 @@ import { isNotFoundError } from './utils'; import { MAX_ES_SEARCH_SIZE } from '../constants'; export class RuleMigrationsDataMigrationClient extends RuleMigrationsDataBaseClient { - async create(): Promise { + async create(name: string): Promise { const migrationId = uuidV4(); const index = await this.getIndexName(); const profileUid = await this.getProfileUid(); @@ -28,6 +28,7 @@ export class RuleMigrationsDataMigrationClient extends RuleMigrationsDataBaseCli document: { created_by: profileUid, created_at: createdAt, + name, }, }) .catch((error) => { @@ -153,4 +154,17 @@ export class RuleMigrationsDataMigrationClient extends RuleMigrationsDataBaseCli throw error; }); } + + /** + * Updates the migration document with the provided values. + */ + async update(id: string, doc: Partial): Promise { + const index = await this.getIndexName(); + await this.esClient + .update({ index, id, doc, refresh: 'wait_for', retry_on_conflict: 1 }) + .catch((error) => { + this.logger.error(`Error updating migration: ${error}`); + throw error; + }); + } } diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_rules_client.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_rules_client.ts index 73d596e236f14..651546caade13 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_rules_client.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_rules_client.ts @@ -39,7 +39,7 @@ export type AddRuleMigrationRulesInput = Omit< RuleMigrationRule, '@timestamp' | 'id' | 'status' | 'created_by' >; -export type RuleMigrationDataStats = Omit; +export type RuleMigrationDataStats = Omit; export type RuleMigrationAllDataStats = RuleMigrationDataStats[]; export interface RuleMigrationGetRulesOptions { diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_field_maps.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_field_maps.ts index e22bc73a351b6..f962effaa7ea3 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_field_maps.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_field_maps.ts @@ -97,6 +97,7 @@ export const getPrebuiltRulesFieldMap: ({ export const migrationsFieldMaps: FieldMap< SchemaFieldMapKeys> > = { + name: { type: 'keyword', required: true }, created_at: { type: 'date', required: true }, created_by: { type: 'keyword', required: true }, }; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/index_migrators/rule_migrations_index_migrator.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/index_migrators/rule_migrations_index_migrator.test.ts index e0f7ce0bc8131..50e4641431841 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/index_migrators/rule_migrations_index_migrator.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/index_migrators/rule_migrations_index_migrator.test.ts @@ -5,11 +5,12 @@ * 2.0. */ -import type { ElasticsearchClient, Logger } from '@kbn/core/server'; +import type { ElasticsearchClient } from '@kbn/core/server'; import { RuleMigrationIndexMigrator } from '.'; import * as RuleMigrationSpaceIndexMigratorModule from './rule_migrations_per_space_index_migrator'; import type { Adapters } from '../types'; import { IndexPatternAdapter } from '@kbn/index-adapter'; +import { loggerMock } from '@kbn/logging-mocks'; const rulesIndexName = '.kibana-siem-rule-migrations-rules'; const esClientMock = { @@ -28,9 +29,7 @@ const ruleMigrationIndexAdapters = { }), } as unknown as Adapters; -const loggerMock = { - info: jest.fn(), -} as unknown as Logger; +const mockLogger = loggerMock.create(); const mockPerSpaceIndexMigrator = jest.spyOn( RuleMigrationSpaceIndexMigratorModule, @@ -53,21 +52,21 @@ describe('Index migrator', () => { const migrator = new RuleMigrationIndexMigrator( ruleMigrationIndexAdapters, esClientMock, - loggerMock + mockLogger ); await migrator.run(); expect(mockPerSpaceIndexMigrator).toHaveBeenNthCalledWith( 1, 'space1', esClientMock, - loggerMock, + mockLogger, ruleMigrationIndexAdapters ); expect(mockPerSpaceIndexMigrator).toHaveBeenNthCalledWith( 2, 'space2', esClientMock, - loggerMock, + mockLogger, ruleMigrationIndexAdapters ); @@ -75,7 +74,7 @@ describe('Index migrator', () => { 3, 'space3', esClientMock, - loggerMock, + mockLogger, ruleMigrationIndexAdapters ); }); @@ -84,10 +83,10 @@ describe('Index migrator', () => { const migrator = new RuleMigrationIndexMigrator( ruleMigrationIndexAdapters, esClientMock, - loggerMock + mockLogger ); await migrator.run(); - expect(loggerMock.info).toHaveBeenCalledWith('No spaces or index found for index migration'); + expect(mockLogger.debug).toHaveBeenCalledWith('No spaces or index found for index migration'); expect(mockPerSpaceIndexMigrator).not.toHaveBeenCalled(); }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/index_migrators/rule_migrations_index_migrator.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/index_migrators/rule_migrations_index_migrator.ts index dea1831b01c25..9b12ffbb52f71 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/index_migrators/rule_migrations_index_migrator.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/index_migrators/rule_migrations_index_migrator.ts @@ -33,10 +33,10 @@ export class RuleMigrationIndexMigrator { async run() { const allSpaces = await this.getSpaceListForMigrations(); if (allSpaces.length === 0) { - this.logger.info('No spaces or index found for index migration'); + this.logger.debug('No spaces or index found for index migration'); return; } - this.logger.info( + this.logger.debug( `Starting index migration for rule migrations for spaces :${allSpaces.join(', ')}` ); for (const spaceId of allSpaces) { @@ -48,6 +48,6 @@ export class RuleMigrationIndexMigrator { ); await migrator.run(); } - this.logger.info('Finished index migration for rule migrations successfully'); + this.logger.debug('Finished index migration for rule migrations successfully'); } } diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/index_migrators/rule_migrations_per_space_index_migrator.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/index_migrators/rule_migrations_per_space_index_migrator.test.ts index 4e5aa32eb9559..5e0c819194ebe 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/index_migrators/rule_migrations_per_space_index_migrator.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/index_migrators/rule_migrations_per_space_index_migrator.test.ts @@ -93,9 +93,9 @@ describe('RuleMigrationSpaceIndexMigrator', () => { refresh: 'wait_for', operations: [ { create: { _id: 'migration1', _index: '.kibana-siem-rule-migrations-migrations-space1' } }, - { id: 'migration1', created_at: '2023-01-01T00:00:00Z', created_by: 'user1' }, + { created_at: '2023-01-01T00:00:00Z', created_by: 'user1' }, { create: { _id: 'migration2', _index: '.kibana-siem-rule-migrations-migrations-space1' } }, - { id: 'migration2', created_at: '2023-01-02T00:00:00Z', created_by: 'user2' }, + { created_at: '2023-01-02T00:00:00Z', created_by: 'user2' }, ], }); }); @@ -109,6 +109,7 @@ describe('RuleMigrationSpaceIndexMigrator', () => { _source: { created_at: '2023-01-01T00:00:00Z', created_by: 'user1', + name: 'SIEM Migration 1', }, }, ], @@ -133,7 +134,61 @@ describe('RuleMigrationSpaceIndexMigrator', () => { refresh: 'wait_for', operations: [ { create: { _id: 'migration2', _index: '.kibana-siem-rule-migrations-migrations-space1' } }, - { id: 'migration2', created_at: '2023-01-02T00:00:00Z', created_by: 'user2' }, + { + created_at: '2023-01-02T00:00:00Z', + created_by: 'user2', + }, + ], + }); + }); + + it('should update migrations with missing names', async () => { + const mockMigrationIndexResultWithMissingNames = { + hits: { + hits: [ + { + _id: 'migration1', + _source: { + created_at: '2023-01-01T00:00:00Z', + created_by: 'user1', + name: '', + }, + }, + { + _id: 'migration2', + _source: { + created_at: '2023-01-02T00:00:00Z', + created_by: 'user2', + name: '', + }, + }, + ], + }, + } as unknown as SearchResponseBody; + + esClientMock.search.mockImplementation( + getMockedESSearchFunction( + mockRuleIndexAggregationsResult, + mockMigrationIndexResultWithMissingNames + ) as unknown as ElasticsearchClient['search'] + ); + + const migrator = new RuleMigrationSpaceIndexMigrator( + 'space1', + esClientMock, + loggerMock, + ruleMigrationIndexAdapters + ); + + await migrator.run(); + + expect(esClientMock.bulk).toHaveBeenNthCalledWith(1, { + refresh: 'wait_for', + operations: [ + { update: { _id: 'migration1', _index: '.kibana-siem-rule-migrations-migrations-space1' } }, + { doc: { name: 'SIEM rules migration #1' } }, + { update: { _id: 'migration2', _index: '.kibana-siem-rule-migrations-migrations-space1' } }, + { doc: { name: 'SIEM rules migration #2' } }, ], }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/index_migrators/rule_migrations_per_space_index_migrator.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/index_migrators/rule_migrations_per_space_index_migrator.ts index 93bc0fefc0d9d..cea83004a23a9 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/index_migrators/rule_migrations_per_space_index_migrator.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/index_migrators/rule_migrations_per_space_index_migrator.ts @@ -23,7 +23,111 @@ export class RuleMigrationSpaceIndexMigrator { private ruleMigrationIndexAdapters: Adapters ) {} - private async getExistingMigrationFromRulesIndex() { + /** + * Runs the migrators for the rule migration index in the specified space. + * It migrates existing rules to create migration documents and populates migration names. + * + * If any errors occur they are logged but should not prevent the server from starting. + */ + async run() { + this.logger.debug(`Starting migrators for space ${this.spaceId}`); + try { + await this.migrateRuleMigrationIndex(); + } catch (error) { + this.logger.error( + `Error migrating rule migration index for space ${this.spaceId}: ${error.message}` + ); + } + try { + await this.populateMigrationNames(); + } catch (error) { + this.logger.error( + `Error populating migration names for space ${this.spaceId}: ${error.message}` + ); + } + this.logger.debug(`Finished migrators for space ${this.spaceId}`); + } + + /** + * Migrates the rule migration index by creating migration documents for existing rules + * that do not have corresponding migration documents in the migrations index. + */ + private async migrateRuleMigrationIndex() { + const installedIndexName = + await this.ruleMigrationIndexAdapters.migrations.getInstalledIndexName(this.spaceId); + if (!installedIndexName) { + await this.ruleMigrationIndexAdapters.migrations.createIndex(this.spaceId); + } + + const migrationsFromRulesIndex = await this.getMigrationFromRulesIndex(); + const migrationsFromMigrationsIndex = await this.getMigrationIdsFromMigrationsIndex(); + + const migrationsToIndex = migrationsFromRulesIndex.filter( + (migration) => !migrationsFromMigrationsIndex.some((id) => id === migration.id) + ); + + if (migrationsToIndex.length > 0) { + await this.createMigrationDocs( + migrationsToIndex.map((migration) => ({ + ...migration, + created_by: migration.created_by ?? '', + created_at: migration.created_at ?? new Date().toISOString(), + })) + ); + this.logger.debug(`Created ${migrationsToIndex.length} migration documents missing.`); + } + } + + /** + * Populates migration documents that do not have a name field with generated names. + * The names are generated based on the migration creation order, like the existing migrations are named in the runtime. + */ + private async populateMigrationNames() { + const migrationIdsWithoutName = await this.getMigrationIdsWithoutName(); + + if (migrationIdsWithoutName.length > 0) { + const namesMap = await this.getMigrationsNamesMap(); + + const migrationsToUpdate = migrationIdsWithoutName.map((id) => { + const name = namesMap.get(id) ?? `SIEM Migration ${id}`; // Fallback name using the ID (should never happen, but just in case) + return { id, name }; + }); + + await this.updateMigrationDocs(migrationsToUpdate); + this.logger.debug(`Updated ${migrationsToUpdate.length} migrations with generated name.`); + } + } + + /** + * Creates migration documents in the migrations index. + */ + private async createMigrationDocs(docs: Array>) { + const _index = this.ruleMigrationIndexAdapters.migrations.getIndexName(this.spaceId); + const operations = docs.flatMap(({ id: _id, ...doc }) => [ + { create: { _id, _index } }, + { ...doc }, + ]); + return this.esClient.bulk({ refresh: 'wait_for', operations }); + } + + /** + * Updates migration documents in the migrations index. + */ + private async updateMigrationDocs(docs: Array>) { + const _index = this.ruleMigrationIndexAdapters.migrations.getIndexName(this.spaceId); + const operations = docs.flatMap(({ id: _id, ...doc }) => [ + { update: { _id, _index } }, + { doc }, + ]); + return this.esClient.bulk({ refresh: 'wait_for', operations }); + } + + /** + * Retrieves existing migrations from the rules index. + * It aggregates by migration_id and returns the earliest created_at and created_by for each migration. + * Results are ordered by `@timestamp` in ascending order. This is important for name generation + */ + private async getMigrationFromRulesIndex() { const index = this.ruleMigrationIndexAdapters.rules.getIndexName(this.spaceId); const aggregations: Record = { migrationIds: { @@ -34,12 +138,7 @@ export class RuleMigrationSpaceIndexMigrator { }, }, }; - const result = await this.esClient - .search({ index, aggregations, _source: false }) - .catch((error) => { - this.logger.error(`Error getting all rule migrations stats: ${error.message}`); - throw error; - }); + const result = await this.esClient.search({ index, aggregations, _source: false }); const migrationsAgg = result.aggregations?.migrationIds as AggregationsStringTermsAggregate; const buckets = (migrationsAgg?.buckets as AggregationsStringTermsBucket[]) ?? []; @@ -54,15 +153,17 @@ export class RuleMigrationSpaceIndexMigrator { })); } - private async getExistingMigrationFromMigrationsIndex() { + /** + * Retrieves existing migrations from the migrations index. + * It returns the IDs of all migration documents. + */ + private async getMigrationIdsFromMigrationsIndex(): Promise { const index = this.ruleMigrationIndexAdapters.migrations.getIndexName(this.spaceId); const result = await this.esClient.search({ index, size: MAX_ES_SEARCH_SIZE, - query: { - match_all: {}, - }, - _source: true, + query: { match_all: {} }, + _source: false, }); return result.hits.hits.map(({ _id }) => { @@ -71,58 +172,38 @@ export class RuleMigrationSpaceIndexMigrator { }); } - private async indexMigrationDocs(docs: StoredSiemMigration[]) { - const indexName = this.ruleMigrationIndexAdapters.migrations.getIndexName(this.spaceId); - const createOperations = docs.flatMap((doc) => [ - { - create: { - _id: doc.id, - _index: indexName, - }, - }, - { - ...doc, - }, - ]); + /** + * Retrieves migration IDs from the migrations index that do not have a name field. + */ + private async getMigrationIdsWithoutName(): Promise { + const index = this.ruleMigrationIndexAdapters.migrations.getIndexName(this.spaceId); - return this.esClient.bulk({ - refresh: 'wait_for', - operations: createOperations, + const result = await this.esClient.search({ + index, + query: { bool: { must_not: { exists: { field: 'name' } } } }, + size: MAX_ES_SEARCH_SIZE, + _source: false, }); - } - async run() { - await this.migrateRuleMigrationIndex(); + return result.hits.hits.map(({ _id }) => { + assert(_id, 'document should have _id'); + return _id; + }); } /** - * Creates the rule migration index if it doesn't exist and indexes any missing migration documents - * from the rules index. - * + * Creates the mapping of names by id for all the migrations from the rules index. + * The names are generated based in creation order. */ - private async migrateRuleMigrationIndex() { - const installedIndexName = - await this.ruleMigrationIndexAdapters.migrations.getInstalledIndexName(this.spaceId); - if (!installedIndexName) { - await this.ruleMigrationIndexAdapters.migrations.createIndex(this.spaceId); - } - - const existingMigrationsFromRulesIndex = await this.getExistingMigrationFromRulesIndex(); - const existingMigrationsFromMigrationsIndex = - await this.getExistingMigrationFromMigrationsIndex(); + private async getMigrationsNamesMap(): Promise> { + // Cache the names map to avoid repeat the aggregation query + const migrations = await this.getMigrationFromRulesIndex(); - const migrationsToIndex = existingMigrationsFromRulesIndex.filter( - (migration) => !existingMigrationsFromMigrationsIndex.some((id) => id === migration.id) - ); + const namesMap = migrations.reduce((acc, migration, i) => { + acc.set(migration.id, `SIEM rules migration #${i + 1}`); + return acc; + }, new Map()); - if (migrationsToIndex.length > 0) { - this.logger.info( - `Found ${migrationsToIndex.length} rule migration documents from rules index with an absent migration doc. Creating corresponding migration documents.` - ); - await this.indexMigrationDocs(migrationsToIndex); - this.logger.info( - `Created ${migrationsToIndex.length} rule migration documents from rules index with an absent migration doc.` - ); - } + return namesMap; } } diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/rule_migrations_task_client.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/rule_migrations_task_client.test.ts index e38766850243d..f7102eeb5c583 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/rule_migrations_task_client.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/rule_migrations_task_client.test.ts @@ -329,6 +329,7 @@ describe('RuleMigrationsTaskClient', () => { data.migrations.get.mockResolvedValue({ id: 'migration-1', + name: 'Test Migration', created_at: new Date().toISOString(), created_by: 'test-user', last_execution: { diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/rule_migrations_task_client.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/rule_migrations_task_client.ts index 4b0776b912c37..2f656eed10d87 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/rule_migrations_task_client.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/rule_migrations_task_client.ts @@ -135,7 +135,7 @@ export class RuleMigrationsTaskClient { } const dataStats = await this.data.rules.getStats(migrationId); const taskStats = this.getTaskStats(migration, dataStats.rules); - return { ...taskStats, ...dataStats }; + return { ...taskStats, ...dataStats, name: migration.name }; } /** Returns the stats of all migrations */ @@ -151,8 +151,8 @@ export class RuleMigrationsTaskClient { for (const dataStats of allDataStats) { const migration = allMigrationsMap.get(dataStats.id); if (migration) { - const taksStats = this.getTaskStats(migration, dataStats.rules); - allStats.push({ ...taksStats, ...dataStats }); + const tasksStats = this.getTaskStats(migration, dataStats.rules); + allStats.push({ ...tasksStats, ...dataStats, name: migration.name }); } } return allStats; diff --git a/x-pack/test/api_integration/services/security_solution_api.gen.ts b/x-pack/test/api_integration/services/security_solution_api.gen.ts index 2174994cf1e85..6d85e2656701b 100644 --- a/x-pack/test/api_integration/services/security_solution_api.gen.ts +++ b/x-pack/test/api_integration/services/security_solution_api.gen.ts @@ -33,6 +33,7 @@ import { CopyTimelineRequestBodyInput } from '@kbn/security-solution-plugin/comm import { CreateAlertsMigrationRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/signals_migration/create_signals_migration/create_signals_migration.gen'; import { CreateAssetCriticalityRecordRequestBodyInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/asset_criticality/create_asset_criticality.gen'; import { CreateRuleRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_management/crud/create_rule/create_rule_route.gen'; +import { CreateRuleMigrationRequestBodyInput } from '@kbn/security-solution-plugin/common/siem_migrations/model/api/rules/rule_migration.gen'; import { CreateRuleMigrationRulesRequestParamsInput, CreateRuleMigrationRulesRequestBodyInput, @@ -160,7 +161,10 @@ import { StopRuleMigrationRequestParamsInput } from '@kbn/security-solution-plug import { SuggestUserProfilesRequestQueryInput } from '@kbn/security-solution-plugin/common/api/detection_engine/users/suggest_user_profiles_route.gen'; import { TriggerRiskScoreCalculationRequestBodyInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/risk_engine/entity_calculation_route.gen'; import { UpdateRuleRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_management/crud/update_rule/update_rule_route.gen'; -import { UpdateRuleMigrationRequestParamsInput } from '@kbn/security-solution-plugin/common/siem_migrations/model/api/rules/rule_migration.gen'; +import { + UpdateRuleMigrationRequestParamsInput, + UpdateRuleMigrationRequestBodyInput, +} from '@kbn/security-solution-plugin/common/siem_migrations/model/api/rules/rule_migration.gen'; import { UpdateRuleMigrationRulesRequestParamsInput, UpdateRuleMigrationRulesRequestBodyInput, @@ -481,12 +485,13 @@ For detailed information on Kibana actions and alerting, and additional API call /** * Creates a new rule migration and returns the corresponding migration_id */ - createRuleMigration(kibanaSpace: string = 'default') { + createRuleMigration(props: CreateRuleMigrationProps, kibanaSpace: string = 'default') { return supertest .put(routeWithNamespace('/internal/siem_migrations/rules', kibanaSpace)) .set('kbn-xsrf', 'true') .set(ELASTIC_HTTP_VERSION_HEADER, '1') - .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana'); + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') + .send(props.body as object); }, /** * Adds original vendor rules to an already existing migration. Can be called multiple times to add more rules @@ -1848,7 +1853,8 @@ The difference between the `id` and `rule_id` is that the `id` is a unique rule ) .set('kbn-xsrf', 'true') .set(ELASTIC_HTTP_VERSION_HEADER, '1') - .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana'); + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') + .send(props.body as object); }, /** * Updates rules migrations attributes @@ -1950,6 +1956,9 @@ export interface CreateAssetCriticalityRecordProps { export interface CreateRuleProps { body: CreateRuleRequestBodyInput; } +export interface CreateRuleMigrationProps { + body: CreateRuleMigrationRequestBodyInput; +} export interface CreateRuleMigrationRulesProps { params: CreateRuleMigrationRulesRequestParamsInput; body: CreateRuleMigrationRulesRequestBodyInput; @@ -2213,6 +2222,7 @@ export interface UpdateRuleProps { } export interface UpdateRuleMigrationProps { params: UpdateRuleMigrationRequestParamsInput; + body: UpdateRuleMigrationRequestBodyInput; } export interface UpdateRuleMigrationRulesProps { params: UpdateRuleMigrationRulesRequestParamsInput; diff --git a/x-pack/test/security_solution_api_integration/test_suites/siem_migrations/rules/trial_license_complete_tier/create.ts b/x-pack/test/security_solution_api_integration/test_suites/siem_migrations/rules/trial_license_complete_tier/create.ts index 8d92974e85dba..24cf0d4d652b0 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/siem_migrations/rules/trial_license_complete_tier/create.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/siem_migrations/rules/trial_license_complete_tier/create.ts @@ -21,20 +21,20 @@ export default ({ getService }: FtrProviderContext) => { describe('Happy path', () => { it('should create migrations without any issues', async () => { + const name = 'creation test migration name'; const { body: { migration_id: migrationId }, - } = await ruleMigrationRoutes.create({}); + } = await ruleMigrationRoutes.create({ body: { name } }); expect(migrationId).not.toBeNull(); - const { - body: { id, created_by: createdBy }, - } = await ruleMigrationRoutes.get({ + const { body } = await ruleMigrationRoutes.get({ migrationId, }); - expect(id).toBe(migrationId); - expect(createdBy).not.toBeNull(); + expect(body.id).toBe(migrationId); + expect(body.name).toBe(name); + expect(body.created_by).not.toBeNull(); }); }); }); diff --git a/x-pack/test/security_solution_api_integration/test_suites/siem_migrations/utils/mocks.ts b/x-pack/test/security_solution_api_integration/test_suites/siem_migrations/utils/mocks.ts index b9794bab675a1..23c0000de799f 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/siem_migrations/utils/mocks.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/siem_migrations/utils/mocks.ts @@ -154,6 +154,7 @@ export const statsOverrideCallbackFactory = ({ }; const getDefaultMigrationDoc: () => Omit = () => ({ + name: 'Default Migration', created_by: SOME_USER_ID, created_at: new Date().toISOString(), last_execution: { diff --git a/x-pack/test/security_solution_api_integration/test_suites/siem_migrations/utils/rules.ts b/x-pack/test/security_solution_api_integration/test_suites/siem_migrations/utils/rules.ts index 342cf511b45a3..c8c5dbf81f084 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/siem_migrations/utils/rules.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/siem_migrations/utils/rules.ts @@ -27,6 +27,7 @@ import { SIEM_RULE_MIGRATIONS_INTEGRATIONS_STATS_PATH, } from '@kbn/security-solution-plugin/common/siem_migrations/constants'; import { + CreateRuleMigrationRequestBody, CreateRuleMigrationResponse, GetAllStatsRuleMigrationResponse, GetRuleMigrationIntegrationsResponse, @@ -55,6 +56,9 @@ export interface RequestParams { expectStatusCode?: number; } +export interface CreateMigrationRequestParams extends RequestParams { + body?: CreateRuleMigrationRequestBody; +} export interface MigrationRequestParams extends RequestParams { /** `id` of the migration to get rules documents for */ migrationId: string; @@ -90,14 +94,17 @@ export type StartMigrationRuleParams = MigrationRequestParams & { export const ruleMigrationRouteHelpersFactory = (supertest: SuperTest.Agent) => { return { create: async ({ + body = { name: 'test migration' }, expectStatusCode = 200, - }: RequestParams): Promise<{ body: CreateRuleMigrationResponse }> => { + }: CreateMigrationRequestParams): Promise<{ + body: CreateRuleMigrationResponse; + }> => { const response = await supertest .put(SIEM_RULE_MIGRATIONS_PATH) .set('kbn-xsrf', 'true') .set(ELASTIC_HTTP_VERSION_HEADER, API_VERSIONS.internal.v1) .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') - .send(); + .send(body); assertStatusCode(expectStatusCode, response);