diff --git a/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.gen.ts b/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.gen.ts index df89c8d7f1c4e..47c06e1e02c7a 100644 --- a/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.gen.ts +++ b/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.gen.ts @@ -17,11 +17,8 @@ import { z } from '@kbn/zod'; import { ArrayFromString } from '@kbn/zod-helpers'; -import { NonEmptyString } from '../../../../api/model/primitives.gen'; import { - ElasticRulePartial, - RuleMigrationTranslationResult, - RuleMigrationComments, + UpdateRuleMigrationData, RuleMigrationTaskStats, OriginalRule, RuleMigration, @@ -31,6 +28,7 @@ import { RuleMigrationResourceType, RuleMigrationResource, } from '../../rule_migration.gen'; +import { NonEmptyString } from '../../../../api/model/primitives.gen'; import { ConnectorId, LangSmithOptions } from '../../common.gen'; export type CreateRuleMigrationRequestParams = z.infer; @@ -62,6 +60,7 @@ export const GetRuleMigrationRequestQuery = z.object({ sort_field: NonEmptyString.optional(), sort_direction: z.enum(['asc', 'desc']).optional(), search_term: z.string().optional(), + ids: ArrayFromString(NonEmptyString).optional(), }); export type GetRuleMigrationRequestQueryInput = z.input; @@ -251,26 +250,7 @@ export const StopRuleMigrationResponse = z.object({ }); export type UpdateRuleMigrationRequestBody = z.infer; -export const UpdateRuleMigrationRequestBody = z.array( - z.object({ - /** - * The rule migration id - */ - id: NonEmptyString, - /** - * The migrated elastic rule attributes to update. - */ - elastic_rule: ElasticRulePartial.optional(), - /** - * The rule translation result. - */ - translation_result: RuleMigrationTranslationResult.optional(), - /** - * The comments for the migration including a summary from the LLM in markdown. - */ - comments: RuleMigrationComments.optional(), - }) -); +export const UpdateRuleMigrationRequestBody = z.array(UpdateRuleMigrationData); export type UpdateRuleMigrationRequestBodyInput = z.input; export type UpdateRuleMigrationResponse = z.infer; diff --git a/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.schema.yaml b/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.schema.yaml index fce14a2ac87b1..69e43b57dabd3 100644 --- a/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.schema.yaml +++ b/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.schema.yaml @@ -20,22 +20,7 @@ paths: schema: type: array items: - type: object - required: - - id - properties: - id: - description: The rule migration id - $ref: '../../../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' - elastic_rule: - description: The migrated elastic rule attributes to update. - $ref: '../../rule_migration.schema.yaml#/components/schemas/ElasticRulePartial' - translation_result: - description: The rule translation result. - $ref: '../../rule_migration.schema.yaml#/components/schemas/RuleMigrationTranslationResult' - comments: - description: The comments for the migration including a summary from the LLM in markdown. - $ref: '../../rule_migration.schema.yaml#/components/schemas/RuleMigrationComments' + $ref: '../../rule_migration.schema.yaml#/components/schemas/UpdateRuleMigrationData' responses: 200: description: Indicates rules migrations have been updated correctly. @@ -151,6 +136,14 @@ paths: required: false schema: type: string + - name: ids + in: query + required: false + schema: + type: array + items: + description: The rule migration id + $ref: '../../../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' responses: 200: diff --git a/x-pack/plugins/security_solution/common/siem_migrations/model/rule_migration.gen.ts b/x-pack/plugins/security_solution/common/siem_migrations/model/rule_migration.gen.ts index 9fd3876e141a8..064df05c5ad41 100644 --- a/x-pack/plugins/security_solution/common/siem_migrations/model/rule_migration.gen.ts +++ b/x-pack/plugins/security_solution/common/siem_migrations/model/rule_migration.gen.ts @@ -303,6 +303,29 @@ export const RuleMigrationTranslationStats = z.object({ }), }); +/** + * The rule migration data object for rule update operation + */ +export type UpdateRuleMigrationData = z.infer; +export const UpdateRuleMigrationData = z.object({ + /** + * The rule migration id + */ + id: NonEmptyString, + /** + * The migrated elastic rule attributes to update. + */ + elastic_rule: ElasticRulePartial.optional(), + /** + * The rule translation result. + */ + translation_result: RuleMigrationTranslationResult.optional(), + /** + * The comments for the migration including a summary from the LLM in markdown. + */ + comments: RuleMigrationComments.optional(), +}); + /** * The type of the rule migration resource. */ diff --git a/x-pack/plugins/security_solution/common/siem_migrations/model/rule_migration.schema.yaml b/x-pack/plugins/security_solution/common/siem_migrations/model/rule_migration.schema.yaml index 0a99bd5ce701f..3171a62298f15 100644 --- a/x-pack/plugins/security_solution/common/siem_migrations/model/rule_migration.schema.yaml +++ b/x-pack/plugins/security_solution/common/siem_migrations/model/rule_migration.schema.yaml @@ -276,6 +276,25 @@ components: items: type: string + UpdateRuleMigrationData: + type: object + description: The rule migration data object for rule update operation + required: + - id + properties: + id: + description: The rule migration id + $ref: '../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' + elastic_rule: + description: The migrated elastic rule attributes to update. + $ref: '#/components/schemas/ElasticRulePartial' + translation_result: + description: The rule translation result. + $ref: '#/components/schemas/RuleMigrationTranslationResult' + comments: + description: The comments for the migration including a summary from the LLM in markdown. + $ref: '#/components/schemas/RuleMigrationComments' + ## Rule migration resources RuleMigrationResourceType: diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/api/index.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/api/index.ts index 17aa00dd0f01e..02fb423b05279 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/api/index.ts +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/api/index.ts @@ -7,6 +7,7 @@ import { replaceParams } from '@kbn/openapi-common/shared'; +import type { UpdateRuleMigrationData } from '../../../../common/siem_migrations/model/rule_migration.gen'; import type { LangSmithOptions } from '../../../../common/siem_migrations/model/common.gen'; import { KibanaServices } from '../../../common/lib/kibana'; @@ -37,6 +38,7 @@ import type { UpsertRuleMigrationResourcesRequestBody, UpsertRuleMigrationResourcesResponse, GetRuleMigrationPrebuiltRulesResponse, + UpdateRuleMigrationResponse, } from '../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; export interface GetRuleMigrationStatsParams { @@ -168,6 +170,8 @@ export interface GetRuleMigrationParams { sortDirection?: 'asc' | 'desc'; /** Optional search term to filter documents */ searchTerm?: string; + /** Optional rules ids to filter documents */ + ids?: string[]; /** Optional AbortSignal for cancelling request */ signal?: AbortSignal; } @@ -179,6 +183,7 @@ export const getRuleMigrations = async ({ sortField, sortDirection, searchTerm, + ids, signal, }: GetRuleMigrationParams): Promise => { return KibanaServices.get().http.get( @@ -191,6 +196,7 @@ export const getRuleMigrations = async ({ sort_field: sortField, sort_direction: sortDirection, search_term: searchTerm, + ids, }, signal, } @@ -272,3 +278,21 @@ export const getRuleMigrationsPrebuiltRules = async ({ { version: '1', signal } ); }; + +export interface UpdateRulesParams { + /** The list of migration rules data to update */ + rulesToUpdate: UpdateRuleMigrationData[]; + /** Optional AbortSignal for cancelling request */ + signal?: AbortSignal; +} +/** Updates provided migration rules. */ +export const updateMigrationRules = async ({ + rulesToUpdate, + signal, +}: UpdateRulesParams): Promise => { + return KibanaServices.get().http.put(SIEM_RULE_MIGRATIONS_PATH, { + version: '1', + body: JSON.stringify(rulesToUpdate), + signal, + }); +}; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rule_details_flyout/index.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rule_details_flyout/index.tsx index 9762cc578e0cc..fa13c44d764a8 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rule_details_flyout/index.tsx +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rule_details_flyout/index.tsx @@ -6,7 +6,7 @@ */ import type { FC, PropsWithChildren } from 'react'; -import React, { useMemo, useState, useEffect } from 'react'; +import React, { useMemo, useState, useEffect, useCallback } from 'react'; import { css } from '@emotion/css'; import { euiThemeVars } from '@kbn/ui-theme'; import { @@ -21,16 +21,21 @@ import { EuiFlexGroup, EuiFlexItem, useGeneratedHtmlId, + EuiSkeletonLoading, + EuiSkeletonTitle, + EuiSkeletonText, } from '@elastic/eui'; import type { EuiTabbedContentTab, EuiTabbedContentProps, EuiFlyoutProps } from '@elastic/eui'; import type { RuleMigration } from '../../../../../common/siem_migrations/model/rule_migration.gen'; +import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; import { RuleOverviewTab, useOverviewTabSections, } from '../../../../detection_engine/rule_management/components/rule_details/rule_overview_tab'; import type { RuleResponse } from '../../../../../common/api/detection_engine/model/rule_schema'; +import * as logicI18n from '../../logic/translations'; import * as i18n from './translations'; import { DEFAULT_DESCRIPTION_LIST_COLUMN_WIDTHS, @@ -41,6 +46,7 @@ import { convertMigrationCustomRuleToSecurityRulePayload, isMigrationCustomRule, } from '../../../../../common/siem_migrations/rules/utils'; +import { useUpdateMigrationRules } from '../../logic/use_update_migration_rules'; /* * Fixes tabs to the top and allows the content to scroll. @@ -62,11 +68,12 @@ export const TabContentPadding: FC> = ({ children }) ); interface MigrationRuleDetailsFlyoutProps { - ruleActions?: React.ReactNode; ruleMigration: RuleMigration; + ruleActions?: React.ReactNode; matchedPrebuiltRule?: RuleResponse; size?: EuiFlyoutProps['size']; extraTabs?: EuiTabbedContentTab[]; + isDataLoading?: boolean; closeFlyout: () => void; } @@ -77,19 +84,52 @@ export const MigrationRuleDetailsFlyout: React.FC { + const { addError } = useAppToasts(); + const { expandedOverviewSections, toggleOverviewSection } = useOverviewTabSections(); - const rule = useMemo(() => { - if (isMigrationCustomRule(ruleMigration.elastic_rule)) { - return convertMigrationCustomRuleToSecurityRulePayload( - ruleMigration.elastic_rule, - false - ) as RuleResponse; // TODO: we need to adjust RuleOverviewTab to allow partial RuleResponse as a parameter; + const { mutateAsync: updateMigrationRules } = useUpdateMigrationRules( + ruleMigration.migration_id + ); + + const [isUpdating, setIsUpdating] = useState(false); + const isLoading = isDataLoading || isUpdating; + + const handleTranslationUpdate = useCallback( + async (ruleName: string, ruleQuery: string) => { + if (!ruleMigration || isLoading) { + return; + } + setIsUpdating(true); + try { + await updateMigrationRules([ + { + id: ruleMigration.id, + elastic_rule: { + title: ruleName, + query: ruleQuery, + }, + }, + ]); + } catch (error) { + addError(error, { title: logicI18n.UPDATE_MIGRATION_RULES_FAILURE }); + } finally { + setIsUpdating(false); + } + }, + [addError, ruleMigration, isLoading, updateMigrationRules] + ); + + const ruleDetailsToOverview = useMemo(() => { + const elasticRule = ruleMigration?.elastic_rule; + if (isMigrationCustomRule(elasticRule)) { + return convertMigrationCustomRuleToSecurityRulePayload(elasticRule, false) as RuleResponse; // TODO: we need to adjust RuleOverviewTab to allow partial RuleResponse as a parameter; } return matchedPrebuiltRule; - }, [matchedPrebuiltRule, ruleMigration]); + }, [ruleMigration, matchedPrebuiltRule]); const translationTab: EuiTabbedContentTab = useMemo( () => ({ @@ -97,14 +137,17 @@ export const MigrationRuleDetailsFlyout: React.FC - + {ruleMigration && ( + + )} ), }), - [matchedPrebuiltRule, ruleMigration] + [ruleMigration, handleTranslationUpdate, matchedPrebuiltRule] ); const overviewTab: EuiTabbedContentTab = useMemo( @@ -113,9 +156,9 @@ export const MigrationRuleDetailsFlyout: React.FC - {rule && ( + {ruleDetailsToOverview && ( ), }), - [rule, size, expandedOverviewSections, toggleOverviewSection] + [ruleDetailsToOverview, size, expandedOverviewSections, toggleOverviewSection] ); const tabs = useMemo(() => { @@ -149,6 +192,16 @@ export const MigrationRuleDetailsFlyout: React.FC { + return ( + + ); + }, [selectedTab, tabs]); + const migrationsRulesFlyoutTitleId = useGeneratedHtmlId({ prefix: 'migrationRulesFlyoutTitle', }); @@ -166,16 +219,23 @@ export const MigrationRuleDetailsFlyout: React.FC

