From 46cf54359366636227c8bd62f0bd7d45a86ebc0d Mon Sep 17 00:00:00 2001 From: Ievgen Sorokopud Date: Fri, 29 Nov 2024 18:05:20 +0100 Subject: [PATCH] [Rules migration] Add `install` and `install all` migration rules endpoints (#11283) (#202026) ## Summary [Internal link](https://github.com/elastic/security-team/issues/10820) to the feature details With these changes we two new routes: * `/internal/siem_migrations/rules/install`: allows to install a specific set of migration rules * `/internal/siem_migrations/rules/install_translated`: allows to install all translated rules in specified migration Also we connect these two new API calls with the "Install" button within the "migration rules" table and the "Install translated rules" button on the "SIEM migration rules" page. ### Screenshots https://github.com/user-attachments/assets/29390d07-eab5-4157-8958-1e3f8459db09 --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Sergi Massaneda (cherry picked from commit 07fbb925859121d391271a183c8ba00109f53ce1) # Conflicts: # x-pack/plugins/security_solution/public/siem_migrations/rules/api/api.ts # x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/nodes/match_prebuilt_rule/match_prebuilt_rule.ts # x-pack/test/api_integration/services/security_solution_api.gen.ts --- .../common/api/quickstart_client.gen.ts | 46 ++++ .../common/siem_migrations/constants.ts | 8 + .../model/api/rules/rule_migration.gen.ts | 42 ++++ .../api/rules/rule_migration.schema.yaml | 67 ++++++ .../public/siem_migrations/rules/api/api.ts | 35 +++ ...ns.ts => use_get_migration_rules_query.ts} | 25 +- ...se_install_all_migration_rules_mutation.ts | 39 +++ .../use_install_migration_rules_mutation.ts | 36 +++ .../components/rules_table/bulk_actions.tsx | 68 ++++++ .../rules/components/rules_table/filters.tsx | 9 +- .../rules/components/rules_table/index.tsx | 95 ++++++-- .../components/rules_table/translations.ts | 29 +++ .../rules_table_columns/actions.tsx | 111 +++++++++ .../rules_table_columns/constants.tsx} | 10 +- .../rules_table_columns/index.tsx} | 9 +- .../components/rules_table_columns/name.tsx | 49 ++++ .../rules_table_columns/risk_score.tsx | 27 +++ .../rules_table_columns/severity.tsx | 27 +++ .../components/rules_table_columns/status.tsx | 25 ++ .../rules_table_columns/translations.ts | 71 ++++++ .../translation_details_flyout/index.tsx | 8 +- .../rules/hooks/use_rules_table_columns.tsx | 115 ++------- .../rules/logic/translations.ts | 22 ++ .../rules/logic/use_get_migration_rules.ts | 20 ++ .../logic/use_install_all_migration_rules.ts | 20 ++ .../logic/use_install_migration_rules.ts | 20 ++ .../siem_migrations/rules/pages/index.tsx | 21 +- .../public/siem_migrations/rules/types.ts | 11 + .../lib/siem_migrations/rules/api/get.ts | 2 +- .../lib/siem_migrations/rules/api/index.ts | 4 + .../rules/api/rules/install.ts | 69 ++++++ .../rules/api/rules/install_translated.ts | 67 ++++++ .../rules/api/util/installation.ts | 223 ++++++++++++++++++ .../data/rule_migrations_data_rules_client.ts | 41 +++- .../match_prebuilt_rule.ts | 2 + .../services/security_solution_api.gen.ts | 49 ++++ 36 files changed, 1361 insertions(+), 161 deletions(-) rename x-pack/plugins/security_solution/public/siem_migrations/rules/api/hooks/{use_get_rule_migrations.ts => use_get_migration_rules_query.ts} (58%) create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/api/hooks/use_install_all_migration_rules_mutation.ts create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/api/hooks/use_install_migration_rules_mutation.ts create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table/bulk_actions.tsx create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/actions.tsx rename x-pack/plugins/security_solution/public/siem_migrations/rules/{hooks/translations.ts => components/rules_table_columns/constants.tsx} (53%) rename x-pack/plugins/security_solution/public/siem_migrations/rules/{utils/constants.ts => components/rules_table_columns/index.tsx} (57%) create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/name.tsx create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/risk_score.tsx create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/severity.tsx create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/status.tsx create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/translations.ts create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/logic/translations.ts create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/logic/use_get_migration_rules.ts create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/logic/use_install_all_migration_rules.ts create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/logic/use_install_migration_rules.ts create mode 100644 x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/rules/install.ts create mode 100644 x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/rules/install_translated.ts create mode 100644 x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/util/installation.ts diff --git a/x-pack/plugins/security_solution/common/api/quickstart_client.gen.ts b/x-pack/plugins/security_solution/common/api/quickstart_client.gen.ts index 599af96944447..6a85c76c27e56 100644 --- a/x-pack/plugins/security_solution/common/api/quickstart_client.gen.ts +++ b/x-pack/plugins/security_solution/common/api/quickstart_client.gen.ts @@ -378,6 +378,11 @@ import type { GetRuleMigrationResourcesResponse, GetRuleMigrationStatsRequestParamsInput, GetRuleMigrationStatsResponse, + InstallMigrationRulesRequestParamsInput, + InstallMigrationRulesRequestBodyInput, + InstallMigrationRulesResponse, + InstallTranslatedMigrationRulesRequestParamsInput, + InstallTranslatedMigrationRulesResponse, StartRuleMigrationRequestParamsInput, StartRuleMigrationRequestBodyInput, StartRuleMigrationResponse, @@ -1608,6 +1613,22 @@ finalize it. }) .catch(catchAxiosErrorFormatAndThrow); } + /** + * Installs migration rules + */ + async installMigrationRules(props: InstallMigrationRulesProps) { + this.log.info(`${new Date().toISOString()} Calling API InstallMigrationRules`); + return this.kbnClient + .request({ + path: replaceParams('/internal/siem_migrations/rules/{migration_id}/install', props.params), + headers: { + [ELASTIC_HTTP_VERSION_HEADER]: '1', + }, + method: 'POST', + body: props.body, + }) + .catch(catchAxiosErrorFormatAndThrow); + } /** * Install and update all Elastic prebuilt detection rules and Timelines. */ @@ -1636,6 +1657,24 @@ finalize it. }) .catch(catchAxiosErrorFormatAndThrow); } + /** + * Installs all translated migration rules + */ + async installTranslatedMigrationRules(props: InstallTranslatedMigrationRulesProps) { + this.log.info(`${new Date().toISOString()} Calling API InstallTranslatedMigrationRules`); + return this.kbnClient + .request({ + path: replaceParams( + '/internal/siem_migrations/rules/{migration_id}/install_translated', + props.params + ), + headers: { + [ELASTIC_HTTP_VERSION_HEADER]: '1', + }, + method: 'POST', + }) + .catch(catchAxiosErrorFormatAndThrow); + } async internalUploadAssetCriticalityRecords(props: InternalUploadAssetCriticalityRecordsProps) { this.log.info(`${new Date().toISOString()} Calling API InternalUploadAssetCriticalityRecords`); return this.kbnClient @@ -2367,9 +2406,16 @@ export interface InitEntityEngineProps { export interface InitEntityStoreProps { body: InitEntityStoreRequestBodyInput; } +export interface InstallMigrationRulesProps { + params: InstallMigrationRulesRequestParamsInput; + body: InstallMigrationRulesRequestBodyInput; +} export interface InstallPrepackedTimelinesProps { body: InstallPrepackedTimelinesRequestBodyInput; } +export interface InstallTranslatedMigrationRulesProps { + params: InstallTranslatedMigrationRulesRequestParamsInput; +} export interface InternalUploadAssetCriticalityRecordsProps { attachment: FormData; } diff --git a/x-pack/plugins/security_solution/common/siem_migrations/constants.ts b/x-pack/plugins/security_solution/common/siem_migrations/constants.ts index 8a2d6cf3775c9..d5009bdafc99d 100644 --- a/x-pack/plugins/security_solution/common/siem_migrations/constants.ts +++ b/x-pack/plugins/security_solution/common/siem_migrations/constants.ts @@ -5,6 +5,8 @@ * 2.0. */ +import type { Severity } from '@kbn/securitysolution-io-ts-alerting-types'; + export const SIEM_MIGRATIONS_PATH = '/internal/siem_migrations' as const; export const SIEM_RULE_MIGRATIONS_PATH = `${SIEM_MIGRATIONS_PATH}/rules` as const; @@ -14,6 +16,9 @@ export const SIEM_RULE_MIGRATION_START_PATH = `${SIEM_RULE_MIGRATION_PATH}/start export const SIEM_RULE_MIGRATION_RETRY_PATH = `${SIEM_RULE_MIGRATION_PATH}/retry` as const; export const SIEM_RULE_MIGRATION_STATS_PATH = `${SIEM_RULE_MIGRATION_PATH}/stats` as const; export const SIEM_RULE_MIGRATION_STOP_PATH = `${SIEM_RULE_MIGRATION_PATH}/stop` as const; +export const SIEM_RULE_MIGRATION_INSTALL_PATH = `${SIEM_RULE_MIGRATION_PATH}/install` as const; +export const SIEM_RULE_MIGRATION_INSTALL_TRANSLATED_PATH = + `${SIEM_RULE_MIGRATION_PATH}/install_translated` as const; export const SIEM_RULE_MIGRATION_RESOURCES_PATH = `${SIEM_RULE_MIGRATION_PATH}/resources` as const; @@ -29,3 +34,6 @@ export enum SiemMigrationRuleTranslationResult { PARTIAL = 'partial', UNTRANSLATABLE = 'untranslatable', } + +export const DEFAULT_TRANSLATION_RISK_SCORE = 21; +export const DEFAULT_TRANSLATION_SEVERITY: Severity = 'low'; 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 36728e0e928a0..24875d6ccfcf1 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 @@ -88,6 +88,48 @@ export type GetRuleMigrationStatsRequestParamsInput = z.input< export type GetRuleMigrationStatsResponse = z.infer; export const GetRuleMigrationStatsResponse = RuleMigrationTaskStats; +export type InstallMigrationRulesRequestParams = z.infer; +export const InstallMigrationRulesRequestParams = z.object({ + migration_id: NonEmptyString, +}); +export type InstallMigrationRulesRequestParamsInput = z.input< + typeof InstallMigrationRulesRequestParams +>; + +export type InstallMigrationRulesRequestBody = z.infer; +export const InstallMigrationRulesRequestBody = z.array(NonEmptyString); +export type InstallMigrationRulesRequestBodyInput = z.input< + typeof InstallMigrationRulesRequestBody +>; + +export type InstallMigrationRulesResponse = z.infer; +export const InstallMigrationRulesResponse = z.object({ + /** + * Indicates rules migrations have been installed. + */ + installed: z.boolean(), +}); + +export type InstallTranslatedMigrationRulesRequestParams = z.infer< + typeof InstallTranslatedMigrationRulesRequestParams +>; +export const InstallTranslatedMigrationRulesRequestParams = z.object({ + migration_id: NonEmptyString, +}); +export type InstallTranslatedMigrationRulesRequestParamsInput = z.input< + typeof InstallTranslatedMigrationRulesRequestParams +>; + +export type InstallTranslatedMigrationRulesResponse = z.infer< + typeof InstallTranslatedMigrationRulesResponse +>; +export const InstallTranslatedMigrationRulesResponse = z.object({ + /** + * Indicates rules migrations have been installed. + */ + installed: z.boolean(), +}); + export type StartRuleMigrationRequestParams = z.infer; export const StartRuleMigrationRequestParams = z.object({ migration_id: NonEmptyString, 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 fdb589e7b45cd..31cd77bef2635 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 @@ -79,6 +79,73 @@ paths: type: boolean description: Indicates rules migrations have been updated. + /internal/siem_migrations/rules/{migration_id}/install: + post: + summary: Installs translated migration rules + operationId: InstallMigrationRules + x-codegen-enabled: true + description: Installs migration rules + tags: + - SIEM Rule Migrations + parameters: + - name: migration_id + in: path + required: true + schema: + description: The migration id to isnstall rules for + $ref: '../../common.schema.yaml#/components/schemas/NonEmptyString' + requestBody: + required: true + content: + application/json: + schema: + type: array + items: + description: The rule migration id + $ref: '../../common.schema.yaml#/components/schemas/NonEmptyString' + responses: + 200: + description: Indicates rules migrations have been installed correctly. + content: + application/json: + schema: + type: object + required: + - installed + properties: + installed: + type: boolean + description: Indicates rules migrations have been installed. + + /internal/siem_migrations/rules/{migration_id}/install_translated: + post: + summary: Installs all translated migration rules + operationId: InstallTranslatedMigrationRules + x-codegen-enabled: true + description: Installs all translated migration rules + tags: + - SIEM Rule Migrations + parameters: + - name: migration_id + in: path + required: true + schema: + description: The migration id to install translated rules for + $ref: '../../common.schema.yaml#/components/schemas/NonEmptyString' + responses: + 200: + description: Indicates rules migrations have been installed correctly. + content: + application/json: + schema: + type: object + required: + - installed + properties: + installed: + type: boolean + description: Indicates rules migrations have been installed. + /internal/siem_migrations/rules/stats: get: summary: Retrieves the stats for all rule migrations diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/api/api.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/api/api.ts index 7232cb722bd1a..3c1edb9ad6249 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/api/api.ts +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/api/api.ts @@ -11,12 +11,17 @@ import { KibanaServices } from '../../../common/lib/kibana'; import { SIEM_RULE_MIGRATIONS_ALL_STATS_PATH, + SIEM_RULE_MIGRATION_INSTALL_TRANSLATED_PATH, + SIEM_RULE_MIGRATION_INSTALL_PATH, SIEM_RULE_MIGRATION_PATH, } from '../../../../common/siem_migrations/constants'; import type { GetAllStatsRuleMigrationResponse, GetRuleMigrationResponse, + InstallTranslatedMigrationRulesResponse, + InstallMigrationRulesResponse, } from '../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; +import type { InstallTranslatedRulesProps, InstallRulesProps } from '../types'; /** * Retrieves the stats for all the existing migrations, aggregated by `migration_id`. @@ -64,3 +69,33 @@ export const getRuleMigrations = async ({ } ); }; + +export const installMigrationRules = async ({ + migrationId, + ids, + signal, +}: InstallRulesProps): Promise => { + return KibanaServices.get().http.fetch( + replaceParams(SIEM_RULE_MIGRATION_INSTALL_PATH, { migration_id: migrationId }), + { + method: 'POST', + version: '1', + body: JSON.stringify(ids), + signal, + } + ); +}; + +export const installTranslatedMigrationRules = async ({ + migrationId, + signal, +}: InstallTranslatedRulesProps): Promise => { + return KibanaServices.get().http.fetch( + replaceParams(SIEM_RULE_MIGRATION_INSTALL_TRANSLATED_PATH, { migration_id: migrationId }), + { + method: 'POST', + version: '1', + signal, + } + ); +}; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/api/hooks/use_get_rule_migrations.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/api/hooks/use_get_migration_rules_query.ts similarity index 58% rename from x-pack/plugins/security_solution/public/siem_migrations/rules/api/hooks/use_get_rule_migrations.ts rename to x-pack/plugins/security_solution/public/siem_migrations/rules/api/hooks/use_get_migration_rules_query.ts index 76cf01c6c35d0..fece8f8c3ca07 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/api/hooks/use_get_rule_migrations.ts +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/api/hooks/use_get_migration_rules_query.ts @@ -6,14 +6,15 @@ */ import type { UseQueryOptions } from '@tanstack/react-query'; -import { useQuery } from '@tanstack/react-query'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; import { replaceParams } from '@kbn/openapi-common/shared'; +import { useCallback } from 'react'; import { DEFAULT_QUERY_OPTIONS } from './constants'; import { getRuleMigrations } from '../api'; import type { GetRuleMigrationResponse } from '../../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; import { SIEM_RULE_MIGRATION_PATH } from '../../../../../common/siem_migrations/constants'; -export const useGetRuleMigrationsQuery = ( +export const useGetMigrationRulesQuery = ( migrationId: string, options?: UseQueryOptions ) => { @@ -31,3 +32,23 @@ export const useGetRuleMigrationsQuery = ( } ); }; + +/** + * We should use this hook to invalidate the rule migrations cache. For + * example, rule migrations mutations, like installing a rule, should lead to cache invalidation. + * + * @returns A rule migrations cache invalidation callback + */ +export const useInvalidateGetMigrationRulesQuery = (migrationId: string) => { + const queryClient = useQueryClient(); + + const SPECIFIC_MIGRATION_PATH = replaceParams(SIEM_RULE_MIGRATION_PATH, { + migration_id: migrationId, + }); + + return useCallback(() => { + queryClient.invalidateQueries(['GET', SPECIFIC_MIGRATION_PATH], { + refetchType: 'active', + }); + }, [SPECIFIC_MIGRATION_PATH, queryClient]); +}; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/api/hooks/use_install_all_migration_rules_mutation.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/api/hooks/use_install_all_migration_rules_mutation.ts new file mode 100644 index 0000000000000..f946dc165450f --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/api/hooks/use_install_all_migration_rules_mutation.ts @@ -0,0 +1,39 @@ +/* + * 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 { UseMutationOptions } from '@tanstack/react-query'; +import { useMutation } from '@tanstack/react-query'; +import type { InstallTranslatedMigrationRulesResponse } from '../../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; +import { SIEM_RULE_MIGRATION_INSTALL_TRANSLATED_PATH } from '../../../../../common/siem_migrations/constants'; +import { installTranslatedMigrationRules } from '../api'; +import { useInvalidateGetMigrationRulesQuery } from './use_get_migration_rules_query'; + +export const INSTALL_ALL_MIGRATION_RULES_MUTATION_KEY = [ + 'POST', + SIEM_RULE_MIGRATION_INSTALL_TRANSLATED_PATH, +]; + +export const useInstallAllMigrationRulesMutation = ( + migrationId: string, + options?: UseMutationOptions +) => { + const invalidateGetRuleMigrationsQuery = useInvalidateGetMigrationRulesQuery(migrationId); + + return useMutation( + () => installTranslatedMigrationRules({ migrationId }), + { + ...options, + mutationKey: INSTALL_ALL_MIGRATION_RULES_MUTATION_KEY, + onSettled: (...args) => { + invalidateGetRuleMigrationsQuery(); + + if (options?.onSettled) { + options.onSettled(...args); + } + }, + } + ); +}; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/api/hooks/use_install_migration_rules_mutation.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/api/hooks/use_install_migration_rules_mutation.ts new file mode 100644 index 0000000000000..6aaff55e24513 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/api/hooks/use_install_migration_rules_mutation.ts @@ -0,0 +1,36 @@ +/* + * 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 { UseMutationOptions } from '@tanstack/react-query'; +import { useMutation } from '@tanstack/react-query'; +import type { InstallMigrationRulesResponse } from '../../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; +import { SIEM_RULE_MIGRATION_INSTALL_PATH } from '../../../../../common/siem_migrations/constants'; +import { installMigrationRules } from '../api'; +import { useInvalidateGetMigrationRulesQuery } from './use_get_migration_rules_query'; + +export const INSTALL_MIGRATION_RULES_MUTATION_KEY = ['POST', SIEM_RULE_MIGRATION_INSTALL_PATH]; + +export const useInstallMigrationRulesMutation = ( + migrationId: string, + options?: UseMutationOptions +) => { + const invalidateGetRuleMigrationsQuery = useInvalidateGetMigrationRulesQuery(migrationId); + + return useMutation( + (ids: string[]) => installMigrationRules({ migrationId, ids }), + { + ...options, + mutationKey: INSTALL_MIGRATION_RULES_MUTATION_KEY, + onSettled: (...args) => { + invalidateGetRuleMigrationsQuery(); + + if (options?.onSettled) { + options.onSettled(...args); + } + }, + } + ); +}; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table/bulk_actions.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table/bulk_actions.tsx new file mode 100644 index 0000000000000..df6d01d876fce --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table/bulk_actions.tsx @@ -0,0 +1,68 @@ +/* + * 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 { EuiButton, EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; +import * as i18n from './translations'; + +export interface BulkActionsProps { + isTableLoading: boolean; + numberOfTranslatedRules: number; + numberOfSelectedRules: number; + installTranslatedRule?: () => void; + installSelectedRule?: () => void; +} + +/** + * Collection of buttons to perform bulk actions on migration rules within the SIEM Rules Migrations table. + */ +export const BulkActions: React.FC = React.memo( + ({ + isTableLoading, + numberOfTranslatedRules, + numberOfSelectedRules, + installTranslatedRule, + installSelectedRule, + }) => { + const showInstallTranslatedRulesButton = numberOfTranslatedRules > 0; + const showInstallSelectedRulesButton = + showInstallTranslatedRulesButton && numberOfSelectedRules > 0; + return ( + + {showInstallSelectedRulesButton ? ( + + + {i18n.INSTALL_SELECTED_RULES(numberOfSelectedRules)} + {isTableLoading && } + + + ) : null} + {showInstallTranslatedRulesButton ? ( + + + {i18n.INSTALL_ALL_RULES(numberOfTranslatedRules)} + {isTableLoading && } + + + ) : null} + + ); + } +); +BulkActions.displayName = 'BulkActions'; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table/filters.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table/filters.tsx index 5f4ae3098b6a3..25dffc64cccc5 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table/filters.tsx +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table/filters.tsx @@ -8,15 +8,10 @@ import { EuiFlexGroup } from '@elastic/eui'; import type { Dispatch, SetStateAction } from 'react'; import React, { useCallback } from 'react'; -import styled from 'styled-components'; import * as i18n from './translations'; import { RuleSearchField } from '../../../../detection_engine/rule_management_ui/components/rules_table/rules_table_filters/rule_search_field'; import type { TableFilterOptions } from '../../hooks/use_filter_rules_to_install'; -const FilterWrapper = styled(EuiFlexGroup)` - margin-bottom: ${({ theme }) => theme.eui.euiSizeM}; -`; - export interface FiltersComponentProps { /** * Currently selected table filter @@ -45,13 +40,13 @@ const FiltersComponent: React.FC = ({ filterOptions, setF ); return ( - + - + ); }; 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 0cd3e07ea11a4..16f93a1cdebaf 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 @@ -13,8 +13,9 @@ import { EuiSkeletonText, EuiFlexGroup, EuiFlexItem, + EuiSpacer, } from '@elastic/eui'; -import React, { useState } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import type { RuleMigration } from '../../../../../common/siem_migrations/model/rule_migration.gen'; import { @@ -24,32 +25,26 @@ import { import { NoItemsMessage } from './no_items_message'; import { Filters } from './filters'; import { useRulesTableColumns } from '../../hooks/use_rules_table_columns'; -import { useGetRuleMigrationsQuery } from '../../api/hooks/use_get_rule_migrations'; import type { TableFilterOptions } from '../../hooks/use_filter_rules_to_install'; import { useFilterRulesToInstall } from '../../hooks/use_filter_rules_to_install'; +import { useRulePreviewFlyout } from '../../hooks/use_rule_preview_flyout'; +import { useInstallMigrationRules } from '../../logic/use_install_migration_rules'; +import { useGetMigrationRules } from '../../logic/use_get_migration_rules'; +import { useInstallAllMigrationRules } from '../../logic/use_install_all_migration_rules'; +import { BulkActions } from './bulk_actions'; export interface RulesTableComponentProps { /** * Selected rule migration id */ migrationId: string; - - /** - * Opens the flyout with the details of the rule migration - * @param rule Rule migration - * @returns - */ - openRulePreview: (rule: RuleMigration) => void; } /** * Table Component for displaying SIEM rules migrations */ -const RulesTableComponent: React.FC = ({ - migrationId, - openRulePreview, -}) => { - const { data: ruleMigrations, isLoading } = useGetRuleMigrationsQuery(migrationId); +const RulesTableComponent: React.FC = ({ migrationId }) => { + const { data: ruleMigrations, isLoading: isDataLoading } = useGetMigrationRules(migrationId); const [selectedRuleMigrations, setSelectedRuleMigrations] = useState([]); @@ -62,10 +57,60 @@ const RulesTableComponent: React.FC = ({ ruleMigrations: ruleMigrations ?? [], }); - const shouldShowProgress = isLoading; + const { mutateAsync: installMigrationRules } = useInstallMigrationRules(migrationId); + const { mutateAsync: installAllMigrationRules } = useInstallAllMigrationRules(migrationId); + + const numberOfTranslatedRules = useMemo(() => { + return filteredRuleMigrations.filter( + (rule) => + !rule.elastic_rule?.id && + (rule.elastic_rule?.prebuilt_rule_id || rule.translation_result === 'full') + ).length; + }, [filteredRuleMigrations]); + + const [isTableLoading, setTableLoading] = useState(false); + const installSingleRule = useCallback( + async (migrationRule: RuleMigration, enable?: boolean) => { + setTableLoading(true); + try { + await installMigrationRules([migrationRule.id]); + } finally { + setTableLoading(false); + } + }, + [installMigrationRules] + ); + + const installTranslatedRules = useCallback( + async (enable?: boolean) => { + setTableLoading(true); + try { + await installAllMigrationRules(); + } finally { + setTableLoading(false); + } + }, + [installAllMigrationRules] + ); + + const ruleActionsFactory = useCallback( + (ruleMigration: RuleMigration, closeRulePreview: () => void) => { + // TODO: Add flyout action buttons + return null; + }, + [] + ); + + const { rulePreviewFlyout, openRulePreview } = useRulePreviewFlyout({ + ruleActionsFactory, + }); + + const shouldShowProgress = isDataLoading; const rulesColumns = useRulesTableColumns({ - openRulePreview, + disableActions: isTableLoading, + openMigrationRulePreview: openRulePreview, + installMigrationRule: installSingleRule, }); return ( @@ -79,7 +124,7 @@ const RulesTableComponent: React.FC = ({ /> )} @@ -91,13 +136,22 @@ const RulesTableComponent: React.FC = ({ ) : ( <> - - + + + + + - + = ({ ) } /> + {rulePreviewFlyout} ); }; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table/translations.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table/translations.ts index 812f26f628e49..3da9886659916 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table/translations.ts +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table/translations.ts @@ -34,3 +34,32 @@ export const GO_BACK_TO_RULES_TABLE_BUTTON = i18n.translate( defaultMessage: 'Go back to SIEM Migrations', } ); + +export const INSTALL_SELECTED_RULES = (numberOfSelectedRules: number) => { + return i18n.translate('xpack.securitySolution.siemMigrations.rules.table.installSelectedRules', { + defaultMessage: 'Install selected ({numberOfSelectedRules})', + values: { numberOfSelectedRules }, + }); +}; + +export const INSTALL_ALL_RULES = (numberOfAllRules: number) => { + return i18n.translate('xpack.securitySolution.siemMigrations.rules.table.installAllRules', { + defaultMessage: + 'Install translated {numberOfAllRules, plural, one {rule} other {rules}} ({numberOfAllRules})', + values: { numberOfAllRules }, + }); +}; + +export const INSTALL_SELECTED_ARIA_LABEL = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.table.installSelectedButtonAriaLabel', + { + defaultMessage: 'Install selected translated rules', + } +); + +export const INSTALL_ALL_ARIA_LABEL = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.table.installAllButtonAriaLabel', + { + defaultMessage: 'Install all translated rules', + } +); diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/actions.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/actions.tsx new file mode 100644 index 0000000000000..7122949dee907 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/actions.tsx @@ -0,0 +1,111 @@ +/* + * 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 { EuiLink } from '@elastic/eui'; +import { getRuleDetailsUrl } from '../../../../common/components/link_to'; +import { useKibana } from '../../../../common/lib/kibana'; +import { APP_UI_ID, SecurityPageName } from '../../../../../common'; +import type { RuleMigration } from '../../../../../common/siem_migrations/model/rule_migration.gen'; +import * as i18n from './translations'; +import type { TableColumn } from './constants'; + +interface ActionNameProps { + disableActions?: boolean; + migrationRule: RuleMigration; + openMigrationRulePreview: (migrationRule: RuleMigration) => void; + installMigrationRule: (migrationRule: RuleMigration, enable?: boolean) => void; +} + +const ActionName = ({ + disableActions, + migrationRule, + openMigrationRulePreview, + installMigrationRule, +}: ActionNameProps) => { + const { navigateToApp } = useKibana().services.application; + if (migrationRule.elastic_rule?.id) { + const ruleId = migrationRule.elastic_rule.id; + return ( + { + navigateToApp(APP_UI_ID, { + deepLinkId: SecurityPageName.rules, + path: getRuleDetailsUrl(ruleId), + }); + }} + data-test-subj="viewRule" + > + {i18n.ACTIONS_VIEW_LABEL} + + ); + } + + if (migrationRule.status === 'failed') { + return ( + {}} data-test-subj="restartRule"> + {i18n.ACTIONS_RESTART_LABEL} + + ); + } + + if (migrationRule.translation_result === 'full') { + return ( + { + installMigrationRule(migrationRule); + }} + data-test-subj="installRule" + > + {i18n.ACTIONS_INSTALL_LABEL} + + ); + } + + return ( + { + openMigrationRulePreview(migrationRule); + }} + data-test-subj="editRule" + > + {i18n.ACTIONS_EDIT_LABEL} + + ); +}; + +interface CreateActionsColumnProps { + disableActions?: boolean; + openMigrationRulePreview: (migrationRule: RuleMigration) => void; + installMigrationRule: (migrationRule: RuleMigration, enable?: boolean) => void; +} + +export const createActionsColumn = ({ + disableActions, + openMigrationRulePreview, + installMigrationRule, +}: CreateActionsColumnProps): TableColumn => { + return { + field: 'elastic_rule', + name: i18n.COLUMN_ACTIONS, + render: (value: RuleMigration['elastic_rule'], migrationRule: RuleMigration) => { + return ( + + ); + }, + width: '10%', + align: 'center', + }; +}; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/hooks/translations.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/constants.tsx similarity index 53% rename from x-pack/plugins/security_solution/public/siem_migrations/rules/hooks/translations.ts rename to x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/constants.tsx index 74845b5f257ad..724e4dcb101a1 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/hooks/translations.ts +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/constants.tsx @@ -5,11 +5,7 @@ * 2.0. */ -import { i18n } from '@kbn/i18n'; +import type { EuiBasicTableColumn } from '@elastic/eui'; +import type { RuleMigration } from '../../../../../common/siem_migrations/model/rule_migration.gen'; -export const COLUMN_STATUS = i18n.translate( - 'xpack.securitySolution.siemMigrations.rules.columns.statusTitle', - { - defaultMessage: 'Status', - } -); +export type TableColumn = EuiBasicTableColumn; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/utils/constants.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/index.tsx similarity index 57% rename from x-pack/plugins/security_solution/public/siem_migrations/rules/utils/constants.ts rename to x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/index.tsx index 7400d4b0bcb63..a402e61a444af 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/utils/constants.ts +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/index.tsx @@ -5,7 +5,10 @@ * 2.0. */ -import type { Severity } from '@kbn/securitysolution-io-ts-alerting-types'; +export * from './constants'; -export const DEFAULT_TRANSLATION_RISK_SCORE = 21; -export const DEFAULT_TRANSLATION_SEVERITY: Severity = 'low'; +export * from './actions'; +export * from './name'; +export * from './risk_score'; +export * from './severity'; +export * from './status'; 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 new file mode 100644 index 0000000000000..7b7cf228895fc --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/name.tsx @@ -0,0 +1,49 @@ +/* + * 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 { EuiLink } from '@elastic/eui'; +import type { RuleMigration } from '../../../../../common/siem_migrations/model/rule_migration.gen'; +import * as i18n from './translations'; +import type { TableColumn } from './constants'; + +interface NameProps { + name: string; + rule: RuleMigration; + openMigrationRulePreview: (rule: RuleMigration) => void; +} + +const Name = ({ name, rule, openMigrationRulePreview }: NameProps) => { + return ( + { + openMigrationRulePreview(rule); + }} + data-test-subj="ruleName" + > + {name} + + ); +}; + +export const createNameColumn = ({ + openMigrationRulePreview, +}: { + openMigrationRulePreview: (rule: RuleMigration) => void; +}): TableColumn => { + return { + field: 'original_rule.title', + name: i18n.COLUMN_NAME, + render: (value: RuleMigration['original_rule']['title'], rule: RuleMigration) => ( + + ), + sortable: true, + truncateText: true, + width: '40%', + align: 'left', + }; +}; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/risk_score.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/risk_score.tsx new file mode 100644 index 0000000000000..e9eca65736a51 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/risk_score.tsx @@ -0,0 +1,27 @@ +/* + * 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 { EuiText } from '@elastic/eui'; +import { DEFAULT_TRANSLATION_RISK_SCORE } from '../../../../../common/siem_migrations/constants'; +import * as i18n from './translations'; +import type { TableColumn } from './constants'; + +export const createRiskScoreColumn = (): TableColumn => { + return { + field: 'risk_score', + name: i18n.COLUMN_RISK_SCORE, + render: () => ( + + {DEFAULT_TRANSLATION_RISK_SCORE} + + ), + sortable: true, + truncateText: true, + width: '75px', + }; +}; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/severity.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/severity.tsx new file mode 100644 index 0000000000000..4ea737844ad53 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/severity.tsx @@ -0,0 +1,27 @@ +/* + * 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 type { Severity } from '@kbn/securitysolution-io-ts-alerting-types'; +import { DEFAULT_TRANSLATION_SEVERITY } from '../../../../../common/siem_migrations/constants'; +import { getNormalizedSeverity } from '../../../../detection_engine/rule_management_ui/components/rules_table/helpers'; +import { SeverityBadge } from '../../../../common/components/severity_badge'; +import type { RuleMigration } from '../../../../../common/siem_migrations/model/rule_migration.gen'; +import type { TableColumn } from './constants'; +import * as i18n from './translations'; + +export const createSeverityColumn = (): TableColumn => { + return { + field: 'elastic_rule.severity', + name: i18n.COLUMN_SEVERITY, + render: (value?: Severity) => , + sortable: ({ elastic_rule: elasticRule }: RuleMigration) => + getNormalizedSeverity((elasticRule?.severity as Severity) ?? DEFAULT_TRANSLATION_SEVERITY), + truncateText: true, + width: '12%', + }; +}; 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 new file mode 100644 index 0000000000000..982f6ba7580b2 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/status.tsx @@ -0,0 +1,25 @@ +/* + * 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 type { RuleMigration } from '../../../../../common/siem_migrations/model/rule_migration.gen'; +import * as i18n from './translations'; +import type { TableColumn } from './constants'; +import { StatusBadge } from '../status_badge'; + +export const createStatusColumn = (): TableColumn => { + return { + field: 'translation_result', + name: i18n.COLUMN_STATUS, + render: (value: RuleMigration['translation_result'], rule: RuleMigration) => ( + + ), + sortable: false, + truncateText: true, + width: '12%', + }; +}; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/translations.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/translations.ts new file mode 100644 index 0000000000000..906e752d79aa0 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/translations.ts @@ -0,0 +1,71 @@ +/* + * 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 COLUMN_ACTIONS = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.tableColumn.actionsLabel', + { + defaultMessage: 'Actions', + } +); + +export const ACTIONS_VIEW_LABEL = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.tableColumn.actionsViewLabel', + { + defaultMessage: 'View', + } +); + +export const ACTIONS_EDIT_LABEL = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.tableColumn.actionsEditLabel', + { + defaultMessage: 'Edit', + } +); + +export const ACTIONS_INSTALL_LABEL = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.tableColumn.actionsInstallLabel', + { + defaultMessage: 'Install', + } +); + +export const ACTIONS_RESTART_LABEL = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.tableColumn.actionsRestartLabel', + { + defaultMessage: 'Restart', + } +); + +export const COLUMN_NAME = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.tableColumn.nameLabel', + { + defaultMessage: 'Name', + } +); + +export const COLUMN_STATUS = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.tableColumn.statusLabel', + { + defaultMessage: 'Status', + } +); + +export const COLUMN_RISK_SCORE = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.tableColumn.riskScoreLabel', + { + defaultMessage: 'Risk score', + } +); + +export const COLUMN_SEVERITY = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.tableColumn.severityLabel', + { + defaultMessage: 'Severity', + } +); diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/translation_details_flyout/index.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/translation_details_flyout/index.tsx index 4aaff21281d64..b6dce09c311e1 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/translation_details_flyout/index.tsx +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/translation_details_flyout/index.tsx @@ -25,6 +25,10 @@ import { } from '@elastic/eui'; import type { EuiTabbedContentTab, EuiTabbedContentProps, EuiFlyoutProps } from '@elastic/eui'; +import { + DEFAULT_TRANSLATION_RISK_SCORE, + DEFAULT_TRANSLATION_SEVERITY, +} from '../../../../../common/siem_migrations/constants'; import type { RuleMigration } from '../../../../../common/siem_migrations/model/rule_migration.gen'; import { RuleOverviewTab, @@ -41,10 +45,6 @@ import { LARGE_DESCRIPTION_LIST_COLUMN_WIDTHS, } from './constants'; import { TranslationTab } from './translation_tab'; -import { - DEFAULT_TRANSLATION_RISK_SCORE, - DEFAULT_TRANSLATION_SEVERITY, -} from '../../utils/constants'; const StyledEuiFlyoutBody = styled(EuiFlyoutBody)` .euiFlyoutBody__overflow { diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/hooks/use_rules_table_columns.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/hooks/use_rules_table_columns.tsx index 3b13b9e631ccb..b7e06b4ea938a 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/hooks/use_rules_table_columns.tsx +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/hooks/use_rules_table_columns.tsx @@ -5,103 +5,38 @@ * 2.0. */ -import type { EuiBasicTableColumn } from '@elastic/eui'; -import { EuiText, EuiLink } from '@elastic/eui'; -import React, { useMemo } from 'react'; -import type { Severity } from '@kbn/securitysolution-io-ts-alerting-types'; +import { useMemo } from 'react'; import type { RuleMigration } from '../../../../common/siem_migrations/model/rule_migration.gen'; -import { SeverityBadge } from '../../../common/components/severity_badge'; -import * as rulesI18n from '../../../detections/pages/detection_engine/rules/translations'; -import * as i18n from './translations'; -import { getNormalizedSeverity } from '../../../detection_engine/rule_management_ui/components/rules_table/helpers'; -import { StatusBadge } from '../components/status_badge'; -import { DEFAULT_TRANSLATION_RISK_SCORE, DEFAULT_TRANSLATION_SEVERITY } from '../utils/constants'; - -export type TableColumn = EuiBasicTableColumn; - -interface RuleNameProps { - name: string; - rule: RuleMigration; - openRulePreview: (rule: RuleMigration) => void; -} - -const RuleName = ({ name, rule, openRulePreview }: RuleNameProps) => { - return ( - { - openRulePreview(rule); - }} - data-test-subj="ruleName" - > - {name} - - ); -}; - -const createRuleNameColumn = ({ - openRulePreview, -}: { - openRulePreview: (rule: RuleMigration) => void; -}): TableColumn => { - return { - field: 'original_rule.title', - name: rulesI18n.COLUMN_RULE, - render: (value: RuleMigration['original_rule']['title'], rule: RuleMigration) => ( - - ), - sortable: true, - truncateText: true, - width: '40%', - align: 'left', - }; -}; - -const STATUS_COLUMN: TableColumn = { - field: 'translation_result', - name: i18n.COLUMN_STATUS, - render: (value: RuleMigration['translation_result'], rule: RuleMigration) => ( - - ), - sortable: false, - truncateText: true, - width: '12%', -}; +import type { TableColumn } from '../components/rules_table_columns'; +import { + createActionsColumn, + createNameColumn, + createRiskScoreColumn, + createSeverityColumn, + createStatusColumn, +} from '../components/rules_table_columns'; export const useRulesTableColumns = ({ - openRulePreview, + disableActions, + openMigrationRulePreview, + installMigrationRule, }: { - openRulePreview: (rule: RuleMigration) => void; + disableActions?: boolean; + openMigrationRulePreview: (rule: RuleMigration) => void; + installMigrationRule: (migrationRule: RuleMigration, enable?: boolean) => void; }): TableColumn[] => { return useMemo( () => [ - createRuleNameColumn({ openRulePreview }), - STATUS_COLUMN, - { - field: 'risk_score', - name: rulesI18n.COLUMN_RISK_SCORE, - render: () => ( - - {DEFAULT_TRANSLATION_RISK_SCORE} - - ), - sortable: true, - truncateText: true, - width: '75px', - }, - { - field: 'elastic_rule.severity', - name: rulesI18n.COLUMN_SEVERITY, - render: (value?: Severity) => ( - - ), - sortable: ({ elastic_rule: elasticRule }: RuleMigration) => - getNormalizedSeverity( - (elasticRule?.severity as Severity) ?? DEFAULT_TRANSLATION_SEVERITY - ), - truncateText: true, - width: '12%', - }, + createNameColumn({ openMigrationRulePreview }), + createStatusColumn(), + createRiskScoreColumn(), + createSeverityColumn(), + createActionsColumn({ + disableActions, + openMigrationRulePreview, + installMigrationRule, + }), ], - [openRulePreview] + [disableActions, installMigrationRule, openMigrationRulePreview] ); }; 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 new file mode 100644 index 0000000000000..23f5a6e3849a0 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/logic/translations.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const GET_MIGRATION_RULES_FAILURE = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.getMigrationRulesFailDescription', + { + defaultMessage: 'Failed to fetch migration rules', + } +); + +export const INSTALL_MIGRATION_RULES_FAILURE = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.installMigrationRulesFailDescription', + { + defaultMessage: 'Failed to install 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 new file mode 100644 index 0000000000000..27637daf142ff --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/logic/use_get_migration_rules.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useAppToasts } from '../../../common/hooks/use_app_toasts'; +import { useGetMigrationRulesQuery } from '../api/hooks/use_get_migration_rules_query'; +import * as i18n from './translations'; + +export const useGetMigrationRules = (migrationId: string) => { + const { addError } = useAppToasts(); + + return useGetMigrationRulesQuery(migrationId, { + onError: (error) => { + addError(error, { title: i18n.GET_MIGRATION_RULES_FAILURE }); + }, + }); +}; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/logic/use_install_all_migration_rules.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/logic/use_install_all_migration_rules.ts new file mode 100644 index 0000000000000..105ea651d0a8c --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/logic/use_install_all_migration_rules.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useAppToasts } from '../../../common/hooks/use_app_toasts'; +import { useInstallAllMigrationRulesMutation } from '../api/hooks/use_install_all_migration_rules_mutation'; +import * as i18n from './translations'; + +export const useInstallAllMigrationRules = (migrationId: string) => { + const { addError } = useAppToasts(); + + return useInstallAllMigrationRulesMutation(migrationId, { + onError: (error) => { + addError(error, { title: i18n.INSTALL_MIGRATION_RULES_FAILURE }); + }, + }); +}; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/logic/use_install_migration_rules.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/logic/use_install_migration_rules.ts new file mode 100644 index 0000000000000..dcc19f290f87f --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/logic/use_install_migration_rules.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useAppToasts } from '../../../common/hooks/use_app_toasts'; +import { useInstallMigrationRulesMutation } from '../api/hooks/use_install_migration_rules_mutation'; +import * as i18n from './translations'; + +export const useInstallMigrationRules = (migrationId: string) => { + const { addError } = useAppToasts(); + + return useInstallMigrationRulesMutation(migrationId, { + onError: (error) => { + addError(error, { title: i18n.INSTALL_MIGRATION_RULES_FAILURE }); + }, + }); +}; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/pages/index.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/pages/index.tsx index 26199616b3777..dabdb83cccbab 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/pages/index.tsx +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/pages/index.tsx @@ -5,12 +5,11 @@ * 2.0. */ -import React, { useCallback, useEffect, useMemo } from 'react'; +import React, { useEffect, useMemo } from 'react'; import { EuiSkeletonLoading, EuiSkeletonText, EuiSkeletonTitle } from '@elastic/eui'; import type { RouteComponentProps } from 'react-router-dom'; import { useNavigation } from '../../../common/lib/kibana'; -import type { RuleMigration } from '../../../../common/siem_migrations/model/rule_migration.gen'; import { HeaderPage } from '../../../common/components/header_page'; import { SecuritySolutionPageWrapper } from '../../../common/components/page_wrapper'; import { SecurityPageName } from '../../../app/types'; @@ -20,7 +19,6 @@ import { RulesTable } from '../components/rules_table'; import { NeedAdminForUpdateRulesCallOut } from '../../../detections/components/callouts/need_admin_for_update_callout'; import { MissingPrivilegesCallOut } from '../../../detections/components/callouts/missing_privileges_callout'; import { HeaderButtons } from '../components/header_buttons'; -import { useRulePreviewFlyout } from '../hooks/use_rule_preview_flyout'; import { UnknownMigration } from '../components/unknown_migration'; import { useLatestStats } from '../hooks/use_latest_stats'; @@ -66,24 +64,12 @@ export const RulesPage: React.FC = React.memo( navigateTo({ deepLinkId: SecurityPageName.siemMigrationsRules, path: selectedId }); }; - const ruleActionsFactory = useCallback( - (ruleMigration: RuleMigration, closeRulePreview: () => void) => { - // TODO: Add flyout action buttons - return null; - }, - [] - ); - - const { rulePreviewFlyout, openRulePreview } = useRulePreviewFlyout({ - ruleActionsFactory, - }); - const content = useMemo(() => { if (!migrationId || !migrationsIds.includes(migrationId)) { return ; } - return ; - }, [migrationId, migrationsIds, openRulePreview]); + return ; + }, [migrationId, migrationsIds]); return ( <> @@ -108,7 +94,6 @@ export const RulesPage: React.FC = React.memo( } loadedContent={content} /> - {rulePreviewFlyout} ); diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/types.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/types.ts index db9ca9507702f..ef1338b5945e4 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/types.ts +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/types.ts @@ -11,3 +11,14 @@ export interface RuleMigrationStats extends RuleMigrationTaskStats { /** The sequential number of the migration */ number: number; } + +export interface InstallRulesProps { + migrationId: string; + ids: string[]; + signal?: AbortSignal; +} + +export interface InstallTranslatedRulesProps { + migrationId: string; + signal?: AbortSignal; +} 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 e6edb05b3a68a..0d880484877f6 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 @@ -38,7 +38,7 @@ export const registerSiemRuleMigrationsGetRoute = ( const ctx = await context.resolve(['securitySolution']); const ruleMigrationsClient = ctx.securitySolution.getSiemRuleMigrationsClient(); - const migrationRules = await ruleMigrationsClient.data.rules.get(migrationId); + const migrationRules = await ruleMigrationsClient.data.rules.get({ migrationId }); return res.ok({ body: migrationRules }); } catch (err) { diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/index.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/index.ts index c6ea6b8bf897b..601b156aee040 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/index.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/index.ts @@ -17,6 +17,8 @@ import { registerSiemRuleMigrationsStatsAllRoute } from './stats_all'; import { registerSiemRuleMigrationsResourceUpsertRoute } from './resources/upsert'; import { registerSiemRuleMigrationsResourceGetRoute } from './resources/get'; import { registerSiemRuleMigrationsRetryRoute } from './retry'; +import { registerSiemRuleMigrationsInstallRoute } from './rules/install'; +import { registerSiemRuleMigrationsInstallTranslatedRoute } from './rules/install_translated'; export const registerSiemRuleMigrationsRoutes = ( router: SecuritySolutionPluginRouter, @@ -30,6 +32,8 @@ export const registerSiemRuleMigrationsRoutes = ( registerSiemRuleMigrationsRetryRoute(router, logger); registerSiemRuleMigrationsStatsRoute(router, logger); registerSiemRuleMigrationsStopRoute(router, logger); + registerSiemRuleMigrationsInstallRoute(router, logger); + registerSiemRuleMigrationsInstallTranslatedRoute(router, logger); registerSiemRuleMigrationsResourceUpsertRoute(router, logger); registerSiemRuleMigrationsResourceGetRoute(router, logger); diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/rules/install.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/rules/install.ts new file mode 100644 index 0000000000000..659534891b289 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/rules/install.ts @@ -0,0 +1,69 @@ +/* + * 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_INSTALL_PATH } from '../../../../../../common/siem_migrations/constants'; +import type { InstallMigrationRulesResponse } from '../../../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; +import { + InstallMigrationRulesRequestBody, + InstallMigrationRulesRequestParams, +} from '../../../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; +import type { SecuritySolutionPluginRouter } from '../../../../../types'; +import { withLicense } from '../util/with_license'; +import { installTranslated } from '../util/installation'; + +export const registerSiemRuleMigrationsInstallRoute = ( + router: SecuritySolutionPluginRouter, + logger: Logger +) => { + router.versioned + .post({ + path: SIEM_RULE_MIGRATION_INSTALL_PATH, + access: 'internal', + security: { authz: { requiredPrivileges: ['securitySolution'] } }, + }) + .addVersion( + { + version: '1', + validate: { + request: { + params: buildRouteValidationWithZod(InstallMigrationRulesRequestParams), + body: buildRouteValidationWithZod(InstallMigrationRulesRequestBody), + }, + }, + }, + withLicense( + async (context, req, res): Promise> => { + const { migration_id: migrationId } = req.params; + const ids = req.body; + + try { + const ctx = await context.resolve(['core', 'alerting', 'securitySolution']); + + const securitySolutionContext = ctx.securitySolution; + const savedObjectsClient = ctx.core.savedObjects.client; + const rulesClient = ctx.alerting.getRulesClient(); + + await installTranslated({ + migrationId, + ids, + securitySolutionContext, + savedObjectsClient, + rulesClient, + logger, + }); + + return res.ok({ body: { installed: true } }); + } catch (err) { + logger.error(err); + return res.badRequest({ body: err.message }); + } + } + ) + ); +}; diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/rules/install_translated.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/rules/install_translated.ts new file mode 100644 index 0000000000000..ae4328e0ccf37 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/rules/install_translated.ts @@ -0,0 +1,67 @@ +/* + * 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_INSTALL_TRANSLATED_PATH } from '../../../../../../common/siem_migrations/constants'; +import type { InstallTranslatedMigrationRulesResponse } from '../../../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; +import { InstallTranslatedMigrationRulesRequestParams } from '../../../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; +import type { SecuritySolutionPluginRouter } from '../../../../../types'; +import { withLicense } from '../util/with_license'; +import { installTranslated } from '../util/installation'; + +export const registerSiemRuleMigrationsInstallTranslatedRoute = ( + router: SecuritySolutionPluginRouter, + logger: Logger +) => { + router.versioned + .post({ + path: SIEM_RULE_MIGRATION_INSTALL_TRANSLATED_PATH, + access: 'internal', + security: { authz: { requiredPrivileges: ['securitySolution'] } }, + }) + .addVersion( + { + version: '1', + validate: { + request: { + params: buildRouteValidationWithZod(InstallTranslatedMigrationRulesRequestParams), + }, + }, + }, + withLicense( + async ( + context, + req, + res + ): Promise> => { + const { migration_id: migrationId } = req.params; + + try { + const ctx = await context.resolve(['core', 'alerting', 'securitySolution']); + + const securitySolutionContext = ctx.securitySolution; + const savedObjectsClient = ctx.core.savedObjects.client; + const rulesClient = ctx.alerting.getRulesClient(); + + await installTranslated({ + migrationId, + securitySolutionContext, + savedObjectsClient, + rulesClient, + logger, + }); + + return res.ok({ body: { installed: true } }); + } catch (err) { + logger.error(err); + return res.badRequest({ body: err.message }); + } + } + ) + ); +}; 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 new file mode 100644 index 0000000000000..ee211e8a935de --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/util/installation.ts @@ -0,0 +1,223 @@ +/* + * 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 { Logger, SavedObjectsClientContract } from '@kbn/core/server'; +import type { RulesClient } from '@kbn/alerting-plugin/server'; +import { + DEFAULT_TRANSLATION_RISK_SCORE, + DEFAULT_TRANSLATION_SEVERITY, +} from '../../../../../../common/siem_migrations/constants'; +import type { SecuritySolutionApiRequestHandlerContext } from '../../../../..'; +import { createPrebuiltRuleObjectsClient } from '../../../../detection_engine/prebuilt_rules/logic/rule_objects/prebuilt_rule_objects_client'; +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 { PrebuiltRuleAsset } from '../../../../detection_engine/prebuilt_rules'; +import { getRuleGroups } from '../../../../detection_engine/prebuilt_rules/model/rule_groups/get_rule_groups'; +import { fetchRuleVersionsTriad } from '../../../../detection_engine/prebuilt_rules/logic/rule_versions/fetch_rule_versions_triad'; +import { createPrebuiltRuleAssetsClient } from '../../../../detection_engine/prebuilt_rules/logic/rule_assets/prebuilt_rule_assets_client'; +import type { IDetectionRulesClient } from '../../../../detection_engine/rule_management/logic/detection_rules_client/detection_rules_client_interface'; +import type { RuleCreateProps } from '../../../../../../common/api/detection_engine'; +import type { UpdateRuleMigrationInput } from '../../data/rule_migrations_data_rules_client'; +import type { StoredRuleMigration } from '../../types'; + +const installPrebuiltRules = async ( + rulesToInstall: StoredRuleMigration[], + securitySolutionContext: SecuritySolutionApiRequestHandlerContext, + rulesClient: RulesClient, + savedObjectsClient: SavedObjectsClientContract, + detectionRulesClient: IDetectionRulesClient +): Promise => { + const ruleAssetsClient = createPrebuiltRuleAssetsClient(savedObjectsClient); + const ruleObjectsClient = createPrebuiltRuleObjectsClient(rulesClient); + const ruleVersionsMap = await fetchRuleVersionsTriad({ + ruleAssetsClient, + ruleObjectsClient, + }); + const { currentRules, installableRules } = getRuleGroups(ruleVersionsMap); + + const rulesToUpdate: UpdateRuleMigrationInput[] = []; + const assetsToInstall: PrebuiltRuleAsset[] = []; + rulesToInstall.forEach((ruleToInstall) => { + // If prebuilt rule has already been install, then just update migration rule with the installed rule id + const installedRule = currentRules.find( + (rule) => rule.rule_id === ruleToInstall.elastic_rule?.prebuilt_rule_id + ); + if (installedRule) { + rulesToUpdate.push({ + id: ruleToInstall.id, + elastic_rule: { + id: installedRule.id, + }, + }); + return; + } + + // If prebuilt rule is not installed, then keep reference to install it + const installableRule = installableRules.find( + (rule) => rule.rule_id === ruleToInstall.elastic_rule?.prebuilt_rule_id + ); + if (installableRule) { + assetsToInstall.push(installableRule); + } + }); + + // Filter out any duplicates which can occur when multiple translated rules matched the same prebuilt rule + const filteredAssetsToInstall = assetsToInstall.filter( + (value, index, self) => index === self.findIndex((rule) => rule.rule_id === value.rule_id) + ); + + // TODO: we need to do an error handling which can happen during the rule installation + const { results: installedRules } = await createPrebuiltRules( + detectionRulesClient, + filteredAssetsToInstall + ); + await performTimelinesInstallation(securitySolutionContext); + + installedRules.forEach((installedRule) => { + const rules = rulesToInstall.filter( + (rule) => rule.elastic_rule?.prebuilt_rule_id === installedRule.result.rule_id + ); + rules.forEach((prebuiltRule) => { + rulesToUpdate.push({ + id: prebuiltRule.id, + elastic_rule: { + id: installedRule.result.id, + }, + }); + }); + }); + + return rulesToUpdate; +}; + +const installCustomRules = async ( + rulesToInstall: StoredRuleMigration[], + detectionRulesClient: IDetectionRulesClient, + logger: Logger +): Promise => { + const rulesToUpdate: UpdateRuleMigrationInput[] = []; + await Promise.all( + rulesToInstall.map(async (rule) => { + if (!rule.elastic_rule?.query || !rule.elastic_rule?.description) { + return; + } + try { + const payloadRule: RuleCreateProps = { + type: 'esql', + language: 'esql', + query: rule.elastic_rule.query, + name: rule.elastic_rule.title, + description: rule.elastic_rule.description, + severity: DEFAULT_TRANSLATION_SEVERITY, + risk_score: DEFAULT_TRANSLATION_RISK_SCORE, + }; + const createdRule = await detectionRulesClient.createCustomRule({ + params: payloadRule, + }); + rulesToUpdate.push({ + id: rule.id, + elastic_rule: { + id: createdRule.id, + }, + }); + } catch (err) { + // TODO: we need to do an error handling which can happen during the rule creation + logger.debug(`Could not create a rule because of error: ${JSON.stringify(err)}`); + } + }) + ); + return rulesToUpdate; +}; + +interface InstallTranslatedProps { + /** + * The migration id + */ + migrationId: string; + + /** + * If specified, then installable translated rules in theThe list will be installed, + * otherwise all installable translated rules will be installed. + */ + ids?: string[]; + + /** + * The security solution context + */ + securitySolutionContext: SecuritySolutionApiRequestHandlerContext; + + /** + * The rules client to create rules + */ + rulesClient: RulesClient; + + /** + * The saved objects client + */ + savedObjectsClient: SavedObjectsClientContract; + + /** + * The logger + */ + logger: Logger; +} + +export const installTranslated = async ({ + migrationId, + ids, + securitySolutionContext, + rulesClient, + savedObjectsClient, + logger, +}: InstallTranslatedProps) => { + const detectionRulesClient = securitySolutionContext.getDetectionRulesClient(); + const ruleMigrationsClient = securitySolutionContext.getSiemRuleMigrationsClient(); + + const rulesToInstall = await ruleMigrationsClient.data.rules.get({ + migrationId, + ids, + installable: true, + }); + + const { customRulesToInstall, prebuiltRulesToInstall } = rulesToInstall.reduce( + (acc, item) => { + if (item.elastic_rule?.prebuilt_rule_id) { + acc.prebuiltRulesToInstall.push(item); + } else { + acc.customRulesToInstall.push(item); + } + return acc; + }, + { customRulesToInstall: [], prebuiltRulesToInstall: [] } as { + customRulesToInstall: StoredRuleMigration[]; + prebuiltRulesToInstall: StoredRuleMigration[]; + } + ); + + const updatedPrebuiltRules = await installPrebuiltRules( + prebuiltRulesToInstall, + securitySolutionContext, + rulesClient, + savedObjectsClient, + detectionRulesClient + ); + + const updatedCustomRules = await installCustomRules( + customRulesToInstall, + detectionRulesClient, + logger + ); + + const rulesToUpdate: UpdateRuleMigrationInput[] = [ + ...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 06e257b9862c5..0a82e2c311906 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 @@ -34,6 +34,13 @@ export type UpdateRuleMigrationInput = { elastic_rule?: Partial } & export type RuleMigrationDataStats = Omit; export type RuleMigrationAllDataStats = RuleMigrationDataStats[]; +export interface RuleMigrationFilterOptions { + migrationId: string; + status?: SiemMigrationStatus | SiemMigrationStatus[]; + ids?: string[]; + installable?: boolean; +} + /* BULK_MAX_SIZE defines the number to break down the bulk operations by. * The 500 number was chosen as a reasonable number to avoid large payloads. It can be adjusted if needed. */ @@ -101,9 +108,9 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient } /** Retrieves an array of rule documents of a specific migrations */ - async get(migrationId: string): Promise { + async get(filters: RuleMigrationFilterOptions): Promise { const index = await this.getIndexName(); - const query = this.getFilterQuery(migrationId); + const query = this.getFilterQuery(filters); const storedRuleMigrations = await this.esClient .search({ index, query, sort: '_doc' }) @@ -123,7 +130,7 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient */ async takePending(migrationId: string, size: number): Promise { const index = await this.getIndexName(); - const query = this.getFilterQuery(migrationId, SiemMigrationStatus.PENDING); + const query = this.getFilterQuery({ migrationId, status: SiemMigrationStatus.PENDING }); const storedRuleMigrations = await this.esClient .search({ index, query, sort: '_doc', size }) @@ -202,7 +209,7 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient { refresh = false }: { refresh?: boolean } = {} ): Promise { const index = await this.getIndexName(); - const query = this.getFilterQuery(migrationId, statusToQuery); + const query = this.getFilterQuery({ migrationId, status: statusToQuery }); const script = { source: `ctx._source['status'] = '${statusToUpdate}'` }; await this.esClient.updateByQuery({ index, query, script, refresh }).catch((error) => { this.logger.error(`Error updating rule migrations status: ${error.message}`); @@ -213,7 +220,7 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient /** Retrieves the stats for the rule migrations with the provided id */ async getStats(migrationId: string): Promise { const index = await this.getIndexName(); - const query = this.getFilterQuery(migrationId); + const query = this.getFilterQuery({ migrationId }); const aggregations = { pending: { filter: { term: { status: SiemMigrationStatus.PENDING } } }, processing: { filter: { term: { status: SiemMigrationStatus.PROCESSING } } }, @@ -283,10 +290,12 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient })); } - private getFilterQuery( - migrationId: string, - status?: SiemMigrationStatus | SiemMigrationStatus[] - ): QueryDslQueryContainer { + private getFilterQuery({ + migrationId, + status, + ids, + installable, + }: RuleMigrationFilterOptions): QueryDslQueryContainer { const filter: QueryDslQueryContainer[] = [{ term: { migration_id: migrationId } }]; if (status) { if (Array.isArray(status)) { @@ -295,6 +304,20 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient filter.push({ term: { status } }); } } + if (ids) { + filter.push({ terms: { _id: ids } }); + } + if (installable) { + filter.push( + { term: { translation_result: 'full' } }, + { + nested: { + path: 'elastic_rule', + query: { bool: { must_not: { exists: { field: 'elastic_rule.id' } } } }, + }, + } + ); + } return { bool: { filter } }; } } diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/nodes/match_prebuilt_rule/match_prebuilt_rule.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/nodes/match_prebuilt_rule/match_prebuilt_rule.ts index 4a0404acf653d..eee5a3e0f6545 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/nodes/match_prebuilt_rule/match_prebuilt_rule.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/nodes/match_prebuilt_rule/match_prebuilt_rule.ts @@ -7,6 +7,7 @@ import type { Logger } from '@kbn/core/server'; import { StringOutputParser } from '@langchain/core/output_parsers'; +import { SiemMigrationRuleTranslationResult } from '../../../../../../../../common/siem_migrations/constants'; import type { ChatModel } from '../../../util/actions_client_chat'; import type { GraphNode } from '../../types'; import { filterPrebuiltRules, type PrebuiltRulesMapByName } from '../../../util/prebuilt_rules'; @@ -51,6 +52,7 @@ export const getMatchPrebuiltRuleNode = description: result.rule.description, prebuilt_rule_id: result.rule.rule_id, id: result.installedRuleId, + translation_result: SiemMigrationRuleTranslationResult.FULL, }, }; } 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 ab1e8aa42d389..c4a8979b78e2e 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 @@ -110,7 +110,12 @@ import { InitEntityEngineRequestBodyInput, } from '@kbn/security-solution-plugin/common/api/entity_analytics/entity_store/engine/init.gen'; import { InitEntityStoreRequestBodyInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/entity_store/enablement.gen'; +import { + InstallMigrationRulesRequestParamsInput, + InstallMigrationRulesRequestBodyInput, +} from '@kbn/security-solution-plugin/common/siem_migrations/model/api/rules/rule_migration.gen'; import { InstallPrepackedTimelinesRequestBodyInput } from '@kbn/security-solution-plugin/common/api/timeline/install_prepackaged_timelines/install_prepackaged_timelines_route.gen'; +import { InstallTranslatedMigrationRulesRequestParamsInput } from '@kbn/security-solution-plugin/common/siem_migrations/model/api/rules/rule_migration.gen'; import { ListEntitiesRequestQueryInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/entity_store/entities/list_entities.gen'; import { PatchRuleRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_management/crud/patch_rule/patch_rule_route.gen'; import { PatchTimelineRequestBodyInput } from '@kbn/security-solution-plugin/common/api/timeline/patch_timelines/patch_timeline_route.gen'; @@ -1067,6 +1072,22 @@ finalize it. .set(ELASTIC_HTTP_VERSION_HEADER, '1') .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana'); }, + /** + * Installs migration rules + */ + installMigrationRules(props: InstallMigrationRulesProps, kibanaSpace: string = 'default') { + return supertest + .post( + routeWithNamespace( + replaceParams('/internal/siem_migrations/rules/{migration_id}/install', props.params), + kibanaSpace + ) + ) + .set('kbn-xsrf', 'true') + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') + .send(props.body as object); + }, /** * Install and update all Elastic prebuilt detection rules and Timelines. */ @@ -1088,6 +1109,27 @@ finalize it. .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .send(props.body as object); }, + /** + * Installs all translated migration rules + */ + installTranslatedMigrationRules( + props: InstallTranslatedMigrationRulesProps, + kibanaSpace: string = 'default' + ) { + return supertest + .post( + routeWithNamespace( + replaceParams( + '/internal/siem_migrations/rules/{migration_id}/install_translated', + props.params + ), + kibanaSpace + ) + ) + .set('kbn-xsrf', 'true') + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana'); + }, internalUploadAssetCriticalityRecords(kibanaSpace: string = 'default') { return supertest .post(routeWithNamespace('/internal/asset_criticality/upload_csv', kibanaSpace)) @@ -1668,9 +1710,16 @@ export interface InitEntityEngineProps { export interface InitEntityStoreProps { body: InitEntityStoreRequestBodyInput; } +export interface InstallMigrationRulesProps { + params: InstallMigrationRulesRequestParamsInput; + body: InstallMigrationRulesRequestBodyInput; +} export interface InstallPrepackedTimelinesProps { body: InstallPrepackedTimelinesRequestBodyInput; } +export interface InstallTranslatedMigrationRulesProps { + params: InstallTranslatedMigrationRulesRequestParamsInput; +} export interface ListEntitiesProps { query: ListEntitiesRequestQueryInput; }