- {rule?.name ?? ruleMigration.original_rule.title} + {ruleDetailsToOverview?.name ?? + ruleMigration?.original_rule.title ?? + i18n.UNKNOWN_MIGRATION_RULE_TITLE}

- + + + + } + loadedContent={tabsContent} /> diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rule_details_flyout/translation_tab/callout.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rule_details_flyout/translation_tab/callout.tsx new file mode 100644 index 0000000000000..fd014b28fcd8e --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rule_details_flyout/translation_tab/callout.tsx @@ -0,0 +1,65 @@ +/* + * 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 { FC } from 'react'; +import React from 'react'; +import type { IconType } from '@elastic/eui'; +import { EuiCallOut } from '@elastic/eui'; +import { + RuleMigrationTranslationResultEnum, + type RuleMigrationTranslationResult, +} from '../../../../../../common/siem_migrations/model/rule_migration.gen'; +import * as i18n from './translations'; + +const getCallOutInfo = ( + translationResult: RuleMigrationTranslationResult +): { title: string; message?: string; icon: IconType; color: 'success' | 'warning' | 'danger' } => { + switch (translationResult) { + case RuleMigrationTranslationResultEnum.full: + return { + title: i18n.CALLOUT_TRANSLATED_RULE_TITLE, + icon: 'checkInCircleFilled', + color: 'success', + }; + case RuleMigrationTranslationResultEnum.partial: + return { + title: i18n.CALLOUT_PARTIALLY_TRANSLATED_RULE_TITLE, + message: i18n.CALLOUT_PARTIALLY_TRANSLATED_RULE_DESCRIPTION, + icon: 'warningFilled', + color: 'warning', + }; + case RuleMigrationTranslationResultEnum.untranslatable: + return { + title: i18n.CALLOUT_NOT_TRANSLATED_RULE_TITLE, + message: i18n.CALLOUT_NOT_TRANSLATED_RULE_DESCRIPTION, + icon: 'checkInCircleFilled', + color: 'danger', + }; + } +}; + +export interface TranslationCallOutProps { + translationResult: RuleMigrationTranslationResult; +} + +export const TranslationCallOut: FC = React.memo( + ({ translationResult }) => { + const { title, message, icon, color } = getCallOutInfo(translationResult); + + return ( + + {message} + + ); + } +); +TranslationCallOut.displayName = 'TranslationCallOut'; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rule_details_flyout/translation_tab/index.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rule_details_flyout/translation_tab/index.tsx index a80480b8837bb..28a07dc0caa3f 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rule_details_flyout/translation_tab/index.tsx +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rule_details_flyout/translation_tab/index.tsx @@ -9,10 +9,8 @@ import React, { useMemo } from 'react'; import { EuiAccordion, EuiBadge, - EuiFieldText, EuiFlexGroup, EuiFlexItem, - EuiFormRow, EuiSpacer, EuiSplitPanel, EuiTitle, @@ -29,20 +27,22 @@ import { convertTranslationResultIntoColor, convertTranslationResultIntoText, } from '../../../utils/helpers'; +import { TranslationCallOut } from './callout'; interface TranslationTabProps { ruleMigration: RuleMigration; matchedPrebuiltRule?: RuleResponse; + onTranslationUpdate?: (ruleName: string, ruleQuery: string) => Promise; } export const TranslationTab: React.FC = React.memo( - ({ ruleMigration, matchedPrebuiltRule }) => { + ({ ruleMigration, matchedPrebuiltRule, onTranslationUpdate }) => { const { euiTheme } = useEuiTheme(); - const name = useMemo( - () => ruleMigration.elastic_rule?.title ?? ruleMigration.original_rule.title, - [ruleMigration.elastic_rule?.title, ruleMigration.original_rule.title] - ); + const isInstalled = !!ruleMigration.elastic_rule?.id; + const canEdit = !matchedPrebuiltRule && !isInstalled; + + const ruleName = matchedPrebuiltRule?.name ?? ruleMigration.elastic_rule?.title; const originalQuery = ruleMigration.original_rule.query; const elasticQuery = useMemo(() => { let query = ruleMigration.elastic_rule?.query; @@ -55,10 +55,12 @@ export const TranslationTab: React.FC = React.memo( return ( <> - - - - + {ruleMigration.translation_result && !isInstalled && ( + <> + + + + )} } @@ -85,7 +87,9 @@ export const TranslationTab: React.FC = React.memo( onClick={() => {}} onClickAriaLabel={'Click to update translation status'} > - {convertTranslationResultIntoText(ruleMigration.translation_result)} + {isInstalled + ? i18n.INSTALLED_LABEL + : convertTranslationResultIntoText(ruleMigration.translation_result)} @@ -95,6 +99,7 @@ export const TranslationTab: React.FC = React.memo( @@ -108,13 +113,11 @@ export const TranslationTab: React.FC = React.memo( /> diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rule_details_flyout/translation_tab/migration_rule_query.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rule_details_flyout/translation_tab/migration_rule_query.tsx index 534f765da97bc..252fd11ead9fb 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rule_details_flyout/translation_tab/migration_rule_query.tsx +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rule_details_flyout/translation_tab/migration_rule_query.tsx @@ -5,29 +5,71 @@ * 2.0. */ -import React, { useMemo } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import { + EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, - EuiMarkdownEditor, EuiMarkdownFormat, + EuiSpacer, EuiTitle, useEuiTheme, } from '@elastic/eui'; import { css } from '@emotion/react'; +import { VALIDATION_WARNING_CODES } from '../../../../../detection_engine/rule_creation/constants/validation_warning_codes'; +import { useFormWithWarnings } from '../../../../../common/hooks/use_form_with_warnings'; +import { EsqlQueryEdit } from '../../../../../detection_engine/rule_creation/components/esql_query_edit'; +import { Field, Form, getUseField } from '../../../../../shared_imports'; +import type { RuleTranslationSchema } from './types'; +import { schema } from './schema'; import * as i18n from './translations'; +const CommonUseField = getUseField({ component: Field }); + interface MigrationRuleQueryProps { title: string; + ruleName?: string; query: string; canEdit?: boolean; + onTranslationUpdate?: (ruleName: string, ruleQuery: string) => Promise; } export const MigrationRuleQuery: React.FC = React.memo( - ({ title, query, canEdit }) => { + ({ title, ruleName, query, canEdit, onTranslationUpdate }) => { const { euiTheme } = useEuiTheme(); + const formDefaultValue: RuleTranslationSchema = useMemo(() => { + return { + ruleName: ruleName ?? '', + queryBar: { + query: { + query, + language: 'esql', + }, + }, + }; + }, [query, ruleName]); + const { form } = useFormWithWarnings({ + defaultValue: formDefaultValue, + options: { stripEmptyFields: false, warningValidationCodes: VALIDATION_WARNING_CODES }, + schema, + }); + + const [editMode, setEditMode] = useState(false); + const onCancel = useCallback(() => setEditMode(false), []); + const onEdit = useCallback(() => { + form.reset({ defaultValue: formDefaultValue }); + setEditMode(true); + }, [form, formDefaultValue]); + const onSave = useCallback(async () => { + const { data, isValid } = await form.submit(); + if (isValid) { + await onTranslationUpdate?.(data.ruleName, data.queryBar.query.query); + setEditMode(false); + } + }, [form, onTranslationUpdate]); + const headerComponent = useMemo(() => { return ( = React.memo( ); }, [euiTheme, title]); - const queryTextComponent = useMemo(() => { - if (canEdit) { - return ( - {}} - height={400} - initialViewMode={'viewing'} - /> - ); - } else { - return {query}; + const readQueryComponent = useMemo(() => { + if (editMode) { + return null; } - }, [canEdit, query]); + return ( + <> + {canEdit ? ( + + + + {i18n.EDIT} + + + + ) : ( + + )} + +

{ruleName}

+
+ + {query} + + ); + }, [canEdit, editMode, onEdit, query, ruleName]); + + const editQueryComponent = useMemo(() => { + if (!editMode) { + return null; + } + return ( +
+ + + + {i18n.CANCEL} + + + + + {i18n.SAVE} + + + + + + + + ); + }, [editMode, form, onCancel, onSave]); return ( <> {headerComponent} - - {queryTextComponent} + + {readQueryComponent} + {editQueryComponent} ); } diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rule_details_flyout/translation_tab/schema.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rule_details_flyout/translation_tab/schema.tsx new file mode 100644 index 0000000000000..5b49fbc91f57a --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rule_details_flyout/translation_tab/schema.tsx @@ -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 { queryRequiredValidatorFactory } from '../../../../../detection_engine/rule_creation_ui/validators/query_required_validator_factory'; +import { FIELD_TYPES, fieldValidators, type FormSchema } from '../../../../../shared_imports'; +import type { RuleTranslationSchema } from './types'; +import * as i18n from './translations'; + +export const schema: FormSchema = { + ruleName: { + type: FIELD_TYPES.TEXT, + label: i18n.NAME_LABEL, + validations: [ + { + validator: fieldValidators.emptyField(i18n.NAME_REQUIRED_ERROR_MESSAGE), + }, + ], + }, + queryBar: { + fieldsToValidateOnChange: ['queryBar'], + validations: [ + { + validator: (...args) => { + return queryRequiredValidatorFactory('esql')(...args); + }, + }, + ], + }, +}; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rule_details_flyout/translation_tab/translations.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rule_details_flyout/translation_tab/translations.ts index 1592a80d32478..70325c76b9325 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rule_details_flyout/translation_tab/translations.ts +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rule_details_flyout/translation_tab/translations.ts @@ -21,17 +21,17 @@ export const NAME_LABEL = i18n.translate( } ); -export const SPLUNK_QUERY_TITLE = i18n.translate( - 'xpack.securitySolution.siemMigrations.rules.translationDetails.translationTab.splunkQueryTitle', +export const NAME_REQUIRED_ERROR_MESSAGE = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.translationDetails.translationTab.nameFieldRequiredError', { - defaultMessage: 'Splunk query', + defaultMessage: 'A name is required.', } ); -export const PREBUILT_RULE_QUERY_TITLE = i18n.translate( - 'xpack.securitySolution.siemMigrations.rules.translationDetails.translationTab.prebuiltRuleQueryTitle', +export const SPLUNK_QUERY_TITLE = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.translationDetails.translationTab.splunkQueryTitle', { - defaultMessage: 'Prebuilt rule query', + defaultMessage: 'Splunk query', } ); @@ -48,3 +48,71 @@ export const TRANSLATED_QUERY_AREAL_LABEL = i18n.translate( defaultMessage: 'Translated query', } ); + +export const INSTALLED_LABEL = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.translationDetails.translationTab.installedLabel', + { + defaultMessage: 'Installed', + } +); + +export const EDIT = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.translationDetails.translationTab.editButtonLabel', + { + defaultMessage: 'Edit', + } +); + +export const SAVE = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.translationDetails.translationTab.saveButtonLabel', + { + defaultMessage: 'Save', + } +); + +export const CANCEL = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.translationDetails.translationTab.cancelButtonLabel', + { + defaultMessage: 'Cancel', + } +); + +export const CALLOUT_TRANSLATED_RULE_TITLE = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.translationDetails.translationTab.translatedRuleCalloutTitle', + { + defaultMessage: + 'This rule has been fully translated. Install rule to finish migration. Once installed, you’ll be able to fine tune the rule.', + } +); + +export const CALLOUT_PARTIALLY_TRANSLATED_RULE_TITLE = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.translationDetails.translationTab.partiallyTranslatedRuleCalloutTitle', + { + defaultMessage: + 'Parts of the query couldn’t be translated, please complete to Install the rule and finish migrating.', + } +); + +export const CALLOUT_PARTIALLY_TRANSLATED_RULE_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.translationDetails.translationTab.partiallyTranslatedRuleCalloutDescription', + { + defaultMessage: + 'In order to save this SIEM Rule to Elastic, you’ll need to finish the query and define the rule severity below. Complete the required fields and finalize the query to save as Rule. Or if you need help, please reach out to Elastic support.', + } +); + +export const CALLOUT_NOT_TRANSLATED_RULE_TITLE = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.translationDetails.translationTab.notTranslatedRuleCalloutTitle', + { + defaultMessage: + 'This query couldn’t be translated, please complete to Install the rule and finish migrating.', + } +); + +export const CALLOUT_NOT_TRANSLATED_RULE_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.translationDetails.translationTab.notTranslatedRuleCalloutDescription', + { + defaultMessage: + 'When a query cannot be partially translated, there could be a misalignment in features between the SIEM product.', + } +); diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rule_details_flyout/translation_tab/types.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rule_details_flyout/translation_tab/types.ts new file mode 100644 index 0000000000000..1b509a71f47a6 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rule_details_flyout/translation_tab/types.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +type EsqlLanguage = 'esql'; + +export interface RuleTranslationSchema { + ruleName: string; + // The type is compatible with the validation function used in form schema + queryBar: { query: { query: string; language: EsqlLanguage } }; +} diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rule_details_flyout/translations.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rule_details_flyout/translations.ts index 8e6582b8c198e..47a476ef42899 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rule_details_flyout/translations.ts +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rule_details_flyout/translations.ts @@ -7,6 +7,13 @@ import { i18n } from '@kbn/i18n'; +export const UNKNOWN_MIGRATION_RULE_TITLE = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.translationDetails.unknownMigrationRuleTitle', + { + defaultMessage: 'Unknown migration rule', + } +); + export const OVERVIEW_TAB_LABEL = i18n.translate( 'xpack.securitySolution.siemMigrations.rules.translationDetails.overviewTabLabel', { diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table/index.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table/index.tsx index 106e7ba514d3f..07ba44d4d167e 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table/index.tsx +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table/index.tsx @@ -224,11 +224,22 @@ export const MigrationRulesTable: React.FC = React.mem [installSingleRule, isLoading] ); + const getMigrationRule = useCallback( + (ruleId: string) => { + if (!isLoading && ruleMigrations.length) { + return ruleMigrations.find((item) => item.id === ruleId); + } + }, + [isLoading, ruleMigrations] + ); + const { migrationRuleDetailsFlyout: rulePreviewFlyout, openMigrationRuleDetails: openRulePreview, } = useMigrationRuleDetailsFlyout({ + isLoading, prebuiltRules, + getMigrationRule, ruleActionsFactory, }); diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/name.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/name.tsx index dd77636521eda..ce0e1d3c99d8d 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/name.tsx +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/name.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { EuiLink } from '@elastic/eui'; +import { EuiLink, EuiText } from '@elastic/eui'; import type { RuleMigration } from '../../../../../common/siem_migrations/model/rule_migration.gen'; import * as i18n from './translations'; import type { TableColumn } from './constants'; @@ -17,6 +17,13 @@ interface NameProps { } const Name = ({ rule, openMigrationRuleDetails }: NameProps) => { + if (!rule.elastic_rule) { + return ( + + {rule.original_rule.title} + + ); + } return ( { @@ -24,7 +31,7 @@ const Name = ({ rule, openMigrationRuleDetails }: NameProps) => { }} data-test-subj="ruleName" > - {rule.elastic_rule?.title} + {rule.elastic_rule.title} ); }; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/status.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/status.tsx index 5daec1f1b4fa9..2936878c93b8b 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/status.tsx +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/status.tsx @@ -15,9 +15,7 @@ export const createStatusColumn = (): TableColumn => { return { field: 'translation_result', name: i18n.COLUMN_STATUS, - render: (value: RuleMigration['translation_result'], rule: RuleMigration) => ( - - ), + render: (_, rule: RuleMigration) => , sortable: true, truncateText: true, width: '15%', diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/status_badge/index.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/status_badge/index.tsx index 8f8bcff40f674..f1f435c7e14ad 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/status_badge/index.tsx +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/status_badge/index.tsx @@ -10,7 +10,11 @@ import { euiLightVars } from '@kbn/ui-theme'; import { EuiFlexGroup, EuiFlexItem, EuiHealth, EuiIcon, EuiToolTip } from '@elastic/eui'; import { css } from '@emotion/css'; -import type { RuleMigrationTranslationResult } from '../../../../../common/siem_migrations/model/rule_migration.gen'; +import { + RuleMigrationStatusEnum, + type RuleMigration, + type RuleMigrationTranslationResult, +} from '../../../../../common/siem_migrations/model/rule_migration.gen'; import { convertTranslationResultIntoText } from '../../utils/helpers'; import * as i18n from './translations'; @@ -27,35 +31,51 @@ const statusToColorMap: Record = { }; interface StatusBadgeProps { - value?: RuleMigrationTranslationResult; - installedRuleId?: string; + migrationRule: RuleMigration; 'data-test-subj'?: string; } export const StatusBadge: React.FC = React.memo( - ({ value, installedRuleId, 'data-test-subj': dataTestSubj = 'translation-result' }) => { - const translationResult = installedRuleId ? 'full' : value ?? 'untranslatable'; - const displayValue = installedRuleId - ? i18n.RULE_STATUS_INSTALLED - : convertTranslationResultIntoText(translationResult); - const color = statusToColorMap[translationResult]; + ({ migrationRule, 'data-test-subj': dataTestSubj = 'translation-result' }) => { + // Installed + if (migrationRule.elastic_rule?.id) { + return ( + + + + + + {i18n.RULE_STATUS_INSTALLED} + + + ); + } - return ( - - {installedRuleId ? ( + // Failed + if (migrationRule.status === RuleMigrationStatusEnum.failed) { + return ( + - + - {displayValue} + {i18n.RULE_STATUS_FAILED} - ) : ( - -
- {displayValue} -
-
- )} +
+ ); + } + + const translationResult = migrationRule.translation_result ?? 'untranslatable'; + const displayValue = convertTranslationResultIntoText(translationResult); + const color = statusToColorMap[translationResult]; + + return ( + + +
+ {displayValue} +
+
); } diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/status_badge/translations.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/status_badge/translations.ts index 0a7b1c37f7acf..a84fd298ed364 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/status_badge/translations.ts +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/status_badge/translations.ts @@ -13,3 +13,10 @@ export const RULE_STATUS_INSTALLED = i18n.translate( defaultMessage: 'Installed', } ); + +export const RULE_STATUS_FAILED = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.status.failedLabel', + { + defaultMessage: 'Error', + } +); diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/hooks/use_migration_rule_preview_flyout.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/hooks/use_migration_rule_preview_flyout.tsx index 4df54d6331f66..4efaa4aba7181 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/hooks/use_migration_rule_preview_flyout.tsx +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/hooks/use_migration_rule_preview_flyout.tsx @@ -8,7 +8,6 @@ import type { ReactNode } from 'react'; import React, { useCallback, useState, useMemo } from 'react'; import type { EuiTabbedContentTab } from '@elastic/eui'; -import type { RuleResponse } from '../../../../common/api/detection_engine'; import type { PrebuiltRuleVersion, RuleMigration, @@ -16,7 +15,9 @@ import type { import { MigrationRuleDetailsFlyout } from '../components/rule_details_flyout'; interface UseMigrationRuleDetailsFlyoutParams { + isLoading?: boolean; prebuiltRules: Record; + getMigrationRule: (ruleId: string) => RuleMigration | undefined; ruleActionsFactory: (ruleMigration: RuleMigration, closeRulePreview: () => void) => ReactNode; extraTabsFactory?: (ruleMigration: RuleMigration) => EuiTabbedContentTab[]; } @@ -28,13 +29,34 @@ interface UseMigrationRuleDetailsFlyoutResult { } export function useMigrationRuleDetailsFlyout({ + isLoading, prebuiltRules, + getMigrationRule, extraTabsFactory, ruleActionsFactory, }: UseMigrationRuleDetailsFlyoutParams): UseMigrationRuleDetailsFlyoutResult { - const [ruleMigration, setMigrationRuleForPreview] = useState(); - const [matchedPrebuiltRule, setMatchedPrebuiltRule] = useState(); - const closeMigrationRuleDetails = useCallback(() => setMigrationRuleForPreview(undefined), []); + const [migrationRuleId, setMigrationRuleId] = useState(); + + const ruleMigration = useMemo(() => { + if (migrationRuleId) { + return getMigrationRule(migrationRuleId); + } + }, [getMigrationRule, migrationRuleId]); + const matchedPrebuiltRule = useMemo(() => { + if (ruleMigration) { + // Find matched prebuilt rule if any and prioritize its installed version + const matchedPrebuiltRuleVersion = ruleMigration.elastic_rule?.prebuilt_rule_id + ? prebuiltRules[ruleMigration.elastic_rule.prebuilt_rule_id] + : undefined; + return matchedPrebuiltRuleVersion?.current ?? matchedPrebuiltRuleVersion?.target; + } + }, [prebuiltRules, ruleMigration]); + + const openMigrationRuleDetails = useCallback((rule: RuleMigration) => { + setMigrationRuleId(rule.id); + }, []); + const closeMigrationRuleDetails = useCallback(() => setMigrationRuleId(undefined), []); + const ruleActions = useMemo( () => ruleMigration && ruleActionsFactory(ruleMigration, closeMigrationRuleDetails), [ruleMigration, ruleActionsFactory, closeMigrationRuleDetails] @@ -44,21 +66,6 @@ export function useMigrationRuleDetailsFlyout({ [ruleMigration, extraTabsFactory] ); - const openMigrationRuleDetails = useCallback( - (rule: RuleMigration) => { - setMigrationRuleForPreview(rule); - - // Find matched prebuilt rule if any and prioritize its installed version - const matchedPrebuiltRuleVersion = rule.elastic_rule?.prebuilt_rule_id - ? prebuiltRules[rule.elastic_rule.prebuilt_rule_id] - : undefined; - const prebuiltRule = - matchedPrebuiltRuleVersion?.current ?? matchedPrebuiltRuleVersion?.target; - setMatchedPrebuiltRule(prebuiltRule); - }, - [prebuiltRules] - ); - return { migrationRuleDetailsFlyout: ruleMigration && ( ), openMigrationRuleDetails, diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/logic/translations.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/logic/translations.ts index 3f92da4e8ddcc..ef3521fd37301 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/logic/translations.ts +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/logic/translations.ts @@ -34,3 +34,10 @@ export const INSTALL_MIGRATION_RULES_FAILURE = i18n.translate( defaultMessage: 'Failed to install migration rules', } ); + +export const UPDATE_MIGRATION_RULES_FAILURE = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.updateMigrationRulesFailDescription', + { + defaultMessage: 'Failed to update migration rules', + } +); diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/logic/use_get_migration_rules.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/logic/use_get_migration_rules.ts index 4109575459233..b06f041e2c58e 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/logic/use_get_migration_rules.ts +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/logic/use_get_migration_rules.ts @@ -18,9 +18,10 @@ export const useGetMigrationRules = (params: { migrationId: string; page?: number; perPage?: number; - sortField: string; - sortDirection: 'asc' | 'desc'; + sortField?: string; + sortDirection?: 'asc' | 'desc'; searchTerm?: string; + ids?: string[]; }) => { const { addError } = useAppToasts(); diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/logic/use_update_migration_rules.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/logic/use_update_migration_rules.ts new file mode 100644 index 0000000000000..1e0fa22c466f0 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/logic/use_update_migration_rules.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useMutation } from '@tanstack/react-query'; +import type { UpdateRuleMigrationData } from '../../../../common/siem_migrations/model/rule_migration.gen'; +import { SIEM_RULE_MIGRATIONS_PATH } from '../../../../common/siem_migrations/constants'; +import type { UpdateRuleMigrationResponse } from '../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; +import { useAppToasts } from '../../../common/hooks/use_app_toasts'; +import * as i18n from './translations'; +import { useInvalidateGetMigrationRules } from './use_get_migration_rules'; +import { useInvalidateGetMigrationTranslationStats } from './use_get_migration_translation_stats'; +import { updateMigrationRules } from '../api'; + +export const UPDATE_MIGRATION_RULES_MUTATION_KEY = ['PUT', SIEM_RULE_MIGRATIONS_PATH]; + +export const useUpdateMigrationRules = (migrationId: string) => { + const { addError } = useAppToasts(); + + const invalidateGetRuleMigrations = useInvalidateGetMigrationRules(migrationId); + const invalidateGetMigrationTranslationStats = + useInvalidateGetMigrationTranslationStats(migrationId); + + return useMutation( + (rulesToUpdate) => updateMigrationRules({ rulesToUpdate }), + { + mutationKey: UPDATE_MIGRATION_RULES_MUTATION_KEY, + onError: (error) => { + addError(error, { title: i18n.UPDATE_MIGRATION_RULES_FAILURE }); + }, + onSettled: () => { + invalidateGetRuleMigrations(); + invalidateGetMigrationTranslationStats(); + }, + } + ); +}; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/utils/translations.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/utils/translations.ts index 366ad435c61b4..03f76cb833818 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/utils/translations.ts +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/utils/translations.ts @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n'; export const SIEM_TRANSLATION_RESULT_FULL_LABEL = i18n.translate( 'xpack.securitySolution.siemMigrations.rules.translationResult.full', { - defaultMessage: 'Fully translated', + defaultMessage: 'Translated', } ); diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/get.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/get.ts index 30037aeea88ae..2450bd02edadc 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/get.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/get.ts @@ -45,13 +45,14 @@ export const registerSiemRuleMigrationsGetRoute = ( sort_field: sortField, sort_direction: sortDirection, search_term: searchTerm, + ids, } = req.query; try { const ctx = await context.resolve(['securitySolution']); const ruleMigrationsClient = ctx.securitySolution.getSiemRuleMigrationsClient(); const options: RuleMigrationGetOptions = { - filters: { searchTerm }, + filters: { searchTerm, ids }, sort: { sortField, sortDirection }, size: perPage, from: page && perPage ? page * perPage : 0, diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/util/installation.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/util/installation.ts index 8716c83ce6ba3..de95d818dd18d 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/util/installation.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/util/installation.ts @@ -7,13 +7,13 @@ import type { Logger, SavedObjectsClientContract } from '@kbn/core/server'; import type { RulesClient } from '@kbn/alerting-plugin/server'; +import type { UpdateRuleMigrationData } from '../../../../../../common/siem_migrations/model/rule_migration.gen'; import { initPromisePool } from '../../../../../utils/promise_pool'; import type { SecuritySolutionApiRequestHandlerContext } from '../../../../..'; import { performTimelinesInstallation } from '../../../../detection_engine/prebuilt_rules/logic/perform_timelines_installation'; import { createPrebuiltRules } from '../../../../detection_engine/prebuilt_rules/logic/rule_objects/create_prebuilt_rules'; import type { IDetectionRulesClient } from '../../../../detection_engine/rule_management/logic/detection_rules_client/detection_rules_client_interface'; import type { RuleResponse } from '../../../../../../common/api/detection_engine'; -import type { UpdateRuleMigrationInput } from '../../data/rule_migrations_data_rules_client'; import type { StoredRuleMigration } from '../../types'; import { getPrebuiltRules, getUniquePrebuiltRuleIds } from './prebuilt_rules'; import { @@ -32,7 +32,7 @@ const installPrebuiltRules = async ( rulesClient: RulesClient, savedObjectsClient: SavedObjectsClientContract, detectionRulesClient: IDetectionRulesClient -): Promise => { +): Promise => { // Get required prebuilt rules const prebuiltRulesIds = getUniquePrebuiltRuleIds(rulesToInstall); const prebuiltRules = await getPrebuiltRules(rulesClient, savedObjectsClient, prebuiltRulesIds); @@ -66,7 +66,7 @@ const installPrebuiltRules = async ( ]; // Create migration rules updates templates - const rulesToUpdate: UpdateRuleMigrationInput[] = []; + const rulesToUpdate: UpdateRuleMigrationData[] = []; installedRules.forEach((installedRule) => { const filteredRules = rulesToInstall.filter( (rule) => rule.elastic_rule?.prebuilt_rule_id === installedRule.rule_id @@ -89,8 +89,8 @@ export const installCustomRules = async ( enabled: boolean, detectionRulesClient: IDetectionRulesClient, logger: Logger -): Promise => { - const rulesToUpdate: UpdateRuleMigrationInput[] = []; +): Promise => { + const rulesToUpdate: UpdateRuleMigrationData[] = []; const createCustomRulesOutcome = await initPromisePool({ concurrency: MAX_CUSTOM_RULES_TO_CREATE_IN_PARALLEL, items: rulesToInstall, @@ -211,10 +211,7 @@ export const installTranslated = async ({ logger ); - const rulesToUpdate: UpdateRuleMigrationInput[] = [ - ...updatedPrebuiltRules, - ...updatedCustomRules, - ]; + const rulesToUpdate: UpdateRuleMigrationData[] = [...updatedPrebuiltRules, ...updatedCustomRules]; if (rulesToUpdate.length) { await ruleMigrationsClient.data.rules.update(rulesToUpdate); diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_rules_client.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_rules_client.ts index 8cdc776f631b0..b483b3bdd4fbb 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_rules_client.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_rules_client.ts @@ -17,24 +17,21 @@ import type { } from '@elastic/elasticsearch/lib/api/types'; import type { StoredRuleMigration } from '../types'; import { SiemMigrationStatus } from '../../../../../common/siem_migrations/constants'; -import type { - ElasticRule, - RuleMigration, - RuleMigrationTaskStats, - RuleMigrationTranslationStats, +import { + type RuleMigration, + type RuleMigrationTaskStats, + type RuleMigrationTranslationStats, + type UpdateRuleMigrationData, } from '../../../../../common/siem_migrations/model/rule_migration.gen'; import { RuleMigrationsDataBaseClient } from './rule_migrations_data_base_client'; import { getSortingOptions, type RuleMigrationSort } from './sort'; import { conditions as searchConditions } from './search'; +import { convertEsqlQueryToTranslationResult } from './utils'; export type CreateRuleMigrationInput = Omit< RuleMigration, '@timestamp' | 'id' | 'status' | 'created_by' >; -export type UpdateRuleMigrationInput = { elastic_rule?: Partial } & Pick< - RuleMigration, - 'id' | 'translation_result' | 'comments' ->; export type RuleMigrationDataStats = Omit; export type RuleMigrationAllDataStats = RuleMigrationDataStats[]; @@ -90,22 +87,30 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient } /** Updates an array of rule migrations to be processed */ - async update(ruleMigrations: UpdateRuleMigrationInput[]): Promise { + async update(ruleMigrations: UpdateRuleMigrationData[]): Promise { const index = await this.getIndexName(); - let ruleMigrationsSlice: UpdateRuleMigrationInput[]; + let ruleMigrationsSlice: UpdateRuleMigrationData[]; const updatedAt = new Date().toISOString(); while ((ruleMigrationsSlice = ruleMigrations.splice(0, BULK_MAX_SIZE)).length) { await this.esClient .bulk({ refresh: 'wait_for', operations: ruleMigrationsSlice.flatMap((ruleMigration) => { - const { id, ...rest } = ruleMigration; + const { + id, + translation_result: translationResult, + elastic_rule: elasticRule, + ...rest + } = ruleMigration; return [ { update: { _index: index, _id: id } }, { doc: { ...rest, + elastic_rule: elasticRule, + translation_result: + translationResult ?? convertEsqlQueryToTranslationResult(elasticRule?.query), updated_by: this.username, updated_at: updatedAt, }, diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/utils.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/utils.ts new file mode 100644 index 0000000000000..ca547da00e8c9 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/utils.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { parseEsqlQuery } from '@kbn/securitysolution-utils'; +import { + RuleMigrationTranslationResultEnum, + type RuleMigrationTranslationResult, +} from '../../../../../common/siem_migrations/model/rule_migration.gen'; + +export const isValidEsqlQuery = (esqlQuery: string) => { + const { isEsqlQueryAggregating, hasMetadataOperator, errors } = parseEsqlQuery(esqlQuery); + + // Check if there are any syntax errors + if (errors.length) { + return false; + } + + // non-aggregating query which does not have metadata, is not a valid one + if (!isEsqlQueryAggregating && !hasMetadataOperator) { + return false; + } + + return true; +}; + +export const convertEsqlQueryToTranslationResult = ( + esqlQuery?: string +): RuleMigrationTranslationResult | undefined => { + if (esqlQuery === undefined) { + return undefined; + } + if (esqlQuery === '') { + return RuleMigrationTranslationResultEnum.untranslatable; + } + return isValidEsqlQuery(esqlQuery) + ? RuleMigrationTranslationResultEnum.full + : RuleMigrationTranslationResultEnum.partial; +};