diff --git a/x-pack/solutions/security/plugins/security_solution/common/api/quickstart_client.gen.ts b/x-pack/solutions/security/plugins/security_solution/common/api/quickstart_client.gen.ts index fa2a097b24926..0df8b92fed856 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/api/quickstart_client.gen.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/api/quickstart_client.gen.ts @@ -379,11 +379,11 @@ import type { ResolveTimelineResponse, } from './timeline/resolve_timeline/resolve_timeline_route.gen'; import type { - CreateRuleMigrationRequestParamsInput, - CreateRuleMigrationRequestBodyInput, CreateRuleMigrationResponse, + CreateRuleMigrationRulesRequestParamsInput, + CreateRuleMigrationRulesRequestBodyInput, + DeleteRuleMigrationRequestParamsInput, GetAllStatsRuleMigrationResponse, - GetRuleMigrationRequestQueryInput, GetRuleMigrationRequestParamsInput, GetRuleMigrationResponse, GetRuleMigrationIntegrationsResponse, @@ -395,6 +395,9 @@ import type { GetRuleMigrationResourcesResponse, GetRuleMigrationResourcesMissingRequestParamsInput, GetRuleMigrationResourcesMissingResponse, + GetRuleMigrationRulesRequestQueryInput, + GetRuleMigrationRulesRequestParamsInput, + GetRuleMigrationRulesResponse, GetRuleMigrationStatsRequestParamsInput, GetRuleMigrationStatsResponse, GetRuleMigrationTranslationStatsRequestParamsInput, @@ -408,8 +411,10 @@ import type { StopRuleMigrationRequestParamsInput, StopRuleMigrationResponse, UpdateRuleMigrationRequestParamsInput, - UpdateRuleMigrationRequestBodyInput, UpdateRuleMigrationResponse, + UpdateRuleMigrationRulesRequestParamsInput, + UpdateRuleMigrationRulesRequestBodyInput, + UpdateRuleMigrationRulesResponse, UpsertRuleMigrationResourcesRequestParamsInput, UpsertRuleMigrationResourcesRequestBodyInput, UpsertRuleMigrationResourcesResponse, @@ -806,13 +811,28 @@ For detailed information on Kibana actions and alerting, and additional API call .catch(catchAxiosErrorFormatAndThrow); } /** - * Creates a new SIEM rules migration using the original vendor rules provided + * Creates a new rule migration and returns the corresponding migration_id */ - async createRuleMigration(props: CreateRuleMigrationProps) { + async createRuleMigration() { this.log.info(`${new Date().toISOString()} Calling API CreateRuleMigration`); return this.kbnClient .request({ - path: replaceParams('/internal/siem_migrations/rules/{migration_id}', props.params), + path: '/internal/siem_migrations/rules', + headers: { + [ELASTIC_HTTP_VERSION_HEADER]: '1', + }, + method: 'PUT', + }) + .catch(catchAxiosErrorFormatAndThrow); + } + /** + * Adds original vendor rules to an already existing migration. Can be called multiple times to add more rules + */ + async createRuleMigrationRules(props: CreateRuleMigrationRulesProps) { + this.log.info(`${new Date().toISOString()} Calling API CreateRuleMigrationRules`); + return this.kbnClient + .request({ + path: replaceParams('/internal/siem_migrations/rules/{migration_id}/rules', props.params), headers: { [ELASTIC_HTTP_VERSION_HEADER]: '1', }, @@ -937,6 +957,21 @@ The difference between the `id` and `rule_id` is that the `id` is a unique rule }) .catch(catchAxiosErrorFormatAndThrow); } + /** + * Deletes a rule migration document stored in the system given the rule migration id + */ + async deleteRuleMigration(props: DeleteRuleMigrationProps) { + this.log.info(`${new Date().toISOString()} Calling API DeleteRuleMigration`); + return this.kbnClient + .request({ + path: replaceParams('/internal/siem_migrations/rules/{migration_id}', props.params), + headers: { + [ELASTIC_HTTP_VERSION_HEADER]: '1', + }, + method: 'DELETE', + }) + .catch(catchAxiosErrorFormatAndThrow); + } /** * Delete one or more Timelines or Timeline templates. */ @@ -1604,7 +1639,7 @@ finalize it. .catch(catchAxiosErrorFormatAndThrow); } /** - * Retrieves the rule documents stored in the system given the rule migration id + * Retrieves the rule migration document stored in the system given the rule migration id */ async getRuleMigration(props: GetRuleMigrationProps) { this.log.info(`${new Date().toISOString()} Calling API GetRuleMigration`); @@ -1615,8 +1650,6 @@ finalize it. [ELASTIC_HTTP_VERSION_HEADER]: '1', }, method: 'GET', - - query: props.query, }) .catch(catchAxiosErrorFormatAndThrow); } @@ -1706,6 +1739,23 @@ finalize it. }) .catch(catchAxiosErrorFormatAndThrow); } + /** + * Retrieves the the list of rules included in a migration given the migration id + */ + async getRuleMigrationRules(props: GetRuleMigrationRulesProps) { + this.log.info(`${new Date().toISOString()} Calling API GetRuleMigrationRules`); + return this.kbnClient + .request({ + path: replaceParams('/internal/siem_migrations/rules/{migration_id}/rules', props.params), + headers: { + [ELASTIC_HTTP_VERSION_HEADER]: '1', + }, + method: 'GET', + + query: props.query, + }) + .catch(catchAxiosErrorFormatAndThrow); + } /** * Retrieves the stats of a SIEM rules migration using the migration id provided */ @@ -2387,7 +2437,7 @@ The difference between the `id` and `rule_id` is that the `id` is a unique rule headers: { [ELASTIC_HTTP_VERSION_HEADER]: '1', }, - method: 'PUT', + method: 'POST', body: props.body, }) .catch(catchAxiosErrorFormatAndThrow); @@ -2415,7 +2465,7 @@ The difference between the `id` and `rule_id` is that the `id` is a unique rule headers: { [ELASTIC_HTTP_VERSION_HEADER]: '1', }, - method: 'PUT', + method: 'POST', }) .catch(catchAxiosErrorFormatAndThrow); } @@ -2476,7 +2526,7 @@ The difference between the `id` and `rule_id` is that the `id` is a unique rule .catch(catchAxiosErrorFormatAndThrow); } /** - * Updates rules migrations attributes + * Updates rules migrations data */ async updateRuleMigration(props: UpdateRuleMigrationProps) { this.log.info(`${new Date().toISOString()} Calling API UpdateRuleMigration`); @@ -2486,7 +2536,22 @@ The difference between the `id` and `rule_id` is that the `id` is a unique rule headers: { [ELASTIC_HTTP_VERSION_HEADER]: '1', }, - method: 'PUT', + method: 'PATCH', + }) + .catch(catchAxiosErrorFormatAndThrow); + } + /** + * Updates rules migrations attributes + */ + async updateRuleMigrationRules(props: UpdateRuleMigrationRulesProps) { + this.log.info(`${new Date().toISOString()} Calling API UpdateRuleMigrationRules`); + return this.kbnClient + .request({ + path: replaceParams('/internal/siem_migrations/rules/{migration_id}/rules', props.params), + headers: { + [ELASTIC_HTTP_VERSION_HEADER]: '1', + }, + method: 'PATCH', body: props.body, }) .catch(catchAxiosErrorFormatAndThrow); @@ -2577,9 +2642,9 @@ export interface CreateAssetCriticalityRecordProps { export interface CreateRuleProps { body: CreateRuleRequestBodyInput; } -export interface CreateRuleMigrationProps { - params: CreateRuleMigrationRequestParamsInput; - body: CreateRuleMigrationRequestBodyInput; +export interface CreateRuleMigrationRulesProps { + params: CreateRuleMigrationRulesRequestParamsInput; + body: CreateRuleMigrationRulesRequestBodyInput; } export interface CreateTimelinesProps { body: CreateTimelinesRequestBodyInput; @@ -2601,6 +2666,9 @@ export interface DeleteNoteProps { export interface DeleteRuleProps { query: DeleteRuleRequestQueryInput; } +export interface DeleteRuleMigrationProps { + params: DeleteRuleMigrationRequestParamsInput; +} export interface DeleteTimelinesProps { body: DeleteTimelinesRequestBodyInput; } @@ -2712,7 +2780,6 @@ export interface GetRuleExecutionResultsProps { params: GetRuleExecutionResultsRequestParamsInput; } export interface GetRuleMigrationProps { - query: GetRuleMigrationRequestQueryInput; params: GetRuleMigrationRequestParamsInput; } export interface GetRuleMigrationPrebuiltRulesProps { @@ -2725,6 +2792,10 @@ export interface GetRuleMigrationResourcesProps { export interface GetRuleMigrationResourcesMissingProps { params: GetRuleMigrationResourcesMissingRequestParamsInput; } +export interface GetRuleMigrationRulesProps { + query: GetRuleMigrationRulesRequestQueryInput; + params: GetRuleMigrationRulesRequestParamsInput; +} export interface GetRuleMigrationStatsProps { params: GetRuleMigrationStatsRequestParamsInput; } @@ -2841,7 +2912,10 @@ export interface UpdateRuleProps { } export interface UpdateRuleMigrationProps { params: UpdateRuleMigrationRequestParamsInput; - body: UpdateRuleMigrationRequestBodyInput; +} +export interface UpdateRuleMigrationRulesProps { + params: UpdateRuleMigrationRulesRequestParamsInput; + body: UpdateRuleMigrationRulesRequestBodyInput; } export interface UpdateWorkflowInsightProps { params: UpdateWorkflowInsightRequestParamsInput; diff --git a/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/constants.ts b/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/constants.ts index 6da4dc8f472f5..35a07ecb80623 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/constants.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/constants.ts @@ -13,9 +13,8 @@ export const SIEM_RULE_MIGRATIONS_PATH = `${SIEM_MIGRATIONS_PATH}/rules` as cons export const SIEM_RULE_MIGRATIONS_ALL_STATS_PATH = `${SIEM_RULE_MIGRATIONS_PATH}/stats` as const; export const SIEM_RULE_MIGRATIONS_INTEGRATIONS_PATH = `${SIEM_RULE_MIGRATIONS_PATH}/integrations` as const; -export const SIEM_RULE_MIGRATION_CREATE_PATH = - `${SIEM_RULE_MIGRATIONS_PATH}/{migration_id?}` as const; export const SIEM_RULE_MIGRATION_PATH = `${SIEM_RULE_MIGRATIONS_PATH}/{migration_id}` as const; +export const SIEM_RULE_MIGRATION_RULES_PATH = `${SIEM_RULE_MIGRATION_PATH}/rules` as const; export const SIEM_RULE_MIGRATION_START_PATH = `${SIEM_RULE_MIGRATION_PATH}/start` as const; export const SIEM_RULE_MIGRATION_STATS_PATH = `${SIEM_RULE_MIGRATION_PATH}/stats` as const; export const SIEM_RULE_MIGRATION_TRANSLATION_STATS_PATH = diff --git a/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.gen.ts b/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.gen.ts index 95e40f76f2d81..938c1349b142c 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.gen.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.gen.ts @@ -19,9 +19,10 @@ import { ArrayFromString, BooleanFromString } from '@kbn/zod-helpers'; import { RuleMigrationTaskStats, - OriginalRule, - UpdateRuleMigrationData, RuleMigration, + OriginalRule, + RuleMigrationRule, + UpdateRuleMigrationRule, RuleMigrationRetryFilter, RuleMigrationTranslationStats, PrebuiltRuleVersion, @@ -34,18 +35,6 @@ import { RelatedIntegration } from '../../../../api/detection_engine/model/rule_ import { NonEmptyString } from '../../../../api/model/primitives.gen'; import { ConnectorId, LangSmithOptions } from '../../common.gen'; -export type CreateRuleMigrationRequestParams = z.infer; -export const CreateRuleMigrationRequestParams = z.object({ - migration_id: NonEmptyString.optional(), -}); -export type CreateRuleMigrationRequestParamsInput = z.input< - typeof CreateRuleMigrationRequestParams ->; - -export type CreateRuleMigrationRequestBody = z.infer; -export const CreateRuleMigrationRequestBody = z.array(OriginalRule); -export type CreateRuleMigrationRequestBodyInput = z.input; - export type CreateRuleMigrationResponse = z.infer; export const CreateRuleMigrationResponse = z.object({ /** @@ -54,24 +43,34 @@ export const CreateRuleMigrationResponse = z.object({ migration_id: NonEmptyString, }); +export type CreateRuleMigrationRulesRequestParams = z.infer< + typeof CreateRuleMigrationRulesRequestParams +>; +export const CreateRuleMigrationRulesRequestParams = z.object({ + migration_id: NonEmptyString, +}); +export type CreateRuleMigrationRulesRequestParamsInput = z.input< + typeof CreateRuleMigrationRulesRequestParams +>; + +export type CreateRuleMigrationRulesRequestBody = z.infer< + typeof CreateRuleMigrationRulesRequestBody +>; +export const CreateRuleMigrationRulesRequestBody = z.array(OriginalRule); +export type CreateRuleMigrationRulesRequestBodyInput = z.input< + typeof CreateRuleMigrationRulesRequestBody +>; + +export type DeleteRuleMigrationRequestParams = z.infer; +export const DeleteRuleMigrationRequestParams = z.object({ + migration_id: NonEmptyString, +}); +export type DeleteRuleMigrationRequestParamsInput = z.input< + typeof DeleteRuleMigrationRequestParams +>; + export type GetAllStatsRuleMigrationResponse = z.infer; export const GetAllStatsRuleMigrationResponse = z.array(RuleMigrationTaskStats); -export type GetRuleMigrationRequestQuery = z.infer; -export const GetRuleMigrationRequestQuery = z.object({ - page: z.coerce.number().optional(), - per_page: z.coerce.number().optional(), - sort_field: NonEmptyString.optional(), - sort_direction: z.enum(['asc', 'desc']).optional(), - search_term: z.string().optional(), - ids: ArrayFromString(NonEmptyString).optional(), - is_prebuilt: BooleanFromString.optional(), - is_installed: BooleanFromString.optional(), - is_fully_translated: BooleanFromString.optional(), - is_partially_translated: BooleanFromString.optional(), - is_untranslatable: BooleanFromString.optional(), - is_failed: BooleanFromString.optional(), -}); -export type GetRuleMigrationRequestQueryInput = z.input; export type GetRuleMigrationRequestParams = z.infer; export const GetRuleMigrationRequestParams = z.object({ @@ -80,13 +79,7 @@ export const GetRuleMigrationRequestParams = z.object({ export type GetRuleMigrationRequestParamsInput = z.input; export type GetRuleMigrationResponse = z.infer; -export const GetRuleMigrationResponse = z.object({ - /** - * The total number of rules in migration. - */ - total: z.number(), - data: z.array(RuleMigration), -}); +export const GetRuleMigrationResponse = RuleMigration; /** * The map of related integrations, with the integration id as a key @@ -173,6 +166,41 @@ export type GetRuleMigrationResourcesMissingResponse = z.infer< typeof GetRuleMigrationResourcesMissingResponse >; export const GetRuleMigrationResourcesMissingResponse = z.array(RuleMigrationResourceBase); +export type GetRuleMigrationRulesRequestQuery = z.infer; +export const GetRuleMigrationRulesRequestQuery = z.object({ + page: z.coerce.number().optional(), + per_page: z.coerce.number().optional(), + sort_field: NonEmptyString.optional(), + sort_direction: z.enum(['asc', 'desc']).optional(), + search_term: z.string().optional(), + ids: ArrayFromString(NonEmptyString).optional(), + is_prebuilt: BooleanFromString.optional(), + is_installed: BooleanFromString.optional(), + is_fully_translated: BooleanFromString.optional(), + is_partially_translated: BooleanFromString.optional(), + is_untranslatable: BooleanFromString.optional(), + is_failed: BooleanFromString.optional(), +}); +export type GetRuleMigrationRulesRequestQueryInput = z.input< + typeof GetRuleMigrationRulesRequestQuery +>; + +export type GetRuleMigrationRulesRequestParams = z.infer; +export const GetRuleMigrationRulesRequestParams = z.object({ + migration_id: NonEmptyString, +}); +export type GetRuleMigrationRulesRequestParamsInput = z.input< + typeof GetRuleMigrationRulesRequestParams +>; + +export type GetRuleMigrationRulesResponse = z.infer; +export const GetRuleMigrationRulesResponse = z.object({ + /** + * The total number of rules in migration. + */ + total: z.number(), + data: z.array(RuleMigrationRule), +}); export type GetRuleMigrationStatsRequestParams = z.infer; export const GetRuleMigrationStatsRequestParams = z.object({ @@ -275,12 +303,29 @@ export type UpdateRuleMigrationRequestParamsInput = z.input< typeof UpdateRuleMigrationRequestParams >; -export type UpdateRuleMigrationRequestBody = z.infer; -export const UpdateRuleMigrationRequestBody = z.array(UpdateRuleMigrationData); -export type UpdateRuleMigrationRequestBodyInput = z.input; - export type UpdateRuleMigrationResponse = z.infer; -export const UpdateRuleMigrationResponse = z.object({ +export const UpdateRuleMigrationResponse = RuleMigration; + +export type UpdateRuleMigrationRulesRequestParams = z.infer< + typeof UpdateRuleMigrationRulesRequestParams +>; +export const UpdateRuleMigrationRulesRequestParams = z.object({ + migration_id: NonEmptyString, +}); +export type UpdateRuleMigrationRulesRequestParamsInput = z.input< + typeof UpdateRuleMigrationRulesRequestParams +>; + +export type UpdateRuleMigrationRulesRequestBody = z.infer< + typeof UpdateRuleMigrationRulesRequestBody +>; +export const UpdateRuleMigrationRulesRequestBody = z.array(UpdateRuleMigrationRule); +export type UpdateRuleMigrationRulesRequestBodyInput = z.input< + typeof UpdateRuleMigrationRulesRequestBody +>; + +export type UpdateRuleMigrationRulesResponse = z.infer; +export const UpdateRuleMigrationRulesResponse = z.object({ /** * Indicates rules migrations have been updated. */ diff --git a/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.schema.yaml b/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.schema.yaml index 263456a128558..bb55e37bae001 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.schema.yaml +++ b/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.schema.yaml @@ -44,35 +44,18 @@ paths: additionalProperties: $ref: '../../../../../common/api/detection_engine/model/rule_schema/common_attributes.schema.yaml#/components/schemas/RelatedIntegration' - ## Specific rule migration APIs - - /internal/siem_migrations/rules/{migration_id}: - post: + /internal/siem_migrations/rules: + put: summary: Creates a new rule migration - operationId: CreateRuleMigration + operationId: "CreateRuleMigration" x-codegen-enabled: true x-internal: true - description: Creates a new SIEM rules migration using the original vendor rules provided + description: Creates a new rule migration and returns the corresponding migration_id tags: - SIEM Rule Migrations - parameters: - - name: migration_id - in: path - required: false - schema: - description: The migration id to create rules for - $ref: '../../../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' - requestBody: - required: true - content: - application/json: - schema: - type: array - items: - $ref: '../../rule_migration.schema.yaml#/components/schemas/OriginalRule' responses: 200: - description: Indicates migration have been created correctly. + description: The migration was created successfully and migrationId is returned content: application/json: schema: @@ -80,16 +63,18 @@ paths: required: - migration_id properties: - migration_id: - description: The migration id created. - $ref: '../../../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' + migration_id: + description: The migration id created. + $ref: '../../../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' - put: - summary: Updates rules of a migrations + ## Specific rule migration APIs + /internal/siem_migrations/rules/{migration_id}: + patch: + summary: Updates rule migration data operationId: UpdateRuleMigration x-codegen-enabled: true x-internal: true - description: Updates rules migrations attributes + description: Updates rules migrations data tags: - SIEM Rule Migrations parameters: @@ -99,6 +84,76 @@ paths: schema: description: The migration id to start $ref: '../../../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' + responses: + 200: + description: Indicates rules migrations have been updated correctly. + content: + application/json: + schema: + $ref: '../../rule_migration.schema.yaml#/components/schemas/RuleMigration' + 404: + description: Indicates the migration id was not found. + get: + summary: Retrieves a rule migration + operationId: GetRuleMigration + x-codegen-enabled: true + x-internal: true + description: Retrieves the rule migration document stored in the system given the rule migration id + tags: + - SIEM Rule Migrations + parameters: + - name: migration_id + in: path + required: true + schema: + description: The migration id to retrieve + $ref: '../../../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' + responses: + 200: + description: Indicates rules migrations have been retrieved correctly. + content: + application/json: + schema: + $ref: '../../rule_migration.schema.yaml#/components/schemas/RuleMigration' + 404: + description: Indicates the migration id was not found. + delete: + summary: Deletes a rule migration and its associated resources + operationId: DeleteRuleMigration + x-codegen-enabled: true + x-internal: true + description: Deletes a rule migration document stored in the system given the rule migration id + tags: + - SIEM Rule Migrations + parameters: + - name: migration_id + in: path + required: true + schema: + description: The migration id to delete + $ref: '../../../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' + responses: + 200: + description: Indicates rules migrations have been deleted correctly. + 404: + description: Indicates the migration id was not found. + + /internal/siem_migrations/rules/{migration_id}/rules: + post: + summary: Add rules to a rule migration + operationId: CreateRuleMigrationRules + x-codegen-enabled: true + x-internal: true + description: Adds original vendor rules to an already existing migration. Can be called multiple times to add more rules + tags: + - SIEM Rule Migrations + parameters: + - name: migration_id + in: path + required: true + schema: + description: The migration id to create rules for + $ref: '../../../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' requestBody: required: true content: @@ -106,27 +161,16 @@ paths: schema: type: array items: - $ref: '../../rule_migration.schema.yaml#/components/schemas/UpdateRuleMigrationData' + $ref: '../../rule_migration.schema.yaml#/components/schemas/OriginalRule' responses: 200: - description: Indicates rules migrations have been updated correctly. - content: - application/json: - schema: - type: object - required: - - updated - properties: - updated: - type: boolean - description: Indicates rules migrations have been updated. - + description: Indicates rules have been added to the migration successfully. get: summary: Retrieves all the rules of a migration - operationId: GetRuleMigration + operationId: GetRuleMigrationRules x-codegen-enabled: true x-internal: true - description: Retrieves the rule documents stored in the system given the rule migration id + description: Retrieves the the list of rules included in a migration given the migration id tags: - SIEM Rule Migrations parameters: @@ -202,7 +246,6 @@ paths: required: false schema: type: boolean - responses: 200: description: Indicates rule migration have been retrieved correctly. @@ -220,9 +263,45 @@ paths: data: type: array items: - $ref: '../../rule_migration.schema.yaml#/components/schemas/RuleMigration' - 204: + $ref: '../../rule_migration.schema.yaml#/components/schemas/RuleMigrationRule' + 404: description: Indicates the migration id was not found. + patch: + summary: Updates rules of a migrations + operationId: UpdateRuleMigrationRules + x-codegen-enabled: true + x-internal: true + description: Updates rules migrations attributes + tags: + - SIEM Rule Migrations + parameters: + - name: migration_id + in: path + required: true + schema: + description: The migration id to start + $ref: '../../../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' + requestBody: + required: true + content: + application/json: + schema: + type: array + items: + $ref: '../../rule_migration.schema.yaml#/components/schemas/UpdateRuleMigrationRule' + responses: + 200: + description: Indicates rules migrations have been updated correctly. + content: + application/json: + schema: + type: object + required: + - updated + properties: + updated: + type: boolean + description: Indicates rules migrations have been updated. /internal/siem_migrations/rules/{migration_id}/install: post: @@ -269,7 +348,7 @@ paths: description: Indicates the number of successfully installed migration rules. /internal/siem_migrations/rules/{migration_id}/start: - put: + post: summary: Starts a rule migration operationId: StartRuleMigration x-codegen-enabled: true @@ -368,7 +447,7 @@ paths: description: Indicates the migration id was not found. /internal/siem_migrations/rules/{migration_id}/stop: - put: + post: summary: Stops an existing rule migration operationId: StopRuleMigration x-codegen-enabled: true @@ -427,7 +506,6 @@ paths: $ref: '../../rule_migration.schema.yaml#/components/schemas/PrebuiltRuleVersion' # Rule migration resources APIs - /internal/siem_migrations/rules/{migration_id}/resources: post: summary: Creates or updates rule migration resources for a migration diff --git a/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/rule_migration.gen.ts b/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/rule_migration.gen.ts index 86c47f647e554..9b193d5e7ba86 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/rule_migration.gen.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/rule_migration.gen.ts @@ -141,6 +141,34 @@ export const PrebuiltRuleVersion = z.object({ current: RuleResponse.optional(), }); +/** + * The rule migration object ( without Id ) with its settings. + */ +export type RuleMigrationData = z.infer; +export const RuleMigrationData = z.object({ + /** + * The user profile ID of the user who created the migration. + */ + created_by: NonEmptyString, + /** + * The moment migration was created + */ + created_at: NonEmptyString, +}); + +/** + * The rule migration object with its settings. + */ +export type RuleMigration = z.infer; +export const RuleMigration = z + .object({ + /** + * The rule migration id + */ + id: NonEmptyString, + }) + .merge(RuleMigrationData); + /** * The rule translation result. */ @@ -185,8 +213,8 @@ export const RuleMigrationComments = z.array(RuleMigrationComment); /** * The rule migration document object. */ -export type RuleMigrationData = z.infer; -export const RuleMigrationData = z.object({ +export type RuleMigrationRuleData = z.infer; +export const RuleMigrationRuleData = z.object({ /** * The moment of creation */ @@ -232,15 +260,15 @@ export const RuleMigrationData = z.object({ /** * The rule migration document object. */ -export type RuleMigration = z.infer; -export const RuleMigration = z +export type RuleMigrationRule = z.infer; +export const RuleMigrationRule = z .object({ /** * The rule migration id */ id: NonEmptyString, }) - .merge(RuleMigrationData); + .merge(RuleMigrationRuleData); /** * The status of the migration task. @@ -363,8 +391,8 @@ export const RuleMigrationTranslationStats = z.object({ /** * The rule migration data object for rule update operation */ -export type UpdateRuleMigrationData = z.infer; -export const UpdateRuleMigrationData = z.object({ +export type UpdateRuleMigrationRule = z.infer; +export const UpdateRuleMigrationRule = z.object({ /** * The rule migration id */ diff --git a/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/rule_migration.schema.yaml b/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/rule_migration.schema.yaml index 8b508c099a653..19810ba94b641 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/rule_migration.schema.yaml +++ b/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/rule_migration.schema.yaml @@ -117,7 +117,7 @@ components: $ref: '../../../common/api/detection_engine/model/rule_schema/rule_schemas.schema.yaml#/components/schemas/RuleResponse' RuleMigration: - description: The rule migration document object. + description: The rule migration object with its settings. allOf: - type: object required: @@ -129,6 +129,33 @@ components: - $ref: '#/components/schemas/RuleMigrationData' RuleMigrationData: + type: object + description: The rule migration object ( without Id ) with its settings. + required: + - created_by + - created_at + properties: + created_by: + description: The user profile ID of the user who created the migration. + $ref: '../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' + created_at: + description: The moment migration was created + $ref: '../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' + + + RuleMigrationRule: + description: The rule migration document object. + allOf: + - type: object + required: + - id + properties: + id: + description: The rule migration id + $ref: '../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' + - $ref: '#/components/schemas/RuleMigrationRuleData' + + RuleMigrationRuleData: type: object description: The rule migration document object. required: @@ -331,7 +358,7 @@ components: description: The comments for the migration $ref: '#/components/schemas/RuleMigrationComment' - UpdateRuleMigrationData: + UpdateRuleMigrationRule: type: object description: The rule migration data object for rule update operation required: diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/rule_migrations_panels.tsx b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/rule_migrations_panels.tsx index cb154b6c6f425..9be2f0cbfeb59 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/rule_migrations_panels.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/rule_migrations_panels.tsx @@ -68,7 +68,11 @@ export const RuleMigrationsPanels = React.memo( {latestMigrationsStats.map((migrationStats) => ( - + {(migrationStats.status === SiemMigrationTaskStatus.READY || migrationStats.status === SiemMigrationTaskStatus.STOPPED) && ( diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/common/mocks/migration_result.data.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/common/mocks/migration_result.data.ts index f59214beef8e4..f903b914d1ead 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/common/mocks/migration_result.data.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/common/mocks/migration_result.data.ts @@ -8,7 +8,7 @@ import { v4 as uuidv4 } from 'uuid'; import { SiemMigrationTaskStatus } from '../../../../common/siem_migrations/constants'; import type { - GetRuleMigrationResponse, + GetRuleMigrationRulesResponse, GetRuleMigrationTranslationStatsResponse, } from '../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; import type { RuleMigrationStats } from '../../rules/types'; @@ -19,9 +19,9 @@ const getMockMigrationResultRule = ({ status = 'completed', }: { migrationId: string; - status?: GetRuleMigrationResponse['data'][number]['status']; - translationResult?: GetRuleMigrationResponse['data'][number]['translation_result']; -}): GetRuleMigrationResponse['data'][number] => { + status?: GetRuleMigrationRulesResponse['data'][number]['status']; + translationResult?: GetRuleMigrationRulesResponse['data'][number]['translation_result']; +}): GetRuleMigrationRulesResponse['data'][number] => { const ruleId = uuidv4(); return { migration_id: migrationId, @@ -92,7 +92,7 @@ export const mockedMigrationLatestStatsData: RuleMigrationStats[] = [ }, ]; -export const mockedMigrationResultsObj: Record = { +export const mockedMigrationResultsObj: Record = { '1': { total: 2, data: [ diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/api/index.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/api/index.ts index e4b976a51ab3f..c224f1b42c1ee 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/api/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/api/index.ts @@ -7,8 +7,8 @@ import { replaceParams } from '@kbn/openapi-common/shared'; +import type { UpdateRuleMigrationRule } from '../../../../common/siem_migrations/model/rule_migration.gen'; import type { RuleMigrationFilters } from '../../../../common/siem_migrations/types'; -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'; @@ -17,7 +17,6 @@ import { SIEM_RULE_MIGRATIONS_PATH, SIEM_RULE_MIGRATIONS_ALL_STATS_PATH, SIEM_RULE_MIGRATION_INSTALL_PATH, - SIEM_RULE_MIGRATION_PATH, SIEM_RULE_MIGRATION_START_PATH, SIEM_RULE_MIGRATION_STATS_PATH, SIEM_RULE_MIGRATION_TRANSLATION_STATS_PATH, @@ -26,12 +25,11 @@ import { SIEM_RULE_MIGRATIONS_PREBUILT_RULES_PATH, SIEM_RULE_MIGRATIONS_INTEGRATIONS_PATH, SIEM_RULE_MIGRATION_MISSING_PRIVILEGES_PATH, + SIEM_RULE_MIGRATION_RULES_PATH, } from '../../../../common/siem_migrations/constants'; import type { - CreateRuleMigrationRequestBody, CreateRuleMigrationResponse, GetAllStatsRuleMigrationResponse, - GetRuleMigrationResponse, GetRuleMigrationTranslationStatsResponse, InstallMigrationRulesResponse, StartRuleMigrationRequestBody, @@ -44,6 +42,8 @@ import type { StartRuleMigrationResponse, GetRuleMigrationIntegrationsResponse, GetRuleMigrationPrivilegesResponse, + GetRuleMigrationRulesResponse, + CreateRuleMigrationRulesRequestBody, } from '../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; export interface GetRuleMigrationStatsParams { @@ -78,22 +78,36 @@ export const getRuleMigrationsStatsAll = async ({ }; export interface CreateRuleMigrationParams { - /** Optional `id` of migration to add the rules to. - * The id is necessary only for batching the migration creation in multiple requests */ - migrationId?: string; - /** The body containing the `connectorId` to use for the migration */ - body: CreateRuleMigrationRequestBody; /** Optional AbortSignal for cancelling request */ signal?: AbortSignal; } /** Starts a new migration with the provided rules. */ export const createRuleMigration = async ({ + signal, +}: CreateRuleMigrationParams): Promise => { + return KibanaServices.get().http.put(SIEM_RULE_MIGRATIONS_PATH, { + version: '1', + signal, + }); +}; + +export interface AddRulesToMigrationParams { + /** `id` of the migration to add the rules to */ + migrationId: string; + /** The body containing the list of rules to be added to the migration */ + body: CreateRuleMigrationRulesRequestBody; + /** Optional AbortSignal for cancelling request */ + signal?: AbortSignal; +} + +/** Adds provided rules to an existing migration */ +export const addRulesToMigration = async ({ migrationId, body, signal, -}: CreateRuleMigrationParams): Promise => { - return KibanaServices.get().http.post( - `${SIEM_RULE_MIGRATIONS_PATH}${migrationId ? `/${migrationId}` : ''}`, +}: AddRulesToMigrationParams) => { + return KibanaServices.get().http.post( + replaceParams(SIEM_RULE_MIGRATION_RULES_PATH, { migration_id: migrationId }), { body: JSON.stringify(body), version: '1', signal } ); }; @@ -160,13 +174,13 @@ export const startRuleMigration = async ({ retry, langsmith_options: langSmithOptions, }; - return KibanaServices.get().http.put( + return KibanaServices.get().http.post( replaceParams(SIEM_RULE_MIGRATION_START_PATH, { migration_id: migrationId }), { body: JSON.stringify(body), version: '1', signal } ); }; -export interface GetRuleMigrationParams { +export interface GetMigrationRulesParams { /** `id` of the migration to get rules documents for */ migrationId: string; /** Optional page number to retrieve */ @@ -183,7 +197,7 @@ export interface GetRuleMigrationParams { signal?: AbortSignal; } /** Retrieves all the migration rule documents of a specific migration. */ -export const getRuleMigrations = async ({ +export const getMigrationRules = async ({ migrationId, page, perPage, @@ -191,9 +205,9 @@ export const getRuleMigrations = async ({ sortDirection, filters, signal, -}: GetRuleMigrationParams): Promise => { - return KibanaServices.get().http.get( - replaceParams(SIEM_RULE_MIGRATION_PATH, { migration_id: migrationId }), +}: GetMigrationRulesParams): Promise => { + return KibanaServices.get().http.get( + replaceParams(SIEM_RULE_MIGRATION_RULES_PATH, { migration_id: migrationId }), { version: '1', query: { @@ -306,7 +320,7 @@ export interface UpdateRulesParams { /** `id` of the migration to install rules for */ migrationId: string; /** The list of migration rules data to update */ - rulesToUpdate: UpdateRuleMigrationData[]; + rulesToUpdate: UpdateRuleMigrationRule[]; /** Optional AbortSignal for cancelling request */ signal?: AbortSignal; } @@ -316,8 +330,8 @@ export const updateMigrationRules = async ({ rulesToUpdate, signal, }: UpdateRulesParams): Promise => { - return KibanaServices.get().http.put( - replaceParams(SIEM_RULE_MIGRATION_PATH, { migration_id: migrationId }), + return KibanaServices.get().http.patch( + replaceParams(SIEM_RULE_MIGRATION_RULES_PATH, { migration_id: migrationId }), { version: '1', body: JSON.stringify(rulesToUpdate), signal } ); }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/data_input_flyout.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/data_input_flyout.tsx index 803ed18aba1f1..4d5d7318f7249 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/data_input_flyout.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/data_input_flyout.tsx @@ -158,6 +158,7 @@ export const MigrationDataInputFlyout = React.memo {isRetry ? ( ( ({ createMigration, apiError, isLoading, isCreated }) => { - const [rulesToUpload, setRulesToUpload] = useState([]); + const [rulesToUpload, setRulesToUpload] = useState([]); const filePickerRef = useRef(null); const createRules = useCallback(() => { diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/migration_result_panel.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/migration_result_panel.tsx index 25e3fb030bb29..30ac1f4cce5e8 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/migration_result_panel.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/migration_result_panel.tsx @@ -147,7 +147,7 @@ export const MigrationResultPanel = React.memo( ) : ( translationStats && ( <> - + {i18n.RULE_MIGRATION_SUMMARY_CHART_TITLE} @@ -248,7 +248,7 @@ const columns: Array> = [ name: i18n.RULE_MIGRATION_TABLE_COLUMN_STATUS, render: (title: string, { color }) => ( - {title} + {title} ), }, @@ -256,7 +256,11 @@ const columns: Array> = [ field: 'value', name: i18n.RULE_MIGRATION_TABLE_COLUMN_RULES, align: 'right', - render: (value: string) => {value}, + render: (value: string, { title }) => ( + + {value} + + ), }, ]; @@ -290,6 +294,13 @@ const TranslationResultsTable = React.memo<{ [translationStats, translationResultColors] ); - return ; + return ( + + ); }); TranslationResultsTable.displayName = 'TranslationResultsTable'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rule_details_flyout/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rule_details_flyout/index.tsx index 96d63bd24d054..4cad3ddc93071 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rule_details_flyout/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rule_details_flyout/index.tsx @@ -28,7 +28,7 @@ import { import type { EuiTabbedContentTab, EuiTabbedContentProps, EuiFlyoutProps } from '@elastic/eui'; import { RuleTranslationResult } from '../../../../../common/siem_migrations/constants'; -import type { RuleMigration } from '../../../../../common/siem_migrations/model/rule_migration.gen'; +import type { RuleMigrationRule } from '../../../../../common/siem_migrations/model/rule_migration.gen'; import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; import { RuleOverviewTab, @@ -70,7 +70,7 @@ export const TabContentPadding: FC> = ({ children }) ); interface MigrationRuleDetailsFlyoutProps { - ruleMigration: RuleMigration; + migrationRule: RuleMigrationRule; ruleActions?: React.ReactNode; matchedPrebuiltRule?: RuleResponse; size?: EuiFlyoutProps['size']; @@ -82,7 +82,7 @@ interface MigrationRuleDetailsFlyoutProps { export const MigrationRuleDetailsFlyout: React.FC = React.memo( ({ ruleActions, - ruleMigration, + migrationRule, matchedPrebuiltRule, size = 'm', extraTabs = [], @@ -93,7 +93,7 @@ export const MigrationRuleDetailsFlyout: React.FC { - const elasticRule = ruleMigration?.elastic_rule; + const elasticRule = migrationRule?.elastic_rule; if (isMigrationCustomRule(elasticRule)) { return convertMigrationCustomRuleToSecurityRulePayload(elasticRule, false); } return matchedPrebuiltRule; - }, [ruleMigration, matchedPrebuiltRule]); + }, [migrationRule, matchedPrebuiltRule]); const translationTab: EuiTabbedContentTab = useMemo( () => ({ @@ -137,14 +137,14 @@ export const MigrationRuleDetailsFlyout: React.FC ), }), - [ruleMigration, handleTranslationUpdate, matchedPrebuiltRule] + [migrationRule, handleTranslationUpdate, matchedPrebuiltRule] ); const overviewTab: EuiTabbedContentTab = useMemo( @@ -167,14 +167,14 @@ export const MigrationRuleDetailsFlyout: React.FC ), - disabled: ruleMigration.translation_result === RuleTranslationResult.UNTRANSLATABLE, + disabled: migrationRule.translation_result === RuleTranslationResult.UNTRANSLATABLE, }), [ ruleDetailsToOverview, size, expandedOverviewSections, toggleOverviewSection, - ruleMigration.translation_result, + migrationRule.translation_result, ] ); @@ -184,11 +184,11 @@ export const MigrationRuleDetailsFlyout: React.FC - + ), }), - [ruleMigration] + [migrationRule] ); const tabs = useMemo(() => { @@ -237,12 +237,12 @@ export const MigrationRuleDetailsFlyout: React.FC

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

- + = React.memo(({ ruleMigration }) => { +export const SummaryTab: React.FC = React.memo(({ migrationRule }) => { const userProfileIds = useMemo>(() => { - if (!ruleMigration.comments) { + if (!migrationRule.comments) { return new Set(); } - return ruleMigration.comments.reduce((acc, { created_by: createdBy }) => { + return migrationRule.comments.reduce((acc, { created_by: createdBy }) => { if (createdBy !== SIEM_MIGRATIONS_ASSISTANT_USER) acc.add(createdBy); return acc; }, new Set()); - }, [ruleMigration.comments]); + }, [migrationRule.comments]); const { isLoading: isLoadingUserProfiles, data: userProfiles } = useBulkGetUserProfiles({ uids: userProfileIds, }); @@ -42,7 +42,7 @@ export const SummaryTab: React.FC = React.memo(({ ruleMigration if (isLoadingUserProfiles) { return undefined; } - return ruleMigration.comments?.map( + return migrationRule.comments?.map( ({ message, created_at: createdAt, created_by: createdBy }) => { const profile = userProfiles?.find(({ uid }) => uid === createdBy); const isCreatedByAssistant = createdBy === SIEM_MIGRATIONS_ASSISTANT_USER || !profile; @@ -63,7 +63,7 @@ export const SummaryTab: React.FC = React.memo(({ ruleMigration /> ), event: - ruleMigration.translation_result === RuleTranslationResult.UNTRANSLATABLE + migrationRule.translation_result === RuleTranslationResult.UNTRANSLATABLE ? i18n.COMMENT_EVENT_UNTRANSLATABLE : i18n.COMMENT_EVENT_TRANSLATED, timestamp: moment(createdAt).format('ll'), // Date formats https://momentjs.com/docs/#/displaying/format/ @@ -73,8 +73,8 @@ export const SummaryTab: React.FC = React.memo(({ ruleMigration ); }, [ isLoadingUserProfiles, - ruleMigration.comments, - ruleMigration.translation_result, + migrationRule.comments, + migrationRule.translation_result, userProfiles, ]); diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rule_details_flyout/tabs/translation/callout.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rule_details_flyout/tabs/translation/callout.tsx index 597c2669a8801..53b8ab34c4ae2 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rule_details_flyout/tabs/translation/callout.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rule_details_flyout/tabs/translation/callout.tsx @@ -9,10 +9,8 @@ import type { FC } from 'react'; import React from 'react'; import type { IconType } from '@elastic/eui'; import { EuiCallOut } from '@elastic/eui'; -import { - type RuleMigration, - type RuleMigrationTranslationResult, -} from '../../../../../../../common/siem_migrations/model/rule_migration.gen'; +import type { RuleMigrationRule } from '../../../../../../../common/siem_migrations/model/rule_migration.gen'; +import { type RuleMigrationTranslationResult } from '../../../../../../../common/siem_migrations/model/rule_migration.gen'; import * as i18n from './translations'; type RuleMigrationTranslationCallOutMode = RuleMigrationTranslationResult | 'mapped'; @@ -51,17 +49,17 @@ const getCallOutInfo = ( }; export interface TranslationCallOutProps { - ruleMigration: RuleMigration; + migrationRule: RuleMigrationRule; } -export const TranslationCallOut: FC = React.memo(({ ruleMigration }) => { - if (!ruleMigration.translation_result) { +export const TranslationCallOut: FC = React.memo(({ migrationRule }) => { + if (!migrationRule.translation_result) { return null; } - const mode = ruleMigration.elastic_rule?.prebuilt_rule_id + const mode = migrationRule.elastic_rule?.prebuilt_rule_id ? 'mapped' - : ruleMigration.translation_result; + : migrationRule.translation_result; const { title, message, icon, color } = getCallOutInfo(mode); return ( diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rule_details_flyout/tabs/translation/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rule_details_flyout/tabs/translation/index.tsx index d22da9cd03c57..8e82917c3815d 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rule_details_flyout/tabs/translation/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rule_details_flyout/tabs/translation/index.tsx @@ -21,7 +21,7 @@ import { css } from '@emotion/css'; import { FormattedMessage } from '@kbn/i18n-react'; import { RuleTranslationResult } from '../../../../../../../common/siem_migrations/constants'; import type { RuleResponse } from '../../../../../../../common/api/detection_engine'; -import type { RuleMigration } from '../../../../../../../common/siem_migrations/model/rule_migration.gen'; +import type { RuleMigrationRule } from '../../../../../../../common/siem_migrations/model/rule_migration.gen'; import { TranslationTabHeader } from './header'; import * as i18n from './translations'; import { @@ -32,23 +32,23 @@ import { TranslationCallOut } from './callout'; import { OriginalRuleQuery, TranslatedRuleQuery } from './query_details'; interface TranslationTabProps { - ruleMigration: RuleMigration; + migrationRule: RuleMigrationRule; matchedPrebuiltRule?: RuleResponse; onTranslationUpdate?: (ruleName: string, ruleQuery: string) => Promise; } export const TranslationTab: React.FC = React.memo( - ({ ruleMigration, matchedPrebuiltRule, onTranslationUpdate }) => { + ({ migrationRule, matchedPrebuiltRule, onTranslationUpdate }) => { const { euiTheme } = useEuiTheme(); - const isInstalled = !!ruleMigration.elastic_rule?.id; + const isInstalled = !!migrationRule.elastic_rule?.id; return ( <> - {ruleMigration.translation_result && !isInstalled && ( + {migrationRule.translation_result && !isInstalled && ( <> - + )} @@ -74,11 +74,12 @@ export const TranslationTab: React.FC = React.memo(
{isInstalled ? i18n.INSTALLED_LABEL - : convertTranslationResultIntoText(ruleMigration.translation_result)} + : convertTranslationResultIntoText(migrationRule.translation_result)} @@ -86,7 +87,7 @@ export const TranslationTab: React.FC = React.memo( - + = React.memo( /> @@ -107,8 +108,8 @@ export const TranslationTab: React.FC = React.memo( - {ruleMigration.translation_result === RuleTranslationResult.FULL && - !ruleMigration.elastic_rule?.id && ( + {migrationRule.translation_result === RuleTranslationResult.FULL && + !migrationRule.elastic_rule?.id && ( <> = React.memo( - ({ ruleMigration }) => { + ({ migrationRule }) => { return ( <> ); diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rule_details_flyout/tabs/translation/query_details/query_editor.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rule_details_flyout/tabs/translation/query_details/query_editor.tsx index e518466fc3049..fbe88526a7f08 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rule_details_flyout/tabs/translation/query_details/query_editor.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rule_details_flyout/tabs/translation/query_details/query_editor.tsx @@ -64,7 +64,12 @@ export const QueryEditor: React.FC = React.memo( - + {i18n.SAVE} diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rule_details_flyout/tabs/translation/query_details/query_viewer.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rule_details_flyout/tabs/translation/query_details/query_viewer.tsx index fc4005c3b38e1..8ceb7250d51b7 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rule_details_flyout/tabs/translation/query_details/query_viewer.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rule_details_flyout/tabs/translation/query_details/query_viewer.tsx @@ -39,7 +39,12 @@ export const QueryViewer: React.FC = React.memo( {onEdit ? ( - + {i18n.EDIT} @@ -53,6 +58,7 @@ export const QueryViewer: React.FC = React.memo( {query.length ? ( { }; interface TranslatedRuleQueryProps { - ruleMigration: RuleMigration; + migrationRule: RuleMigrationRule; matchedPrebuiltRule?: RuleResponse; onTranslationUpdate?: (ruleName: string, ruleQuery: string) => Promise; } export const TranslatedRuleQuery: React.FC = React.memo( - ({ ruleMigration, matchedPrebuiltRule, onTranslationUpdate }) => { - const isInstalled = !!ruleMigration.elastic_rule?.id; + ({ migrationRule, matchedPrebuiltRule, onTranslationUpdate }) => { + const isInstalled = !!migrationRule.elastic_rule?.id; const canEdit = !matchedPrebuiltRule && !isInstalled; const translatedData = useMemo(() => { - let ruleName = ruleMigration.elastic_rule?.title ?? ''; + let ruleName = migrationRule.elastic_rule?.title ?? ''; let title = i18n.CUSTOM_TRANSLATION_TITLE; let titleTooltip = i18n.TRANSLATION_QUERY_TOOLTIP; - let query = ruleMigration.elastic_rule?.query ?? ''; - let language = ruleMigration.elastic_rule?.query_language ?? ''; + let query = migrationRule.elastic_rule?.query ?? ''; + let language = migrationRule.elastic_rule?.query_language ?? ''; let queryPlaceholder = i18n.TRANSLATION_QUERY_PLACEHOLDER; if (matchedPrebuiltRule) { ruleName = matchedPrebuiltRule.name; @@ -72,7 +72,7 @@ export const TranslatedRuleQuery: React.FC = React.mem language, queryPlaceholder, }; - }, [matchedPrebuiltRule, ruleMigration.elastic_rule]); + }, [matchedPrebuiltRule, migrationRule.elastic_rule]); const formDefaultValue: RuleTranslationSchema = useMemo(() => { return { diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rule_details_flyout/updated_by/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rule_details_flyout/updated_by/index.tsx index 6b615ee32deeb..421ca94f15e6b 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rule_details_flyout/updated_by/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rule_details_flyout/updated_by/index.tsx @@ -11,19 +11,19 @@ import { EuiText } from '@elastic/eui'; import { FormattedDate } from '../../../../../common/components/formatted_date'; import { useBulkGetUserProfiles } from '../../../../../common/components/user_profiles/use_bulk_get_user_profiles'; -import type { RuleMigration } from '../../../../../../common/siem_migrations/model/rule_migration.gen'; +import type { RuleMigrationRule } from '../../../../../../common/siem_migrations/model/rule_migration.gen'; import * as i18n from './translations'; interface UpdatedByLabelProps { - ruleMigration: RuleMigration; + migrationRule: RuleMigrationRule; } export const UpdatedByLabel: React.FC = React.memo( - ({ ruleMigration }: UpdatedByLabelProps) => { + ({ migrationRule }: UpdatedByLabelProps) => { const userProfileId = useMemo( - () => new Set([ruleMigration.updated_by ?? ruleMigration.created_by]), - [ruleMigration.created_by, ruleMigration.updated_by] + () => new Set([migrationRule.updated_by ?? migrationRule.created_by]), + [migrationRule.created_by, migrationRule.updated_by] ); const { isLoading: isLoadingUserProfiles, data: userProfiles } = useBulkGetUserProfiles({ uids: userProfileId, @@ -35,7 +35,7 @@ export const UpdatedByLabel: React.FC = React.memo( const userProfile = userProfiles[0]; const updatedBy = userProfile.user.full_name ?? userProfile.user.username; - const updatedAt = ruleMigration.updated_at ?? ruleMigration['@timestamp']; + const updatedAt = migrationRule.updated_at ?? migrationRule['@timestamp']; return ( = React.mem const [pageIndex, setPageIndex] = useState(0); const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE); - const [sortField, setSortField] = useState(DEFAULT_SORT_FIELD); + const [sortField, setSortField] = useState(DEFAULT_SORT_FIELD); const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>(DEFAULT_SORT_DIRECTION); const [searchTerm, setSearchTerm] = useState(); @@ -93,7 +93,7 @@ export const MigrationRulesTable: React.FC = React.mem useGetMigrationPrebuiltRules(migrationId); const { - data: { ruleMigrations, total } = { ruleMigrations: [], total: 0 }, + data: { migrationRules, total } = { migrationRules: [], total: 0 }, isLoading: isDataLoading, } = useGetMigrationRules({ migrationId, @@ -107,13 +107,13 @@ export const MigrationRulesTable: React.FC = React.mem }, }); - const [selectedRuleMigrations, setSelectedRuleMigrations] = useState([]); - const tableSelection: EuiTableSelectionType = useMemo( + const [selectedMigrationRules, setSelectedMigrationRules] = useState([]); + const tableSelection: EuiTableSelectionType = useMemo( () => ({ - selectable: (item: RuleMigration) => { + selectable: (item: RuleMigrationRule) => { return !item.elastic_rule?.id && item.translation_result === RuleTranslationResult.FULL; }, - selectableMessage: (selectable: boolean, item: RuleMigration) => { + selectableMessage: (selectable: boolean, item: RuleMigrationRule) => { if (selectable) { return ''; } @@ -121,10 +121,10 @@ export const MigrationRulesTable: React.FC = React.mem ? i18n.ALREADY_TRANSLATED_RULE_TOOLTIP : i18n.NOT_FULLY_TRANSLATED_RULE_TOOLTIP; }, - onSelectionChange: setSelectedRuleMigrations, - selected: selectedRuleMigrations, + onSelectionChange: setSelectedMigrationRules, + selected: selectedMigrationRules, }), - [selectedRuleMigrations] + [selectedMigrationRules] ); const pagination = useMemo(() => { @@ -144,17 +144,20 @@ export const MigrationRulesTable: React.FC = React.mem }; }, [sortDirection, sortField]); - const onTableChange = useCallback(({ page, sort }: CriteriaWithPagination) => { - if (page) { - setPageIndex(page.index); - setPageSize(page.size); - } - if (sort) { - const { field, direction } = sort; - setSortField(field); - setSortDirection(direction); - } - }, []); + const onTableChange = useCallback( + ({ page, sort }: CriteriaWithPagination) => { + if (page) { + setPageIndex(page.index); + setPageSize(page.size); + } + if (sort) { + const { field, direction } = sort; + setSortField(field); + setSortDirection(direction); + } + }, + [] + ); const handleOnSearch = useCallback((value: string) => { setSearchTerm(value.trim()); @@ -169,10 +172,10 @@ export const MigrationRulesTable: React.FC = React.mem const [isTableLoading, setTableLoading] = useState(false); const installSingleRule = useCallback( - async (ruleMigration: RuleMigration, enabled?: boolean) => { + async (migrationRule: RuleMigrationRule, enabled?: boolean) => { setTableLoading(true); try { - await installMigrationRule({ ruleMigration, enabled }); + await installMigrationRule({ migrationRule, enabled }); } catch (error) { addError(error, { title: logicI18n.INSTALL_MIGRATION_RULES_FAILURE }); } finally { @@ -187,17 +190,17 @@ export const MigrationRulesTable: React.FC = React.mem setTableLoading(true); try { await installMigrationRules({ - ids: selectedRuleMigrations.map((rule) => rule.id), + ids: selectedMigrationRules.map((rule) => rule.id), enabled, }); } catch (error) { addError(error, { title: logicI18n.INSTALL_MIGRATION_RULES_FAILURE }); } finally { setTableLoading(false); - setSelectedRuleMigrations([]); + setSelectedMigrationRules([]); } }, - [addError, installMigrationRules, selectedRuleMigrations] + [addError, installMigrationRules, selectedMigrationRules] ); const installTranslatedRules = useCallback( @@ -222,18 +225,18 @@ export const MigrationRulesTable: React.FC = React.mem isPrebuiltRulesLoading || isDataLoading || isTableLoading || isRetryLoading; const ruleActionsFactory = useCallback( - (ruleMigration: RuleMigration, closeRulePreview: () => void) => { + (migrationRule: RuleMigrationRule, closeRulePreview: () => void) => { const canMigrationRuleBeInstalled = !isRulesLoading && - !ruleMigration.elastic_rule?.id && - ruleMigration.translation_result === RuleTranslationResult.FULL; + !migrationRule.elastic_rule?.id && + migrationRule.translation_result === RuleTranslationResult.FULL; return ( { - installSingleRule(ruleMigration); + installSingleRule(migrationRule); closeRulePreview(); }} data-test-subj="installMigrationRuleFromFlyoutButton" @@ -241,12 +244,12 @@ export const MigrationRulesTable: React.FC = React.mem {i18n.INSTALL_WITHOUT_ENABLING_BUTTON_LABEL} - {isMigrationPrebuiltRule(ruleMigration.elastic_rule) && ( + {isMigrationPrebuiltRule(migrationRule.elastic_rule) && ( { - installSingleRule(ruleMigration, true); + installSingleRule(migrationRule, true); closeRulePreview(); }} fill @@ -264,27 +267,32 @@ export const MigrationRulesTable: React.FC = React.mem const getMigrationRuleData = useCallback( (ruleId: string) => { - if (!isRulesLoading && ruleMigrations.length) { - const ruleMigration = ruleMigrations.find((item) => item.id === ruleId); + if (!isRulesLoading && migrationRules.length) { + const migrationRule = migrationRules.find((item) => item.id === ruleId); let matchedPrebuiltRule: RuleResponse | undefined; let relatedIntegrations: RelatedIntegration[] = []; - if (ruleMigration) { + if (migrationRule) { // Find matched prebuilt rule if any and prioritize its installed version - const prebuiltRuleId = ruleMigration.elastic_rule?.prebuilt_rule_id; + const prebuiltRuleId = migrationRule.elastic_rule?.prebuilt_rule_id; const prebuiltRuleVersions = prebuiltRuleId ? prebuiltRules[prebuiltRuleId] : undefined; matchedPrebuiltRule = prebuiltRuleVersions?.current ?? prebuiltRuleVersions?.target; - const integrationIds = ruleMigration.elastic_rule?.integration_ids; + const integrationIds = migrationRule.elastic_rule?.integration_ids; if (integrations && integrationIds) { relatedIntegrations = integrationIds .map((integrationId) => integrations[integrationId]) .filter((integration) => integration != null); } } - return { ruleMigration, matchedPrebuiltRule, relatedIntegrations, isIntegrationsLoading }; + return { + migrationRule, + matchedPrebuiltRule, + relatedIntegrations, + isIntegrationsLoading, + }; } }, - [integrations, isIntegrationsLoading, isRulesLoading, prebuiltRules, ruleMigrations] + [integrations, isIntegrationsLoading, isRulesLoading, prebuiltRules, migrationRules] ); const { @@ -340,7 +348,7 @@ export const MigrationRulesTable: React.FC = React.mem isTableLoading={isRulesLoading} numberOfFailedRules={translationStats.rules.failed} numberOfTranslatedRules={translationStats.rules.success.installable} - numberOfSelectedRules={selectedRuleMigrations.length} + numberOfSelectedRules={selectedMigrationRules.length} installTranslatedRule={installTranslatedRules} installSelectedRule={installSelectedRule} reprocessFailedRules={reprocessFailedRules} @@ -348,9 +356,9 @@ export const MigrationRulesTable: React.FC = React.mem - + loading={isTableLoading} - items={ruleMigrations} + items={migrationRules} pagination={pagination} sorting={sorting} onChange={onTableChange} diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/actions.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/actions.tsx index c72001e394f28..8bf4399b8471c 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/actions.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/actions.tsx @@ -15,16 +15,16 @@ import { } from '../../../../../common/siem_migrations/constants'; import { getRuleDetailsUrl } from '../../../../common/components/link_to'; import { SecurityPageName } from '../../../../../common'; -import { type RuleMigration } from '../../../../../common/siem_migrations/model/rule_migration.gen'; +import { type RuleMigrationRule } from '../../../../../common/siem_migrations/model/rule_migration.gen'; import * as i18n from './translations'; import { type TableColumn } from './constants'; import { TableHeader } from './header'; interface ActionNameProps { disableActions?: boolean; - migrationRule: RuleMigration; - openMigrationRuleDetails: (migrationRule: RuleMigration) => void; - installMigrationRule: (migrationRule: RuleMigration, enable?: boolean) => void; + migrationRule: RuleMigrationRule; + openMigrationRuleDetails: (migrationRule: RuleMigrationRule) => void; + installMigrationRule: (migrationRule: RuleMigrationRule, enable?: boolean) => void; } const ActionName = ({ @@ -82,8 +82,8 @@ const ActionName = ({ interface CreateActionsColumnProps { disableActions?: boolean; - openMigrationRuleDetails: (migrationRule: RuleMigration) => void; - installMigrationRule: (migrationRule: RuleMigration, enable?: boolean) => void; + openMigrationRuleDetails: (migrationRule: RuleMigrationRule) => void; + installMigrationRule: (migrationRule: RuleMigrationRule, enable?: boolean) => void; } export const createActionsColumn = ({ @@ -121,7 +121,7 @@ export const createActionsColumn = ({ } /> ), - render: (_, rule: RuleMigration) => { + render: (_, rule: RuleMigrationRule) => { return ( { } /> ), - render: (_, rule: RuleMigration) => { + render: (_, rule: RuleMigrationRule) => { return rule.status === SiemMigrationStatus.FAILED ? ( <>{COLUMN_EMPTY_VALUE} ) : ( diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/constants.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/constants.tsx index 1576b13f76dc6..c73d246f3c544 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/constants.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/constants.tsx @@ -6,8 +6,8 @@ */ import type { EuiBasicTableColumn } from '@elastic/eui'; -import type { RuleMigration } from '../../../../../common/siem_migrations/model/rule_migration.gen'; +import type { RuleMigrationRule } from '../../../../../common/siem_migrations/model/rule_migration.gen'; -export type TableColumn = EuiBasicTableColumn; +export type TableColumn = EuiBasicTableColumn; export const COLUMN_EMPTY_VALUE = '-'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/integrations.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/integrations.tsx index 2f8dcb4b7a38e..4a6abbcdf633b 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/integrations.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/integrations.tsx @@ -10,7 +10,7 @@ import { EuiHorizontalRule, EuiLoadingSpinner, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import type { RelatedIntegration } from '../../../../../common/api/detection_engine'; import { IntegrationsPopover } from '../../../../detections/components/rules/related_integrations/integrations_popover'; -import type { RuleMigration } from '../../../../../common/siem_migrations/model/rule_migration.gen'; +import type { RuleMigrationRule } from '../../../../../common/siem_migrations/model/rule_migration.gen'; import * as i18n from './translations'; import type { TableColumn } from './constants'; import { TableHeader } from './header'; @@ -45,7 +45,7 @@ export const createIntegrationsColumn = ({ } /> ), - render: (_, rule: RuleMigration) => { + render: (_, rule: RuleMigrationRule) => { const migrationRuleData = getMigrationRuleData(rule.id); if (migrationRuleData?.isIntegrationsLoading) { return ; diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/name.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/name.tsx index c09e06bff3a8a..62b01e9ea75a6 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/name.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/name.tsx @@ -8,13 +8,13 @@ import React from 'react'; import { EuiLink, EuiText } from '@elastic/eui'; import { SiemMigrationStatus } from '../../../../../common/siem_migrations/constants'; -import { type RuleMigration } from '../../../../../common/siem_migrations/model/rule_migration.gen'; +import { type RuleMigrationRule } from '../../../../../common/siem_migrations/model/rule_migration.gen'; import * as i18n from './translations'; import type { TableColumn } from './constants'; interface NameProps { - rule: RuleMigration; - openMigrationRuleDetails: (rule: RuleMigration) => void; + rule: RuleMigrationRule; + openMigrationRuleDetails: (rule: RuleMigrationRule) => void; } const Name = ({ rule, openMigrationRuleDetails }: NameProps) => { @@ -40,12 +40,12 @@ const Name = ({ rule, openMigrationRuleDetails }: NameProps) => { export const createNameColumn = ({ openMigrationRuleDetails, }: { - openMigrationRuleDetails: (rule: RuleMigration) => void; + openMigrationRuleDetails: (rule: RuleMigrationRule) => void; }): TableColumn => { return { field: 'elastic_rule.title', name: i18n.COLUMN_NAME, - render: (_, rule: RuleMigration) => ( + render: (_, rule: RuleMigrationRule) => ( ), sortable: true, diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/risk_score.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/risk_score.tsx index 938d86dde01b0..fc40f4c9a5bd6 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/risk_score.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/risk_score.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { EuiText } from '@elastic/eui'; -import { type RuleMigration } from '../../../../../common/siem_migrations/model/rule_migration.gen'; +import { type RuleMigrationRule } from '../../../../../common/siem_migrations/model/rule_migration.gen'; import { SiemMigrationStatus } from '../../../../../common/siem_migrations/constants'; import * as i18n from './translations'; import { COLUMN_EMPTY_VALUE, type TableColumn } from './constants'; @@ -16,7 +16,7 @@ export const createRiskScoreColumn = (): TableColumn => { return { field: 'elastic_rule.risk_score', name: i18n.COLUMN_RISK_SCORE, - render: (riskScore, rule: RuleMigration) => ( + render: (riskScore, rule: RuleMigrationRule) => ( {rule.status === SiemMigrationStatus.FAILED ? COLUMN_EMPTY_VALUE : riskScore} diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/severity.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/severity.tsx index 6911e23a421fb..1a6c554e0fda2 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/severity.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/severity.tsx @@ -9,7 +9,7 @@ import React from 'react'; import type { Severity } from '@kbn/securitysolution-io-ts-alerting-types'; import { EuiHorizontalRule, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; -import { type RuleMigration } from '../../../../../common/siem_migrations/model/rule_migration.gen'; +import type { RuleMigrationRule } from '../../../../../common/siem_migrations/model/rule_migration.gen'; import { SiemMigrationStatus } from '../../../../../common/siem_migrations/constants'; import { SeverityBadge } from '../../../../common/components/severity_badge'; import { COLUMN_EMPTY_VALUE, type TableColumn } from './constants'; @@ -40,7 +40,7 @@ export const createSeverityColumn = (): TableColumn => { } /> ), - render: (value: Severity, rule: RuleMigration) => + render: (value: Severity, rule: RuleMigrationRule) => rule.status === SiemMigrationStatus.FAILED ? ( <>{COLUMN_EMPTY_VALUE} ) : ( diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/status.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/status.tsx index 0687f5f727f76..355502861e123 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/status.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/status.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiHorizontalRule, EuiText } from '@elastic/eui'; import { RuleTranslationResult } from '../../../../../common/siem_migrations/constants'; -import type { RuleMigration } from '../../../../../common/siem_migrations/model/rule_migration.gen'; +import type { RuleMigrationRule } from '../../../../../common/siem_migrations/model/rule_migration.gen'; import * as i18n from './translations'; import type { TableColumn } from './constants'; import { StatusBadge } from '../status_badge'; @@ -56,7 +56,12 @@ export const createStatusColumn = (): TableColumn => { } /> ), - render: (_, rule: RuleMigration) => , + render: (_, rule: RuleMigrationRule) => ( + + ), sortable: true, truncateText: true, width: '15%', diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/updated.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/updated.tsx index aaf4e75ac4917..f793fe570f90a 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/updated.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/rules_table_columns/updated.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { FormattedRelativePreferenceDate } from '../../../../common/components/formatted_date'; -import type { RuleMigration } from '../../../../../common/siem_migrations/model/rule_migration.gen'; +import type { RuleMigrationRule } from '../../../../../common/siem_migrations/model/rule_migration.gen'; import * as i18n from './translations'; import type { TableColumn } from './constants'; @@ -15,7 +15,7 @@ export const createUpdatedColumn = (): TableColumn => { return { field: 'updated_at', name: i18n.COLUMN_UPDATED, - render: (value: RuleMigration['updated_at']) => ( + render: (value: RuleMigrationRule['updated_at']) => ( ), sortable: true, diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/status_badge/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/status_badge/index.tsx index 7a584bbd2c7db..bb3f9e153ec6c 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/status_badge/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/status_badge/index.tsx @@ -14,10 +14,8 @@ import { convertTranslationResultIntoText, useResultVisColors, } from '../../utils/translation_results'; -import { - RuleMigrationStatusEnum, - type RuleMigration, -} from '../../../../../common/siem_migrations/model/rule_migration.gen'; +import type { RuleMigrationRule } from '../../../../../common/siem_migrations/model/rule_migration.gen'; +import { RuleMigrationStatusEnum } from '../../../../../common/siem_migrations/model/rule_migration.gen'; import * as i18n from './translations'; const statusTextWrapperClassName = css` @@ -26,7 +24,7 @@ const statusTextWrapperClassName = css` `; interface StatusBadgeProps { - migrationRule: RuleMigration; + migrationRule: RuleMigrationRule; 'data-test-subj'?: string; } @@ -41,7 +39,9 @@ export const StatusBadge: React.FC = React.memo( - {i18n.RULE_STATUS_INSTALLED} + + {i18n.RULE_STATUS_INSTALLED} + ); @@ -58,7 +58,9 @@ export const StatusBadge: React.FC = React.memo( - {i18n.RULE_STATUS_FAILED} + + {i18n.RULE_STATUS_FAILED} + ); diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/hooks/use_migration_rule_preview_flyout.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/hooks/use_migration_rule_preview_flyout.tsx index 9dad5a30ab073..d20b2d8cc3465 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/hooks/use_migration_rule_preview_flyout.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/hooks/use_migration_rule_preview_flyout.tsx @@ -9,24 +9,24 @@ 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 { RuleMigration } from '../../../../common/siem_migrations/model/rule_migration.gen'; +import type { RuleMigrationRule } from '../../../../common/siem_migrations/model/rule_migration.gen'; import { MigrationRuleDetailsFlyout } from '../components/rule_details_flyout'; interface UseMigrationRuleDetailsFlyoutParams { isLoading?: boolean; getMigrationRuleData: (ruleId: string) => | { - ruleMigration?: RuleMigration; + migrationRule?: RuleMigrationRule; matchedPrebuiltRule?: RuleResponse; } | undefined; - ruleActionsFactory: (ruleMigration: RuleMigration, closeRulePreview: () => void) => ReactNode; - extraTabsFactory?: (ruleMigration: RuleMigration) => EuiTabbedContentTab[]; + ruleActionsFactory: (migrationRule: RuleMigrationRule, closeRulePreview: () => void) => ReactNode; + extraTabsFactory?: (migrationRule: RuleMigrationRule) => EuiTabbedContentTab[]; } interface UseMigrationRuleDetailsFlyoutResult { migrationRuleDetailsFlyout: ReactNode; - openMigrationRuleDetails: (rule: RuleMigration) => void; + openMigrationRuleDetails: (rule: RuleMigrationRule) => void; closeMigrationRuleDetails: () => void; } @@ -44,29 +44,29 @@ export function useMigrationRuleDetailsFlyout({ } }, [getMigrationRuleData, migrationRuleId]); - const openMigrationRuleDetails = useCallback((rule: RuleMigration) => { + const openMigrationRuleDetails = useCallback((rule: RuleMigrationRule) => { setMigrationRuleId(rule.id); }, []); const closeMigrationRuleDetails = useCallback(() => setMigrationRuleId(undefined), []); const ruleActions = useMemo( () => - migrationRuleData?.ruleMigration && - ruleActionsFactory(migrationRuleData.ruleMigration, closeMigrationRuleDetails), - [migrationRuleData?.ruleMigration, ruleActionsFactory, closeMigrationRuleDetails] + migrationRuleData?.migrationRule && + ruleActionsFactory(migrationRuleData.migrationRule, closeMigrationRuleDetails), + [migrationRuleData?.migrationRule, ruleActionsFactory, closeMigrationRuleDetails] ); const extraTabs = useMemo( () => - migrationRuleData?.ruleMigration && extraTabsFactory - ? extraTabsFactory(migrationRuleData.ruleMigration) + migrationRuleData?.migrationRule && extraTabsFactory + ? extraTabsFactory(migrationRuleData.migrationRule) : [], - [extraTabsFactory, migrationRuleData?.ruleMigration] + [extraTabsFactory, migrationRuleData?.migrationRule] ); return { - migrationRuleDetailsFlyout: migrationRuleData?.ruleMigration && ( + migrationRuleDetailsFlyout: migrationRuleData?.migrationRule && ( void; - installMigrationRule: (migrationRule: RuleMigration, enable?: boolean) => void; + openMigrationRuleDetails: (rule: RuleMigrationRule) => void; + installMigrationRule: (migrationRule: RuleMigrationRule, enable?: boolean) => void; getMigrationRuleData: ( ruleId: string ) => { relatedIntegrations?: RelatedIntegration[]; isIntegrationsLoading?: boolean } | undefined; diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/logic/use_get_migration_rules.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/logic/use_get_migration_rules.ts index 66e934a8bf8e9..3acfa61a8d4eb 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/logic/use_get_migration_rules.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/logic/use_get_migration_rules.ts @@ -12,7 +12,7 @@ import type { RuleMigrationFilters } from '../../../../common/siem_migrations/ty import { SIEM_RULE_MIGRATION_PATH } from '../../../../common/siem_migrations/constants'; import { useAppToasts } from '../../../common/hooks/use_app_toasts'; import * as i18n from './translations'; -import { getRuleMigrations } from '../api'; +import { getMigrationRules } from '../api'; import { DEFAULT_QUERY_OPTIONS } from './constants'; export const useGetMigrationRules = (params: { @@ -33,9 +33,9 @@ export const useGetMigrationRules = (params: { return useQuery( ['GET', SPECIFIC_MIGRATION_PATH, params], async ({ signal }) => { - const response = await getRuleMigrations({ signal, ...params }); + const response = await getMigrationRules({ signal, ...params }); - return { ruleMigrations: response.data, total: response.total }; + return { migrationRules: response.data, total: response.total }; }, { ...DEFAULT_QUERY_OPTIONS, diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/logic/use_install_migration_rule.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/logic/use_install_migration_rule.ts index 4cd022673cb75..01d746bc22505 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/logic/use_install_migration_rule.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/logic/use_install_migration_rule.ts @@ -7,8 +7,8 @@ import { useMutation } from '@tanstack/react-query'; import { useCallback } from 'react'; +import type { RuleMigrationRule } from '../../../../common/siem_migrations/model/rule_migration.gen'; import { useKibana } from '../../../common/lib/kibana/kibana_react'; -import type { RuleMigration } from '../../../../common/siem_migrations/model/rule_migration.gen'; 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 { useAppToasts } from '../../../common/hooks/use_app_toasts'; @@ -20,7 +20,7 @@ import { installMigrationRules } from '../api'; export const INSTALL_MIGRATION_RULE_MUTATION_KEY = ['POST', SIEM_RULE_MIGRATION_INSTALL_PATH]; interface InstallMigrationRuleParams { - ruleMigration: RuleMigration; + migrationRule: RuleMigrationRule; enabled?: boolean; } @@ -29,8 +29,8 @@ export const useInstallMigrationRule = (migrationId: string) => { const { telemetry } = useKibana().services.siemMigrations.rules; const reportTelemetry = useCallback( - ({ ruleMigration, enabled = false }: InstallMigrationRuleParams, error?: Error) => { - telemetry.reportTranslatedRuleInstall({ ruleMigration, enabled, error }); + ({ migrationRule, enabled = false }: InstallMigrationRuleParams, error?: Error) => { + telemetry.reportTranslatedRuleInstall({ migrationRule, enabled, error }); }, [telemetry] ); @@ -39,7 +39,7 @@ export const useInstallMigrationRule = (migrationId: string) => { const invalidateGetMigrationTranslationStats = useInvalidateGetMigrationTranslationStats(); return useMutation( - ({ ruleMigration, enabled }) => + ({ migrationRule: ruleMigration, enabled }) => installMigrationRules({ migrationId, ids: [ruleMigration.id], enabled }), { mutationKey: INSTALL_MIGRATION_RULE_MUTATION_KEY, diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/logic/use_update_migration_rule.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/logic/use_update_migration_rule.ts index b9e09214b90f4..36b659a6757f2 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/logic/use_update_migration_rule.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/logic/use_update_migration_rule.ts @@ -8,10 +8,10 @@ import { useMutation } from '@tanstack/react-query'; import { useCallback } from 'react'; import type { - RuleMigration, - UpdateRuleMigrationData, + RuleMigrationRule, + UpdateRuleMigrationRule, } from '../../../../common/siem_migrations/model/rule_migration.gen'; -import { SIEM_RULE_MIGRATION_PATH } from '../../../../common/siem_migrations/constants'; +import { SIEM_RULE_MIGRATION_RULES_PATH } from '../../../../common/siem_migrations/constants'; import type { UpdateRuleMigrationResponse } from '../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; import { useAppToasts } from '../../../common/hooks/use_app_toasts'; import { useKibana } from '../../../common/lib/kibana/kibana_react'; @@ -20,25 +20,25 @@ import { useInvalidateGetMigrationRules } from './use_get_migration_rules'; import { useInvalidateGetMigrationTranslationStats } from './use_get_migration_translation_stats'; import { updateMigrationRules } from '../api'; -export const UPDATE_MIGRATION_RULE_MUTATION_KEY = ['PUT', SIEM_RULE_MIGRATION_PATH]; +export const UPDATE_MIGRATION_RULE_MUTATION_KEY = ['PUT', SIEM_RULE_MIGRATION_RULES_PATH]; -export const useUpdateMigrationRule = (ruleMigration: RuleMigration) => { +export const useUpdateMigrationRule = (migrationRule: RuleMigrationRule) => { const { addError } = useAppToasts(); const { telemetry } = useKibana().services.siemMigrations.rules; - const migrationId = ruleMigration.migration_id; + const migrationId = migrationRule.migration_id; const reportTelemetry = useCallback( (error?: Error) => { - telemetry.reportTranslatedRuleUpdate({ ruleMigration, error }); + telemetry.reportTranslatedRuleUpdate({ migrationRule, error }); }, - [telemetry, ruleMigration] + [telemetry, migrationRule] ); const invalidateGetRuleMigrations = useInvalidateGetMigrationRules(); const invalidateGetMigrationTranslationStats = useInvalidateGetMigrationTranslationStats(); - return useMutation( + return useMutation( (ruleUpdateData) => updateMigrationRules({ migrationId, rulesToUpdate: [ruleUpdateData] }), { mutationKey: UPDATE_MIGRATION_RULE_MUTATION_KEY, diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/pages/index.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/pages/index.test.tsx index be66b66e87161..8985e607c615b 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/pages/index.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/pages/index.test.tsx @@ -133,7 +133,7 @@ const mockUseGetMigrationRules: typeof useGetMigrationRulesModule.useGetMigratio const { data, total } = mockedMigrationResultsObj[migrationId]; return { data: { - ruleMigrations: data, + migrationRules: data, total, }, isLoading: false, diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/hooks/use_create_migration.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/hooks/use_create_migration.ts index 18a4ebe47bf8d..dfa6451ed9145 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/hooks/use_create_migration.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/hooks/use_create_migration.ts @@ -7,8 +7,8 @@ import { useCallback, useReducer } from 'react'; import { i18n } from '@kbn/i18n'; +import type { CreateRuleMigrationRulesRequestBody } from '../../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; import type { RuleMigrationTaskStats } from '../../../../../common/siem_migrations/model/rule_migration.gen'; -import type { CreateRuleMigrationRequestBody } from '../../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; import { useKibana } from '../../../../common/lib/kibana/kibana_react'; import { reducer, initialState } from './common/api_request_reducer'; @@ -26,7 +26,7 @@ export const RULES_DATA_INPUT_CREATE_MIGRATION_ERROR = i18n.translate( { defaultMessage: 'Failed to upload rules file' } ); -export type CreateMigration = (data: CreateRuleMigrationRequestBody) => void; +export type CreateMigration = (data: CreateRuleMigrationRulesRequestBody) => void; export type OnSuccess = (migrationStats: RuleMigrationTaskStats) => void; export const useCreateMigration = (onSuccess: OnSuccess) => { diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.test.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.test.ts index 6e955d5d98066..608f02a507d31 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.test.ts @@ -23,8 +23,8 @@ import { getRuleMigrationsStatsAll, getMissingResources, getIntegrations, + addRulesToMigration, } from '../api'; -import type { CreateRuleMigrationRequestBody } from '../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; import { createTelemetryServiceMock } from '../../../common/lib/telemetry/telemetry_service.mock'; import { SiemMigrationRetryFilter, @@ -37,6 +37,7 @@ import { REQUEST_POLLING_INTERVAL_SECONDS, SiemRulesMigrationsService, } from './rule_migrations_service'; +import type { CreateRuleMigrationRulesRequestBody } from '../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; // --- Mocks for external modules --- @@ -48,6 +49,7 @@ jest.mock('../api', () => ({ getRuleMigrationsStatsAll: jest.fn(), getMissingResources: jest.fn(), getIntegrations: jest.fn(), + addRulesToMigration: jest.fn(), })); jest.mock('./capabilities', () => ({ @@ -132,37 +134,42 @@ describe('SiemRulesMigrationsService', () => { }); it('should create migration with a single batch', async () => { - const body = [{ id: 'rule1' }] as CreateRuleMigrationRequestBody; + const body = [{ id: 'rule1' }] as CreateRuleMigrationRulesRequestBody; (createRuleMigration as jest.Mock).mockResolvedValue({ migration_id: 'mig-1' }); + (addRulesToMigration as jest.Mock).mockResolvedValue(undefined); const migrationId = await service.createRuleMigration(body); expect(createRuleMigration).toHaveBeenCalledTimes(1); - expect(createRuleMigration).toHaveBeenCalledWith({ migrationId: undefined, body }); + expect(createRuleMigration).toHaveBeenCalledWith({}); + expect(addRulesToMigration).toHaveBeenCalledWith({ migrationId: 'mig-1', body }); expect(migrationId).toBe('mig-1'); }); it('should create migration in batches if body length exceeds the batch size', async () => { // Create an array of 51 items (the service batches in chunks of 50) const body = new Array(51).fill({ rule: 'rule' }); - (createRuleMigration as jest.Mock) - .mockResolvedValueOnce({ migration_id: 'mig-1' }) - .mockResolvedValueOnce({ migration_id: 'mig-2' }); + (createRuleMigration as jest.Mock).mockResolvedValueOnce({ migration_id: 'mig-1' }); + (addRulesToMigration as jest.Mock).mockResolvedValue(undefined); const migrationId = await service.createRuleMigration(body); - expect(createRuleMigration).toHaveBeenCalledTimes(2); + expect(createRuleMigration).toHaveBeenCalledTimes(1); + expect(addRulesToMigration).toHaveBeenCalledTimes(2); // First call: first 50 items, migrationId undefined - expect((createRuleMigration as jest.Mock).mock.calls[0][0]).toEqual({ - migrationId: undefined, + expect(createRuleMigration).toHaveBeenNthCalledWith(1, {}); + + expect(addRulesToMigration).toHaveBeenNthCalledWith(1, { + migrationId: 'mig-1', body: body.slice(0, 50), }); - // Second call: remaining 1 item, migrationId passed from previous batch - expect((createRuleMigration as jest.Mock).mock.calls[1][0]).toEqual({ + + expect(addRulesToMigration).toHaveBeenNthCalledWith(2, { migrationId: 'mig-1', body: body.slice(50, 51), }); - expect(migrationId).toBe('mig-2'); + + expect(migrationId).toBe('mig-1'); }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.ts index abeb52df7708c..a37b1db3a3404 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.ts @@ -19,7 +19,7 @@ import type { RuleMigrationTaskStats, } from '../../../../common/siem_migrations/model/rule_migration.gen'; import type { - CreateRuleMigrationRequestBody, + CreateRuleMigrationRulesRequestBody, GetRuleMigrationStatsResponse, StartRuleMigrationResponse, UpsertRuleMigrationResourcesRequestBody, @@ -39,6 +39,7 @@ import { getMissingResources, upsertMigrationResources, getIntegrations, + addRulesToMigration, } from '../api'; import { getMissingCapabilities, @@ -119,22 +120,36 @@ export class SiemRulesMigrationsService { }); } - public async createRuleMigration(body: CreateRuleMigrationRequestBody): Promise { - const rulesCount = body.length; + public async addRulesToMigration( + migrationId: string, + rules: CreateRuleMigrationRulesRequestBody + ) { + const rulesCount = rules.length; + if (rulesCount === 0) { + throw new Error(i18n.EMPTY_RULES_ERROR); + } + + // Batching creation to avoid hitting the max payload size limit of the API + for (let i = 0; i < rulesCount; i += CREATE_MIGRATION_BODY_BATCH_SIZE) { + const rulesBatch = rules.slice(i, i + CREATE_MIGRATION_BODY_BATCH_SIZE); + await addRulesToMigration({ migrationId, body: rulesBatch }); + } + } + + public async createRuleMigration(data: CreateRuleMigrationRulesRequestBody): Promise { + const rulesCount = data.length; if (rulesCount === 0) { throw new Error(i18n.EMPTY_RULES_ERROR); } try { - let migrationId: string | undefined; - // Batching creation to avoid hitting the max payload size limit of the API - for (let i = 0; i < rulesCount; i += CREATE_MIGRATION_BODY_BATCH_SIZE) { - const bodyBatch = body.slice(i, i + CREATE_MIGRATION_BODY_BATCH_SIZE); - const response = await createRuleMigration({ migrationId, body: bodyBatch }); - migrationId = response.migration_id; - } + // create the migration + const { migration_id: migrationId } = await createRuleMigration({}); + + await this.addRulesToMigration(migrationId, data); + this.telemetry.reportSetupMigrationCreated({ migrationId, rulesCount }); - return migrationId as string; + return migrationId; } catch (error) { this.telemetry.reportSetupMigrationCreated({ rulesCount, error }); throw error; diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/telemetry.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/telemetry.ts index 862078ac2cdab..aca82d394ec8a 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/telemetry.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/telemetry.ts @@ -9,8 +9,8 @@ import type { ActionConnector } from '@kbn/triggers-actions-ui-plugin/public'; import { siemMigrationEventNames } from '../../../common/lib/telemetry/events/siem_migrations'; import type { SiemMigrationRetryFilter } from '../../../../common/siem_migrations/constants'; import type { - RuleMigration, RuleMigrationResourceType, + RuleMigrationRule, } from '../../../../common/siem_migrations/model/rule_migration.gen'; import type { TelemetryServiceStart } from '../../../common/lib/telemetry'; import type { @@ -125,36 +125,36 @@ export class SiemRulesMigrationsTelemetry { // Translated rule actions - reportTranslatedRuleUpdate = (params: { ruleMigration: RuleMigration; error?: Error }) => { - const { ruleMigration, error } = params; + reportTranslatedRuleUpdate = (params: { migrationRule: RuleMigrationRule; error?: Error }) => { + const { migrationRule, error } = params; this.telemetryService.reportEvent(SiemMigrationsEventTypes.TranslatedRuleUpdate, { eventName: siemMigrationEventNames[SiemMigrationsEventTypes.TranslatedRuleUpdate], - migrationId: ruleMigration.migration_id, - ruleMigrationId: ruleMigration.id, + migrationId: migrationRule.migration_id, + ruleMigrationId: migrationRule.id, ...this.getBaseResultParams(error), }); }; reportTranslatedRuleInstall = (params: { - ruleMigration: RuleMigration; + migrationRule: RuleMigrationRule; enabled: boolean; error?: Error; }) => { - const { ruleMigration, enabled, error } = params; + const { migrationRule, enabled, error } = params; const eventParams: ReportTranslatedRuleInstallActionParams = { eventName: siemMigrationEventNames[SiemMigrationsEventTypes.TranslatedRuleInstall], - migrationId: ruleMigration.migration_id, - ruleMigrationId: ruleMigration.id, + migrationId: migrationRule.migration_id, + ruleMigrationId: migrationRule.id, author: 'custom', enabled, ...this.getBaseResultParams(error), }; - if (ruleMigration.elastic_rule?.prebuilt_rule_id) { + if (migrationRule.elastic_rule?.prebuilt_rule_id) { eventParams.author = 'elastic'; eventParams.prebuiltRule = { - id: ruleMigration.elastic_rule.prebuilt_rule_id, - title: ruleMigration.elastic_rule.title, + id: migrationRule.elastic_rule.prebuilt_rule_id, + title: migrationRule.elastic_rule.title, }; } diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/create.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/create.ts index 14560b0431040..2efb532694bd9 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/create.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/create.ts @@ -6,16 +6,9 @@ */ import type { IKibanaResponse, Logger } from '@kbn/core/server'; -import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; -import { SIEM_RULE_MIGRATION_CREATE_PATH } from '../../../../../common/siem_migrations/constants'; -import { - CreateRuleMigrationRequestBody, - CreateRuleMigrationRequestParams, - type CreateRuleMigrationResponse, -} from '../../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; -import { ResourceIdentifier } from '../../../../../common/siem_migrations/rules/resources'; +import { SIEM_RULE_MIGRATIONS_PATH } from '../../../../../common/siem_migrations/constants'; +import { type CreateRuleMigrationResponse } from '../../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; import type { SecuritySolutionPluginRouter } from '../../../../types'; -import type { CreateRuleMigrationInput } from '../data/rule_migrations_data_rules_client'; import { SiemMigrationAuditLogger } from './util/audit'; import { authz } from './util/authz'; import { withLicense } from './util/with_license'; @@ -25,67 +18,31 @@ export const registerSiemRuleMigrationsCreateRoute = ( logger: Logger ) => { router.versioned - .post({ - path: SIEM_RULE_MIGRATION_CREATE_PATH, + .put({ + path: SIEM_RULE_MIGRATIONS_PATH, access: 'internal', security: { authz }, }) .addVersion( { version: '1', - validate: { - request: { - body: buildRouteValidationWithZod(CreateRuleMigrationRequestBody), - params: buildRouteValidationWithZod(CreateRuleMigrationRequestParams), - }, - }, + // no request body or params to validate + validate: false, }, withLicense( async (context, req, res): Promise> => { - const originalRules = req.body; const siemMigrationAuditLogger = new SiemMigrationAuditLogger(context.securitySolution); - const providedMigrationId = req.params?.migration_id; try { - const [firstOriginalRule] = originalRules; - if (!firstOriginalRule) { - return res.noContent(); - } const ctx = await context.resolve(['securitySolution']); const ruleMigrationsClient = ctx.securitySolution.getSiemRuleMigrationsClient(); - await siemMigrationAuditLogger.logCreateMigration({ migrationId: providedMigrationId }); + await siemMigrationAuditLogger.logCreateMigration(); - let migrationId: string; - - if (!providedMigrationId) { - /** if new migration */ - migrationId = await ruleMigrationsClient.data.migrations.create(); - } else { - /** if updating existing migration */ - migrationId = providedMigrationId; - } - - const ruleMigrations = originalRules.map((originalRule) => ({ - migration_id: migrationId, - original_rule: originalRule, - })); - - await ruleMigrationsClient.data.rules.create(ruleMigrations); - - // Create identified resource documents without content to keep track of them - const resourceIdentifier = new ResourceIdentifier(firstOriginalRule.vendor); - const resources = resourceIdentifier - .fromOriginalRules(originalRules) - .map((resource) => ({ ...resource, migration_id: migrationId })); - - if (resources.length > 0) { - await ruleMigrationsClient.data.resources.create(resources); - } + const migrationId = await ruleMigrationsClient.data.migrations.create(); return res.ok({ body: { migration_id: migrationId } }); } catch (error) { logger.error(error); await siemMigrationAuditLogger.logCreateMigration({ - migrationId: providedMigrationId, error, }); return res.badRequest({ body: error.message }); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/update.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/delete.ts similarity index 50% rename from x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/update.ts rename to x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/delete.ts index a1d93eee2c55e..ce35853db5e8d 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/update.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/delete.ts @@ -5,26 +5,22 @@ * 2.0. */ -import type { IKibanaResponse, Logger } from '@kbn/core/server'; +import type { Logger } from '@kbn/core/server'; import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; import { SIEM_RULE_MIGRATION_PATH } from '../../../../../common/siem_migrations/constants'; -import { - UpdateRuleMigrationRequestBody, - UpdateRuleMigrationRequestParams, - type UpdateRuleMigrationResponse, -} from '../../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; +import { GetRuleMigrationRequestParams } from '../../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; import type { SecuritySolutionPluginRouter } from '../../../../types'; -import { authz } from './util/authz'; import { SiemMigrationAuditLogger } from './util/audit'; -import { transformToInternalUpdateRuleMigrationData } from './util/update_rules'; +import { authz } from './util/authz'; import { withLicense } from './util/with_license'; +import { withExistingMigration } from './util/with_existing_migration_id'; -export const registerSiemRuleMigrationsUpdateRoute = ( +export const registerSiemRuleMigrationsDeleteRoute = ( router: SecuritySolutionPluginRouter, logger: Logger ) => { router.versioned - .put({ + .delete({ path: SIEM_RULE_MIGRATION_PATH, access: 'internal', security: { authz }, @@ -34,40 +30,40 @@ export const registerSiemRuleMigrationsUpdateRoute = ( version: '1', validate: { request: { - params: buildRouteValidationWithZod(UpdateRuleMigrationRequestParams), - body: buildRouteValidationWithZod(UpdateRuleMigrationRequestBody), + params: buildRouteValidationWithZod(GetRuleMigrationRequestParams), }, }, }, withLicense( - async (context, req, res): Promise> => { - const { migration_id: migrationId } = req.params; - const rulesToUpdate = req.body; - - if (rulesToUpdate.length === 0) { - return res.noContent(); - } - const ids = rulesToUpdate.map((rule) => rule.id); - + withExistingMigration(async (context, req, res) => { const siemMigrationAuditLogger = new SiemMigrationAuditLogger(context.securitySolution); + + const { migration_id: migrationId } = req.params; try { const ctx = await context.resolve(['securitySolution']); const ruleMigrationsClient = ctx.securitySolution.getSiemRuleMigrationsClient(); + await siemMigrationAuditLogger.logDeleteMigration({ migrationId }); - await siemMigrationAuditLogger.logUpdateRules({ migrationId, ids }); + if (ruleMigrationsClient.task.isMigrationRunning(migrationId)) { + return res.conflict({ + body: 'A running migration cannot be deleted. Please stop the migration first and try again', + }); + } - const transformedRuleToUpdate = rulesToUpdate.map( - transformToInternalUpdateRuleMigrationData - ); - await ruleMigrationsClient.data.rules.update(transformedRuleToUpdate); + await ruleMigrationsClient.data.deleteMigration(migrationId); - return res.ok({ body: { updated: true } }); + return res.ok(); } catch (error) { logger.error(error); - await siemMigrationAuditLogger.logUpdateRules({ migrationId, ids, error }); - return res.badRequest({ body: error.message }); + await siemMigrationAuditLogger.logDeleteMigration({ + migrationId, + error, + }); + return res.badRequest({ + body: error.message, + }); } - } + }) ) ); }; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/get.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/get.ts index 5fa24810eafcb..f400132b5a2b1 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/get.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/get.ts @@ -8,16 +8,13 @@ import type { IKibanaResponse, Logger } from '@kbn/core/server'; import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; import { SIEM_RULE_MIGRATION_PATH } from '../../../../../common/siem_migrations/constants'; -import { - GetRuleMigrationRequestParams, - GetRuleMigrationRequestQuery, - type GetRuleMigrationResponse, -} from '../../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; +import type { GetRuleMigrationResponse } from '../../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; +import { GetRuleMigrationRequestParams } from '../../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; import type { SecuritySolutionPluginRouter } from '../../../../types'; -import type { RuleMigrationGetOptions } from '../data/rule_migrations_data_rules_client'; import { SiemMigrationAuditLogger } from './util/audit'; import { authz } from './util/authz'; import { withLicense } from './util/with_license'; +import { MIGRATION_ID_NOT_FOUND } from './util/with_existing_migration_id'; export const registerSiemRuleMigrationsGetRoute = ( router: SecuritySolutionPluginRouter, @@ -35,42 +32,37 @@ export const registerSiemRuleMigrationsGetRoute = ( validate: { request: { params: buildRouteValidationWithZod(GetRuleMigrationRequestParams), - query: buildRouteValidationWithZod(GetRuleMigrationRequestQuery), }, }, }, withLicense(async (context, req, res): Promise> => { - const { migration_id: migrationId } = req.params; - const siemMigrationAuditLogger = new SiemMigrationAuditLogger(context.securitySolution); + + const { migration_id: migrationId } = req.params; try { const ctx = await context.resolve(['securitySolution']); const ruleMigrationsClient = ctx.securitySolution.getSiemRuleMigrationsClient(); + await siemMigrationAuditLogger.logGetMigration({ migrationId }); - const { page, per_page: size } = req.query; - const options: RuleMigrationGetOptions = { - filters: { - searchTerm: req.query.search_term, - ids: req.query.ids, - prebuilt: req.query.is_prebuilt, - installed: req.query.is_installed, - fullyTranslated: req.query.is_fully_translated, - partiallyTranslated: req.query.is_partially_translated, - untranslatable: req.query.is_untranslatable, - failed: req.query.is_failed, - }, - sort: { sortField: req.query.sort_field, sortDirection: req.query.sort_direction }, - size, - from: page && size ? page * size : 0, - }; + const storedMigration = await ruleMigrationsClient.data.migrations.get({ + id: migrationId, + }); - const result = await ruleMigrationsClient.data.rules.get(migrationId, options); + if (!storedMigration) { + return res.notFound({ + body: MIGRATION_ID_NOT_FOUND(migrationId), + }); + } - await siemMigrationAuditLogger.logGetMigration({ migrationId }); - return res.ok({ body: result }); + return res.ok({ + body: storedMigration, + }); } catch (error) { logger.error(error); - await siemMigrationAuditLogger.logGetMigration({ migrationId, error }); + await siemMigrationAuditLogger.logGetMigration({ + migrationId, + error, + }); return res.badRequest({ body: error.message }); } }) diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/index.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/index.ts index a17b0e7862b16..4d49dc0aad896 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/index.ts @@ -9,7 +9,7 @@ import type { Logger } from '@kbn/core/server'; import type { ConfigType } from '../../../../config'; import type { SecuritySolutionPluginRouter } from '../../../../types'; import { registerSiemRuleMigrationsCreateRoute } from './create'; -import { registerSiemRuleMigrationsUpdateRoute } from './update'; +import { registerSiemRuleMigrationsUpdateRulesRoute } from './rules/update'; import { registerSiemRuleMigrationsGetRoute } from './get'; import { registerSiemRuleMigrationsStartRoute } from './start'; import { registerSiemRuleMigrationsStatsRoute } from './stats'; @@ -24,27 +24,45 @@ import { registerSiemRuleMigrationsPrebuiltRulesRoute } from './get_prebuilt_rul import { registerSiemRuleMigrationsIntegrationsRoute } from './get_integrations'; import { registerSiemRuleMigrationsGetMissingPrivilegesRoute } from './privileges/get_missing_privileges'; import { registerSiemRuleMigrationsEvaluateRoute } from './evaluation/evaluate'; +import { registerSiemRuleMigrationsCreateRulesRoute } from './rules/create'; +import { registerSiemRuleMigrationsGetRulesRoute } from './rules/get'; +import { registerSiemRuleMigrationsDeleteRoute } from './delete'; export const registerSiemRuleMigrationsRoutes = ( router: SecuritySolutionPluginRouter, config: ConfigType, logger: Logger ) => { + /** Rules Migrations */ registerSiemRuleMigrationsCreateRoute(router, logger); - registerSiemRuleMigrationsUpdateRoute(router, logger); + registerSiemRuleMigrationsGetRoute(router, logger); + registerSiemRuleMigrationsDeleteRoute(router, logger); + /** *******/ + + /** Rules */ + registerSiemRuleMigrationsCreateRulesRoute(router, logger); + registerSiemRuleMigrationsGetRulesRoute(router, logger); + registerSiemRuleMigrationsUpdateRulesRoute(router, logger); + /** *******/ + + /** Tasks **/ registerSiemRuleMigrationsStatsAllRoute(router, logger); registerSiemRuleMigrationsPrebuiltRulesRoute(router, logger); - registerSiemRuleMigrationsGetRoute(router, logger); registerSiemRuleMigrationsStartRoute(router, logger); registerSiemRuleMigrationsStatsRoute(router, logger); registerSiemRuleMigrationsTranslationStatsRoute(router, logger); registerSiemRuleMigrationsStopRoute(router, logger); + /** *******/ + registerSiemRuleMigrationsInstallRoute(router, logger); + registerSiemRuleMigrationsIntegrationsRoute(router, logger); + /** Resources */ registerSiemRuleMigrationsResourceUpsertRoute(router, logger); registerSiemRuleMigrationsResourceGetRoute(router, logger); registerSiemRuleMigrationsResourceGetMissingRoute(router, logger); + /** *******/ registerSiemRuleMigrationsGetMissingPrivilegesRoute(router, logger); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/rules/create.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/rules/create.ts new file mode 100644 index 0000000000000..e4f7f1a6832ea --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/rules/create.ts @@ -0,0 +1,96 @@ +/* + * 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 type { RuleMigrationRule } from '../../../../../../common/siem_migrations/model/rule_migration.gen'; +import { SIEM_RULE_MIGRATION_RULES_PATH } from '../../../../../../common/siem_migrations/constants'; +import { + CreateRuleMigrationRulesRequestBody, + CreateRuleMigrationRulesRequestParams, +} from '../../../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; +import { ResourceIdentifier } from '../../../../../../common/siem_migrations/rules/resources'; +import type { SecuritySolutionPluginRouter } from '../../../../../types'; +import type { AddRuleMigrationRulesInput } from '../../data/rule_migrations_data_rules_client'; +import { SiemMigrationAuditLogger } from '../util/audit'; +import { authz } from '../util/authz'; +import { withExistingMigration } from '../util/with_existing_migration_id'; +import { withLicense } from '../util/with_license'; + +export const registerSiemRuleMigrationsCreateRulesRoute = ( + router: SecuritySolutionPluginRouter, + logger: Logger +) => { + router.versioned + .post({ + path: SIEM_RULE_MIGRATION_RULES_PATH, + access: 'internal', + security: { authz }, + }) + .addVersion( + { + version: '1', + validate: { + request: { + body: buildRouteValidationWithZod(CreateRuleMigrationRulesRequestBody), + params: buildRouteValidationWithZod(CreateRuleMigrationRulesRequestParams), + }, + }, + }, + withLicense( + withExistingMigration( + async (context, req, res): Promise> => { + const { migration_id: migrationId } = req.params; + const originalRules = req.body; + const rulesCount = originalRules.length; + const siemMigrationAuditLogger = new SiemMigrationAuditLogger(context.securitySolution); + try { + const [firstOriginalRule] = originalRules; + if (!firstOriginalRule) { + return res.noContent(); + } + const ctx = await context.resolve(['securitySolution']); + const ruleMigrationsClient = ctx.securitySolution.getSiemRuleMigrationsClient(); + await siemMigrationAuditLogger.logAddRules({ + migrationId, + count: rulesCount, + }); + + const ruleMigrations = originalRules.map( + (originalRule) => ({ + migration_id: migrationId, + original_rule: originalRule, + }) + ); + + await ruleMigrationsClient.data.rules.create(ruleMigrations); + + // Create identified resource documents without content to keep track of them + const resourceIdentifier = new ResourceIdentifier(firstOriginalRule.vendor); + const resources = resourceIdentifier + .fromOriginalRules(originalRules) + .map((resource) => ({ ...resource, migration_id: migrationId })); + + if (resources.length > 0) { + await ruleMigrationsClient.data.resources.create(resources); + } + + return res.ok(); + } catch (error) { + logger.error(error); + await siemMigrationAuditLogger.logAddRules({ + migrationId, + count: rulesCount, + error, + }); + return res.badRequest({ body: error.message }); + } + } + ) + ) + ); +}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/rules/get.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/rules/get.ts new file mode 100644 index 0000000000000..2e927c4ad6763 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/rules/get.ts @@ -0,0 +1,83 @@ +/* + * 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_RULES_PATH } from '../../../../../../common/siem_migrations/constants'; +import type { GetRuleMigrationRulesResponse } from '../../../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; +import { + GetRuleMigrationRulesRequestParams, + GetRuleMigrationRulesRequestQuery, +} from '../../../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; +import type { SecuritySolutionPluginRouter } from '../../../../../types'; +import type { RuleMigrationGetRulesOptions } from '../../data/rule_migrations_data_rules_client'; +import { SiemMigrationAuditLogger } from '../util/audit'; +import { authz } from '../util/authz'; +import { withLicense } from '../util/with_license'; +import { withExistingMigration } from '../util/with_existing_migration_id'; + +export const registerSiemRuleMigrationsGetRulesRoute = ( + router: SecuritySolutionPluginRouter, + logger: Logger +) => { + router.versioned + .get({ + path: SIEM_RULE_MIGRATION_RULES_PATH, + access: 'internal', + security: { authz }, + }) + .addVersion( + { + version: '1', + validate: { + request: { + params: buildRouteValidationWithZod(GetRuleMigrationRulesRequestParams), + query: buildRouteValidationWithZod(GetRuleMigrationRulesRequestQuery), + }, + }, + }, + withLicense( + withExistingMigration( + async (context, req, res): Promise> => { + const { migration_id: migrationId } = req.params; + + const siemMigrationAuditLogger = new SiemMigrationAuditLogger(context.securitySolution); + try { + const ctx = await context.resolve(['securitySolution']); + const ruleMigrationsClient = ctx.securitySolution.getSiemRuleMigrationsClient(); + + const { page, per_page: size } = req.query; + const options: RuleMigrationGetRulesOptions = { + filters: { + searchTerm: req.query.search_term, + ids: req.query.ids, + prebuilt: req.query.is_prebuilt, + installed: req.query.is_installed, + fullyTranslated: req.query.is_fully_translated, + partiallyTranslated: req.query.is_partially_translated, + untranslatable: req.query.is_untranslatable, + failed: req.query.is_failed, + }, + sort: { sortField: req.query.sort_field, sortDirection: req.query.sort_direction }, + size, + from: page && size ? page * size : 0, + }; + + const result = await ruleMigrationsClient.data.rules.get(migrationId, options); + + await siemMigrationAuditLogger.logGetMigrationRules({ migrationId }); + return res.ok({ body: result }); + } catch (error) { + logger.error(error); + await siemMigrationAuditLogger.logGetMigrationRules({ migrationId, error }); + return res.badRequest({ body: error.message }); + } + } + ) + ) + ); +}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/rules/update.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/rules/update.ts new file mode 100644 index 0000000000000..5b13d1fec86a6 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/rules/update.ts @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { IKibanaResponse, Logger } from '@kbn/core/server'; +import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; +import { SIEM_RULE_MIGRATION_RULES_PATH } from '../../../../../../common/siem_migrations/constants'; +import type { UpdateRuleMigrationRulesResponse } from '../../../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; +import { + UpdateRuleMigrationRulesRequestBody, + UpdateRuleMigrationRulesRequestParams, +} from '../../../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; +import type { SecuritySolutionPluginRouter } from '../../../../../types'; +import { authz } from '../util/authz'; +import { SiemMigrationAuditLogger } from '../util/audit'; +import { transformToInternalUpdateRuleMigrationData } from '../util/update_rules'; +import { withLicense } from '../util/with_license'; +import { withExistingMigration } from '../util/with_existing_migration_id'; + +export const registerSiemRuleMigrationsUpdateRulesRoute = ( + router: SecuritySolutionPluginRouter, + logger: Logger +) => { + router.versioned + .patch({ + path: SIEM_RULE_MIGRATION_RULES_PATH, + access: 'internal', + security: { authz }, + }) + .addVersion( + { + version: '1', + validate: { + request: { + params: buildRouteValidationWithZod(UpdateRuleMigrationRulesRequestParams), + body: buildRouteValidationWithZod(UpdateRuleMigrationRulesRequestBody), + }, + }, + }, + withLicense( + withExistingMigration( + async (context, req, res): Promise> => { + const { migration_id: migrationId } = req.params; + const rulesToUpdate = req.body; + + if (rulesToUpdate.length === 0) { + return res.noContent(); + } + const ids = rulesToUpdate.map((rule) => rule.id); + + const siemMigrationAuditLogger = new SiemMigrationAuditLogger(context.securitySolution); + try { + const ctx = await context.resolve(['securitySolution']); + const ruleMigrationsClient = ctx.securitySolution.getSiemRuleMigrationsClient(); + + await siemMigrationAuditLogger.logUpdateRules({ migrationId, ids }); + + const transformedRuleToUpdate = rulesToUpdate.map( + transformToInternalUpdateRuleMigrationData + ); + await ruleMigrationsClient.data.rules.update(transformedRuleToUpdate); + + return res.ok({ body: { updated: true } }); + } catch (error) { + logger.error(error); + await siemMigrationAuditLogger.logUpdateRules({ migrationId, ids, error }); + return res.badRequest({ body: error.message }); + } + } + ) + ) + ); +}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/start.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/start.ts index 038a70387bb99..a00bf3a2e65bd 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/start.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/start.ts @@ -19,13 +19,14 @@ import { authz } from './util/authz'; import { getRetryFilter } from './util/retry'; import { withLicense } from './util/with_license'; import { createTracersCallbacks } from './util/tracing'; +import { withExistingMigration } from './util/with_existing_migration_id'; export const registerSiemRuleMigrationsStartRoute = ( router: SecuritySolutionPluginRouter, logger: Logger ) => { router.versioned - .put({ + .post({ path: SIEM_RULE_MIGRATION_START_PATH, access: 'internal', security: { authz }, @@ -41,56 +42,63 @@ export const registerSiemRuleMigrationsStartRoute = ( }, }, withLicense( - async (context, req, res): Promise> => { - const migrationId = req.params.migration_id; - const { - langsmith_options: langsmithOptions, - connector_id: connectorId, - retry, - } = req.body; + withExistingMigration( + async (context, req, res): Promise> => { + const migrationId = req.params.migration_id; + const { + langsmith_options: langsmithOptions, + connector_id: connectorId, + retry, + } = req.body; - const siemMigrationAuditLogger = new SiemMigrationAuditLogger(context.securitySolution); - try { - const ctx = await context.resolve(['core', 'actions', 'alerting', 'securitySolution']); + const siemMigrationAuditLogger = new SiemMigrationAuditLogger(context.securitySolution); + try { + const ctx = await context.resolve([ + 'core', + 'actions', + 'alerting', + 'securitySolution', + ]); - // Check if the connector exists and user has permissions to read it - const connector = await ctx.actions.getActionsClient().get({ id: connectorId }); - if (!connector) { - return res.badRequest({ body: `Connector with id ${connectorId} not found` }); - } + // Check if the connector exists and user has permissions to read it + const connector = await ctx.actions.getActionsClient().get({ id: connectorId }); + if (!connector) { + return res.badRequest({ body: `Connector with id ${connectorId} not found` }); + } - const ruleMigrationsClient = ctx.securitySolution.getSiemRuleMigrationsClient(); - if (retry) { - const { updated } = await ruleMigrationsClient.task.updateToRetry( - migrationId, - getRetryFilter(retry) - ); - if (!updated) { - return res.ok({ body: { started: false } }); + const ruleMigrationsClient = ctx.securitySolution.getSiemRuleMigrationsClient(); + if (retry) { + const { updated } = await ruleMigrationsClient.task.updateToRetry( + migrationId, + getRetryFilter(retry) + ); + if (!updated) { + return res.ok({ body: { started: false } }); + } } - } - const callbacks = createTracersCallbacks(langsmithOptions, logger); + const callbacks = createTracersCallbacks(langsmithOptions, logger); - const { exists, started } = await ruleMigrationsClient.task.start({ - migrationId, - connectorId, - invocationConfig: { callbacks }, - }); + const { exists, started } = await ruleMigrationsClient.task.start({ + migrationId, + connectorId, + invocationConfig: { callbacks }, + }); - if (!exists) { - return res.notFound(); - } + if (!exists) { + return res.notFound(); + } - await siemMigrationAuditLogger.logStart({ migrationId }); + await siemMigrationAuditLogger.logStart({ migrationId }); - return res.ok({ body: { started } }); - } catch (error) { - logger.error(error); - await siemMigrationAuditLogger.logStart({ migrationId, error }); - return res.badRequest({ body: error.message }); + return res.ok({ body: { started } }); + } catch (error) { + logger.error(error); + await siemMigrationAuditLogger.logStart({ migrationId, error }); + return res.badRequest({ body: error.message }); + } } - } + ) ) ); }; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/stats.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/stats.ts index 529eecbac38c6..ae1d0df66245f 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/stats.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/stats.ts @@ -15,6 +15,7 @@ import { SIEM_RULE_MIGRATION_STATS_PATH } from '../../../../../common/siem_migra import type { SecuritySolutionPluginRouter } from '../../../../types'; import { authz } from './util/authz'; import { withLicense } from './util/with_license'; +import { withExistingMigration } from './util/with_existing_migration_id'; export const registerSiemRuleMigrationsStatsRoute = ( router: SecuritySolutionPluginRouter, @@ -34,23 +35,25 @@ export const registerSiemRuleMigrationsStatsRoute = ( }, }, withLicense( - async (context, req, res): Promise> => { - const migrationId = req.params.migration_id; - try { - const ctx = await context.resolve(['securitySolution']); - const ruleMigrationsClient = ctx.securitySolution.getSiemRuleMigrationsClient(); + withExistingMigration( + async (context, req, res): Promise> => { + const migrationId = req.params.migration_id; + try { + const ctx = await context.resolve(['securitySolution']); + const ruleMigrationsClient = ctx.securitySolution.getSiemRuleMigrationsClient(); - const stats = await ruleMigrationsClient.task.getStats(migrationId); + const stats = await ruleMigrationsClient.task.getStats(migrationId); - if (stats.rules.total === 0) { - return res.noContent(); + if (stats.rules.total === 0) { + return res.noContent(); + } + return res.ok({ body: stats }); + } catch (err) { + logger.error(err); + return res.badRequest({ body: err.message }); } - return res.ok({ body: stats }); - } catch (err) { - logger.error(err); - return res.badRequest({ body: err.message }); } - } + ) ) ); }; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/stop.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/stop.ts index 45298a7b28ca4..24d414d048a2a 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/stop.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/stop.ts @@ -16,13 +16,14 @@ import type { SecuritySolutionPluginRouter } from '../../../../types'; import { SiemMigrationAuditLogger } from './util/audit'; import { authz } from './util/authz'; import { withLicense } from './util/with_license'; +import { withExistingMigration } from './util/with_existing_migration_id'; export const registerSiemRuleMigrationsStopRoute = ( router: SecuritySolutionPluginRouter, logger: Logger ) => { router.versioned - .put({ + .post({ path: SIEM_RULE_MIGRATION_STOP_PATH, access: 'internal', security: { authz }, @@ -35,27 +36,29 @@ export const registerSiemRuleMigrationsStopRoute = ( }, }, withLicense( - async (context, req, res): Promise> => { - const migrationId = req.params.migration_id; - const siemMigrationAuditLogger = new SiemMigrationAuditLogger(context.securitySolution); - try { - const ctx = await context.resolve(['securitySolution']); - const ruleMigrationsClient = ctx.securitySolution.getSiemRuleMigrationsClient(); + withExistingMigration( + async (context, req, res): Promise> => { + const migrationId = req.params.migration_id; + const siemMigrationAuditLogger = new SiemMigrationAuditLogger(context.securitySolution); + try { + const ctx = await context.resolve(['securitySolution']); + const ruleMigrationsClient = ctx.securitySolution.getSiemRuleMigrationsClient(); - const { exists, stopped } = await ruleMigrationsClient.task.stop(migrationId); + const { exists, stopped } = await ruleMigrationsClient.task.stop(migrationId); - if (!exists) { - return res.notFound(); - } - await siemMigrationAuditLogger.logStop({ migrationId }); + if (!exists) { + return res.notFound(); + } + await siemMigrationAuditLogger.logStop({ migrationId }); - return res.ok({ body: { stopped } }); - } catch (error) { - logger.error(error); - await siemMigrationAuditLogger.logStop({ migrationId, error }); - return res.badRequest({ body: error.message }); + return res.ok({ body: { stopped } }); + } catch (error) { + logger.error(error); + await siemMigrationAuditLogger.logStop({ migrationId, error }); + return res.badRequest({ body: error.message }); + } } - } + ) ) ); }; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/translation_stats.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/translation_stats.ts index aa95d8e898147..32db85c101bd6 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/translation_stats.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/translation_stats.ts @@ -13,6 +13,7 @@ import { SIEM_RULE_MIGRATION_TRANSLATION_STATS_PATH } from '../../../../../commo import type { SecuritySolutionPluginRouter } from '../../../../types'; import { authz } from './util/authz'; import { withLicense } from './util/with_license'; +import { withExistingMigration } from './util/with_existing_migration_id'; export const registerSiemRuleMigrationsTranslationStatsRoute = ( router: SecuritySolutionPluginRouter, @@ -34,27 +35,29 @@ export const registerSiemRuleMigrationsTranslationStatsRoute = ( }, }, withLicense( - async ( - context, - req, - res - ): Promise> => { - const migrationId = req.params.migration_id; - try { - const ctx = await context.resolve(['securitySolution']); - const ruleMigrationsClient = ctx.securitySolution.getSiemRuleMigrationsClient(); + withExistingMigration( + async ( + context, + req, + res + ): Promise> => { + const migrationId = req.params.migration_id; + try { + const ctx = await context.resolve(['securitySolution']); + const ruleMigrationsClient = ctx.securitySolution.getSiemRuleMigrationsClient(); - const stats = await ruleMigrationsClient.data.rules.getTranslationStats(migrationId); + const stats = await ruleMigrationsClient.data.rules.getTranslationStats(migrationId); - if (stats.rules.total === 0) { - return res.noContent(); + if (stats.rules.total === 0) { + return res.noContent(); + } + return res.ok({ body: stats }); + } catch (err) { + logger.error(err); + return res.badRequest({ body: err.message }); } - return res.ok({ body: stats }); - } catch (err) { - logger.error(err); - return res.badRequest({ body: err.message }); } - } + ) ) ); }; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/util/audit.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/util/audit.ts index a3a134099fb17..191ad3faab904 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/util/audit.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/util/audit.ts @@ -12,6 +12,9 @@ import type { SecuritySolutionApiRequestHandlerContext } from '../../../../..'; export enum SiemMigrationsAuditActions { SIEM_MIGRATION_CREATED = 'siem_migration_created', SIEM_MIGRATION_RETRIEVED = 'siem_migration_retrieved', + SIEM_MIGRATION_DELETED = 'siem_migration_deleted', + SIEM_MIGRATION_ADDED_RULES = 'siem_migration_added_rules', + SIEM_MIGRATION_RETRIEVED_RULES = 'siem_migration_retrieved_rules', SIEM_MIGRATION_UPLOADED_RESOURCES = 'siem_migration_uploaded_resources', SIEM_MIGRATION_RETRIEVED_RESOURCES = 'siem_migration_retrieved_resources', SIEM_MIGRATION_STARTED = 'siem_migration_started', @@ -53,6 +56,9 @@ export const siemMigrationAuditEventType: Record< [SiemMigrationsAuditActions.SIEM_MIGRATION_STOPPED]: AUDIT_TYPE.END, [SiemMigrationsAuditActions.SIEM_MIGRATION_UPDATED_RULE]: AUDIT_TYPE.CHANGE, [SiemMigrationsAuditActions.SIEM_MIGRATION_INSTALLED_RULES]: AUDIT_TYPE.CREATION, + [SiemMigrationsAuditActions.SIEM_MIGRATION_ADDED_RULES]: AUDIT_TYPE.CREATION, + [SiemMigrationsAuditActions.SIEM_MIGRATION_RETRIEVED_RULES]: AUDIT_TYPE.ACCESS, + [SiemMigrationsAuditActions.SIEM_MIGRATION_DELETED]: AUDIT_TYPE.CHANGE, }; interface SiemMigrationAuditEvent { @@ -106,9 +112,9 @@ export class SiemMigrationAuditLogger { } } - public async logCreateMigration(params: { migrationId?: string; error?: Error }): Promise { - const { migrationId, error } = params; - const message = `User created a new SIEM migration with [id=${migrationId}]`; + public async logCreateMigration(params: { error?: Error } = {}): Promise { + const { error } = params; + const message = `User created a new SIEM migration`; return this.log({ action: SiemMigrationsAuditActions.SIEM_MIGRATION_CREATED, message, @@ -126,6 +132,42 @@ export class SiemMigrationAuditLogger { }); } + public async logDeleteMigration(params: { migrationId: string; error?: Error }): Promise { + const { migrationId, error } = params; + const message = `User deleted the SIEM migration with [id=${migrationId}]`; + return this.log({ + action: SiemMigrationsAuditActions.SIEM_MIGRATION_DELETED, + message, + error, + }); + } + + public async logGetMigrationRules(params: { migrationId: string; error?: Error }): Promise { + const { migrationId, error } = params; + const message = `User retrieved rules for SIEM migration with [id=${migrationId}]`; + return this.log({ + action: SiemMigrationsAuditActions.SIEM_MIGRATION_RETRIEVED_RULES, + message, + error, + }); + } + + public async logAddRules(params: { + migrationId: string; + error?: Error; + count?: number; + }): Promise { + const { migrationId, error, count } = params; + const message = `User added ${ + count ?? '' + } rules to the SIEM migration with [id=${migrationId}]`; + return this.log({ + action: SiemMigrationsAuditActions.SIEM_MIGRATION_ADDED_RULES, + message, + error, + }); + } + public async logUploadResources(params: { migrationId: string; error?: Error }): Promise { const { migrationId, error } = params; const message = `User uploaded resources to the SIEM migration with [id=${migrationId}]`; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/util/installation.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/util/installation.ts index 978dcbcdb9c35..8553879b5bb99 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/util/installation.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/util/installation.ts @@ -8,7 +8,7 @@ import type { SavedObjectsClientContract } from '@kbn/core/server'; import type { RulesClient } from '@kbn/alerting-plugin/server'; import { getErrorMessage } from '../../../../../utils/error_helpers'; -import type { UpdateRuleMigrationData } from '../../../../../../common/siem_migrations/model/rule_migration.gen'; +import type { UpdateRuleMigrationRule } 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'; @@ -31,7 +31,7 @@ const installPrebuiltRules = async ( rulesClient: RulesClient, savedObjectsClient: SavedObjectsClientContract, detectionRulesClient: IDetectionRulesClient -): Promise<{ rulesToUpdate: UpdateRuleMigrationData[]; errors: Error[] }> => { +): Promise<{ rulesToUpdate: UpdateRuleMigrationRule[]; errors: Error[] }> => { // Get required prebuilt rules const prebuiltRulesIds = getUniquePrebuiltRuleIds(rulesToInstall); const prebuiltRules = await getPrebuiltRules(rulesClient, savedObjectsClient, prebuiltRulesIds); @@ -68,7 +68,7 @@ const installPrebuiltRules = async ( ]; // Create migration rules updates templates - const rulesToUpdate: UpdateRuleMigrationData[] = []; + const rulesToUpdate: UpdateRuleMigrationRule[] = []; installedRules.forEach((installedRule) => { const filteredRules = rulesToInstall.filter( (rule) => rule.elastic_rule?.prebuilt_rule_id === installedRule.rule_id @@ -91,11 +91,11 @@ export const installCustomRules = async ( enabled: boolean, detectionRulesClient: IDetectionRulesClient ): Promise<{ - rulesToUpdate: UpdateRuleMigrationData[]; + rulesToUpdate: UpdateRuleMigrationRule[]; errors: Error[]; }> => { const errors: Error[] = []; - const rulesToUpdate: UpdateRuleMigrationData[] = []; + const rulesToUpdate: UpdateRuleMigrationRule[] = []; const createCustomRulesOutcome = await initPromisePool({ concurrency: MAX_CUSTOM_RULES_TO_CREATE_IN_PARALLEL, items: rulesToInstall, diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/util/prebuilt_rules.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/util/prebuilt_rules.ts index cf7317f0bfde0..efdbb255f980c 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/util/prebuilt_rules.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/util/prebuilt_rules.ts @@ -7,15 +7,15 @@ import type { SavedObjectsClientContract } from '@kbn/core/server'; import type { RulesClient } from '@kbn/alerting-plugin/server'; +import type { RuleMigrationRule } from '../../../../../../common/siem_migrations/model/rule_migration.gen'; import type { RuleResponse } from '../../../../../../common/api/detection_engine'; import { createPrebuiltRuleObjectsClient } from '../../../../detection_engine/prebuilt_rules/logic/rule_objects/prebuilt_rule_objects_client'; 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 { convertPrebuiltRuleAssetToRuleResponse } from '../../../../detection_engine/rule_management/logic/detection_rules_client/converters/convert_prebuilt_rule_asset_to_rule_response'; -import type { RuleMigration } from '../../../../../../common/siem_migrations/model/rule_migration.gen'; import type { SiemRuleMigrationsClient } from '../../siem_rule_migrations_service'; -export const getUniquePrebuiltRuleIds = (migrationRules: RuleMigration[]): string[] => { +export const getUniquePrebuiltRuleIds = (migrationRules: RuleMigrationRule[]): string[] => { const rulesIds = new Set(); migrationRules.forEach((rule) => { if (rule.elastic_rule?.prebuilt_rule_id) { diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/util/update_rules.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/util/update_rules.ts index b0b7af8fe3af2..864b4e47394b7 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/util/update_rules.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/util/update_rules.ts @@ -6,12 +6,12 @@ */ import { parseEsqlQuery } from '@kbn/securitysolution-utils'; -import type { UpdateRuleMigrationData } from '../../../../../../common/siem_migrations/model/rule_migration.gen'; +import type { UpdateRuleMigrationRule } from '../../../../../../common/siem_migrations/model/rule_migration.gen'; import { RuleMigrationTranslationResultEnum, type RuleMigrationTranslationResult, } from '../../../../../../common/siem_migrations/model/rule_migration.gen'; -import type { InternalUpdateRuleMigrationData } from '../../types'; +import type { InternalUpdateRuleMigrationRule } from '../../types'; export const isValidEsqlQuery = (esqlQuery: string) => { const { isEsqlQueryAggregating, hasMetadataOperator, errors } = parseEsqlQuery(esqlQuery); @@ -41,8 +41,8 @@ export const convertEsqlQueryToTranslationResult = ( }; export const transformToInternalUpdateRuleMigrationData = ( - ruleMigration: UpdateRuleMigrationData -): InternalUpdateRuleMigrationData => { + ruleMigration: UpdateRuleMigrationRule +): InternalUpdateRuleMigrationRule => { if (ruleMigration.elastic_rule?.query == null) { return ruleMigration; } diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/util/with_existing_migration_id.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/util/with_existing_migration_id.test.ts new file mode 100644 index 0000000000000..ebaade0ac82a4 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/util/with_existing_migration_id.test.ts @@ -0,0 +1,74 @@ +/* + * 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 { KibanaRequest, KibanaResponseFactory } from '@kbn/core/server'; +import type { SecuritySolutionRequestHandlerContext } from '../../../../../types'; +import { withExistingMigration } from './with_existing_migration_id'; + +const mockRuleMigrationsClient = { + data: { + migrations: { + get: jest.fn(), + }, + }, +}; + +const mockSecuritySolutionContext = { + securitySolution: { + getSiemRuleMigrationsClient: jest.fn().mockReturnValue(mockRuleMigrationsClient), + }, +}; + +const mockContext = { + resolve: jest.fn().mockResolvedValue(mockSecuritySolutionContext), +} as unknown as SecuritySolutionRequestHandlerContext; + +const mockMigration = { + id: 'test-migration-id', + created_at: '2023-10-01T00:00:00Z', + created_by: 'test-user', +}; + +const mockReq = { + params: { + migration_id: 'test-migration-id', + }, +} as unknown as KibanaRequest<{ migration_id: string }, unknown, unknown, never>; + +const mockRes = { + notFound: jest.fn(), +} as unknown as KibanaResponseFactory; + +describe('withExistingMigrationId', () => { + describe('when migration exists', () => { + beforeEach(() => { + mockRuleMigrationsClient.data.migrations.get.mockResolvedValue(mockMigration); + }); + it('should call the handler', async () => { + const handler = jest.fn(); + const wrappedHandler = withExistingMigration(handler); + await wrappedHandler(mockContext, mockReq, mockRes); + + expect(handler).toHaveBeenCalledWith(mockContext, mockReq, mockRes); + }); + }); + + describe('when migration does not exist', () => { + beforeEach(() => { + mockRuleMigrationsClient.data.migrations.get.mockResolvedValue(undefined); + }); + it('should return a 404 response', async () => { + const handler = jest.fn(); + const wrappedHandler = withExistingMigration(handler); + await wrappedHandler(mockContext, mockReq, mockRes); + + expect(mockRes.notFound).toHaveBeenCalledWith({ + body: expect.stringContaining('No Migration found with id: test-migration-id'), + }); + }); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/util/with_existing_migration_id.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/util/with_existing_migration_id.ts new file mode 100644 index 0000000000000..8babe35a8f440 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/util/with_existing_migration_id.ts @@ -0,0 +1,51 @@ +/* + * 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 { RequestHandler, RouteMethod } from '@kbn/core/server'; +import { i18n } from '@kbn/i18n'; +import type { SecuritySolutionRequestHandlerContext } from '../../../../../types'; + +export const MIGRATION_ID_NOT_FOUND = (id: string) => + i18n.translate('xpack.securitySolution.api.migrationIdNotFound', { + defaultMessage: `No Migration found with id: {id}`, + values: { + id, + }, + }); + +/** + * Checks the existence of a valid migration before proceeding with the request. + * + * if not found, it returns a 404 error with a message. + * if found, it adds the migration to the context. + * + * */ +export const withExistingMigration = < + P extends { migration_id: string }, + Q = unknown, + B = unknown, + Method extends RouteMethod = never +>( + handler: RequestHandler +): RequestHandler => { + return async (context, req, res) => { + const { migration_id: migrationId } = req.params; + const ctx = await context.resolve(['securitySolution']); + const ruleMigrationsClient = ctx.securitySolution.getSiemRuleMigrationsClient(); + const storedMigration = await ruleMigrationsClient.data.migrations.get({ + id: migrationId, + }); + + if (!storedMigration) { + return res.notFound({ + body: MIGRATION_ID_NOT_FOUND(migrationId), + }); + } + + return handler(context, req, res); + }; +}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/__mocks__/mocks.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/__mocks__/mocks.ts index 1a3e5ba34467d..435cd9874c0ae 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/__mocks__/mocks.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/__mocks__/mocks.ts @@ -5,6 +5,7 @@ * 2.0. */ +import type { RuleMigrationsDataClient } from '../rule_migrations_data_client'; import type { RuleMigrationsDataIntegrationsClient } from '../rule_migrations_data_integrations_client'; import type { RuleMigrationsDataLookupsClient } from '../rule_migrations_data_lookups_client'; import type { RuleMigrationsDataMigrationClient } from '../rule_migrations_data_migration_client'; @@ -63,15 +64,19 @@ export const mockRuleMigrationsDataMigrationsClient = { get: jest.fn().mockResolvedValue(undefined), } as unknown as jest.Mocked; +export const mockDeleteMigration = jest.fn().mockResolvedValue(undefined); + // Rule migrations data client -export const createRuleMigrationsDataClientMock = () => ({ - rules: mockRuleMigrationsDataRulesClient, - resources: mockRuleMigrationsDataResourcesClient, - integrations: mockRuleMigrationsDataIntegrationsClient, - prebuiltRules: mockRuleMigrationsDataPrebuiltRulesClient, - lookups: mockRuleMigrationsDataLookupsClient, - migrations: mockRuleMigrationsDataMigrationsClient, -}); +export const createRuleMigrationsDataClientMock = () => + ({ + rules: mockRuleMigrationsDataRulesClient, + resources: mockRuleMigrationsDataResourcesClient, + integrations: mockRuleMigrationsDataIntegrationsClient, + prebuiltRules: mockRuleMigrationsDataPrebuiltRulesClient, + lookups: mockRuleMigrationsDataLookupsClient, + migrations: mockRuleMigrationsDataMigrationsClient, + deleteMigration: mockDeleteMigration, + } as unknown as jest.MockedObjectDeep); export const MockRuleMigrationsDataClient = jest .fn() @@ -81,10 +86,12 @@ export const MockRuleMigrationsDataClient = jest export const mockIndexName = 'mocked_siem_rule_migrations_index_name'; export const mockInstall = jest.fn().mockResolvedValue(undefined); export const mockCreateClient = jest.fn(() => createRuleMigrationsDataClientMock()); +export const mockSetup = jest.fn().mockResolvedValue(undefined); export const MockRuleMigrationsDataService = jest.fn().mockImplementation(() => ({ createAdapter: jest.fn(), install: mockInstall, createClient: mockCreateClient, createIndexNameProvider: jest.fn().mockResolvedValue(mockIndexName), + setup: mockSetup, })); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_base_client.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_base_client.ts index 8a9e11f32976b..dfa0ed115484c 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_base_client.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_base_client.ts @@ -18,7 +18,8 @@ import type { Logger, } from '@kbn/core/server'; import assert from 'assert'; -import type { IndexNameProvider, SiemRuleMigrationsClientDependencies, Stored } from '../types'; +import type { IndexNameProvider, SiemRuleMigrationsClientDependencies } from '../types'; +import type { Stored } from '../../types'; const DEFAULT_PIT_KEEP_ALIVE: Duration = '30s' as const; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_client.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_client.test.ts new file mode 100644 index 0000000000000..304e5c95b4f7f --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_client.test.ts @@ -0,0 +1,99 @@ +/* + * 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 { AuthenticatedUser } from '@kbn/security-plugin-types-common'; +import { RuleMigrationsDataClient } from './rule_migrations_data_client'; +import { RuleMigrationsDataResourcesClient } from './rule_migrations_data_resources_client'; +import { RuleMigrationsDataRulesClient } from './rule_migrations_data_rules_client'; +import type { IScopedClusterClient, Logger } from '@kbn/core/server'; +import type { SiemRuleMigrationsClientDependencies } from '../types'; + +jest.mock('./rule_migrations_data_rules_client'); +jest.mock('./rule_migrations_data_resources_client'); + +const mockedRulesClient = { + prepareDelete: jest + .fn() + .mockReturnValue([ + { delete: { _id: 'rule1', _index: '.mocked-rule-index' } }, + { delete: { _id: 'rule2', _index: '.mocked-rule-index' } }, + ]), +} as unknown as jest.Mocked; + +const mockedResourcesClient = { + prepareDelete: jest + .fn() + .mockReturnValue([{ delete: { _id: 'resource1', _index: '.mocked-resource-index' } }]), +} as unknown as jest.Mocked; + +const mockIndexNameProviders = { + migrations: jest.fn().mockReturnValue('.mocked-migration-index'), + rules: jest.fn().mockReturnValue('.mocked-rule-index'), + resources: jest.fn().mockReturnValue('.mocked-resource-index'), + prebuiltrules: jest.fn().mockReturnValue('.mocked-prebuilt-rules-index'), + integrations: jest.fn().mockReturnValue('.mocked-integrations-index'), +}; + +const mockCurrentUser = { + username: 'testUser', + profile_uid: 'testProfileUid', +} as unknown as AuthenticatedUser; + +const mockEsClient = { + asInternalUser: { + bulk: jest.fn().mockResolvedValue({ errors: false }), + }, +} as unknown as jest.Mocked; + +const mockLogger = { + error: jest.fn(), + info: jest.fn(), +} as unknown as jest.Mocked; + +const mockSpaceId = 'default'; +const mockDependencies = {} as unknown as jest.Mocked; + +describe('RuleMigrationsDataClient', () => { + beforeEach(() => { + (RuleMigrationsDataRulesClient as unknown as jest.Mock).mockImplementation( + () => mockedRulesClient + ); + (RuleMigrationsDataResourcesClient as unknown as jest.Mock).mockImplementation( + () => mockedResourcesClient + ); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + describe('deleteMigration', () => { + it('should delete the migration and associated rules and resources', async () => { + const dataClient = new RuleMigrationsDataClient( + mockIndexNameProviders, + mockCurrentUser, + mockEsClient, + mockLogger, + mockSpaceId, + mockDependencies + ); + + const migrationId = 'testId'; + + await dataClient.deleteMigration(migrationId); + + expect(mockEsClient.asInternalUser.bulk).toHaveBeenCalledWith({ + refresh: 'wait_for', + operations: [ + { delete: { _id: migrationId, _index: '.mocked-migration-index' } }, + { delete: { _id: 'rule1', _index: '.mocked-rule-index' } }, + { delete: { _id: 'rule2', _index: '.mocked-rule-index' } }, + { delete: { _id: 'resource1', _index: '.mocked-resource-index' } }, + ], + }); + }); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_client.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_client.ts index a80419b92bb3f..f6390fe8c5075 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_client.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_client.ts @@ -15,6 +15,9 @@ import type { IndexNameProviders, SiemRuleMigrationsClientDependencies } from '. import { RuleMigrationsDataMigrationClient } from './rule_migrations_data_migration_client'; export class RuleMigrationsDataClient { + protected logger: Logger; + protected esClient: IScopedClusterClient['asInternalUser']; + public readonly migrations: RuleMigrationsDataMigrationClient; public readonly rules: RuleMigrationsDataRulesClient; public readonly resources: RuleMigrationsDataResourcesClient; @@ -71,5 +74,40 @@ export class RuleMigrationsDataClient { logger, spaceId ); + + this.logger = logger; + this.esClient = esScopedClient.asInternalUser; + } + + /** + * + * Deletes a migration and all its associated rules and resources. + * + */ + async deleteMigration(migrationId: string) { + const migrationDeleteOperations = await this.migrations.prepareDelete({ + id: migrationId, + }); + + const rulesByMigrationIdDeleteOperations = await this.rules.prepareDelete(migrationId); + + const resourcesByMigrationIdDeleteOperations = await this.resources.prepareDelete(migrationId); + + return this.esClient + .bulk({ + refresh: 'wait_for', + operations: [ + ...migrationDeleteOperations, + ...rulesByMigrationIdDeleteOperations, + ...resourcesByMigrationIdDeleteOperations, + ], + }) + .then(() => { + this.logger.info(`Deleted migration ${migrationId}`); + }) + .catch((error) => { + this.logger.error(`Error deleting migration ${migrationId}: ${error}`); + throw error; + }); } } diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_migration_client.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_migration_client.test.ts index 0b48c79a8c52e..694b6044c0c27 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_migration_client.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_migration_client.test.ts @@ -95,6 +95,25 @@ describe('RuleMigrationsDataMigrationClient', () => { id: response._id, }); }); + + test('should return undefined if the migration is not found', async () => { + const id = 'testId'; + const response = { + _index: '.kibana-siem-rule-migrations', + found: false, + }; + + ( + esClient.asInternalUser.get as unknown as jest.MockedFn + ).mockRejectedValueOnce({ + message: JSON.stringify(response), + }); + + const result = await ruleMigrationsDataMigrationClient.get({ id }); + + expect(result).toBeUndefined(); + }); + test('should throw an error if an error occurs', async () => { const id = 'testId'; ( @@ -107,4 +126,26 @@ describe('RuleMigrationsDataMigrationClient', () => { expect(logger.error).toHaveBeenCalledWith(`Error getting migration ${id}: Error: Test error`); }); }); + + describe('prepareDelete', () => { + beforeEach(() => jest.clearAllMocks()); + + it('should delete the migration and associated rules and resources', async () => { + const migrationId = 'testId'; + const index = '.kibana-siem-rule-migrations'; + + const operations = await ruleMigrationsDataMigrationClient.prepareDelete({ + id: migrationId, + }); + + expect(operations).toMatchObject([ + { + delete: { + _index: index, + _id: migrationId, + }, + }, + ]); + }); + }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_migration_client.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_migration_client.ts index 837a0e1a05d01..564b4c5c96e53 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_migration_client.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_migration_client.ts @@ -6,8 +6,10 @@ */ import { v4 as uuidV4 } from 'uuid'; +import type { BulkOperationContainer } from '@elastic/elasticsearch/lib/api/types'; import type { StoredSiemMigration } from '../types'; import { RuleMigrationsDataBaseClient } from './rule_migrations_data_base_client'; +import { isNotFoundError } from './utils'; export class RuleMigrationsDataMigrationClient extends RuleMigrationsDataBaseClient { async create(): Promise { @@ -34,19 +36,42 @@ export class RuleMigrationsDataMigrationClient extends RuleMigrationsDataBaseCli return migrationId; } - async get({ id }: { id: string }): Promise { + /** + * + * Gets the migration document by id or returns undefined if it does not exist. + * + * */ + async get({ id }: { id: string }): Promise { const index = await this.getIndexName(); return this.esClient .get({ index, id, }) - .then((document) => { - return this.processHit(document); - }) + .then(this.processHit) .catch((error) => { + if (isNotFoundError(error)) { + return undefined; + } this.logger.error(`Error getting migration ${id}: ${error}`); throw error; }); } + + /** + * + * Prepares bulk ES delete operation for a migration document based on its id. + * + */ + async prepareDelete({ id }: { id: string }): Promise { + const index = await this.getIndexName(); + const migrationDeleteOperation = { + delete: { + _index: index, + _id: id, + }, + }; + + return [migrationDeleteOperation]; + } } diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_resources_client.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_resources_client.ts index fce0bec7e9d24..af6fb90e87297 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_resources_client.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_resources_client.ts @@ -6,7 +6,11 @@ */ import { sha256 } from 'js-sha256'; -import type { QueryDslQueryContainer, Duration } from '@elastic/elasticsearch/lib/api/types'; +import type { + QueryDslQueryContainer, + Duration, + BulkOperationContainer, +} from '@elastic/elasticsearch/lib/api/types'; import type { RuleMigrationResource, RuleMigrationResourceType, @@ -156,4 +160,21 @@ export class RuleMigrationsDataResourcesClient extends RuleMigrationsDataBaseCli } return { bool: { filter } }; } + + /** + * + * Prepares bulk ES delete operations for the resources of a given migrationId. + * + */ + async prepareDelete(migrationId: string): Promise { + const index = await this.getIndexName(); + const resourcesToBeDeleted = await this.get(migrationId, { size: 10000 }); + const resourcesToBeDeletedDocIds = resourcesToBeDeleted.map((resource) => resource.id); + return resourcesToBeDeletedDocIds.map((docId) => ({ + delete: { + _id: docId, + _index: index, + }, + })); + } } diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_rules_client.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_rules_client.ts index adde8606b25c2..661e87b3d6508 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_rules_client.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_rules_client.ts @@ -14,14 +14,15 @@ import type { AggregationsStringTermsBucket, QueryDslQueryContainer, Duration, + BulkOperationContainer, } from '@elastic/elasticsearch/lib/api/types'; import type { RuleMigrationFilters } from '../../../../../common/siem_migrations/types'; -import type { InternalUpdateRuleMigrationData, StoredRuleMigration } from '../types'; +import type { InternalUpdateRuleMigrationRule, StoredRuleMigration } from '../types'; import { SiemMigrationStatus, RuleTranslationResult, } from '../../../../../common/siem_migrations/constants'; -import type { RuleMigration } from '../../../../../common/siem_migrations/model/rule_migration.gen'; +import type { RuleMigrationRule } from '../../../../../common/siem_migrations/model/rule_migration.gen'; import { type RuleMigrationTaskStats, type RuleMigrationTranslationStats, @@ -30,14 +31,14 @@ import { getSortingOptions, type RuleMigrationSort } from './sort'; import { conditions as searchConditions } from './search'; import { RuleMigrationsDataBaseClient } from './rule_migrations_data_base_client'; -export type CreateRuleMigrationInput = Omit< - RuleMigration, +export type AddRuleMigrationRulesInput = Omit< + RuleMigrationRule, '@timestamp' | 'id' | 'status' | 'created_by' >; export type RuleMigrationDataStats = Omit; export type RuleMigrationAllDataStats = RuleMigrationDataStats[]; -export interface RuleMigrationGetOptions { +export interface RuleMigrationGetRulesOptions { filters?: RuleMigrationFilters; sort?: RuleMigrationSort; from?: number; @@ -53,11 +54,11 @@ const DEFAULT_SEARCH_BATCH_SIZE = 500 as const; export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient { /** Indexes an array of rule migrations to be processed */ - async create(ruleMigrations: CreateRuleMigrationInput[]): Promise { + async create(ruleMigrations: AddRuleMigrationRulesInput[]): Promise { const index = await this.getIndexName(); const profileId = await this.getProfileUid(); - let ruleMigrationsSlice: CreateRuleMigrationInput[]; + let ruleMigrationsSlice: AddRuleMigrationRulesInput[]; const createdAt = new Date().toISOString(); while ((ruleMigrationsSlice = ruleMigrations.splice(0, BULK_MAX_SIZE)).length) { await this.esClient @@ -83,11 +84,11 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient } /** Updates an array of rule migrations to be processed */ - async update(ruleMigrations: InternalUpdateRuleMigrationData[]): Promise { + async update(ruleMigrations: InternalUpdateRuleMigrationRule[]): Promise { const index = await this.getIndexName(); const profileId = await this.getProfileUid(); - let ruleMigrationsSlice: InternalUpdateRuleMigrationData[]; + let ruleMigrationsSlice: InternalUpdateRuleMigrationRule[]; const updatedAt = new Date().toISOString(); while ((ruleMigrationsSlice = ruleMigrations.splice(0, BULK_MAX_SIZE)).length) { await this.esClient @@ -117,14 +118,14 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient /** Retrieves an array of rule documents of a specific migrations */ async get( migrationId: string, - { filters = {}, sort: sortParam = {}, from, size }: RuleMigrationGetOptions = {} + { filters = {}, sort: sortParam = {}, from, size }: RuleMigrationGetRulesOptions = {} ): Promise<{ total: number; data: StoredRuleMigration[] }> { const index = await this.getIndexName(); const query = this.getFilterQuery(migrationId, filters); const sort = sortParam.sortField ? getSortingOptions(sortParam) : undefined; const result = await this.esClient - .search({ index, query, sort, from, size }) + .search({ index, query, sort, from, size }) .catch((error) => { this.logger.error(`Error searching rule migrations: ${error.message}`); throw error; @@ -144,7 +145,7 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient const query = this.getFilterQuery(migrationId, filters); const search = { query, sort: '_doc', scroll, size }; // sort by _doc to ensure consistent order try { - return this.getSearchBatches(search); + return this.getSearchBatches(search); } catch (error) { this.logger.error(`Error scrolling rule migrations: ${error.message}`); throw error; @@ -413,4 +414,22 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient } return { bool: { filter } }; } + + /** + * + * Prepares bulk ES delete operations for the rules based on migrationId. + * + * */ + async prepareDelete(migrationId: string): Promise { + const index = await this.getIndexName(); + const rulesToBeDeleted = await this.get(migrationId, { size: 10000 }); + const rulesToBeDeletedDocIds = rulesToBeDeleted.data.map((rule) => rule.id); + + return rulesToBeDeletedDocIds.map((docId) => ({ + delete: { + _id: docId, + _index: index, + }, + })); + } } diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_service.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_service.test.ts index 0edc230aaced9..fc0dae8f74c24 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_service.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_service.test.ts @@ -8,12 +8,14 @@ import { elasticsearchServiceMock } from '@kbn/core-elasticsearch-server-mocks'; import { loggingSystemMock } from '@kbn/core-logging-server-mocks'; import { securityServiceMock } from '@kbn/core-security-server-mocks'; -import type { InstallParams } from '@kbn/index-adapter'; import { IndexPatternAdapter, IndexAdapter } from '@kbn/index-adapter'; -import { loggerMock } from '@kbn/logging-mocks'; import { Subject } from 'rxjs'; import type { IndexNameProviders, SiemRuleMigrationsClientDependencies } from '../types'; +import type { SetupParams } from './rule_migrations_data_service'; import { INDEX_PATTERN, RuleMigrationsDataService } from './rule_migrations_data_service'; +import { RuleMigrationIndexMigrator } from '../index_migrators'; + +jest.mock('../index_migrators'); jest.mock('@kbn/index-adapter'); @@ -86,18 +88,19 @@ describe('SiemRuleMigrationsDataService', () => { }); describe('install', () => { - it('should install index pattern', async () => { + it('should install index pattern and run the migration', async () => { const service = new RuleMigrationsDataService(logger, kibanaVersion); - const params: Omit = { + const params: SetupParams = { esClient, pluginStop$: new Subject(), }; - await service.install(params); + await service.setup(params); const [indexPatternAdapter] = MockedIndexPatternAdapter.mock.instances; const [indexAdapter] = MockedIndexAdapter.mock.instances; expect(indexPatternAdapter.install).toHaveBeenCalledWith(expect.objectContaining(params)); expect(indexAdapter.install).toHaveBeenCalledWith(expect.objectContaining(params)); + expect(RuleMigrationIndexMigrator).toHaveBeenCalled(); }); }); @@ -112,9 +115,8 @@ describe('SiemRuleMigrationsDataService', () => { it('should install space index pattern', async () => { const service = new RuleMigrationsDataService(logger, kibanaVersion); - const params: InstallParams = { + const params: SetupParams = { esClient, - logger: loggerMock.create(), pluginStop$: new Subject(), }; @@ -122,7 +124,7 @@ describe('SiemRuleMigrationsDataService', () => { MockedIndexPatternAdapter.mock.instances; (rulesIndexPatternAdapter.install as jest.Mock).mockResolvedValueOnce(undefined); - await service.install(params); + await service.setup(params); service.createClient(createClientParams); await mockIndexNameProviders.rules(); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_service.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_service.ts index 3560c432cfab5..41c7e94579559 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_service.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_service.ts @@ -4,7 +4,12 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import type { AuthenticatedUser, IScopedClusterClient, Logger } from '@kbn/core/server'; +import type { + AuthenticatedUser, + ElasticsearchClient, + IScopedClusterClient, + Logger, +} from '@kbn/core/server'; import { IndexAdapter, IndexPatternAdapter, @@ -27,6 +32,7 @@ import { ruleMigrationResourcesFieldMap, ruleMigrationsFieldMap, } from './rule_migrations_field_maps'; +import { RuleMigrationIndexMigrator } from '../index_migrators'; const TOTAL_FIELDS_LIMIT = 2500; export const INDEX_PATTERN = '.kibana-siem-rule-migrations'; @@ -42,6 +48,10 @@ interface CreateAdapterParams { fieldMap: FieldMap; } +export interface SetupParams extends Omit { + esClient: ElasticsearchClient; +} + export class RuleMigrationsDataService { private readonly adapters: Adapters; @@ -96,7 +106,12 @@ export class RuleMigrationsDataService { return adapter; } - public async install(params: Omit): Promise { + private async runIndexMigrations(esClient: SetupParams['esClient']) { + const indexMigrator = new RuleMigrationIndexMigrator(this.adapters, esClient, this.logger); + await indexMigrator.run(); + } + + private async install(params: SetupParams): Promise { await Promise.all([ this.adapters.rules.install({ ...params, logger: this.logger }), this.adapters.resources.install({ ...params, logger: this.logger }), @@ -106,6 +121,11 @@ export class RuleMigrationsDataService { ]); } + public async setup(params: SetupParams): Promise { + await this.install(params); + await this.runIndexMigrations(params.esClient); + } + public createClient({ spaceId, currentUser, esScopedClient, dependencies }: CreateClientParams) { const indexNameProviders: IndexNameProviders = { rules: this.createIndexNameProvider(this.adapters.rules, spaceId), diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_field_maps.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_field_maps.ts index 90e356dde583e..a7ad78edc600c 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_field_maps.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_field_maps.ts @@ -9,10 +9,11 @@ import type { FieldMap, SchemaFieldMapKeys } from '@kbn/data-stream-adapter'; import type { RuleMigration, RuleMigrationResource, + RuleMigrationRule, } from '../../../../../common/siem_migrations/model/rule_migration.gen'; -import type { SiemMigration, RuleMigrationIntegration, RuleMigrationPrebuiltRule } from '../types'; +import type { RuleMigrationIntegration, RuleMigrationPrebuiltRule } from '../types'; -export const ruleMigrationsFieldMap: FieldMap>> = { +export const ruleMigrationsFieldMap: FieldMap>> = { '@timestamp': { type: 'date', required: false }, migration_id: { type: 'keyword', required: true }, created_by: { type: 'keyword', required: true }, @@ -93,7 +94,7 @@ export const getPrebuiltRulesFieldMap: ({ mitre_attack_ids: { type: 'keyword', array: true, required: false }, }); -export const migrationsFieldMaps: FieldMap> = { +export const migrationsFieldMaps: FieldMap>> = { created_at: { type: 'date', required: true }, created_by: { type: 'keyword', required: true }, }; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/utils.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/utils.test.ts new file mode 100644 index 0000000000000..c7d6847c747ad --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/utils.test.ts @@ -0,0 +1,24 @@ +/* + * 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 { isNotFoundError } from './utils'; + +describe('isNotFoundError', () => { + it('should return true if message has key found with value false', () => { + expect(isNotFoundError(new Error('{"key": "value", "found": false}'))).toBe(true); + }); + + it('should return false for invalid JSON strings', () => { + expect(isNotFoundError(new Error('{key: "value"}'))).toBe(false); + expect(isNotFoundError(new Error('Some Non JSON String'))).toBe(false); + }); + + it('should return false if message does not have key `found` or it is `true`', () => { + expect(isNotFoundError(new Error('{"message": {key: "value", "found": true}}'))).toBe(false); + expect(isNotFoundError(new Error('{"message": {key: "value"}}'))).toBe(false); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/utils.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/utils.ts new file mode 100644 index 0000000000000..f6421b4a30579 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/utils.ts @@ -0,0 +1,24 @@ +/* + * 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. + */ + +/** + * + * Probes esClient error to see if it is a not found error + * + */ +export const isNotFoundError = (error: Error) => { + try { + const message = JSON.parse(error.message); + if (Object.hasOwn(message, 'found') && message.found === false) { + return true; + } + } catch (e) { + return false; + } + + return false; +}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/index_migrators/__mocks__/index.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/index_migrators/__mocks__/index.ts new file mode 100644 index 0000000000000..ab5ed368cf793 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/index_migrators/__mocks__/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const RuleMigrationIndexMigrator = jest.fn().mockImplementation(() => { + return { + run: jest.fn(), + }; +}); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/index_migrators/index.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/index_migrators/index.ts new file mode 100644 index 0000000000000..e610b37fac4be --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/index_migrators/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './rule_migrations_index_migrator'; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/index_migrators/rule_migrations_index_migrator.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/index_migrators/rule_migrations_index_migrator.test.ts new file mode 100644 index 0000000000000..e0f7ce0bc8131 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/index_migrators/rule_migrations_index_migrator.test.ts @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ElasticsearchClient, Logger } from '@kbn/core/server'; +import { RuleMigrationIndexMigrator } from '.'; +import * as RuleMigrationSpaceIndexMigratorModule from './rule_migrations_per_space_index_migrator'; +import type { Adapters } from '../types'; +import { IndexPatternAdapter } from '@kbn/index-adapter'; + +const rulesIndexName = '.kibana-siem-rule-migrations-rules'; +const esClientMock = { + indices: { + get: jest.fn().mockResolvedValue({ + '.kibana-siem-rule-migrations-rules-space1': {}, + '.kibana-siem-rule-migrations-rules-space2': {}, + '.kibana-siem-rule-migrations-rules-space3': {}, + }), + }, +} as unknown as ElasticsearchClient; + +const ruleMigrationIndexAdapters = { + rules: new IndexPatternAdapter(rulesIndexName, { + kibanaVersion: '9.0.0', + }), +} as unknown as Adapters; + +const loggerMock = { + info: jest.fn(), +} as unknown as Logger; + +const mockPerSpaceIndexMigrator = jest.spyOn( + RuleMigrationSpaceIndexMigratorModule, + 'RuleMigrationSpaceIndexMigrator' +); + +describe('Index migrator', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockPerSpaceIndexMigrator.mockImplementation( + () => + ({ + run: jest.fn(), + } as unknown as RuleMigrationSpaceIndexMigratorModule.RuleMigrationSpaceIndexMigrator) + ); + }); + + describe('getSpaceListForMigrations', () => { + it('should return a list of spaces with indices', async () => { + const migrator = new RuleMigrationIndexMigrator( + ruleMigrationIndexAdapters, + esClientMock, + loggerMock + ); + await migrator.run(); + expect(mockPerSpaceIndexMigrator).toHaveBeenNthCalledWith( + 1, + 'space1', + esClientMock, + loggerMock, + ruleMigrationIndexAdapters + ); + expect(mockPerSpaceIndexMigrator).toHaveBeenNthCalledWith( + 2, + 'space2', + esClientMock, + loggerMock, + ruleMigrationIndexAdapters + ); + + expect(mockPerSpaceIndexMigrator).toHaveBeenNthCalledWith( + 3, + 'space3', + esClientMock, + loggerMock, + ruleMigrationIndexAdapters + ); + }); + it('should return an empty list if no indices are found', async () => { + (esClientMock.indices.get as jest.Mock).mockResolvedValueOnce({}); + const migrator = new RuleMigrationIndexMigrator( + ruleMigrationIndexAdapters, + esClientMock, + loggerMock + ); + await migrator.run(); + expect(loggerMock.info).toHaveBeenCalledWith('No spaces or index found for index migration'); + expect(mockPerSpaceIndexMigrator).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/index_migrators/rule_migrations_index_migrator.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/index_migrators/rule_migrations_index_migrator.ts new file mode 100644 index 0000000000000..dea1831b01c25 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/index_migrators/rule_migrations_index_migrator.ts @@ -0,0 +1,53 @@ +/* + * 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 { ElasticsearchClient, Logger } from '@kbn/core/server'; +import { RuleMigrationSpaceIndexMigrator } from './rule_migrations_per_space_index_migrator'; +import type { Adapters } from '../types'; + +export class RuleMigrationIndexMigrator { + constructor( + private ruleMigrationIndexAdapters: Adapters, + private esClient: ElasticsearchClient, + private logger: Logger + ) {} + + private async getSpaceListForMigrations() { + const rulesIndicesAcrossSpaces = await this.esClient.indices.get({ + index: this.ruleMigrationIndexAdapters.rules.getIndexName('*'), + allow_no_indices: true, + }); + + const rulesIndexPatternPrefix = this.ruleMigrationIndexAdapters.rules.getIndexName(''); + const spaceList = Object.keys(rulesIndicesAcrossSpaces).map((index) => { + return index.replace(rulesIndexPatternPrefix, ''); + }); + + return spaceList; + } + + async run() { + const allSpaces = await this.getSpaceListForMigrations(); + if (allSpaces.length === 0) { + this.logger.info('No spaces or index found for index migration'); + return; + } + this.logger.info( + `Starting index migration for rule migrations for spaces :${allSpaces.join(', ')}` + ); + for (const spaceId of allSpaces) { + const migrator = new RuleMigrationSpaceIndexMigrator( + spaceId, + this.esClient, + this.logger, + this.ruleMigrationIndexAdapters + ); + await migrator.run(); + } + this.logger.info('Finished index migration for rule migrations successfully'); + } +} diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/index_migrators/rule_migrations_per_space_index_migrator.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/index_migrators/rule_migrations_per_space_index_migrator.test.ts new file mode 100644 index 0000000000000..4e5aa32eb9559 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/index_migrators/rule_migrations_per_space_index_migrator.test.ts @@ -0,0 +1,140 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ElasticsearchClient, Logger } from '@kbn/core/server'; +import type { Adapters, StoredSiemMigration } from '../types'; +import { RuleMigrationSpaceIndexMigrator } from './rule_migrations_per_space_index_migrator'; +import type { SearchResponseBody } from '@elastic/elasticsearch/lib/api/types'; + +const mockRuleIndexAggregationsResult = { + aggregations: { + migrationIds: { + buckets: [ + { + key: 'migration1', + createdAt: { value_as_string: '2023-01-01T00:00:00Z' }, + createdBy: { buckets: [{ key: 'user1' }] }, + }, + { + key: 'migration2', + createdAt: { value_as_string: '2023-01-02T00:00:00Z' }, + createdBy: { buckets: [{ key: 'user2' }, { key: 'user3' }] }, + }, + ], + }, + }, +}; + +const mockMigrationsIndexResult = { + hits: { + hits: [], + }, +} as unknown as SearchResponseBody; + +const getMockedESSearchFunction = ( + rulesIndexAggResult: typeof mockRuleIndexAggregationsResult = mockRuleIndexAggregationsResult, + migrationIndexResult: typeof mockMigrationsIndexResult = mockMigrationsIndexResult +) => + jest.fn((args) => { + if (args.index === '.kibana-siem-rule-migrations-rules-space1') { + return Promise.resolve(rulesIndexAggResult); + } else if (args.index === '.kibana-siem-rule-migrations-migrations-space1') { + return Promise.resolve(migrationIndexResult); + } + return Promise.resolve({ hits: { hits: [] } }); + }); + +const esClientMock = { + search: jest.fn(), + bulk: jest.fn(), +} as unknown as jest.Mocked; + +const loggerMock = { + info: jest.fn(), + error: jest.fn(), + debug: jest.fn(), + warn: jest.fn(), +} as unknown as Logger; + +const ruleMigrationIndexAdapters = { + rules: { + getIndexName: (spaceId: string) => `.kibana-siem-rule-migrations-rules-${spaceId}`, + }, + migrations: { + getIndexName: (spaceId: string) => `.kibana-siem-rule-migrations-migrations-${spaceId}`, + getInstalledIndexName: (spaceId: string) => + `.kibana-siem-rule-migrations-migrations-${spaceId}`, + createIndex: jest.fn(), + }, +} as unknown as Adapters; + +describe('RuleMigrationSpaceIndexMigrator', () => { + beforeEach(() => { + jest.clearAllMocks(); + esClientMock.search.mockImplementation( + getMockedESSearchFunction() as unknown as ElasticsearchClient['search'] + ); + }); + + it('should create correct number of documents when nothing exists in Migration index', async () => { + const migrator = new RuleMigrationSpaceIndexMigrator( + 'space1', + esClientMock, + loggerMock, + ruleMigrationIndexAdapters + ); + await migrator.run(); + + expect(esClientMock.bulk).toHaveBeenNthCalledWith(1, { + refresh: 'wait_for', + operations: [ + { create: { _id: 'migration1', _index: '.kibana-siem-rule-migrations-migrations-space1' } }, + { id: 'migration1', created_at: '2023-01-01T00:00:00Z', created_by: 'user1' }, + { create: { _id: 'migration2', _index: '.kibana-siem-rule-migrations-migrations-space1' } }, + { id: 'migration2', created_at: '2023-01-02T00:00:00Z', created_by: 'user2' }, + ], + }); + }); + + it('should create correct number of documents when some exist in Migration index', async () => { + const mockMigrationIndexResultWithOneDocument = { + hits: { + hits: [ + { + _id: 'migration1', + _source: { + created_at: '2023-01-01T00:00:00Z', + created_by: 'user1', + }, + }, + ], + }, + } as unknown as SearchResponseBody; + esClientMock.search.mockImplementation( + getMockedESSearchFunction( + mockRuleIndexAggregationsResult, + mockMigrationIndexResultWithOneDocument + ) as unknown as ElasticsearchClient['search'] + ); + + const migrator = new RuleMigrationSpaceIndexMigrator( + 'space1', + esClientMock, + loggerMock, + ruleMigrationIndexAdapters + ); + + await migrator.run(); + expect(esClientMock.bulk).toHaveBeenNthCalledWith(1, { + refresh: 'wait_for', + operations: [ + { create: { _id: 'migration2', _index: '.kibana-siem-rule-migrations-migrations-space1' } }, + { id: 'migration2', created_at: '2023-01-02T00:00:00Z', created_by: 'user2' }, + ], + }); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/index_migrators/rule_migrations_per_space_index_migrator.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/index_migrators/rule_migrations_per_space_index_migrator.ts new file mode 100644 index 0000000000000..b3a0c5aa5b5c9 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/index_migrators/rule_migrations_per_space_index_migrator.ts @@ -0,0 +1,129 @@ +/* + * 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 { ElasticsearchClient, Logger } from '@kbn/core/server'; +import assert from 'assert'; +import type { + AggregationsAggregationContainer, + AggregationsMinAggregate, + AggregationsStringTermsAggregate, + AggregationsStringTermsBucket, +} from '@elastic/elasticsearch/lib/api/types'; +import type { Adapters, StoredSiemMigration } from '../types'; + +const MAX_ES_SIZE = 10000; + +export class RuleMigrationSpaceIndexMigrator { + constructor( + private spaceId: string, + private esClient: ElasticsearchClient, + private logger: Logger, + private ruleMigrationIndexAdapters: Adapters + ) {} + + private async getExistingMigrationFromRulesIndex() { + const index = this.ruleMigrationIndexAdapters.rules.getIndexName(this.spaceId); + const aggregations: Record = { + migrationIds: { + terms: { field: 'migration_id', order: { createdAt: 'asc' }, size: MAX_ES_SIZE }, + aggregations: { + createdAt: { min: { field: '@timestamp' } }, + createdBy: { terms: { field: 'created_by' } }, + }, + }, + }; + const result = await this.esClient + .search({ index, aggregations, _source: false }) + .catch((error) => { + this.logger.error(`Error getting all rule migrations stats: ${error.message}`); + throw error; + }); + + const migrationsAgg = result.aggregations?.migrationIds as AggregationsStringTermsAggregate; + const buckets = (migrationsAgg?.buckets as AggregationsStringTermsBucket[]) ?? []; + return buckets.map((bucket) => ({ + id: `${bucket.key}`, + created_at: (bucket.createdAt as AggregationsMinAggregate | undefined) + ?.value_as_string as string, + created_by: ( + (bucket.createdBy as AggregationsStringTermsAggregate) + .buckets as AggregationsStringTermsBucket[] + )[0].key as string, + })); + } + + private async getExistingMigrationFromMigrationsIndex() { + const index = this.ruleMigrationIndexAdapters.migrations.getIndexName(this.spaceId); + const result = await this.esClient.search({ + index, + size: MAX_ES_SIZE, + query: { + match_all: {}, + }, + _source: true, + }); + + return result.hits.hits.map(({ _id }) => { + assert(_id, 'document should have _id'); + return _id; + }); + } + + private async indexMigrationDocs(docs: StoredSiemMigration[]) { + const indexName = this.ruleMigrationIndexAdapters.migrations.getIndexName(this.spaceId); + const createOperations = docs.flatMap((doc) => [ + { + create: { + _id: doc.id, + _index: indexName, + }, + }, + { + ...doc, + }, + ]); + + return this.esClient.bulk({ + refresh: 'wait_for', + operations: createOperations, + }); + } + + async run() { + await this.migrateRuleMigrationIndex(); + } + + /** + * Creates the rule migration index if it doesn't exist and indexes any missing migration documents + * from the rules index. + * + */ + private async migrateRuleMigrationIndex() { + const installedIndexName = + await this.ruleMigrationIndexAdapters.migrations.getInstalledIndexName(this.spaceId); + if (!installedIndexName) { + await this.ruleMigrationIndexAdapters.migrations.createIndex(this.spaceId); + } + + const existingMigrationsFromRulesIndex = await this.getExistingMigrationFromRulesIndex(); + const existingMigrationsFromMigrationsIndex = + await this.getExistingMigrationFromMigrationsIndex(); + + const migrationsToIndex = existingMigrationsFromRulesIndex.filter( + (migration) => !existingMigrationsFromMigrationsIndex.some((id) => id === migration.id) + ); + + if (migrationsToIndex.length > 0) { + this.logger.info( + `Found ${migrationsToIndex.length} rule migration documents from rules index with an absent migration doc. Creating corresponding migration documents.` + ); + await this.indexMigrationDocs(migrationsToIndex); + this.logger.info( + `Created ${migrationsToIndex.length} rule migration documents from rules index with an absent migration doc.` + ); + } + } +} diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/siem_rule_migrations_service.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/siem_rule_migrations_service.test.ts index 5ce7bc795e3b6..88a3b1917d7e3 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/siem_rule_migrations_service.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/siem_rule_migrations_service.test.ts @@ -17,7 +17,7 @@ import { import { Subject } from 'rxjs'; import { MockRuleMigrationsDataService, - mockInstall, + mockSetup, mockCreateClient as mockDataCreateClient, } from './data/__mocks__/mocks'; import { mockCreateClient as mockTaskCreateClient, mockStopAll } from './task/__mocks__/mocks'; @@ -52,7 +52,7 @@ describe('SiemRuleMigrationsService', () => { it('should set esClusterClient and call dataStreamAdapter.install', () => { ruleMigrationsService.setup({ esClusterClient, pluginStop$ }); - expect(mockInstall).toHaveBeenCalledWith({ + expect(mockSetup).toHaveBeenCalledWith({ esClient: esClusterClient.asInternalUser, pluginStop$, }); @@ -60,7 +60,7 @@ describe('SiemRuleMigrationsService', () => { it('should log error when data installation fails', async () => { const error = 'Failed to install'; - mockInstall.mockRejectedValueOnce(error); + mockSetup.mockRejectedValueOnce(error); ruleMigrationsService.setup({ esClusterClient, pluginStop$ }); await waitFor(() => { diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/siem_rule_migrations_service.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/siem_rule_migrations_service.ts index 669b54d85d080..7c9f274063611 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/siem_rule_migrations_service.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/siem_rule_migrations_service.ts @@ -54,7 +54,7 @@ export class SiemRuleMigrationsService { this.esClusterClient = esClusterClient; const esClient = esClusterClient.asInternalUser; - this.dataService.install({ ...params, esClient }).catch((err) => { + this.dataService.setup({ ...params, esClient }).catch((err) => { this.logger.error('Error installing data service.', err); }); } diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/state.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/state.ts index e27480d10dcc9..29edceb80fc69 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/state.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/state.ts @@ -11,7 +11,7 @@ import type { RuleTranslationResult } from '../../../../../../common/siem_migrat import type { ElasticRulePartial, OriginalRule, - RuleMigration, + RuleMigrationRule, } from '../../../../../../common/siem_migrations/model/rule_migration.gen'; import type { RuleMigrationResources } from '../retrievers/rule_resource_retriever'; @@ -30,7 +30,7 @@ export const migrateRuleState = Annotation.Root({ default: () => '', }), translation_result: Annotation(), - comments: Annotation({ + comments: Annotation({ // Translation subgraph causes the original main graph comments to be concatenated again, we need to deduplicate them. reducer: (current, value) => uniq(value ? (current ?? []).concat(value) : current), default: () => [], diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/state.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/state.ts index 68eb349dd171c..456d7bde6cf4e 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/state.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/state.ts @@ -10,7 +10,7 @@ import { RuleTranslationResult } from '../../../../../../../../common/siem_migra import type { ElasticRulePartial, OriginalRule, - RuleMigration, + RuleMigrationRule, } from '../../../../../../../../common/siem_migrations/model/rule_migration.gen'; import type { RuleMigrationResources } from '../../../retrievers/rule_resource_retriever'; import type { RuleMigrationIntegration } from '../../../../types'; @@ -47,7 +47,7 @@ export const translateRuleState = Annotation.Root({ reducer: (current, value) => value ?? current, default: () => RuleTranslationResult.UNTRANSLATABLE, }), - comments: Annotation({ + comments: Annotation({ reducer: (current, value) => (value ? (current ?? []).concat(value) : current), default: () => [], }), diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/retrievers/rule_resource_retriever.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/retrievers/rule_resource_retriever.test.ts index 7008f55b17e90..d58896c17ef0e 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/retrievers/rule_resource_retriever.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/retrievers/rule_resource_retriever.test.ts @@ -6,16 +6,16 @@ */ import { RuleResourceRetriever } from './rule_resource_retriever'; // Adjust path as needed -import type { RuleMigration } from '../../../../../../common/siem_migrations/model/rule_migration.gen'; import { ResourceIdentifier } from '../../../../../../common/siem_migrations/rules/resources'; import type { RuleMigrationsDataClient } from '../../data/rule_migrations_data_client'; +import type { RuleMigrationRule } from '../../../../../../common/siem_migrations/model/rule_migration.gen'; jest.mock('../../data/rule_migrations_data_service'); jest.mock('../../../../../../common/siem_migrations/rules/resources'); const MockResourceIdentifier = ResourceIdentifier as jest.Mock; -const migration = { original_rule: { vendor: 'splunk' } } as unknown as RuleMigration; +const migration = { original_rule: { vendor: 'splunk' } } as unknown as RuleMigrationRule; describe('RuleResourceRetriever', () => { let retriever: RuleResourceRetriever; @@ -25,7 +25,7 @@ describe('RuleResourceRetriever', () => { beforeEach(() => { mockDataClient = { resources: { searchBatches: jest.fn().mockReturnValue({ next: jest.fn(() => []) }) }, - } as unknown as RuleMigrationsDataClient; + } as unknown as jest.Mocked; retriever = new RuleResourceRetriever('mockMigrationId', mockDataClient); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/retrievers/rule_resource_retriever.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/retrievers/rule_resource_retriever.ts index 42e6a975c71d8..03c978812085b 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/retrievers/rule_resource_retriever.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/retrievers/rule_resource_retriever.ts @@ -7,9 +7,9 @@ import { ResourceIdentifier } from '../../../../../../common/siem_migrations/rules/resources'; import type { - RuleMigration, RuleMigrationResource, RuleMigrationResourceType, + RuleMigrationRule, } from '../../../../../../common/siem_migrations/model/rule_migration.gen'; import type { RuleMigrationsDataClient } from '../../data/rule_migrations_data_client'; @@ -54,8 +54,8 @@ export class RuleResourceRetriever { this.existingResources = existingRuleResources; } - public async getResources(ruleMigration: RuleMigration): Promise { - const originalRule = ruleMigration.original_rule; + public async getResources(migrationRule: RuleMigrationRule): Promise { + const originalRule = migrationRule.original_rule; const existingResources = this.existingResources; if (!existingResources) { throw new Error('initialize must be called before calling getResources'); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/rule_migrations_task_client.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/rule_migrations_task_client.ts index 75873bc899416..a6afce48f097a 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/rule_migrations_task_client.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/rule_migrations_task_client.ts @@ -198,4 +198,9 @@ export class RuleMigrationsTaskClient { invocationConfig, }); } + + /** Returns if a migration is running or not */ + isMigrationRunning(migrationId: string): boolean { + return this.migrationsRunning.has(migrationId); + } } diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/types.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/types.ts index 05f3711817046..bf7c7d185e44c 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/types.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/types.ts @@ -14,23 +14,16 @@ import type { InferenceClient } from '@kbn/inference-plugin/server'; import type { IndexAdapter, IndexPatternAdapter } from '@kbn/index-adapter'; import type { RuleMigration, + RuleMigrationRule, RuleMigrationTranslationResult, - UpdateRuleMigrationData, + UpdateRuleMigrationRule, } from '../../../../common/siem_migrations/model/rule_migration.gen'; import { type RuleMigrationResource } from '../../../../common/siem_migrations/model/rule_migration.gen'; import type { RuleVersions } from './data/rule_migrations_data_prebuilt_rules_client'; +import type { Stored } from '../types'; -export type Stored = T & { id: string }; - -export interface SiemMigration { - /** The moment the migration was created */ - created_at: string; - /** The profile id of the user who created the migration */ - created_by: string; -} -export type StoredSiemMigration = Stored; - -export type StoredRuleMigration = Stored; +export type StoredSiemMigration = Stored; +export type StoredRuleMigration = Stored; export type StoredRuleMigrationResource = Stored; export interface SiemRuleMigrationsClientDependencies { @@ -60,7 +53,7 @@ export interface RuleMigrationPrebuiltRule { export type RuleSemanticSearchResult = RuleMigrationPrebuiltRule & RuleVersions; -export type InternalUpdateRuleMigrationData = UpdateRuleMigrationData & { +export type InternalUpdateRuleMigrationRule = UpdateRuleMigrationRule & { translation_result?: RuleMigrationTranslationResult; }; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/types.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/types.ts index 62071c9e8bbbe..a2c6d89e4faa4 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/types.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/types.ts @@ -11,3 +11,5 @@ export interface SiemMigrationsSetupParams { esClusterClient: IClusterClient; tasksTimeoutMs?: number; } + +export type Stored = T & { id: string }; 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 eacab2ca005b6..1ee72e69cc884 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 @@ -34,8 +34,8 @@ import { CreateAlertsMigrationRequestBodyInput } from '@kbn/security-solution-pl import { CreateAssetCriticalityRecordRequestBodyInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/asset_criticality/create_asset_criticality.gen'; import { CreateRuleRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_management/crud/create_rule/create_rule_route.gen'; import { - CreateRuleMigrationRequestParamsInput, - CreateRuleMigrationRequestBodyInput, + CreateRuleMigrationRulesRequestParamsInput, + CreateRuleMigrationRulesRequestBodyInput, } from '@kbn/security-solution-plugin/common/siem_migrations/model/api/rules/rule_migration.gen'; import { CreateTimelinesRequestBodyInput } from '@kbn/security-solution-plugin/common/api/timeline/create_timelines/create_timelines_route.gen'; import { @@ -49,6 +49,7 @@ import { } from '@kbn/security-solution-plugin/common/api/entity_analytics/entity_store/engine/delete.gen'; import { DeleteNoteRequestBodyInput } from '@kbn/security-solution-plugin/common/api/timeline/delete_note/delete_note_route.gen'; import { DeleteRuleRequestQueryInput } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_management/crud/delete_rule/delete_rule_route.gen'; +import { DeleteRuleMigrationRequestParamsInput } from '@kbn/security-solution-plugin/common/siem_migrations/model/api/rules/rule_migration.gen'; import { DeleteTimelinesRequestBodyInput } from '@kbn/security-solution-plugin/common/api/timeline/delete_timelines/delete_timelines_route.gen'; import { DeprecatedTriggerRiskScoreCalculationRequestBodyInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/risk_engine/entity_calculation_route.gen'; import { EndpointExecuteActionRequestBodyInput } from '@kbn/security-solution-plugin/common/api/endpoint/actions/response_actions/execute/execute.gen'; @@ -98,16 +99,17 @@ import { GetRuleExecutionResultsRequestQueryInput, GetRuleExecutionResultsRequestParamsInput, } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_monitoring/rule_execution_logs/get_rule_execution_results/get_rule_execution_results_route.gen'; -import { - GetRuleMigrationRequestQueryInput, - GetRuleMigrationRequestParamsInput, -} from '@kbn/security-solution-plugin/common/siem_migrations/model/api/rules/rule_migration.gen'; +import { GetRuleMigrationRequestParamsInput } from '@kbn/security-solution-plugin/common/siem_migrations/model/api/rules/rule_migration.gen'; import { GetRuleMigrationPrebuiltRulesRequestParamsInput } from '@kbn/security-solution-plugin/common/siem_migrations/model/api/rules/rule_migration.gen'; import { GetRuleMigrationResourcesRequestQueryInput, GetRuleMigrationResourcesRequestParamsInput, } from '@kbn/security-solution-plugin/common/siem_migrations/model/api/rules/rule_migration.gen'; import { GetRuleMigrationResourcesMissingRequestParamsInput } from '@kbn/security-solution-plugin/common/siem_migrations/model/api/rules/rule_migration.gen'; +import { + GetRuleMigrationRulesRequestQueryInput, + GetRuleMigrationRulesRequestParamsInput, +} from '@kbn/security-solution-plugin/common/siem_migrations/model/api/rules/rule_migration.gen'; import { GetRuleMigrationStatsRequestParamsInput } from '@kbn/security-solution-plugin/common/siem_migrations/model/api/rules/rule_migration.gen'; import { GetRuleMigrationTranslationStatsRequestParamsInput } from '@kbn/security-solution-plugin/common/siem_migrations/model/api/rules/rule_migration.gen'; import { GetTimelineRequestQueryInput } from '@kbn/security-solution-plugin/common/api/timeline/get_timeline/get_timeline_route.gen'; @@ -158,9 +160,10 @@ import { StopRuleMigrationRequestParamsInput } from '@kbn/security-solution-plug import { SuggestUserProfilesRequestQueryInput } from '@kbn/security-solution-plugin/common/api/detection_engine/users/suggest_user_profiles_route.gen'; import { TriggerRiskScoreCalculationRequestBodyInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/risk_engine/entity_calculation_route.gen'; import { UpdateRuleRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_management/crud/update_rule/update_rule_route.gen'; +import { UpdateRuleMigrationRequestParamsInput } from '@kbn/security-solution-plugin/common/siem_migrations/model/api/rules/rule_migration.gen'; import { - UpdateRuleMigrationRequestParamsInput, - UpdateRuleMigrationRequestBodyInput, + UpdateRuleMigrationRulesRequestParamsInput, + UpdateRuleMigrationRulesRequestBodyInput, } from '@kbn/security-solution-plugin/common/siem_migrations/model/api/rules/rule_migration.gen'; import { UpdateWorkflowInsightRequestParamsInput, @@ -476,13 +479,26 @@ For detailed information on Kibana actions and alerting, and additional API call .send(props.body as object); }, /** - * Creates a new SIEM rules migration using the original vendor rules provided + * Creates a new rule migration and returns the corresponding migration_id */ - createRuleMigration(props: CreateRuleMigrationProps, kibanaSpace: string = 'default') { + createRuleMigration(kibanaSpace: string = 'default') { + return supertest + .put(routeWithNamespace('/internal/siem_migrations/rules', kibanaSpace)) + .set('kbn-xsrf', 'true') + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana'); + }, + /** + * Adds original vendor rules to an already existing migration. Can be called multiple times to add more rules + */ + createRuleMigrationRules( + props: CreateRuleMigrationRulesProps, + kibanaSpace: string = 'default' + ) { return supertest .post( routeWithNamespace( - replaceParams('/internal/siem_migrations/rules/{migration_id}', props.params), + replaceParams('/internal/siem_migrations/rules/{migration_id}/rules', props.params), kibanaSpace ) ) @@ -585,6 +601,21 @@ The difference between the `id` and `rule_id` is that the `id` is a unique rule .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .query(props.query); }, + /** + * Deletes a rule migration document stored in the system given the rule migration id + */ + deleteRuleMigration(props: DeleteRuleMigrationProps, kibanaSpace: string = 'default') { + return supertest + .delete( + routeWithNamespace( + replaceParams('/internal/siem_migrations/rules/{migration_id}', props.params), + kibanaSpace + ) + ) + .set('kbn-xsrf', 'true') + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana'); + }, /** * Delete one or more Timelines or Timeline templates. */ @@ -1095,7 +1126,7 @@ finalize it. .query(props.query); }, /** - * Retrieves the rule documents stored in the system given the rule migration id + * Retrieves the rule migration document stored in the system given the rule migration id */ getRuleMigration(props: GetRuleMigrationProps, kibanaSpace: string = 'default') { return supertest @@ -1107,8 +1138,7 @@ finalize it. ) .set('kbn-xsrf', 'true') .set(ELASTIC_HTTP_VERSION_HEADER, '1') - .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') - .query(props.query); + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana'); }, /** * Retrieves all related integrations @@ -1191,6 +1221,22 @@ finalize it. .set(ELASTIC_HTTP_VERSION_HEADER, '1') .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana'); }, + /** + * Retrieves the the list of rules included in a migration given the migration id + */ + getRuleMigrationRules(props: GetRuleMigrationRulesProps, kibanaSpace: string = 'default') { + return supertest + .get( + routeWithNamespace( + replaceParams('/internal/siem_migrations/rules/{migration_id}/rules', props.params), + kibanaSpace + ) + ) + .set('kbn-xsrf', 'true') + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') + .query(props.query); + }, /** * Retrieves the stats of a SIEM rules migration using the migration id provided */ @@ -1691,7 +1737,7 @@ The difference between the `id` and `rule_id` is that the `id` is a unique rule */ startRuleMigration(props: StartRuleMigrationProps, kibanaSpace: string = 'default') { return supertest - .put( + .post( routeWithNamespace( replaceParams('/internal/siem_migrations/rules/{migration_id}/start', props.params), kibanaSpace @@ -1719,7 +1765,7 @@ The difference between the `id` and `rule_id` is that the `id` is a unique rule */ stopRuleMigration(props: StopRuleMigrationProps, kibanaSpace: string = 'default') { return supertest - .put( + .post( routeWithNamespace( replaceParams('/internal/siem_migrations/rules/{migration_id}/stop', props.params), kibanaSpace @@ -1773,11 +1819,11 @@ The difference between the `id` and `rule_id` is that the `id` is a unique rule .send(props.body as object); }, /** - * Updates rules migrations attributes + * Updates rules migrations data */ updateRuleMigration(props: UpdateRuleMigrationProps, kibanaSpace: string = 'default') { return supertest - .put( + .patch( routeWithNamespace( replaceParams('/internal/siem_migrations/rules/{migration_id}', props.params), kibanaSpace @@ -1785,6 +1831,24 @@ The difference between the `id` and `rule_id` is that the `id` is a unique rule ) .set('kbn-xsrf', 'true') .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana'); + }, + /** + * Updates rules migrations attributes + */ + updateRuleMigrationRules( + props: UpdateRuleMigrationRulesProps, + kibanaSpace: string = 'default' + ) { + return supertest + .patch( + routeWithNamespace( + replaceParams('/internal/siem_migrations/rules/{migration_id}/rules', 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); }, @@ -1869,9 +1933,9 @@ export interface CreateAssetCriticalityRecordProps { export interface CreateRuleProps { body: CreateRuleRequestBodyInput; } -export interface CreateRuleMigrationProps { - params: CreateRuleMigrationRequestParamsInput; - body: CreateRuleMigrationRequestBodyInput; +export interface CreateRuleMigrationRulesProps { + params: CreateRuleMigrationRulesRequestParamsInput; + body: CreateRuleMigrationRulesRequestBodyInput; } export interface CreateTimelinesProps { body: CreateTimelinesRequestBodyInput; @@ -1893,6 +1957,9 @@ export interface DeleteNoteProps { export interface DeleteRuleProps { query: DeleteRuleRequestQueryInput; } +export interface DeleteRuleMigrationProps { + params: DeleteRuleMigrationRequestParamsInput; +} export interface DeleteTimelinesProps { body: DeleteTimelinesRequestBodyInput; } @@ -2001,7 +2068,6 @@ export interface GetRuleExecutionResultsProps { params: GetRuleExecutionResultsRequestParamsInput; } export interface GetRuleMigrationProps { - query: GetRuleMigrationRequestQueryInput; params: GetRuleMigrationRequestParamsInput; } export interface GetRuleMigrationPrebuiltRulesProps { @@ -2014,6 +2080,10 @@ export interface GetRuleMigrationResourcesProps { export interface GetRuleMigrationResourcesMissingProps { params: GetRuleMigrationResourcesMissingRequestParamsInput; } +export interface GetRuleMigrationRulesProps { + query: GetRuleMigrationRulesRequestQueryInput; + params: GetRuleMigrationRulesRequestParamsInput; +} export interface GetRuleMigrationStatsProps { params: GetRuleMigrationStatsRequestParamsInput; } @@ -2126,7 +2196,10 @@ export interface UpdateRuleProps { } export interface UpdateRuleMigrationProps { params: UpdateRuleMigrationRequestParamsInput; - body: UpdateRuleMigrationRequestBodyInput; +} +export interface UpdateRuleMigrationRulesProps { + params: UpdateRuleMigrationRulesRequestParamsInput; + body: UpdateRuleMigrationRulesRequestBodyInput; } export interface UpdateWorkflowInsightProps { params: UpdateWorkflowInsightRequestParamsInput; diff --git a/x-pack/test/security_solution_api_integration/test_suites/siem_migrations/rules/trial_license_complete_tier/create.ts b/x-pack/test/security_solution_api_integration/test_suites/siem_migrations/rules/trial_license_complete_tier/create.ts index f6aa2b66459a1..8d92974e85dba 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/siem_migrations/rules/trial_license_complete_tier/create.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/siem_migrations/rules/trial_license_complete_tier/create.ts @@ -6,192 +6,35 @@ */ import expect from 'expect'; -import { v4 as uuidv4 } from 'uuid'; -import { SiemMigrationStatus } from '@kbn/security-solution-plugin/common/siem_migrations/constants'; -import { - defaultOriginalRule, - deleteAllMigrationRules, - migrationResourcesRouteHelpersFactory, - migrationRulesRouteHelpersFactory, - splunkRuleWithResources, -} from '../../utils'; +import { deleteAllRuleMigrations, ruleMigrationRouteHelpersFactory } from '../../utils'; import { FtrProviderContext } from '../../../../ftr_provider_context'; export default ({ getService }: FtrProviderContext) => { const es = getService('es'); const supertest = getService('supertest'); - const migrationRulesRoutes = migrationRulesRouteHelpersFactory(supertest); - const migrationResourcesRoutes = migrationResourcesRouteHelpersFactory(supertest); + const ruleMigrationRoutes = ruleMigrationRouteHelpersFactory(supertest); describe('@ess @serverless @serverlessQA Create API', () => { beforeEach(async () => { - await deleteAllMigrationRules(es); + await deleteAllRuleMigrations(es); }); describe('Happy path', () => { - it('should create migrations with provided id', async () => { - const migrationId = uuidv4(); - await migrationRulesRoutes.create({ migrationId, payload: [defaultOriginalRule] }); - - // fetch migration rule - const response = await migrationRulesRoutes.get({ migrationId }); - expect(response.body.total).toEqual(1); - - const migrationRule = response.body.data[0]; - expect(migrationRule).toEqual( - expect.objectContaining({ - migration_id: migrationId, - original_rule: defaultOriginalRule, - status: SiemMigrationStatus.PENDING, - }) - ); - }); - - it('should create migrations without provided id', async () => { + it('should create migrations without any issues', async () => { const { body: { migration_id: migrationId }, - } = await migrationRulesRoutes.create({ payload: [defaultOriginalRule] }); - - // fetch migration rule - const response = await migrationRulesRoutes.get({ migrationId }); - expect(response.body.total).toEqual(1); + } = await ruleMigrationRoutes.create({}); - const migrationRule = response.body.data[0]; - expect(migrationRule).toEqual( - expect.objectContaining({ - migration_id: migrationId, - original_rule: defaultOriginalRule, - status: SiemMigrationStatus.PENDING, - }) - ); - }); + expect(migrationId).not.toBeNull(); - it('should create migrations with the rules that have resources', async () => { - const migrationId = uuidv4(); - await migrationRulesRoutes.create({ migrationId, payload: [splunkRuleWithResources] }); - - // fetch migration rule - const response = await migrationRulesRoutes.get({ migrationId }); - expect(response.body.total).toEqual(1); - - const migrationRule = response.body.data[0]; - expect(migrationRule).toEqual( - expect.objectContaining({ - migration_id: migrationId, - original_rule: splunkRuleWithResources, - status: SiemMigrationStatus.PENDING, - }) - ); - - // fetch missing resources - const resourcesResponse = await migrationResourcesRoutes.getMissingResources({ + const { + body: { id, created_by: createdBy }, + } = await ruleMigrationRoutes.get({ migrationId, }); - expect(resourcesResponse.body).toEqual([ - { type: 'macro', name: 'summariesonly' }, - { type: 'macro', name: 'drop_dm_object_name(1)' }, - { type: 'lookup', name: 'malware_tracker' }, - ]); - }); - }); - - describe('Error handling', () => { - it('should return no content error', async () => { - const migrationId = uuidv4(); - await migrationRulesRoutes.create({ migrationId, payload: [], expectStatusCode: 204 }); - - // fetch migration rule - const response = await migrationRulesRoutes.get({ migrationId }); - expect(response.body.total).toEqual(0); - }); - - it(`should return an error when undefined payload has been passed`, async () => { - const migrationId = uuidv4(); - const response = await migrationRulesRoutes.create({ migrationId, expectStatusCode: 400 }); - expect(response.body).toEqual({ - error: 'Bad Request', - message: '[request body]: Expected array, received null', - statusCode: 400, - }); - }); - - it('should return an error when original rule id is not specified', async () => { - const { id, ...restOfOriginalRule } = defaultOriginalRule; - const response = await migrationRulesRoutes.create({ - payload: [restOfOriginalRule], - expectStatusCode: 400, - }); - expect(response.body).toEqual({ - statusCode: 400, - error: 'Bad Request', - message: '[request body]: 0.id: Required', - }); - }); - - it('should return an error when original rule vendor is not specified', async () => { - const { vendor, ...restOfOriginalRule } = defaultOriginalRule; - const response = await migrationRulesRoutes.create({ - payload: [restOfOriginalRule], - expectStatusCode: 400, - }); - expect(response.body).toEqual({ - statusCode: 400, - error: 'Bad Request', - message: '[request body]: 0.vendor: Invalid literal value, expected "splunk"', - }); - }); - - it('should return an error when original rule title is not specified', async () => { - const { title, ...restOfOriginalRule } = defaultOriginalRule; - const response = await migrationRulesRoutes.create({ - payload: [restOfOriginalRule], - expectStatusCode: 400, - }); - expect(response.body).toEqual({ - statusCode: 400, - error: 'Bad Request', - message: '[request body]: 0.title: Required', - }); - }); - - it('should return an error when original rule description is not specified', async () => { - const { description, ...restOfOriginalRule } = defaultOriginalRule; - const response = await migrationRulesRoutes.create({ - payload: [restOfOriginalRule], - expectStatusCode: 400, - }); - expect(response.body).toEqual({ - statusCode: 400, - error: 'Bad Request', - message: '[request body]: 0.description: Required', - }); - }); - - it('should return an error when original rule query is not specified', async () => { - const { query, ...restOfOriginalRule } = defaultOriginalRule; - const response = await migrationRulesRoutes.create({ - payload: [restOfOriginalRule], - expectStatusCode: 400, - }); - expect(response.body).toEqual({ - statusCode: 400, - error: 'Bad Request', - message: '[request body]: 0.query: Required', - }); - }); - - it('should return an error when original rule query_language is not specified', async () => { - const { query_language: _, ...restOfOriginalRule } = defaultOriginalRule; - const response = await migrationRulesRoutes.create({ - payload: [restOfOriginalRule], - expectStatusCode: 400, - }); - expect(response.body).toEqual({ - statusCode: 400, - error: 'Bad Request', - message: '[request body]: 0.query_language: Required', - }); + expect(id).toBe(migrationId); + expect(createdBy).not.toBeNull(); }); }); }); diff --git a/x-pack/test/security_solution_api_integration/test_suites/siem_migrations/rules/trial_license_complete_tier/delete.ts b/x-pack/test/security_solution_api_integration/test_suites/siem_migrations/rules/trial_license_complete_tier/delete.ts new file mode 100644 index 0000000000000..d0f496a9d9ab8 --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/siem_migrations/rules/trial_license_complete_tier/delete.ts @@ -0,0 +1,157 @@ +/* + * 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 expect from 'expect'; +import { + createLookupsForMigrationId, + createMacrosForMigrationId, + deleteAllRuleMigrations, + ruleMigrationRouteHelpersFactory, + splunkRuleWithResources, +} from '../../utils'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; +import { + getResoucesPerMigrationFromES, + getRuleMigrationFromES, + getRulesPerMigrationFromES, +} from '../../utils/es_queries'; + +export default ({ getService }: FtrProviderContext) => { + const es = getService('es'); + const supertest = getService('supertest'); + const ruleMigrationRoutes = ruleMigrationRouteHelpersFactory(supertest); + + describe('@ess @serverless @serverlessQA Delete API', () => { + let migrationId: string; + beforeEach(async () => { + await deleteAllRuleMigrations(es); + const response = await ruleMigrationRoutes.create({}); + migrationId = response.body.migration_id; + }); + + describe('Happy path', () => { + it('should delete existing migration without any issues', async () => { + await ruleMigrationRoutes.delete({ + migrationId, + expectStatusCode: 200, + }); + + await ruleMigrationRoutes.get({ + migrationId, + expectStatusCode: 404, + }); + }); + + it('should delete migrations, rules and resources associated with the migration', async () => { + await ruleMigrationRoutes.addRulesToMigration({ + migrationId, + /** adding bulk rules so that deletion of all rules can be tested */ + payload: Array.from({ length: 40 }, () => splunkRuleWithResources), + }); + + await createMacrosForMigrationId({ + es, + migrationId, + count: 40, + }); + + await createLookupsForMigrationId({ + es, + migrationId, + count: 40, + }); + + let migrationsFromES = await getRuleMigrationFromES({ + es, + migrationId, + }); + expect(migrationsFromES.hits.hits).toHaveLength(1); + + let rulesFromES = await getRulesPerMigrationFromES({ + es, + migrationId, + }); + expect(rulesFromES.hits.hits).toHaveLength(40); + + let resourcesFromES = await getResoucesPerMigrationFromES({ + es, + migrationId, + }); + + expect(resourcesFromES.hits.hits).toHaveLength(83); + + await ruleMigrationRoutes.delete({ + migrationId, + expectStatusCode: 200, + }); + + rulesFromES = await getRulesPerMigrationFromES({ + es, + migrationId, + }); + + expect(rulesFromES.hits.hits).toHaveLength(0); + + migrationsFromES = await getRuleMigrationFromES({ + es, + migrationId, + }); + expect(migrationsFromES.hits.hits).toHaveLength(0); + + resourcesFromES = await getResoucesPerMigrationFromES({ + es, + migrationId, + }); + expect(resourcesFromES.hits.hits).toHaveLength(0); + }); + + describe('Error handling', () => { + it('should return 409 if migration is already running', async () => { + // start a migration + await ruleMigrationRoutes.addRulesToMigration({ + migrationId, + payload: [splunkRuleWithResources], + }); + + const response = await ruleMigrationRoutes.start({ + migrationId, + payload: { + connector_id: 'preconfigured-bedrock', + }, + }); + + expect(response.body).toMatchObject({ started: true }); + + const deleteResponse = await ruleMigrationRoutes.delete({ + migrationId, + expectStatusCode: 409, + }); + + expect(deleteResponse.body).toMatchObject({ + statusCode: 409, + error: 'Conflict', + message: + 'A running migration cannot be deleted. Please stop the migration first and try again', + }); + }); + + it('should return 404 if migration ID does not exist', async () => { + const { body } = await ruleMigrationRoutes.delete({ + migrationId: 'non-existing-migration-id', + expectStatusCode: 404, + }); + + expect(body).toMatchObject({ + statusCode: 404, + error: 'Not Found', + message: 'No Migration found with id: non-existing-migration-id', + }); + }); + }); + }); + }); +}; diff --git a/x-pack/test/security_solution_api_integration/test_suites/siem_migrations/rules/trial_license_complete_tier/get.ts b/x-pack/test/security_solution_api_integration/test_suites/siem_migrations/rules/trial_license_complete_tier/get.ts index 80544899d6d18..0a07caa1f91c4 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/siem_migrations/rules/trial_license_complete_tier/get.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/siem_migrations/rules/trial_license_complete_tier/get.ts @@ -6,665 +6,42 @@ */ import expect from 'expect'; -import { v4 as uuidv4 } from 'uuid'; -import { - RuleTranslationResult, - SiemMigrationStatus, -} from '@kbn/security-solution-plugin/common/siem_migrations/constants'; -import { - RuleMigrationDocument, - createMigrationRules, - defaultElasticRule, - defaultOriginalRule, - deleteAllMigrationRules, - getMigrationRuleDocument, - getMigrationRuleDocuments, - migrationRulesRouteHelpersFactory, -} from '../../utils'; +import { deleteAllRuleMigrations, ruleMigrationRouteHelpersFactory } from '../../utils'; import { FtrProviderContext } from '../../../../ftr_provider_context'; export default ({ getService }: FtrProviderContext) => { const es = getService('es'); const supertest = getService('supertest'); - const migrationRulesRoutes = migrationRulesRouteHelpersFactory(supertest); + const ruleMigrationRoutes = ruleMigrationRouteHelpersFactory(supertest); describe('@ess @serverless @serverlessQA Get API', () => { + let migrationId: string; beforeEach(async () => { - await deleteAllMigrationRules(es); + await deleteAllRuleMigrations(es); + const creationResponse = await ruleMigrationRoutes.create({}); + migrationId = creationResponse.body.migration_id; }); - describe('Basic', () => { - it('should fetch existing rules within specified migration', async () => { - // create a document - const migrationId = uuidv4(); - const migrationRuleDocument = getMigrationRuleDocument({ migration_id: migrationId }); - await createMigrationRules(es, [migrationRuleDocument]); - - const { '@timestamp': timestamp, updated_at: updatedAt, ...rest } = migrationRuleDocument; - - // fetch migration rule - const response = await migrationRulesRoutes.get({ migrationId }); - expect(response.body.total).toEqual(1); - expect(response.body.data).toEqual(expect.arrayContaining([expect.objectContaining(rest)])); + it('should fetch existing migration', async () => { + const migrationResponse = await ruleMigrationRoutes.get({ + migrationId, }); - }); - - describe('Filtering', () => { - it('should fetch rules filtered by `searchTerm`', async () => { - // create a document - const migrationId = uuidv4(); - - const overrideCallback = (index: number): Partial => { - const title = `${index < 5 ? 'Elastic' : 'Splunk'} rule - ${index}`; - const originalRule = { ...defaultOriginalRule, title }; - const elasticRule = { ...defaultElasticRule, title }; - return { - migration_id: migrationId, - original_rule: originalRule, - elastic_rule: elasticRule, - }; - }; - const migrationRuleDocuments = getMigrationRuleDocuments(10, overrideCallback); - await createMigrationRules(es, migrationRuleDocuments); - - // Search by word `Elastic` - let expectedRuleDocuments = expect.arrayContaining( - migrationRuleDocuments - .slice(0, 5) - .map(({ '@timestamp': timestamp, updated_at: updatedAt, ...rest }) => - expect.objectContaining(rest) - ) - ); - - // fetch migration rules - let response = await migrationRulesRoutes.get({ - migrationId, - queryParams: { search_term: 'Elastic' }, - }); - expect(response.body.total).toEqual(5); - expect(response.body.data).toEqual(expectedRuleDocuments); - - // Search by word `Splunk` - expectedRuleDocuments = expect.arrayContaining( - migrationRuleDocuments - .slice(5) - .map(({ '@timestamp': timestamp, updated_at: updatedAt, ...rest }) => - expect.objectContaining(rest) - ) - ); - - // fetch migration rules - response = await migrationRulesRoutes.get({ - migrationId, - queryParams: { search_term: 'Splunk' }, - }); - expect(response.body.total).toEqual(5); - expect(response.body.data).toEqual(expectedRuleDocuments); - }); - - it('should fetch rules filtered by `ids`', async () => { - // create a document - const migrationId = uuidv4(); - - const migrationRuleDocuments = getMigrationRuleDocuments(10, () => ({ - migration_id: migrationId, - })); - const createdDocumentIds = await createMigrationRules(es, migrationRuleDocuments); - - const expectedIds = createdDocumentIds.slice(0, 3).sort(); - - // fetch migration rules by existing ids - let response = await migrationRulesRoutes.get({ - migrationId, - queryParams: { ids: expectedIds }, - }); - expect(response.body.total).toEqual(3); - expect(response.body.data.map(({ id }) => id).sort()).toEqual(expectedIds); - - // fetch migration rules by non-existing id - response = await migrationRulesRoutes.get({ - migrationId, - queryParams: { ids: [uuidv4()] }, - }); - expect(response.body.total).toEqual(0); - }); - - it('should fetch rules filtered by `prebuilt`', async () => { - // create a document - const migrationId = uuidv4(); - - const overrideCallback = (index: number): Partial => { - const prebuiltRuleId = index < 3 ? uuidv4() : undefined; - const elasticRule = { ...defaultElasticRule, prebuilt_rule_id: prebuiltRuleId }; - return { - migration_id: migrationId, - elastic_rule: elasticRule, - }; - }; - - const migrationRuleDocuments = getMigrationRuleDocuments(10, overrideCallback); - await createMigrationRules(es, migrationRuleDocuments); - - // fetch migration rules matched Elastic prebuilt rules - let response = await migrationRulesRoutes.get({ - migrationId, - queryParams: { is_prebuilt: true }, - }); - expect(response.body.total).toEqual(3); - - // fetch custom translated migration rules - response = await migrationRulesRoutes.get({ - migrationId, - queryParams: { is_prebuilt: false }, - }); - expect(response.body.total).toEqual(7); - }); - - it('should fetch rules filtered by `installed`', async () => { - // create a document - const migrationId = uuidv4(); - - const overrideCallback = (index: number): Partial => { - const installedRuleId = index < 2 ? uuidv4() : undefined; - const elasticRule = { ...defaultElasticRule, id: installedRuleId }; - return { - migration_id: migrationId, - elastic_rule: elasticRule, - }; - }; - - const migrationRuleDocuments = getMigrationRuleDocuments(10, overrideCallback); - await createMigrationRules(es, migrationRuleDocuments); - - // fetch installed migration rules - let response = await migrationRulesRoutes.get({ - migrationId, - queryParams: { is_installed: true }, - }); - expect(response.body.total).toEqual(2); - - // fetch non-installed migration rules - response = await migrationRulesRoutes.get({ - migrationId, - queryParams: { is_installed: false }, - }); - expect(response.body.total).toEqual(8); - }); - - it('should fetch rules filtered by `failed`', async () => { - // create a document - const migrationId = uuidv4(); - - const overrideCallback = (index: number): Partial => { - const status = index < 4 ? SiemMigrationStatus.FAILED : SiemMigrationStatus.COMPLETED; - return { - migration_id: migrationId, - status, - }; - }; - - const migrationRuleDocuments = getMigrationRuleDocuments(10, overrideCallback); - await createMigrationRules(es, migrationRuleDocuments); - - // fetch failed migration rules - let response = await migrationRulesRoutes.get({ - migrationId, - queryParams: { is_failed: true }, - }); - expect(response.body.total).toEqual(4); - - // fetch non-failed migration rules - response = await migrationRulesRoutes.get({ - migrationId, - queryParams: { is_failed: false }, - }); - expect(response.body.total).toEqual(6); - }); - - it('should fetch rules filtered by `fullyTranslated`', async () => { - // create a document - const migrationId = uuidv4(); - - const overrideCallback = (index: number): Partial => { - const translationResult = - index < 6 - ? RuleTranslationResult.FULL - : index < 8 - ? RuleTranslationResult.PARTIAL - : RuleTranslationResult.UNTRANSLATABLE; - return { - migration_id: migrationId, - translation_result: translationResult, - }; - }; - - const migrationRuleDocuments = getMigrationRuleDocuments(10, overrideCallback); - await createMigrationRules(es, migrationRuleDocuments); - - // fetch failed migration rules - let response = await migrationRulesRoutes.get({ - migrationId, - queryParams: { is_fully_translated: true }, - }); - expect(response.body.total).toEqual(6); - - // fetch non-failed migration rules - response = await migrationRulesRoutes.get({ - migrationId, - queryParams: { is_fully_translated: false }, - }); - expect(response.body.total).toEqual(4); - }); - - it('should fetch rules filtered by `partiallyTranslated`', async () => { - // create a document - const migrationId = uuidv4(); - - const overrideCallback = (index: number): Partial => { - const translationResult = - index < 4 - ? RuleTranslationResult.FULL - : index < 8 - ? RuleTranslationResult.PARTIAL - : RuleTranslationResult.UNTRANSLATABLE; - return { - migration_id: migrationId, - translation_result: translationResult, - }; - }; - - const migrationRuleDocuments = getMigrationRuleDocuments(10, overrideCallback); - await createMigrationRules(es, migrationRuleDocuments); - - // fetch failed migration rules - let response = await migrationRulesRoutes.get({ - migrationId, - queryParams: { is_partially_translated: true }, - }); - expect(response.body.total).toEqual(4); - - // fetch non-failed migration rules - response = await migrationRulesRoutes.get({ - migrationId, - queryParams: { is_partially_translated: false }, - }); - expect(response.body.total).toEqual(6); - }); - - it('should fetch rules filtered by `untranslatable`', async () => { - // create a document - const migrationId = uuidv4(); - - const overrideCallback = (index: number): Partial => { - const translationResult = - index < 3 - ? RuleTranslationResult.FULL - : index < 5 - ? RuleTranslationResult.PARTIAL - : RuleTranslationResult.UNTRANSLATABLE; - return { - migration_id: migrationId, - translation_result: translationResult, - }; - }; - - const migrationRuleDocuments = getMigrationRuleDocuments(10, overrideCallback); - await createMigrationRules(es, migrationRuleDocuments); - - // fetch failed migration rules - let response = await migrationRulesRoutes.get({ - migrationId, - queryParams: { is_untranslatable: true }, - }); - expect(response.body.total).toEqual(5); - // fetch non-failed migration rules - response = await migrationRulesRoutes.get({ - migrationId, - queryParams: { is_untranslatable: false }, - }); - expect(response.body.total).toEqual(5); - }); - }); - - describe('Sorting', () => { - it('should fetch rules sorted by `title`', async () => { - const titles = ['Elastic 1', 'Windows', 'Linux', 'Elastic 2']; - - // create a document - const migrationId = uuidv4(); - - const overrideCallback = (index: number): Partial => { - const title = titles[index]; - const originalRule = { ...defaultOriginalRule, title }; - const elasticRule = { ...defaultElasticRule, title }; - return { - migration_id: migrationId, - original_rule: originalRule, - elastic_rule: elasticRule, - }; - }; - const migrationRuleDocuments = getMigrationRuleDocuments(titles.length, overrideCallback); - await createMigrationRules(es, migrationRuleDocuments); - - // fetch migration rules - let response = await migrationRulesRoutes.get({ - migrationId, - queryParams: { sort_field: 'elastic_rule.title', sort_direction: 'asc' }, - }); - expect(response.body.data.map((rule) => rule.elastic_rule?.title)).toEqual(titles.sort()); - - // fetch migration rules - response = await migrationRulesRoutes.get({ - migrationId, - queryParams: { sort_field: 'elastic_rule.title', sort_direction: 'desc' }, - }); - expect(response.body.data.map((rule) => rule.elastic_rule?.title)).toEqual( - titles.sort().reverse() - ); - }); - - it('should fetch rules sorted by `severity`', async () => { - const severities = ['critical', 'low', 'medium', 'low', 'critical']; - - // create a document - const migrationId = uuidv4(); - - const overrideCallback = (index: number): Partial => { - const severity = severities[index]; - const elasticRule = { ...defaultElasticRule, severity }; - return { - migration_id: migrationId, - elastic_rule: elasticRule, - }; - }; - const migrationRuleDocuments = getMigrationRuleDocuments( - severities.length, - overrideCallback - ); - await createMigrationRules(es, migrationRuleDocuments); - - // fetch migration rules - let response = await migrationRulesRoutes.get({ - migrationId, - queryParams: { sort_field: 'elastic_rule.severity', sort_direction: 'asc' }, - }); - expect(response.body.data.map((rule) => rule.elastic_rule?.severity)).toEqual([ - 'low', - 'low', - 'medium', - 'critical', - 'critical', - ]); - - // fetch migration rules - response = await migrationRulesRoutes.get({ - migrationId, - queryParams: { sort_field: 'elastic_rule.severity', sort_direction: 'desc' }, - }); - expect(response.body.data.map((rule) => rule.elastic_rule?.severity)).toEqual([ - 'critical', - 'critical', - 'medium', - 'low', - 'low', - ]); - }); - - it('should fetch rules sorted by `risk_score`', async () => { - const riskScores = [55, 0, 100, 23]; - - // create a document - const migrationId = uuidv4(); - - const overrideCallback = (index: number): Partial => { - const riskScore = riskScores[index]; - const elasticRule = { ...defaultElasticRule, risk_score: riskScore }; - return { - migration_id: migrationId, - elastic_rule: elasticRule, - }; - }; - const migrationRuleDocuments = getMigrationRuleDocuments( - riskScores.length, - overrideCallback - ); - await createMigrationRules(es, migrationRuleDocuments); - - // fetch migration rules - let response = await migrationRulesRoutes.get({ - migrationId, - queryParams: { sort_field: 'elastic_rule.risk_score', sort_direction: 'asc' }, - }); - expect(response.body.data.map((rule) => rule.elastic_rule?.risk_score)).toEqual( - riskScores.sort((a, b) => { - return a - b; - }) - ); - - // fetch migration rules - response = await migrationRulesRoutes.get({ - migrationId, - queryParams: { sort_field: 'elastic_rule.risk_score', sort_direction: 'desc' }, - }); - expect(response.body.data.map((rule) => rule.elastic_rule?.risk_score)).toEqual( - riskScores - .sort((a, b) => { - return a - b; - }) - .reverse() - ); - }); - - it('should fetch rules sorted by `prebuilt_rule_id`', async () => { - const prebuiltRuleIds = ['rule-1', undefined, undefined, 'rule-2', undefined]; - - // create a document - const migrationId = uuidv4(); - - const overrideCallback = (index: number): Partial => { - const prebuiltRuleId = prebuiltRuleIds[index]; - const elasticRule = { ...defaultElasticRule, prebuilt_rule_id: prebuiltRuleId }; - return { - migration_id: migrationId, - elastic_rule: elasticRule, - }; - }; - const migrationRuleDocuments = getMigrationRuleDocuments( - prebuiltRuleIds.length, - overrideCallback - ); - await createMigrationRules(es, migrationRuleDocuments); - - // fetch migration rules - let response = await migrationRulesRoutes.get({ - migrationId, - queryParams: { sort_field: 'elastic_rule.prebuilt_rule_id', sort_direction: 'asc' }, - }); - expect(response.body.data.map((rule) => rule.elastic_rule?.prebuilt_rule_id)).toEqual([ - undefined, - undefined, - undefined, - 'rule-1', - 'rule-2', - ]); - - // fetch migration rules - response = await migrationRulesRoutes.get({ - migrationId, - queryParams: { sort_field: 'elastic_rule.prebuilt_rule_id', sort_direction: 'desc' }, - }); - expect(response.body.data.map((rule) => rule.elastic_rule?.prebuilt_rule_id)).toEqual([ - 'rule-2', - 'rule-1', - undefined, - undefined, - undefined, - ]); - }); - - it('should fetch rules sorted by `translation_result`', async () => { - const translationResults = [ - RuleTranslationResult.UNTRANSLATABLE, - RuleTranslationResult.FULL, - RuleTranslationResult.PARTIAL, - ]; - - // create a document - const migrationId = uuidv4(); - - const overrideCallback = (index: number): Partial => { - return { - migration_id: migrationId, - translation_result: translationResults[index], - }; - }; - const migrationRuleDocuments = getMigrationRuleDocuments( - translationResults.length, - overrideCallback - ); - await createMigrationRules(es, migrationRuleDocuments); - - // fetch migration rules - let response = await migrationRulesRoutes.get({ - migrationId, - queryParams: { sort_field: 'translation_result', sort_direction: 'asc' }, - }); - expect(response.body.data.map((rule) => rule.translation_result)).toEqual([ - RuleTranslationResult.UNTRANSLATABLE, - RuleTranslationResult.PARTIAL, - RuleTranslationResult.FULL, - ]); - - // fetch migration rules - response = await migrationRulesRoutes.get({ - migrationId, - queryParams: { sort_field: 'translation_result', sort_direction: 'desc' }, - }); - expect(response.body.data.map((rule) => rule.translation_result)).toEqual([ - RuleTranslationResult.FULL, - RuleTranslationResult.PARTIAL, - RuleTranslationResult.UNTRANSLATABLE, - ]); - }); - - it('should fetch rules sorted by `updated_at`', async () => { - // create a document - const migrationId = uuidv4(); - - // Creating documents separately to have different `update_at` timestamps - await createMigrationRules(es, [getMigrationRuleDocument({ migration_id: migrationId })]); - await createMigrationRules(es, [getMigrationRuleDocument({ migration_id: migrationId })]); - await createMigrationRules(es, [getMigrationRuleDocument({ migration_id: migrationId })]); - await createMigrationRules(es, [getMigrationRuleDocument({ migration_id: migrationId })]); - await createMigrationRules(es, [getMigrationRuleDocument({ migration_id: migrationId })]); - - // fetch migration rules - let response = await migrationRulesRoutes.get({ - migrationId, - queryParams: { sort_field: 'updated_at', sort_direction: 'asc' }, - }); - const ascSorted = response.body.data.map((rule) => rule.updated_at); - - // fetch migration rules - response = await migrationRulesRoutes.get({ - migrationId, - queryParams: { sort_field: 'updated_at', sort_direction: 'desc' }, - }); - const descSorted = response.body.data.map((rule) => rule.updated_at); - - expect(ascSorted).toEqual(descSorted.reverse()); - }); + expect(migrationResponse.body.id).toBe(migrationId); }); - describe('Pagination', () => { - it('should fetch rules within specific page', async () => { - const titles = Array.from({ length: 50 }, (_, index) => `Migration rule - ${index}`); - - // create a document - const migrationId = uuidv4(); - - const overrideCallback = (index: number): Partial => { - const title = titles[index]; - const originalRule = { ...defaultOriginalRule, title }; - const elasticRule = { ...defaultElasticRule, title }; - return { - migration_id: migrationId, - original_rule: originalRule, - elastic_rule: elasticRule, - }; - }; - const migrationRuleDocuments = getMigrationRuleDocuments(titles.length, overrideCallback); - await createMigrationRules(es, migrationRuleDocuments); - - // fetch migration rules - const response = await migrationRulesRoutes.get({ - migrationId, - queryParams: { page: 3, per_page: 7 }, - }); - const start = 3 * 7; - expect(response.body.data.map((rule) => rule.elastic_rule?.title)).toEqual( - titles.slice(start, start + 7) - ); - }); - - it('should fetch rules within very first page if `perPage` is not specified', async () => { - const titles = Array.from({ length: 50 }, (_, index) => `Migration rule - ${index}`); - - // create a document - const migrationId = uuidv4(); - - const overrideCallback = (index: number): Partial => { - const title = titles[index]; - const originalRule = { ...defaultOriginalRule, title }; - const elasticRule = { ...defaultElasticRule, title }; - return { - migration_id: migrationId, - original_rule: originalRule, - elastic_rule: elasticRule, - }; - }; - const migrationRuleDocuments = getMigrationRuleDocuments(titles.length, overrideCallback); - await createMigrationRules(es, migrationRuleDocuments); - - // fetch migration rules - const response = await migrationRulesRoutes.get({ - migrationId, - queryParams: { page: 3 }, + describe('Error handling', () => { + it('should return 404 if migration ID does not exist', async () => { + const { body } = await ruleMigrationRoutes.get({ + migrationId: 'non-existing-migration-id', + expectStatusCode: 404, }); - const defaultSize = 10; - expect(response.body.data.map((rule) => rule.elastic_rule?.title)).toEqual( - titles.slice(0, defaultSize) - ); - }); - - it('should fetch rules within very first page of a specified size if `perPage` is specified', async () => { - const titles = Array.from({ length: 50 }, (_, index) => `Migration rule - ${index}`); - - // create a document - const migrationId = uuidv4(); - - const overrideCallback = (index: number): Partial => { - const title = titles[index]; - const originalRule = { ...defaultOriginalRule, title }; - const elasticRule = { ...defaultElasticRule, title }; - return { - migration_id: migrationId, - original_rule: originalRule, - elastic_rule: elasticRule, - }; - }; - const migrationRuleDocuments = getMigrationRuleDocuments(titles.length, overrideCallback); - await createMigrationRules(es, migrationRuleDocuments); - // fetch migration rules - const response = await migrationRulesRoutes.get({ - migrationId, - queryParams: { per_page: 18 }, + expect(body).toMatchObject({ + statusCode: 404, + error: 'Not Found', + message: 'No Migration found with id: non-existing-migration-id', }); - expect(response.body.data.map((rule) => rule.elastic_rule?.title)).toEqual( - titles.slice(0, 18) - ); }); }); }); diff --git a/x-pack/test/security_solution_api_integration/test_suites/siem_migrations/rules/trial_license_complete_tier/get_integrations.ts b/x-pack/test/security_solution_api_integration/test_suites/siem_migrations/rules/trial_license_complete_tier/get_integrations.ts index b1d5f91c7d369..9e7cc71f3afd1 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/siem_migrations/rules/trial_license_complete_tier/get_integrations.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/siem_migrations/rules/trial_license_complete_tier/get_integrations.ts @@ -7,11 +7,11 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../ftr_provider_context'; -import { migrationRulesRouteHelpersFactory } from '../../utils'; +import { ruleMigrationRouteHelpersFactory } from '../../utils'; export default ({ getService }: FtrProviderContext) => { const supertest = getService('supertest'); - const migrationRulesRoutes = migrationRulesRouteHelpersFactory(supertest); + const migrationRulesRoutes = ruleMigrationRouteHelpersFactory(supertest); describe('Get Integrations', () => { it('should return all integrations successfully', async () => { diff --git a/x-pack/test/security_solution_api_integration/test_suites/siem_migrations/rules/trial_license_complete_tier/get_prebuilt_rules.ts b/x-pack/test/security_solution_api_integration/test_suites/siem_migrations/rules/trial_license_complete_tier/get_prebuilt_rules.ts index aa73189eca4cd..bbc5ae468068f 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/siem_migrations/rules/trial_license_complete_tier/get_prebuilt_rules.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/siem_migrations/rules/trial_license_complete_tier/get_prebuilt_rules.ts @@ -8,14 +8,14 @@ import expect from 'expect'; import { v4 as uuidv4 } from 'uuid'; import { RuleTranslationResult } from '@kbn/security-solution-plugin/common/siem_migrations/constants'; +import { RuleMigrationRuleData } from '@kbn/security-solution-plugin/common/siem_migrations/model/rule_migration.gen'; import { deleteAllRules } from '../../../../../common/utils/security_solution'; import { - RuleMigrationDocument, createMigrationRules, defaultElasticRule, - deleteAllMigrationRules, + deleteAllRuleMigrations, getMigrationRuleDocuments, - migrationRulesRouteHelpersFactory, + ruleMigrationRouteHelpersFactory, } from '../../utils'; import { FtrProviderContext } from '../../../../ftr_provider_context'; import { @@ -29,14 +29,14 @@ export default ({ getService }: FtrProviderContext) => { const es = getService('es'); const log = getService('log'); const supertest = getService('supertest'); - const migrationRulesRoutes = migrationRulesRouteHelpersFactory(supertest); + const migrationRulesRoutes = ruleMigrationRouteHelpersFactory(supertest); describe('@ess @serverless @serverlessQA Get Prebuilt Rules API', () => { beforeEach(async () => { await deleteAllRules(supertest, log); await deleteAllTimelines(es, log); await deleteAllPrebuiltRuleAssets(es, log); - await deleteAllMigrationRules(es); + await deleteAllRuleMigrations(es); // Add some prebuilt rules const ruleAssetSavedObjects = [ @@ -52,7 +52,7 @@ export default ({ getService }: FtrProviderContext) => { it('should return all prebuilt rules matched by migration rules', async () => { const migrationId = uuidv4(); - const overrideCallback = (index: number): Partial => { + const overrideCallback = (index: number): Partial => { const { query_language: queryLanguage, query, ...rest } = defaultElasticRule; return { migration_id: migrationId, diff --git a/x-pack/test/security_solution_api_integration/test_suites/siem_migrations/rules/trial_license_complete_tier/index.ts b/x-pack/test/security_solution_api_integration/test_suites/siem_migrations/rules/trial_license_complete_tier/index.ts index 6085a1c14cfe2..b4731fb37142e 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/siem_migrations/rules/trial_license_complete_tier/index.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/siem_migrations/rules/trial_license_complete_tier/index.ts @@ -9,11 +9,14 @@ import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('@ess @serverless SecuritySolution SIEM Migrations', () => { loadTestFile(require.resolve('./create')); - loadTestFile(require.resolve('./get_prebuilt_rules')); loadTestFile(require.resolve('./get')); + loadTestFile(require.resolve('./delete')); + loadTestFile(require.resolve('./rules/create')); + loadTestFile(require.resolve('./rules/get')); + loadTestFile(require.resolve('./rules/update')); + loadTestFile(require.resolve('./get_prebuilt_rules')); loadTestFile(require.resolve('./install')); loadTestFile(require.resolve('./stats')); - loadTestFile(require.resolve('./update')); loadTestFile(require.resolve('./start')); loadTestFile(require.resolve('./stop')); loadTestFile(require.resolve('./get_integrations')); diff --git a/x-pack/test/security_solution_api_integration/test_suites/siem_migrations/rules/trial_license_complete_tier/install.ts b/x-pack/test/security_solution_api_integration/test_suites/siem_migrations/rules/trial_license_complete_tier/install.ts index 684db1939e279..2f6ee6ac9d064 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/siem_migrations/rules/trial_license_complete_tier/install.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/siem_migrations/rules/trial_license_complete_tier/install.ts @@ -7,17 +7,19 @@ import expect from 'expect'; import { v4 as uuidv4 } from 'uuid'; -import { ElasticRule } from '@kbn/security-solution-plugin/common/siem_migrations/model/rule_migration.gen'; +import { + ElasticRule, + RuleMigrationRuleData, +} from '@kbn/security-solution-plugin/common/siem_migrations/model/rule_migration.gen'; import { RuleTranslationResult } from '@kbn/security-solution-plugin/common/siem_migrations/constants'; import { RuleResponse } from '@kbn/security-solution-plugin/common/api/detection_engine'; import { deleteAllRules } from '../../../../../common/utils/security_solution'; import { - RuleMigrationDocument, createMigrationRules, defaultElasticRule, - deleteAllMigrationRules, + deleteAllRuleMigrations, getMigrationRuleDocuments, - migrationRulesRouteHelpersFactory, + ruleMigrationRouteHelpersFactory, statsOverrideCallbackFactory, } from '../../utils'; import { FtrProviderContext } from '../../../../ftr_provider_context'; @@ -33,20 +35,20 @@ export default ({ getService }: FtrProviderContext) => { const log = getService('log'); const supertest = getService('supertest'); const securitySolutionApi = getService('securitySolutionApi'); - const migrationRulesRoutes = migrationRulesRouteHelpersFactory(supertest); + const migrationRulesRoutes = ruleMigrationRouteHelpersFactory(supertest); describe('@ess @serverless @serverlessQA Install API', () => { beforeEach(async () => { await deleteAllRules(supertest, log); await deleteAllTimelines(es, log); await deleteAllPrebuiltRuleAssets(es, log); - await deleteAllMigrationRules(es); + await deleteAllRuleMigrations(es); }); it('should install all installable custom migration rules', async () => { const migrationId = uuidv4(); - const overrideCallback = (index: number): Partial => { + const overrideCallback = (index: number): Partial => { const title = `Rule - ${index}`; const elasticRule = { ...defaultElasticRule, title }; return { @@ -63,7 +65,7 @@ export default ({ getService }: FtrProviderContext) => { expect(installResponse.body).toEqual({ installed: 2 }); // fetch installed migration rules information - const response = await migrationRulesRoutes.get({ migrationId }); + const response = await migrationRulesRoutes.getRules({ migrationId }); const installedMigrationRules = response.body.data.reduce((acc, item) => { if (item.elastic_rule?.id) { acc.push(item.elastic_rule); @@ -100,7 +102,7 @@ export default ({ getService }: FtrProviderContext) => { const migrationId = uuidv4(); - const overrideCallback = (index: number): Partial => { + const overrideCallback = (index: number): Partial => { const { query_language: queryLanguage, query, ...rest } = defaultElasticRule; return { migration_id: migrationId, diff --git a/x-pack/test/security_solution_api_integration/test_suites/siem_migrations/rules/trial_license_complete_tier/rules/create.ts b/x-pack/test/security_solution_api_integration/test_suites/siem_migrations/rules/trial_license_complete_tier/rules/create.ts new file mode 100644 index 0000000000000..8caa3a19a1f11 --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/siem_migrations/rules/trial_license_complete_tier/rules/create.ts @@ -0,0 +1,210 @@ +/* + * 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 expect from 'expect'; +import { SiemMigrationStatus } from '@kbn/security-solution-plugin/common/siem_migrations/constants'; +import { + defaultOriginalRule, + deleteAllRuleMigrations, + ruleMigrationResourcesRouteHelpersFactory, + ruleMigrationRouteHelpersFactory, + splunkRuleWithResources, +} from '../../../utils'; +import { FtrProviderContext } from '../../../../../ftr_provider_context'; + +export default ({ getService }: FtrProviderContext) => { + const es = getService('es'); + const supertest = getService('supertest'); + const migrationRulesRoutes = ruleMigrationRouteHelpersFactory(supertest); + const migrationResourcesRoutes = ruleMigrationResourcesRouteHelpersFactory(supertest); + + describe('@ess @serverless @serverlessQA Create Rules API', () => { + let migrationId: string; + beforeEach(async () => { + await deleteAllRuleMigrations(es); + const response = await migrationRulesRoutes.create({}); + migrationId = response.body.migration_id; + }); + + describe('Happy path', () => { + it('should create migrations with provided id', async () => { + await migrationRulesRoutes.addRulesToMigration({ + migrationId, + payload: [defaultOriginalRule], + }); + + // fetch migration rule + const response = await migrationRulesRoutes.getRules({ migrationId }); + expect(response.body.total).toEqual(1); + + const migrationRule = response.body.data[0]; + expect(migrationRule).toEqual( + expect.objectContaining({ + migration_id: migrationId, + original_rule: defaultOriginalRule, + status: SiemMigrationStatus.PENDING, + }) + ); + }); + + it('should create migrations with the rules that have resources', async () => { + await migrationRulesRoutes.addRulesToMigration({ + migrationId, + payload: [splunkRuleWithResources], + }); + + // fetch migration rule + const response = await migrationRulesRoutes.getRules({ migrationId }); + expect(response.body.total).toEqual(1); + + const migrationRule = response.body.data[0]; + expect(migrationRule).toEqual( + expect.objectContaining({ + migration_id: migrationId, + original_rule: splunkRuleWithResources, + status: SiemMigrationStatus.PENDING, + }) + ); + + // fetch missing resources + const resourcesResponse = await migrationResourcesRoutes.getMissingResources({ + migrationId, + }); + expect(resourcesResponse.body).toEqual([ + { type: 'macro', name: 'summariesonly' }, + { type: 'macro', name: 'drop_dm_object_name(1)' }, + { type: 'lookup', name: 'malware_tracker' }, + ]); + }); + }); + + describe('Error handling', () => { + it('should return no content error', async () => { + await migrationRulesRoutes.addRulesToMigration({ + migrationId, + payload: [], + expectStatusCode: 204, + }); + + // fetch migration rule + const response = await migrationRulesRoutes.getRules({ migrationId }); + expect(response.body.total).toEqual(0); + }); + + it('should return 404 if invalid migration id is provided', async () => { + const { body } = await migrationRulesRoutes.addRulesToMigration({ + migrationId: 'non-existing-migration-id', + payload: [defaultOriginalRule], + expectStatusCode: 404, + }); + + expect(body).toMatchObject({ + statusCode: 404, + error: 'Not Found', + message: 'No Migration found with id: non-existing-migration-id', + }); + }); + + it(`should return an error when undefined payload has been passed`, async () => { + const response = await migrationRulesRoutes.addRulesToMigration({ + migrationId, + expectStatusCode: 400, + }); + + expect(response.body).toEqual({ + error: 'Bad Request', + message: '[request body]: Expected array, received null', + statusCode: 400, + }); + }); + + it('should return an error when original rule id is not specified', async () => { + const { id, ...restOfOriginalRule } = defaultOriginalRule; + const response = await migrationRulesRoutes.addRulesToMigration({ + migrationId, + payload: [restOfOriginalRule], + expectStatusCode: 400, + }); + expect(response.body).toEqual({ + statusCode: 400, + error: 'Bad Request', + message: '[request body]: 0.id: Required', + }); + }); + + it('should return an error when original rule vendor is not specified', async () => { + const { vendor, ...restOfOriginalRule } = defaultOriginalRule; + const response = await migrationRulesRoutes.addRulesToMigration({ + migrationId, + payload: [restOfOriginalRule], + expectStatusCode: 400, + }); + expect(response.body).toEqual({ + statusCode: 400, + error: 'Bad Request', + message: '[request body]: 0.vendor: Invalid literal value, expected "splunk"', + }); + }); + + it('should return an error when original rule title is not specified', async () => { + const { title, ...restOfOriginalRule } = defaultOriginalRule; + const response = await migrationRulesRoutes.addRulesToMigration({ + migrationId, + payload: [restOfOriginalRule], + expectStatusCode: 400, + }); + expect(response.body).toEqual({ + statusCode: 400, + error: 'Bad Request', + message: '[request body]: 0.title: Required', + }); + }); + + it('should return an error when original rule description is not specified', async () => { + const { description, ...restOfOriginalRule } = defaultOriginalRule; + const response = await migrationRulesRoutes.addRulesToMigration({ + migrationId, + payload: [restOfOriginalRule], + expectStatusCode: 400, + }); + expect(response.body).toEqual({ + statusCode: 400, + error: 'Bad Request', + message: '[request body]: 0.description: Required', + }); + }); + + it('should return an error when original rule query is not specified', async () => { + const { query, ...restOfOriginalRule } = defaultOriginalRule; + const response = await migrationRulesRoutes.addRulesToMigration({ + migrationId, + payload: [restOfOriginalRule], + expectStatusCode: 400, + }); + expect(response.body).toEqual({ + statusCode: 400, + error: 'Bad Request', + message: '[request body]: 0.query: Required', + }); + }); + + it('should return an error when original rule query_language is not specified', async () => { + const { query_language: _, ...restOfOriginalRule } = defaultOriginalRule; + const response = await migrationRulesRoutes.addRulesToMigration({ + migrationId, + payload: [restOfOriginalRule], + expectStatusCode: 400, + }); + expect(response.body).toEqual({ + statusCode: 400, + error: 'Bad Request', + message: '[request body]: 0.query_language: Required', + }); + }); + }); + }); +}; diff --git a/x-pack/test/security_solution_api_integration/test_suites/siem_migrations/rules/trial_license_complete_tier/rules/get.ts b/x-pack/test/security_solution_api_integration/test_suites/siem_migrations/rules/trial_license_complete_tier/rules/get.ts new file mode 100644 index 0000000000000..621469bffdc9e --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/siem_migrations/rules/trial_license_complete_tier/rules/get.ts @@ -0,0 +1,635 @@ +/* + * 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 expect from 'expect'; +import { v4 as uuidv4 } from 'uuid'; +import { + RuleTranslationResult, + SiemMigrationStatus, +} from '@kbn/security-solution-plugin/common/siem_migrations/constants'; +import { RuleMigrationRuleData } from '@kbn/security-solution-plugin/common/siem_migrations/model/rule_migration.gen'; +import { + createMigrationRules, + defaultElasticRule, + defaultOriginalRule, + deleteAllRuleMigrations, + getMigrationRuleDocument, + getMigrationRuleDocuments, + ruleMigrationRouteHelpersFactory, +} from '../../../utils'; +import { FtrProviderContext } from '../../../../../ftr_provider_context'; + +export default ({ getService }: FtrProviderContext) => { + const es = getService('es'); + const supertest = getService('supertest'); + const migrationRulesRoutes = ruleMigrationRouteHelpersFactory(supertest); + + describe('@ess @serverless @serverlessQA Get Rules API', () => { + beforeEach(async () => { + await deleteAllRuleMigrations(es); + }); + + describe('Basic', () => { + it('should fetch existing rules within specified migration', async () => { + const migrationId = uuidv4(); + const migrationRuleDocument = getMigrationRuleDocument({ migration_id: migrationId }); + await createMigrationRules(es, [migrationRuleDocument]); + + const { '@timestamp': timestamp, updated_at: updatedAt, ...rest } = migrationRuleDocument; + + // fetch migration rule + const response = await migrationRulesRoutes.getRules({ migrationId }); + expect(response.body.total).toEqual(1); + expect(response.body.data).toEqual(expect.arrayContaining([expect.objectContaining(rest)])); + }); + }); + + describe('Filtering', () => { + it('should fetch rules filtered by `searchTerm`', async () => { + const migrationId = uuidv4(); + const overrideCallback = (index: number): Partial => { + const title = `${index < 5 ? 'Elastic' : 'Splunk'} rule - ${index}`; + const originalRule = { ...defaultOriginalRule, title }; + const elasticRule = { ...defaultElasticRule, title }; + return { + migration_id: migrationId, + original_rule: originalRule, + elastic_rule: elasticRule, + }; + }; + const migrationRuleDocuments = getMigrationRuleDocuments(10, overrideCallback); + await createMigrationRules(es, migrationRuleDocuments); + + // Search by word `Elastic` + let expectedRuleDocuments = expect.arrayContaining( + migrationRuleDocuments + .slice(0, 5) + .map(({ '@timestamp': timestamp, updated_at: updatedAt, ...rest }) => + expect.objectContaining(rest) + ) + ); + + // fetch migration rules + let response = await migrationRulesRoutes.getRules({ + migrationId, + queryParams: { search_term: 'Elastic' }, + }); + expect(response.body.total).toEqual(5); + expect(response.body.data).toEqual(expectedRuleDocuments); + + // Search by word `Splunk` + expectedRuleDocuments = expect.arrayContaining( + migrationRuleDocuments + .slice(5) + .map(({ '@timestamp': timestamp, updated_at: updatedAt, ...rest }) => + expect.objectContaining(rest) + ) + ); + + // fetch migration rules + response = await migrationRulesRoutes.getRules({ + migrationId, + queryParams: { search_term: 'Splunk' }, + }); + expect(response.body.total).toEqual(5); + expect(response.body.data).toEqual(expectedRuleDocuments); + }); + + it('should fetch rules filtered by `ids`', async () => { + const migrationId = uuidv4(); + const migrationRuleDocuments = getMigrationRuleDocuments(10, () => ({ + migration_id: migrationId, + })); + const createdDocumentIds = await createMigrationRules(es, migrationRuleDocuments); + + const expectedIds = createdDocumentIds.slice(0, 3).sort(); + + // fetch migration rules by existing ids + let response = await migrationRulesRoutes.getRules({ + migrationId, + queryParams: { ids: expectedIds }, + }); + expect(response.body.total).toEqual(3); + expect(response.body.data.map(({ id }) => id).sort()).toEqual(expectedIds); + + // fetch migration rules by non-existing id + response = await migrationRulesRoutes.getRules({ + migrationId, + queryParams: { ids: [uuidv4()] }, + }); + expect(response.body.total).toEqual(0); + }); + + it('should fetch rules filtered by `prebuilt`', async () => { + const migrationId = uuidv4(); + const overrideCallback = (index: number): Partial => { + const prebuiltRuleId = index < 3 ? uuidv4() : undefined; + const elasticRule = { ...defaultElasticRule, prebuilt_rule_id: prebuiltRuleId }; + return { + migration_id: migrationId, + elastic_rule: elasticRule, + }; + }; + + const migrationRuleDocuments = getMigrationRuleDocuments(10, overrideCallback); + await createMigrationRules(es, migrationRuleDocuments); + + // fetch migration rules matched Elastic prebuilt rules + let response = await migrationRulesRoutes.getRules({ + migrationId, + queryParams: { is_prebuilt: true }, + }); + expect(response.body.total).toEqual(3); + + // fetch custom translated migration rules + response = await migrationRulesRoutes.getRules({ + migrationId, + queryParams: { is_prebuilt: false }, + }); + expect(response.body.total).toEqual(7); + }); + + it('should fetch rules filtered by `installed`', async () => { + const migrationId = uuidv4(); + const overrideCallback = (index: number): Partial => { + const installedRuleId = index < 2 ? uuidv4() : undefined; + const elasticRule = { ...defaultElasticRule, id: installedRuleId }; + return { + migration_id: migrationId, + elastic_rule: elasticRule, + }; + }; + + const migrationRuleDocuments = getMigrationRuleDocuments(10, overrideCallback); + await createMigrationRules(es, migrationRuleDocuments); + + // fetch installed migration rules + let response = await migrationRulesRoutes.getRules({ + migrationId, + queryParams: { is_installed: true }, + }); + expect(response.body.total).toEqual(2); + + // fetch non-installed migration rules + response = await migrationRulesRoutes.getRules({ + migrationId, + queryParams: { is_installed: false }, + }); + expect(response.body.total).toEqual(8); + }); + + it('should fetch rules filtered by `failed`', async () => { + const migrationId = uuidv4(); + const overrideCallback = (index: number): Partial => { + const status = index < 4 ? SiemMigrationStatus.FAILED : SiemMigrationStatus.COMPLETED; + return { + migration_id: migrationId, + status, + }; + }; + + const migrationRuleDocuments = getMigrationRuleDocuments(10, overrideCallback); + await createMigrationRules(es, migrationRuleDocuments); + + // fetch failed migration rules + let response = await migrationRulesRoutes.getRules({ + migrationId, + queryParams: { is_failed: true }, + }); + expect(response.body.total).toEqual(4); + + // fetch non-failed migration rules + response = await migrationRulesRoutes.getRules({ + migrationId, + queryParams: { is_failed: false }, + }); + expect(response.body.total).toEqual(6); + }); + + it('should fetch rules filtered by `fullyTranslated`', async () => { + const migrationId = uuidv4(); + const overrideCallback = (index: number): Partial => { + const translationResult = + index < 6 + ? RuleTranslationResult.FULL + : index < 8 + ? RuleTranslationResult.PARTIAL + : RuleTranslationResult.UNTRANSLATABLE; + return { + migration_id: migrationId, + translation_result: translationResult, + }; + }; + + const migrationRuleDocuments = getMigrationRuleDocuments(10, overrideCallback); + await createMigrationRules(es, migrationRuleDocuments); + + // fetch failed migration rules + let response = await migrationRulesRoutes.getRules({ + migrationId, + queryParams: { is_fully_translated: true }, + }); + expect(response.body.total).toEqual(6); + + // fetch non-failed migration rules + response = await migrationRulesRoutes.getRules({ + migrationId, + queryParams: { is_fully_translated: false }, + }); + expect(response.body.total).toEqual(4); + }); + + it('should fetch rules filtered by `partiallyTranslated`', async () => { + const migrationId = uuidv4(); + const overrideCallback = (index: number): Partial => { + const translationResult = + index < 4 + ? RuleTranslationResult.FULL + : index < 8 + ? RuleTranslationResult.PARTIAL + : RuleTranslationResult.UNTRANSLATABLE; + return { + migration_id: migrationId, + translation_result: translationResult, + }; + }; + + const migrationRuleDocuments = getMigrationRuleDocuments(10, overrideCallback); + await createMigrationRules(es, migrationRuleDocuments); + + // fetch failed migration rules + let response = await migrationRulesRoutes.getRules({ + migrationId, + queryParams: { is_partially_translated: true }, + }); + expect(response.body.total).toEqual(4); + + // fetch non-failed migration rules + response = await migrationRulesRoutes.getRules({ + migrationId, + queryParams: { is_partially_translated: false }, + }); + expect(response.body.total).toEqual(6); + }); + + it('should fetch rules filtered by `untranslatable`', async () => { + const migrationId = uuidv4(); + const overrideCallback = (index: number): Partial => { + const translationResult = + index < 3 + ? RuleTranslationResult.FULL + : index < 5 + ? RuleTranslationResult.PARTIAL + : RuleTranslationResult.UNTRANSLATABLE; + return { + migration_id: migrationId, + translation_result: translationResult, + }; + }; + + const migrationRuleDocuments = getMigrationRuleDocuments(10, overrideCallback); + await createMigrationRules(es, migrationRuleDocuments); + + // fetch failed migration rules + let response = await migrationRulesRoutes.getRules({ + migrationId, + queryParams: { is_untranslatable: true }, + }); + expect(response.body.total).toEqual(5); + + // fetch non-failed migration rules + response = await migrationRulesRoutes.getRules({ + migrationId, + queryParams: { is_untranslatable: false }, + }); + expect(response.body.total).toEqual(5); + }); + }); + + describe('Sorting', () => { + it('should fetch rules sorted by `title`', async () => { + const migrationId = uuidv4(); + const titles = ['Elastic 1', 'Windows', 'Linux', 'Elastic 2']; + + const overrideCallback = (index: number): Partial => { + const title = titles[index]; + const originalRule = { ...defaultOriginalRule, title }; + const elasticRule = { ...defaultElasticRule, title }; + return { + migration_id: migrationId, + original_rule: originalRule, + elastic_rule: elasticRule, + }; + }; + const migrationRuleDocuments = getMigrationRuleDocuments(titles.length, overrideCallback); + await createMigrationRules(es, migrationRuleDocuments); + + // fetch migration rules + let response = await migrationRulesRoutes.getRules({ + migrationId, + queryParams: { sort_field: 'elastic_rule.title', sort_direction: 'asc' }, + }); + expect(response.body.data.map((rule) => rule.elastic_rule?.title)).toEqual(titles.sort()); + + // fetch migration rules + response = await migrationRulesRoutes.getRules({ + migrationId, + queryParams: { sort_field: 'elastic_rule.title', sort_direction: 'desc' }, + }); + expect(response.body.data.map((rule) => rule.elastic_rule?.title)).toEqual( + titles.sort().reverse() + ); + }); + + it('should fetch rules sorted by `severity`', async () => { + const migrationId = uuidv4(); + const severities = ['critical', 'low', 'medium', 'low', 'critical']; + + const overrideCallback = (index: number): Partial => { + const severity = severities[index]; + const elasticRule = { ...defaultElasticRule, severity }; + return { + migration_id: migrationId, + elastic_rule: elasticRule, + }; + }; + const migrationRuleDocuments = getMigrationRuleDocuments( + severities.length, + overrideCallback + ); + await createMigrationRules(es, migrationRuleDocuments); + + // fetch migration rules + let response = await migrationRulesRoutes.getRules({ + migrationId, + queryParams: { sort_field: 'elastic_rule.severity', sort_direction: 'asc' }, + }); + expect(response.body.data.map((rule) => rule.elastic_rule?.severity)).toEqual([ + 'low', + 'low', + 'medium', + 'critical', + 'critical', + ]); + + // fetch migration rules + response = await migrationRulesRoutes.getRules({ + migrationId, + queryParams: { sort_field: 'elastic_rule.severity', sort_direction: 'desc' }, + }); + expect(response.body.data.map((rule) => rule.elastic_rule?.severity)).toEqual([ + 'critical', + 'critical', + 'medium', + 'low', + 'low', + ]); + }); + + it('should fetch rules sorted by `risk_score`', async () => { + const migrationId = uuidv4(); + const riskScores = [55, 0, 100, 23]; + + const overrideCallback = (index: number): Partial => { + const riskScore = riskScores[index]; + const elasticRule = { ...defaultElasticRule, risk_score: riskScore }; + return { + migration_id: migrationId, + elastic_rule: elasticRule, + }; + }; + const migrationRuleDocuments = getMigrationRuleDocuments( + riskScores.length, + overrideCallback + ); + await createMigrationRules(es, migrationRuleDocuments); + + // fetch migration rules + let response = await migrationRulesRoutes.getRules({ + migrationId, + queryParams: { sort_field: 'elastic_rule.risk_score', sort_direction: 'asc' }, + }); + expect(response.body.data.map((rule) => rule.elastic_rule?.risk_score)).toEqual( + riskScores.sort((a, b) => { + return a - b; + }) + ); + + // fetch migration rules + response = await migrationRulesRoutes.getRules({ + migrationId, + queryParams: { sort_field: 'elastic_rule.risk_score', sort_direction: 'desc' }, + }); + expect(response.body.data.map((rule) => rule.elastic_rule?.risk_score)).toEqual( + riskScores + .sort((a, b) => { + return a - b; + }) + .reverse() + ); + }); + + it('should fetch rules sorted by `prebuilt_rule_id`', async () => { + const migrationId = uuidv4(); + const prebuiltRuleIds = ['rule-1', undefined, undefined, 'rule-2', undefined]; + + const overrideCallback = (index: number): Partial => { + const prebuiltRuleId = prebuiltRuleIds[index]; + const elasticRule = { ...defaultElasticRule, prebuilt_rule_id: prebuiltRuleId }; + return { + migration_id: migrationId, + elastic_rule: elasticRule, + }; + }; + const migrationRuleDocuments = getMigrationRuleDocuments( + prebuiltRuleIds.length, + overrideCallback + ); + await createMigrationRules(es, migrationRuleDocuments); + + // fetch migration rules + let response = await migrationRulesRoutes.getRules({ + migrationId, + queryParams: { sort_field: 'elastic_rule.prebuilt_rule_id', sort_direction: 'asc' }, + }); + expect(response.body.data.map((rule) => rule.elastic_rule?.prebuilt_rule_id)).toEqual([ + undefined, + undefined, + undefined, + 'rule-1', + 'rule-2', + ]); + + // fetch migration rules + response = await migrationRulesRoutes.getRules({ + migrationId, + queryParams: { sort_field: 'elastic_rule.prebuilt_rule_id', sort_direction: 'desc' }, + }); + expect(response.body.data.map((rule) => rule.elastic_rule?.prebuilt_rule_id)).toEqual([ + 'rule-2', + 'rule-1', + undefined, + undefined, + undefined, + ]); + }); + + it('should fetch rules sorted by `translation_result`', async () => { + const migrationId = uuidv4(); + const translationResults = [ + RuleTranslationResult.UNTRANSLATABLE, + RuleTranslationResult.FULL, + RuleTranslationResult.PARTIAL, + ]; + + const overrideCallback = (index: number): Partial => { + return { + migration_id: migrationId, + translation_result: translationResults[index], + }; + }; + const migrationRuleDocuments = getMigrationRuleDocuments( + translationResults.length, + overrideCallback + ); + await createMigrationRules(es, migrationRuleDocuments); + + // fetch migration rules + let response = await migrationRulesRoutes.getRules({ + migrationId, + queryParams: { sort_field: 'translation_result', sort_direction: 'asc' }, + }); + expect(response.body.data.map((rule) => rule.translation_result)).toEqual([ + RuleTranslationResult.UNTRANSLATABLE, + RuleTranslationResult.PARTIAL, + RuleTranslationResult.FULL, + ]); + + // fetch migration rules + response = await migrationRulesRoutes.getRules({ + migrationId, + queryParams: { sort_field: 'translation_result', sort_direction: 'desc' }, + }); + expect(response.body.data.map((rule) => rule.translation_result)).toEqual([ + RuleTranslationResult.FULL, + RuleTranslationResult.PARTIAL, + RuleTranslationResult.UNTRANSLATABLE, + ]); + }); + + it('should fetch rules sorted by `updated_at`', async () => { + // Creating documents separately to have different `update_at` timestamps + const migrationId = uuidv4(); + await createMigrationRules(es, [getMigrationRuleDocument({ migration_id: migrationId })]); + await createMigrationRules(es, [getMigrationRuleDocument({ migration_id: migrationId })]); + await createMigrationRules(es, [getMigrationRuleDocument({ migration_id: migrationId })]); + await createMigrationRules(es, [getMigrationRuleDocument({ migration_id: migrationId })]); + await createMigrationRules(es, [getMigrationRuleDocument({ migration_id: migrationId })]); + + // fetch migration rules + let response = await migrationRulesRoutes.getRules({ + migrationId, + queryParams: { sort_field: 'updated_at', sort_direction: 'asc' }, + }); + const ascSorted = response.body.data.map((rule) => rule.updated_at); + + // fetch migration rules + response = await migrationRulesRoutes.getRules({ + migrationId, + queryParams: { sort_field: 'updated_at', sort_direction: 'desc' }, + }); + const descSorted = response.body.data.map((rule) => rule.updated_at); + + expect(ascSorted).toEqual(descSorted.reverse()); + }); + }); + + describe('Pagination', () => { + it('should fetch rules within specific page', async () => { + const migrationId = uuidv4(); + const titles = Array.from({ length: 50 }, (_, index) => `Migration rule - ${index}`); + const overrideCallback = (index: number): Partial => { + const title = titles[index]; + const originalRule = { ...defaultOriginalRule, title }; + const elasticRule = { ...defaultElasticRule, title }; + return { + migration_id: migrationId, + original_rule: originalRule, + elastic_rule: elasticRule, + }; + }; + const migrationRuleDocuments = getMigrationRuleDocuments(titles.length, overrideCallback); + await createMigrationRules(es, migrationRuleDocuments); + + // fetch migration rules + const response = await migrationRulesRoutes.getRules({ + migrationId, + queryParams: { page: 3, per_page: 7 }, + }); + const start = 3 * 7; + expect(response.body.data.map((rule) => rule.elastic_rule?.title)).toEqual( + titles.slice(start, start + 7) + ); + }); + + it('should fetch rules within very first page if `perPage` is not specified', async () => { + const migrationId = uuidv4(); + const titles = Array.from({ length: 50 }, (_, index) => `Migration rule - ${index}`); + + const overrideCallback = (index: number): Partial => { + const title = titles[index]; + const originalRule = { ...defaultOriginalRule, title }; + const elasticRule = { ...defaultElasticRule, title }; + return { + migration_id: migrationId, + original_rule: originalRule, + elastic_rule: elasticRule, + }; + }; + const migrationRuleDocuments = getMigrationRuleDocuments(titles.length, overrideCallback); + await createMigrationRules(es, migrationRuleDocuments); + + // fetch migration rules + const response = await migrationRulesRoutes.getRules({ + migrationId, + queryParams: { page: 3 }, + }); + const defaultSize = 10; + expect(response.body.data.map((rule) => rule.elastic_rule?.title)).toEqual( + titles.slice(0, defaultSize) + ); + }); + + it('should fetch rules within very first page of a specified size if `perPage` is specified', async () => { + const migrationId = uuidv4(); + const titles = Array.from({ length: 50 }, (_, index) => `Migration rule - ${index}`); + + const overrideCallback = (index: number): Partial => { + const title = titles[index]; + const originalRule = { ...defaultOriginalRule, title }; + const elasticRule = { ...defaultElasticRule, title }; + return { + migration_id: migrationId, + original_rule: originalRule, + elastic_rule: elasticRule, + }; + }; + const migrationRuleDocuments = getMigrationRuleDocuments(titles.length, overrideCallback); + await createMigrationRules(es, migrationRuleDocuments); + + // fetch migration rules + const response = await migrationRulesRoutes.getRules({ + migrationId, + queryParams: { per_page: 18 }, + }); + expect(response.body.data.map((rule) => rule.elastic_rule?.title)).toEqual( + titles.slice(0, 18) + ); + }); + }); + }); +}; diff --git a/x-pack/test/security_solution_api_integration/test_suites/siem_migrations/rules/trial_license_complete_tier/update.ts b/x-pack/test/security_solution_api_integration/test_suites/siem_migrations/rules/trial_license_complete_tier/rules/update.ts similarity index 82% rename from x-pack/test/security_solution_api_integration/test_suites/siem_migrations/rules/trial_license_complete_tier/update.ts rename to x-pack/test/security_solution_api_integration/test_suites/siem_migrations/rules/trial_license_complete_tier/rules/update.ts index f114af2de519a..d639e47d787e9 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/siem_migrations/rules/trial_license_complete_tier/update.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/siem_migrations/rules/trial_license_complete_tier/rules/update.ts @@ -9,20 +9,20 @@ import expect from 'expect'; import { v4 as uuidv4 } from 'uuid'; import { createMigrationRules, - deleteAllMigrationRules, + deleteAllRuleMigrations, getMigrationRuleDocument, - migrationRulesRouteHelpersFactory, -} from '../../utils'; -import { FtrProviderContext } from '../../../../ftr_provider_context'; + ruleMigrationRouteHelpersFactory, +} from '../../../utils'; +import { FtrProviderContext } from '../../../../../ftr_provider_context'; export default ({ getService }: FtrProviderContext) => { const es = getService('es'); const supertest = getService('supertest'); - const migrationRulesRoutes = migrationRulesRouteHelpersFactory(supertest); + const ruleMigrationRoutes = ruleMigrationRouteHelpersFactory(supertest); - describe('@ess @serverless @serverlessQA Update API', () => { + describe('@ess @serverless @serverlessQA Update Rules API', () => { beforeEach(async () => { - await deleteAllMigrationRules(es); + await deleteAllRuleMigrations(es); }); describe('Happy path', () => { @@ -32,7 +32,10 @@ export default ({ getService }: FtrProviderContext) => { const [createdDocumentId] = await createMigrationRules(es, [migrationRuleDocument]); const now = new Date().toISOString(); - await migrationRulesRoutes.update({ + + const { + body: { updated }, + } = await ruleMigrationRoutes.updateRules({ migrationId, payload: [ { @@ -43,8 +46,10 @@ export default ({ getService }: FtrProviderContext) => { ], }); + expect(updated).toBe(true); + // fetch migration rule - const response = await migrationRulesRoutes.get({ migrationId }); + const response = await ruleMigrationRoutes.getRules({ migrationId }); expect(response.body.total).toEqual(1); const { @@ -71,7 +76,7 @@ export default ({ getService }: FtrProviderContext) => { const [createdDocumentId] = await createMigrationRules(es, [migrationRuleDocument]); const now = new Date().toISOString(); - await migrationRulesRoutes.update({ + await ruleMigrationRoutes.updateRules({ migrationId, payload: [ { @@ -101,7 +106,7 @@ export default ({ getService }: FtrProviderContext) => { }); // fetch migration rule - const response = await migrationRulesRoutes.get({ migrationId }); + const response = await ruleMigrationRoutes.getRules({ migrationId }); expect(response.body.total).toEqual(1); const migrationRule = response.body.data[0]; @@ -112,7 +117,9 @@ export default ({ getService }: FtrProviderContext) => { describe('Error handling', () => { it('should return empty content response when no rules passed', async () => { const migrationId = uuidv4(); - await migrationRulesRoutes.update({ + const migrationRuleDocument = getMigrationRuleDocument({ migration_id: migrationId }); + await createMigrationRules(es, [migrationRuleDocument]); + await ruleMigrationRoutes.updateRules({ migrationId, payload: [], expectStatusCode: 204, @@ -123,8 +130,7 @@ export default ({ getService }: FtrProviderContext) => { const migrationId = uuidv4(); const migrationRuleDocument = getMigrationRuleDocument({ migration_id: migrationId }); await createMigrationRules(es, [migrationRuleDocument]); - - const response = await migrationRulesRoutes.update({ + const response = await ruleMigrationRoutes.updateRules({ migrationId, payload: [{ elastic_rule: { title: 'Updated title' } }], expectStatusCode: 400, @@ -140,8 +146,7 @@ export default ({ getService }: FtrProviderContext) => { const migrationId = uuidv4(); const migrationRuleDocument = getMigrationRuleDocument({ migration_id: migrationId }); await createMigrationRules(es, [migrationRuleDocument]); - - const response = await migrationRulesRoutes.update({ + const response = await ruleMigrationRoutes.updateRules({ migrationId, expectStatusCode: 400, }); diff --git a/x-pack/test/security_solution_api_integration/test_suites/siem_migrations/rules/trial_license_complete_tier/start.ts b/x-pack/test/security_solution_api_integration/test_suites/siem_migrations/rules/trial_license_complete_tier/start.ts index 333096f1e900d..ec0b4603bbb29 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/siem_migrations/rules/trial_license_complete_tier/start.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/siem_migrations/rules/trial_license_complete_tier/start.ts @@ -4,24 +4,24 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { v4 as uuidv4 } from 'uuid'; import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../ftr_provider_context'; import { SiemMigrationsAPIErrorResponse, defaultOriginalRule, - migrationRulesRouteHelpersFactory, + ruleMigrationRouteHelpersFactory, } from '../../utils'; export default ({ getService }: FtrProviderContext) => { const supertest = getService('supertest'); - const migrationRulesRoutes = migrationRulesRouteHelpersFactory(supertest); + const migrationRulesRoutes = ruleMigrationRouteHelpersFactory(supertest); describe('Start Migration', () => { let migrationId: string; beforeEach(async () => { - migrationId = uuidv4(); - await migrationRulesRoutes.create({ + const createMigrationRespose = await migrationRulesRoutes.create({}); + migrationId = createMigrationRespose.body.migration_id; + await migrationRulesRoutes.addRulesToMigration({ migrationId, payload: [defaultOriginalRule], }); @@ -87,7 +87,7 @@ export default ({ getService }: FtrProviderContext) => { describe('error scenarios', () => { it('should reject if connector_id is incorrect', async () => { const response = await migrationRulesRoutes.start({ - migrationId: 'invalid_migration_id', + migrationId, expectStatusCode: 400, payload: { connector_id: 'preconfigured_bedrock', @@ -122,6 +122,20 @@ export default ({ getService }: FtrProviderContext) => { }, }); }); + + it('should reject with 404 if migrationId is not provided', async () => { + // @ts-expect-error + const response = await migrationRulesRoutes.start({ + expectStatusCode: 404, + payload: { + connector_id: 'preconfigured-bedrock', + }, + }); + + expect((response.body as unknown as SiemMigrationsAPIErrorResponse).message).to.eql( + 'No Migration found with id: undefined' + ); + }); }); }); }; diff --git a/x-pack/test/security_solution_api_integration/test_suites/siem_migrations/rules/trial_license_complete_tier/stats.ts b/x-pack/test/security_solution_api_integration/test_suites/siem_migrations/rules/trial_license_complete_tier/stats.ts index 42c6d4f097150..1bdffa043961d 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/siem_migrations/rules/trial_license_complete_tier/stats.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/siem_migrations/rules/trial_license_complete_tier/stats.ts @@ -9,9 +9,9 @@ import expect from 'expect'; import { v4 as uuidv4 } from 'uuid'; import { createMigrationRules, - deleteAllMigrationRules, + deleteAllRuleMigrations, getMigrationRuleDocuments, - migrationRulesRouteHelpersFactory, + ruleMigrationRouteHelpersFactory, statsOverrideCallbackFactory, } from '../../utils'; import { FtrProviderContext } from '../../../../ftr_provider_context'; @@ -19,11 +19,11 @@ import { FtrProviderContext } from '../../../../ftr_provider_context'; export default ({ getService }: FtrProviderContext) => { const es = getService('es'); const supertest = getService('supertest'); - const migrationRulesRoutes = migrationRulesRouteHelpersFactory(supertest); + const migrationRulesRoutes = ruleMigrationRouteHelpersFactory(supertest); describe('@ess @serverless @serverlessQA Stats API', () => { beforeEach(async () => { - await deleteAllMigrationRules(es); + await deleteAllRuleMigrations(es); }); it('should return stats for the specific migration', async () => { @@ -62,7 +62,7 @@ export default ({ getService }: FtrProviderContext) => { ); }); - it('should return stats for the existing migrations', async () => { + it('should return stats for all existing migrations', async () => { const migrationId1 = uuidv4(); const migrationId2 = uuidv4(); @@ -141,5 +141,20 @@ export default ({ getService }: FtrProviderContext) => { }) ); }); + + describe('Error handling', () => { + it('should return 404 if migration ID does not exist', async () => { + const { body } = await migrationRulesRoutes.stats({ + migrationId: 'non-existing-migration-id', + expectStatusCode: 404, + }); + + expect(body).toMatchObject({ + statusCode: 404, + error: 'Not Found', + message: 'No Migration found with id: non-existing-migration-id', + }); + }); + }); }); }; diff --git a/x-pack/test/security_solution_api_integration/test_suites/siem_migrations/rules/trial_license_complete_tier/stop.ts b/x-pack/test/security_solution_api_integration/test_suites/siem_migrations/rules/trial_license_complete_tier/stop.ts index b513d819c0c6a..b4b3c0d6d0382 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/siem_migrations/rules/trial_license_complete_tier/stop.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/siem_migrations/rules/trial_license_complete_tier/stop.ts @@ -4,20 +4,20 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { v4 as uuidv4 } from 'uuid'; import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../ftr_provider_context'; -import { defaultOriginalRule, migrationRulesRouteHelpersFactory } from '../../utils'; +import { defaultOriginalRule, ruleMigrationRouteHelpersFactory } from '../../utils'; export default ({ getService }: FtrProviderContext) => { const supertest = getService('supertest'); - const migrationRulesRoutes = migrationRulesRouteHelpersFactory(supertest); + const migrationRulesRoutes = ruleMigrationRouteHelpersFactory(supertest); describe('Stop Migration', () => { let migrationId: string; beforeEach(async () => { - migrationId = uuidv4(); - await migrationRulesRoutes.create({ + const createMigrationRespose = await migrationRulesRoutes.create({}); + migrationId = createMigrationRespose.body.migration_id; + await migrationRulesRoutes.addRulesToMigration({ migrationId, payload: [defaultOriginalRule], }); diff --git a/x-pack/test/security_solution_api_integration/test_suites/siem_migrations/utils/es_queries.ts b/x-pack/test/security_solution_api_integration/test_suites/siem_migrations/utils/es_queries.ts new file mode 100644 index 0000000000000..1ee45ab2ac5fb --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/siem_migrations/utils/es_queries.ts @@ -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 { Client } from '@elastic/elasticsearch'; +import { INDEX_PATTERN as SIEM_MIGRATIONS_BASE_INDEX_PATTERN } from '@kbn/security-solution-plugin/server/lib/siem_migrations/rules/data/rule_migrations_data_service'; + +const MIGRATIONS_INDEX_PATTERN = `${SIEM_MIGRATIONS_BASE_INDEX_PATTERN}-migrations-default`; +const RULES_INDEX_PATTERN = `${SIEM_MIGRATIONS_BASE_INDEX_PATTERN}-rules-default`; +const RESOURCES_INDEX_PATTERN = `${SIEM_MIGRATIONS_BASE_INDEX_PATTERN}-resources-default`; + +export const getRuleMigrationFromES = async ({ + es, + migrationId, +}: { + es: Client; + migrationId: string; +}) => { + return await es.search({ + index: MIGRATIONS_INDEX_PATTERN, + query: { + terms: { + _id: [migrationId], + }, + }, + }); +}; + +export const getRulesPerMigrationFromES = async ({ + es, + migrationId, +}: { + es: Client; + migrationId: string; +}) => { + return await es.search({ + index: RULES_INDEX_PATTERN, + size: 10000, + query: { + term: { + migration_id: migrationId, + }, + }, + }); +}; + +export const getResoucesPerMigrationFromES = async ({ + es, + migrationId, +}: { + es: Client; + migrationId: string; +}) => { + return await es.search({ + index: RESOURCES_INDEX_PATTERN, + size: 10000, + query: { + term: { + migration_id: migrationId, + }, + }, + }); +}; diff --git a/x-pack/test/security_solution_api_integration/test_suites/siem_migrations/utils/mocks.ts b/x-pack/test/security_solution_api_integration/test_suites/siem_migrations/utils/mocks.ts index 184e19f8d0c9c..99464cf645fa0 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/siem_migrations/utils/mocks.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/siem_migrations/utils/mocks.ts @@ -14,14 +14,15 @@ import { import { ElasticRule, OriginalRule, - RuleMigration, + RuleMigrationRuleData, } from '@kbn/security-solution-plugin/common/siem_migrations/model/rule_migration.gen'; -import { INDEX_PATTERN as SIEM_MIGRATIONS_INDEX_PATTERN } from '@kbn/security-solution-plugin/server/lib/siem_migrations/rules/data/rule_migrations_data_service'; +import { INDEX_PATTERN as SIEM_MIGRATIONS_BASE_INDEX_PATTERN } from '@kbn/security-solution-plugin/server/lib/siem_migrations/rules/data/rule_migrations_data_service'; import { generateAssistantComment } from '@kbn/security-solution-plugin/server/lib/siem_migrations/rules/task/util/comments'; -const SIEM_MIGRATIONS_RULES_INDEX_PATTERN = `${SIEM_MIGRATIONS_INDEX_PATTERN}-rules-default`; - -export type RuleMigrationDocument = Omit; +const SIEM_MIGRATIONS_INDEX_PATTERN = `${SIEM_MIGRATIONS_BASE_INDEX_PATTERN}-migrations-default`; +const SIEM_MIGRATIONS_RULES_INDEX_PATTERN = `${SIEM_MIGRATIONS_BASE_INDEX_PATTERN}-rules-default`; +const SIEM_MIGRATIONS_RESOURCES_INDEX_PATTERN = `${SIEM_MIGRATIONS_BASE_INDEX_PATTERN}-resources-default`; +const SOME_USER_ID = 'u_mGBROF_q5bmFCATbLXAcCwKa0k8JvONAwSruelyKA5E_0'; export const defaultOriginalRule: OriginalRule = { id: 'https://127.0.0.1:8089/servicesNS/nobody/SA-AccessProtection/saved/searches/Access%20-%20Default%20Account%20Usage%20-%20Rule', @@ -59,13 +60,13 @@ export const defaultElasticRule: ElasticRule = { title: 'Access - Default Account Usage - Rule', }; -const defaultMigrationRuleDocument: RuleMigrationDocument = { +const defaultMigrationRuleDocument: RuleMigrationRuleData = { '@timestamp': '2025-01-13T15:17:43.571Z', migration_id: '25a24356-3aab-401b-a73c-905cb8bf7a6d', original_rule: defaultOriginalRule, status: 'completed', - created_by: 'u_mGBROF_q5bmFCATbLXAcCwKa0k8JvONAwSruelyKA5E_0', - updated_by: 'u_mGBROF_q5bmFCATbLXAcCwKa0k8JvONAwSruelyKA5E_0', + created_by: SOME_USER_ID, + updated_by: SOME_USER_ID, updated_at: '2025-01-13T15:39:48.729Z', comments: [ generateAssistantComment( @@ -81,17 +82,17 @@ const defaultMigrationRuleDocument: RuleMigrationDocument = { }; export const getMigrationRuleDocument = ( - overrideParams: Partial -): RuleMigrationDocument => ({ + overrideParams: Partial +): RuleMigrationRuleData => ({ ...defaultMigrationRuleDocument, ...overrideParams, }); export const getMigrationRuleDocuments = ( count: number, - overrideCallback: (index: number) => Partial -): RuleMigrationDocument[] => { - const docs: RuleMigrationDocument[] = []; + overrideCallback: (index: number) => Partial +): RuleMigrationRuleData[] => { + const docs: RuleMigrationRuleData[] = []; for (let i = 0; i < count; i++) { const overrideParams = overrideCallback(i); docs.push(getMigrationRuleDocument(overrideParams)); @@ -116,7 +117,7 @@ export const statsOverrideCallbackFactory = ({ fullyTranslated?: number; partiallyTranslated?: number; }) => { - const overrideCallback = (index: number): Partial => { + const overrideCallback = (index: number): Partial => { let translationResult; let status = SiemMigrationStatus.PENDING; @@ -153,30 +154,52 @@ export const statsOverrideCallbackFactory = ({ export const createMigrationRules = async ( es: Client, - rules: RuleMigrationDocument[] + rules: RuleMigrationRuleData[] ): Promise => { const createdAt = new Date().toISOString(); + const addRuleOperations = rules.flatMap((ruleMigration) => [ + { create: { _index: SIEM_MIGRATIONS_RULES_INDEX_PATTERN } }, + { + ...ruleMigration, + '@timestamp': createdAt, + updated_at: createdAt, + }, + ]); + + const migrationIdsToBeCreated = new Set(rules.map((rule) => rule.migration_id)); + const createMigrationOperations = Array.from(migrationIdsToBeCreated).flatMap((migrationId) => [ + { create: { _index: SIEM_MIGRATIONS_INDEX_PATTERN, _id: migrationId } }, + { + created_by: SOME_USER_ID, + created_at: new Date().toISOString(), + }, + ]); + const res = await es.bulk({ refresh: 'wait_for', - operations: rules.flatMap((ruleMigration) => [ - { create: { _index: SIEM_MIGRATIONS_RULES_INDEX_PATTERN } }, - { - ...ruleMigration, - '@timestamp': createdAt, - updated_at: createdAt, - }, - ]), + operations: [...createMigrationOperations, ...addRuleOperations], }); + const ids = res.items.reduce((acc, item) => { - if (item.create?._id) { + if (item.create?._id && item.create._index === SIEM_MIGRATIONS_RULES_INDEX_PATTERN) { acc.push(item.create._id); } return acc; }, [] as string[]); + return ids; }; -export const deleteAllMigrationRules = async (es: Client): Promise => { +export const deleteAllRuleMigrations = async (es: Client): Promise => { + await es.deleteByQuery({ + index: [SIEM_MIGRATIONS_INDEX_PATTERN], + query: { + match_all: {}, + }, + ignore_unavailable: true, + refresh: true, + }); + await es.deleteByQuery({ index: [SIEM_MIGRATIONS_RULES_INDEX_PATTERN], body: { @@ -187,4 +210,87 @@ export const deleteAllMigrationRules = async (es: Client): Promise => { ignore_unavailable: true, refresh: true, }); + await es.deleteByQuery({ + index: [SIEM_MIGRATIONS_RESOURCES_INDEX_PATTERN], + query: { + match_all: {}, + }, + ignore_unavailable: true, + refresh: true, + }); +}; + +export const defaultMacroResource = { + type: 'macro', + name: 'host_event_count', + '@timestamp': '2025-05-21T15:23:15.505Z', + updated_by: SOME_USER_ID, + updated_at: '2025-05-21T15:23:15.505Z', + content: '`host_eventcount` | `daysago($lessThan$)` | `hoursago($greaterThan$,' > ')`', +}; + +export const defaultSplunkLookupResource = { + type: 'lookup', + name: 'splunk_lookup', + '@timestamp': '2025-05-21T15:23:15.505Z', + updated_by: SOME_USER_ID, + updated_at: '2025-05-21T15:23:15.505Z', +}; + +export const createMacrosForMigrationId = async ({ + es, + migrationId, + count, +}: { + es: Client; + migrationId: string; + count: number; +}) => { + const macros = []; + for (let i = 0; i < count; i++) { + macros.push({ + ...defaultMacroResource, + migration_id: migrationId, + name: `macro_${i}`, + }); + } + + const createMacroOperations = macros.flatMap((macro) => [ + { create: { _index: SIEM_MIGRATIONS_RESOURCES_INDEX_PATTERN } }, + macro, + ]); + + await es.bulk({ + refresh: 'wait_for', + operations: [...createMacroOperations], + }); +}; + +export const createLookupsForMigrationId = async ({ + es, + migrationId, + count, +}: { + es: Client; + migrationId: string; + count: number; +}) => { + const lookups = []; + for (let i = 0; i < count; i++) { + lookups.push({ + ...defaultSplunkLookupResource, + migration_id: migrationId, + name: `lookup_${i}`, + }); + } + + const createLookupOperations = lookups.flatMap((lookup) => [ + { create: { _index: SIEM_MIGRATIONS_RESOURCES_INDEX_PATTERN } }, + lookup, + ]); + + await es.bulk({ + refresh: 'wait_for', + operations: [...createLookupOperations], + }); }; diff --git a/x-pack/test/security_solution_api_integration/test_suites/siem_migrations/utils/resources.ts b/x-pack/test/security_solution_api_integration/test_suites/siem_migrations/utils/resources.ts index 3bfbb55c8cac1..725e0db58724d 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/siem_migrations/utils/resources.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/siem_migrations/utils/resources.ts @@ -24,7 +24,7 @@ export interface GetRuleMigrationMissingResourcesParams { expectStatusCode?: number; } -export const migrationResourcesRouteHelpersFactory = (supertest: SuperTest.Agent) => { +export const ruleMigrationResourcesRouteHelpersFactory = (supertest: SuperTest.Agent) => { return { getMissingResources: async ({ migrationId, diff --git a/x-pack/test/security_solution_api_integration/test_suites/siem_migrations/utils/rules.ts b/x-pack/test/security_solution_api_integration/test_suites/siem_migrations/utils/rules.ts index 8e283effdfeda..c6e2ce3d5448f 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/siem_migrations/utils/rules.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/siem_migrations/utils/rules.ts @@ -23,20 +23,22 @@ import { SIEM_RULE_MIGRATION_TRANSLATION_STATS_PATH, SIEM_RULE_MIGRATION_STOP_PATH, SIEM_RULE_MIGRATIONS_INTEGRATIONS_PATH, + SIEM_RULE_MIGRATION_RULES_PATH, } from '@kbn/security-solution-plugin/common/siem_migrations/constants'; import { CreateRuleMigrationResponse, GetAllStatsRuleMigrationResponse, GetRuleMigrationIntegrationsResponse, GetRuleMigrationPrebuiltRulesResponse, - GetRuleMigrationRequestQuery, GetRuleMigrationResponse, + GetRuleMigrationRulesRequestQuery, + GetRuleMigrationRulesResponse, GetRuleMigrationStatsResponse, InstallMigrationRulesResponse, StartRuleMigrationRequestBody, StartRuleMigrationResponse, StopRuleMigrationResponse, - UpdateRuleMigrationResponse, + UpdateRuleMigrationRulesResponse, } from '@kbn/security-solution-plugin/common/siem_migrations/model/api/rules/rule_migration.gen'; import { API_VERSIONS } from '@kbn/security-solution-plugin/common/constants'; import { assertStatusCode } from './asserts'; @@ -57,15 +59,15 @@ export interface MigrationRequestParams extends RequestParams { migrationId: string; } -export interface GetRuleMigrationParams extends MigrationRequestParams { +export interface GetRuleMigrationRulesParams extends MigrationRequestParams { /** Optional query parameters */ - queryParams?: GetRuleMigrationRequestQuery; + queryParams?: GetRuleMigrationRulesRequestQuery; } -export interface CreateRuleMigrationParams extends RequestParams { +export interface CreateRuleMigrationRulesParams extends RequestParams { /** Optional `id` of migration to add the rules to. * The id is necessary only for batching the migration creation in multiple requests */ - migrationId?: string; + migrationId: string; /** Optional payload to send */ payload?: any; } @@ -84,15 +86,62 @@ export type StartMigrationRuleParams = MigrationRequestParams & { payload: StartRuleMigrationRequestBody; }; -export const migrationRulesRouteHelpersFactory = (supertest: SuperTest.Agent) => { +export const ruleMigrationRouteHelpersFactory = (supertest: SuperTest.Agent) => { return { + create: async ({ + expectStatusCode = 200, + }: RequestParams): Promise<{ body: CreateRuleMigrationResponse }> => { + const response = await supertest + .put(SIEM_RULE_MIGRATIONS_PATH) + .set('kbn-xsrf', 'true') + .set(ELASTIC_HTTP_VERSION_HEADER, API_VERSIONS.internal.v1) + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') + .send(); + + assertStatusCode(expectStatusCode, response); + + return response; + }, + get: async ({ migrationId, - queryParams = {}, expectStatusCode = 200, - }: GetRuleMigrationParams): Promise<{ body: GetRuleMigrationResponse }> => { + }: MigrationRequestParams): Promise<{ body: GetRuleMigrationResponse }> => { const response = await supertest .get(replaceParams(SIEM_RULE_MIGRATION_PATH, { migration_id: migrationId })) + .set('kbn-xsrf', 'true') + .set(ELASTIC_HTTP_VERSION_HEADER, API_VERSIONS.internal.v1) + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') + .send(); + + assertStatusCode(expectStatusCode, response); + + return response; + }, + + delete: async ({ + migrationId, + expectStatusCode = 200, + }: MigrationRequestParams): Promise<{ body: null }> => { + const response = await supertest + .delete(replaceParams(SIEM_RULE_MIGRATION_PATH, { migration_id: migrationId })) + .set('kbn-xsrf', 'true') + .set(ELASTIC_HTTP_VERSION_HEADER, API_VERSIONS.internal.v1) + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') + .send(); + + assertStatusCode(expectStatusCode, response); + + return response; + }, + + getRules: async ({ + migrationId, + queryParams = {}, + expectStatusCode = 200, + }: GetRuleMigrationRulesParams): Promise<{ body: GetRuleMigrationRulesResponse }> => { + const response = await supertest + .get(replaceParams(SIEM_RULE_MIGRATION_RULES_PATH, { migration_id: migrationId })) .query(queryParams) .set('kbn-xsrf', 'true') .set(ELASTIC_HTTP_VERSION_HEADER, API_VERSIONS.internal.v1) @@ -104,13 +153,14 @@ export const migrationRulesRouteHelpersFactory = (supertest: SuperTest.Agent) => return response; }, - create: async ({ + addRulesToMigration: async ({ migrationId, payload, expectStatusCode = 200, - }: CreateRuleMigrationParams): Promise<{ body: CreateRuleMigrationResponse }> => { + }: CreateRuleMigrationRulesParams): Promise<{ body: null }> => { + const route = replaceParams(SIEM_RULE_MIGRATION_RULES_PATH, { migration_id: migrationId }); const response = await supertest - .post(`${SIEM_RULE_MIGRATIONS_PATH}${migrationId ? `/${migrationId}` : ''}`) + .post(route) .set('kbn-xsrf', 'true') .set(ELASTIC_HTTP_VERSION_HEADER, API_VERSIONS.internal.v1) .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') @@ -121,13 +171,14 @@ export const migrationRulesRouteHelpersFactory = (supertest: SuperTest.Agent) => return response; }, - update: async ({ + updateRules: async ({ migrationId, payload, expectStatusCode = 200, - }: UpdateRulesParams): Promise<{ body: UpdateRuleMigrationResponse }> => { + }: UpdateRulesParams): Promise<{ body: UpdateRuleMigrationRulesResponse }> => { + const route = replaceParams(SIEM_RULE_MIGRATION_RULES_PATH, { migration_id: migrationId }); const response = await supertest - .put(replaceParams(SIEM_RULE_MIGRATION_PATH, { migration_id: migrationId })) + .patch(route) .set('kbn-xsrf', 'true') .set(ELASTIC_HTTP_VERSION_HEADER, API_VERSIONS.internal.v1) .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') @@ -228,7 +279,7 @@ export const migrationRulesRouteHelpersFactory = (supertest: SuperTest.Agent) => body: StartRuleMigrationResponse; }> => { const response = await supertest - .put( + .post( replaceParams(SIEM_RULE_MIGRATION_START_PATH, { migration_id: migrationId, }) @@ -250,7 +301,7 @@ export const migrationRulesRouteHelpersFactory = (supertest: SuperTest.Agent) => body: StopRuleMigrationResponse; }> => { const response = await supertest - .put( + .post( replaceParams(SIEM_RULE_MIGRATION_STOP_PATH, { migration_id: migrationId, }) diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/siem_migrations/rules/onboarding.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/siem_migrations/rules/onboarding.cy.ts new file mode 100644 index 0000000000000..36a6108d48e48 --- /dev/null +++ b/x-pack/test/security_solution_cypress/cypress/e2e/siem_migrations/rules/onboarding.cy.ts @@ -0,0 +1,140 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { + ONBOARDING_RULE_MIGRATIONS_LIST, + ONBOARDING_TRANSLATIONS_RESULT_TABLE, + RULE_MIGRATIONS_GROUP_PANEL, + RULE_MIGRATION_PROGRESS_BAR, +} from '../../../screens/siem_migrations'; +import { deleteConnectors } from '../../../tasks/api_calls/common'; +import { createBedrockConnector } from '../../../tasks/api_calls/connectors'; +import { cleanMigrationData } from '../../../tasks/api_calls/siem_migrations'; +import { login } from '../../../tasks/login'; +import { visit } from '../../../tasks/navigation'; +import { + openUploadRulesFlyout, + selectMigrationConnector, + startMigrationFromFlyout, + uploadRules, + toggleMigrateRulesCard, +} from '../../../tasks/siem_migrations'; +import { GET_STARTED_URL } from '../../../urls/navigation'; + +export const SPLUNK_TEST_RULES = [ + { + preview: false, + result: { + id: 'some_id1', + title: 'Alert with IP Method and URI Filters with Default Severity', + search: + 'source="testing_data.zip:*" clientip="198.35.1.75" method=POST uri_path="/cart/error.do"', + description: '', + 'alert.severity': '3', + }, + }, + { + preview: false, + result: { + id: 'some_id2', + title: 'New Alert with Index filter', + search: 'source="testing_data.zip:*" | search server="MacBookPro.fritz.box" index=main', + description: 'Tutorial data based on host name', + 'alert.severity': '5', + }, + }, + { + preview: false, + result: { + id: 'some_id3', + title: 'Sample Alert in Essentials', + search: 'source="testing_file.zip:*"', + description: '', + 'alert.severity': '3', + }, + }, + { + preview: false, + lastrow: true, + result: { + id: 'some_id4', + title: 'Tutorial data based on host name', + search: 'source="testing_file.zip:*" \n| search host=vendor_sales', + description: 'Tutorial data based on host name', + 'alert.severity': '5', + }, + }, +]; + +describe( + 'Rule Migrations - Basic Workflow', + { + tags: ['@ess', '@serverless', '@serverlessQA'], + }, + () => { + beforeEach(() => { + deleteConnectors(); + cy.task('esArchiverLoad', { + archiveName: 'siem_migrations/rules', + }); + + cy.task('esArchiverLoad', { + archiveName: 'siem_migrations/rule_migrations', + }); + login(Cypress.env('IS_SERVERLESS') ? 'admin' : undefined); + createBedrockConnector(); + visit(GET_STARTED_URL); + }); + + after(() => { + cy.task('esArchiverUnload', { + archiveName: 'siem_migrations/rules', + }); + + cy.task('esArchiverUnload', { + archiveName: 'siem_migrations/rule_migrations', + }); + }); + + context('First Migration', () => { + beforeEach(() => { + cleanMigrationData(); + }); + it('should be able to create migrations', () => { + selectMigrationConnector(); + openUploadRulesFlyout(); + uploadRules(SPLUNK_TEST_RULES); + startMigrationFromFlyout(); + cy.get(RULE_MIGRATIONS_GROUP_PANEL).within(() => { + cy.get(ONBOARDING_RULE_MIGRATIONS_LIST).should('have.length', 1); + cy.get(RULE_MIGRATION_PROGRESS_BAR).should('have.length', 1); + }); + }); + }); + + context('On Successful Translation', () => { + context('Migration Results', () => { + it('should be able to see the result of the completed migration', () => { + selectMigrationConnector(); + toggleMigrateRulesCard(); + cy.get(RULE_MIGRATIONS_GROUP_PANEL).within(() => { + cy.get(ONBOARDING_RULE_MIGRATIONS_LIST).should('have.length', 1); + cy.get(ONBOARDING_TRANSLATIONS_RESULT_TABLE.TRANSLATION_STATUS_COUNT('Failed')).should( + 'have.text', + 1 + ); + cy.get( + ONBOARDING_TRANSLATIONS_RESULT_TABLE.TRANSLATION_STATUS_COUNT('Partially translated') + ).should('have.text', 4); + cy.get( + ONBOARDING_TRANSLATIONS_RESULT_TABLE.TRANSLATION_STATUS_COUNT('Translated') + ).should('have.text', 1); + }); + }); + }); + }); + } +); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/siem_migrations/rules/translated_rules_page.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/siem_migrations/rules/translated_rules_page.cy.ts new file mode 100644 index 0000000000000..a44eda18ed117 --- /dev/null +++ b/x-pack/test/security_solution_cypress/cypress/e2e/siem_migrations/rules/translated_rules_page.cy.ts @@ -0,0 +1,88 @@ +/* + * 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 { + RULE_MIGRATION_PROGRESS_BAR, + RULE_MIGRATION_PROGRESS_BAR_TEXT, + TRANSLATED_RULE_EDIT_BTN, + TRANSLATED_RULE_QUERY_VIEWER, + TRANSLATED_RULE_RESULT_BADGE, + TRANSLATED_RULES_RESULT_TABLE, +} from '../../../screens/siem_migrations'; +import { deleteConnectors } from '../../../tasks/api_calls/common'; +import { createBedrockConnector } from '../../../tasks/api_calls/connectors'; +import { login } from '../../../tasks/login'; +import { visit } from '../../../tasks/navigation'; +import { + editTranslatedRuleByRow, + reprocessFailedRules, + saveUpdatedTranslatedRuleQuery, + selectMigrationConnector, + updateTranslatedRuleQuery, + navigateToTranslatedRulesPage, +} from '../../../tasks/siem_migrations'; +import { GET_STARTED_URL } from '../../../urls/navigation'; + +describe( + 'Rule Migrations - Translated Rules Page', + { + tags: ['@ess', '@serverless', '@serverlessQA'], + }, + () => { + beforeEach(() => { + deleteConnectors(); + cy.task('esArchiverLoad', { + archiveName: 'siem_migrations/rules', + }); + + cy.task('esArchiverLoad', { + archiveName: 'siem_migrations/rule_migrations', + }); + + createBedrockConnector(); + login(Cypress.env('IS_SERVERLESS') ? 'admin' : undefined); + visit(GET_STARTED_URL); + selectMigrationConnector(); + navigateToTranslatedRulesPage(); + }); + + after(() => { + cy.task('esArchiverUnload', { + archiveName: 'siem_migrations/rules', + }); + + cy.task('esArchiverUnload', { + archiveName: 'siem_migrations/rule_migrations', + }); + }); + it('should be able to see the result of the completed migration', () => { + cy.get(TRANSLATED_RULES_RESULT_TABLE.ROWS).should('have.length', 6); + cy.get(TRANSLATED_RULES_RESULT_TABLE.STATUS('partial')).should('have.length', 4); + cy.get(TRANSLATED_RULES_RESULT_TABLE.STATUS('full')).should('have.length', 1); + cy.get(TRANSLATED_RULES_RESULT_TABLE.STATUS('failed')).should('have.length', 1); + }); + + it('should be able to edit a rule with partial translation', () => { + cy.get(TRANSLATED_RULES_RESULT_TABLE.TABLE).should('be.visible'); + + editTranslatedRuleByRow(1); + const newESQLQuery = 'FROM auditbeat-* metadata _id, _version, _index'; + updateTranslatedRuleQuery(newESQLQuery); + saveUpdatedTranslatedRuleQuery(); + + cy.get(TRANSLATED_RULE_EDIT_BTN).should('be.visible'); + cy.get(TRANSLATED_RULE_QUERY_VIEWER).should('contain.text', newESQLQuery); + cy.get(TRANSLATED_RULE_RESULT_BADGE).should('have.text', 'Translated'); + }); + + it('should be able to reprocess a failed Rule', () => { + reprocessFailedRules(); + cy.get(RULE_MIGRATION_PROGRESS_BAR).should('be.visible'); + cy.get(RULE_MIGRATION_PROGRESS_BAR_TEXT).should('contain.text', '83%'); + }); + } +); diff --git a/x-pack/test/security_solution_cypress/cypress/screens/security_header.ts b/x-pack/test/security_solution_cypress/cypress/screens/security_header.ts index 293c4992e0750..c5b61f065eda0 100644 --- a/x-pack/test/security_solution_cypress/cypress/screens/security_header.ts +++ b/x-pack/test/security_solution_cypress/cypress/screens/security_header.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { getDataTestSubjectSelector } from '../helpers/common'; import { GLOBAL_KQL_WRAPPER } from './search_bar'; // main links @@ -78,6 +79,11 @@ export const LOADING_INDICATOR_HIDDEN = '[data-test-subj="globalLoadingIndicator export const KIBANA_LOADING_ICON = '[data-test-subj="kbnLoadingMessage"]'; +// Siem Migrations +export const TRANSLATED_RULES_PAGE = getDataTestSubjectSelector( + 'solutionSideNavPanelLink-siem_migrations-rules' +); + // opens the navigation panel for a given nested link export const openNavigationPanelFor = (page: string) => { let panel; diff --git a/x-pack/test/security_solution_cypress/cypress/screens/serverless_security_header.ts b/x-pack/test/security_solution_cypress/cypress/screens/serverless_security_header.ts index bd99aa8bbd494..bcbf70ef779db 100644 --- a/x-pack/test/security_solution_cypress/cypress/screens/serverless_security_header.ts +++ b/x-pack/test/security_solution_cypress/cypress/screens/serverless_security_header.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { getDataTestSubjectSelector } from '../helpers/common'; + // main panels links export const DASHBOARDS = '[data-test-subj*="nav-item-deepLinkId-securitySolutionUI:dashboards"]'; export const DASHBOARDS_PANEL_BTN = @@ -86,6 +88,12 @@ export const EXCEPTIONS = '[data-test-subj~="panelNavItem-id-exceptions"]'; export const getBreadcrumb = (deepLinkId: string) => { return `breadcrumb-deepLinkId-${deepLinkId}`; }; + +// Siem Migrations +export const TRANSLATED_RULES_PAGE = getDataTestSubjectSelector( + 'panelNavItem panelNavItem-id-siem_migrations-rules' +); + // opens the navigation panel for a given nested link export const openNavigationPanelFor = (pageName: string) => { let panel; diff --git a/x-pack/test/security_solution_cypress/cypress/screens/siem_migrations.ts b/x-pack/test/security_solution_cypress/cypress/screens/siem_migrations.ts new file mode 100644 index 0000000000000..f4bfcb85bbc06 --- /dev/null +++ b/x-pack/test/security_solution_cypress/cypress/screens/siem_migrations.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 { getDataTestSubjectSelector, getDataTestSubjectSelectorStartWith } from '../helpers/common'; +import { bedrockConnectorAPIPayload } from '../tasks/api_calls/connectors'; + +export const ONBOARDING_SIEM_MIGRATION_TOPIC = getDataTestSubjectSelector('siem_migrations'); + +export const ONBOARDING_SIEM_MIGRATION_CARDS = { + AI_CONNECTORS: '#ai_connectors', + SELECT_CONNECTORS: getDataTestSubjectSelector('connector-selector'), + MIGRATE_RULES: '#migrate_rules', +}; + +export const UPLOAD_RULES_BTN = getDataTestSubjectSelector('startMigrationUploadRulesButton'); +export const UPLOAD_RULES_FLYOUT = getDataTestSubjectSelector('uploadRulesFlyout'); +export const UPLOAD_RULES_FILE_PICKER = getDataTestSubjectSelector('rulesFilePicker'); +export const UPLOAD_RULES_FILE_BTN = getDataTestSubjectSelector('uploadFileButton'); + +export const START_MIGRATION_FROM_FLYOUT_BTN = getDataTestSubjectSelector('startMigrationButton'); + +export const RULE_MIGRATIONS_GROUP_PANEL = getDataTestSubjectSelector('ruleMigrationPanelGroup'); +export const ONBOARDING_RULE_MIGRATIONS_LIST = getDataTestSubjectSelectorStartWith('migration-'); +export const ONBOARDING_TRANSLATIONS_RESULT_TABLE = { + TABLE: getDataTestSubjectSelector('translationsResults'), + TRANSLATION_STATUS: (status: string) => getDataTestSubjectSelector(`translationStatus-${status}`), + TRANSLATION_STATUS_COUNT: (status: string) => + getDataTestSubjectSelector(`translationStatusCount-${status}`), +}; + +export const TRANSLATED_RULES_RESULT_TABLE = { + TABLE: getDataTestSubjectSelector('rules-translation-table'), + ROWS: `${getDataTestSubjectSelectorStartWith('rules-translation-table')} .euiTableRow`, + STATUS: (status: string) => getDataTestSubjectSelector(`translationStatus-${status}`), + RULE_NAME: getDataTestSubjectSelector('ruleName'), +}; + +export const TRANSLATED_RULE_DETAILS_FLYOUT = getDataTestSubjectSelector( + 'ruleMigrationDetailsFlyout' +); + +export const TRANSLATED_RULE_QUERY_EDITOR_PARENT = `${getDataTestSubjectSelector( + 'kibanaCodeEditor' +)}`; + +export const TRANSLATED_RULE_QUERY_EDITOR_QUERY_CONTAINER = `${TRANSLATED_RULE_QUERY_EDITOR_PARENT} .view-lines`; + +export const TRANSLATED_RULE_QUERY_EDITOR_INPUT = `${TRANSLATED_RULE_QUERY_EDITOR_PARENT} textarea`; + +export const TRANSLATED_RULE_EDIT_BTN = getDataTestSubjectSelector('editTranslatedRuleBtn'); +export const TRANSLATED_RULE_SAVE_BTN = getDataTestSubjectSelector('saveTranslatedRuleBtn'); + +export const TRANSLATED_RULE_QUERY_VIEWER = getDataTestSubjectSelector('translatedRuleQueryViewer'); +export const TRANSLATED_RULE_RESULT_BADGE = getDataTestSubjectSelector('translationResultBadge'); + +export const RULE_MIGRATION_PROGRESS_BAR = getDataTestSubjectSelector('migrationProgressPanel'); +export const RULE_MIGRATION_PROGRESS_BAR_TEXT = `${RULE_MIGRATION_PROGRESS_BAR} .euiProgress__valueText`; + +export const REPROCESS_FAILED_RULES_BTN = getDataTestSubjectSelector('reprocessFailedRulesButton'); + +export const FAKE_BEDROCK_SELECTOR = getDataTestSubjectSelector( + `connector-${bedrockConnectorAPIPayload.name}` +); diff --git a/x-pack/test/security_solution_cypress/cypress/support/es_archiver.ts b/x-pack/test/security_solution_cypress/cypress/support/es_archiver.ts index 63ae27669c320..84bbfd520bca0 100644 --- a/x-pack/test/security_solution_cypress/cypress/support/es_archiver.ts +++ b/x-pack/test/security_solution_cypress/cypress/support/es_archiver.ts @@ -12,6 +12,20 @@ import { createEsClientForTesting, KbnClient, systemIndicesSuperuser } from '@kb import { ToolingLog } from '@kbn/tooling-log'; import { CA_CERT_PATH } from '@kbn/dev-utils'; +interface ClientOptions { + url: string; + username: string; + password: string; +} + +function createKibanaUrlWithAuth({ url, username, password }: ClientOptions) { + const clientUrl = new URL(url); + clientUrl.username = username; + clientUrl.password = password; + + return clientUrl.toString(); +} + export const esArchiver = ( on: Cypress.PluginEvents, config: Cypress.PluginConfigOptions @@ -26,7 +40,7 @@ export const esArchiver = ( password: config.env.ELASTICSEARCH_PASSWORD, }; - let authOverride; + let authOverride = systemIndicesSuperuser; if (isServerless) { authOverride = isCloudServerless ? serverlessCloudUser : systemIndicesSuperuser; } @@ -39,9 +53,14 @@ export const esArchiver = ( const kibanaUrl = config.env.KIBANA_URL || config.env.BASE_URL; + const kibanaUrlWithAuth = createKibanaUrlWithAuth({ + url: kibanaUrl, + ...authOverride, + }); + const kbnClient = new KbnClient({ log, - url: kibanaUrl as string, + url: kibanaUrlWithAuth, ...(config.env.ELASTICSEARCH_URL.includes('https') ? { certificateAuthorities: [Fs.readFileSync(CA_CERT_PATH)] } : {}), diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/api_calls/siem_migrations.ts b/x-pack/test/security_solution_cypress/cypress/tasks/api_calls/siem_migrations.ts new file mode 100644 index 0000000000000..5d001ede49a40 --- /dev/null +++ b/x-pack/test/security_solution_cypress/cypress/tasks/api_calls/siem_migrations.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { deleteAllDocuments } from './elasticsearch'; + +const MIGRATION_INDEX_PREFIX = '.kibana-siem-rule-migrations'; + +export const cleanMigrationData = () => { + cy.currentSpace().then((spaceId = 'default') => { + const migrationIndices = [ + `${MIGRATION_INDEX_PREFIX}-rules-${spaceId}`, + `${MIGRATION_INDEX_PREFIX}-migrations-${spaceId}`, + ]; + + deleteAllDocuments(migrationIndices.join(',')); + }); +}; diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/privileges.ts b/x-pack/test/security_solution_cypress/cypress/tasks/privileges.ts index 31eca3c55fc22..8ae129b55b209 100644 --- a/x-pack/test/security_solution_cypress/cypress/tasks/privileges.ts +++ b/x-pack/test/security_solution_cypress/cypress/tasks/privileges.ts @@ -72,6 +72,7 @@ export const secAll: Role = { securitySolutionCasesV3: ['all'], actions: ['all'], actionsSimulators: ['all'], + securitySolutionSiemMigrations: ['all'], }, spaces: ['*'], }, diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/siem_migrations.ts b/x-pack/test/security_solution_cypress/cypress/tasks/siem_migrations.ts new file mode 100644 index 0000000000000..168feb95e5968 --- /dev/null +++ b/x-pack/test/security_solution_cypress/cypress/tasks/siem_migrations.ts @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { RULES_PANEL_BTN, TRANSLATED_RULES_PAGE } from '../screens/security_header'; +import { + openNavigationPanel, + RULES_PANEL_BTN as RULES_PANEL_BTN_SERVERLESS, + TRANSLATED_RULES_PAGE as TRANSLATED_RULES_PAGE_SERVERLESS, +} from '../screens/serverless_security_header'; +import * as SELECTORS from '../screens/siem_migrations'; +import { bedrockConnectorAPIPayload } from './api_calls/connectors'; + +export const navigateToTranslatedRulesPage = () => { + if (Cypress.env('IS_SERVERLESS')) { + openNavigationPanel(RULES_PANEL_BTN_SERVERLESS); + cy.get(TRANSLATED_RULES_PAGE_SERVERLESS).click(); + } else { + openNavigationPanel(RULES_PANEL_BTN); + cy.get(TRANSLATED_RULES_PAGE).click(); + } +}; + +export const toggleSiemMigrationsCard = () => { + cy.get(SELECTORS.ONBOARDING_SIEM_MIGRATION_CARDS.AI_CONNECTORS).click(); +}; + +export const selectMigrationConnector = () => { + cy.get(SELECTORS.ONBOARDING_SIEM_MIGRATION_TOPIC).click(); + toggleSiemMigrationsCard(); + cy.get(SELECTORS.ONBOARDING_SIEM_MIGRATION_CARDS.SELECT_CONNECTORS).click(); + cy.get(SELECTORS.FAKE_BEDROCK_SELECTOR).click(); + cy.get(SELECTORS.ONBOARDING_SIEM_MIGRATION_CARDS.SELECT_CONNECTORS).should( + 'have.text', + bedrockConnectorAPIPayload.name + ); + cy.get(SELECTORS.ONBOARDING_SIEM_MIGRATION_CARDS.AI_CONNECTORS).within(() => { + cy.get('[title = "Completed"]').should('exist'); + }); +}; + +export const toggleMigrateRulesCard = () => { + cy.get(SELECTORS.ONBOARDING_SIEM_MIGRATION_CARDS.MIGRATE_RULES).click(); +}; + +export const openUploadRulesFlyout = () => { + toggleMigrateRulesCard(); + cy.get(SELECTORS.UPLOAD_RULES_BTN).click(); + cy.get(SELECTORS.UPLOAD_RULES_FLYOUT).should('exist'); +}; + +export const uploadRules = (splunkRulesJSON: object) => { + cy.get(SELECTORS.UPLOAD_RULES_FILE_PICKER).selectFile({ + contents: Cypress.Buffer.from(JSON.stringify(splunkRulesJSON)), + fileName: 'rules.json', + mimeType: 'text/plain', + }); + cy.get(SELECTORS.UPLOAD_RULES_FILE_BTN).should('not.be.disabled').click(); +}; + +export const startMigrationFromFlyout = () => { + cy.get(SELECTORS.START_MIGRATION_FROM_FLYOUT_BTN).should('not.be.disabled'); + cy.get(SELECTORS.START_MIGRATION_FROM_FLYOUT_BTN).click(); + cy.get(SELECTORS.UPLOAD_RULES_FLYOUT).should('not.exist'); +}; + +export const saveUpdatedTranslatedRuleQuery = () => { + cy.get(SELECTORS.TRANSLATED_RULE_SAVE_BTN).click(); + cy.get(SELECTORS.TRANSLATED_RULE_SAVE_BTN).should('not.exist'); +}; + +export const updateTranslatedRuleQuery = (newQuery: string) => { + cy.get(SELECTORS.TRANSLATED_RULE_EDIT_BTN).click(); + cy.get(SELECTORS.TRANSLATED_RULE_SAVE_BTN).should('be.visible'); + + cy.get(SELECTORS.TRANSLATED_RULE_QUERY_EDITOR_INPUT).type( + Cypress.platform === 'darwin' ? '{cmd+a}' : '{ctrl+a}', + { force: true } + ); + + cy.get(SELECTORS.TRANSLATED_RULE_QUERY_EDITOR_INPUT).type(newQuery, { force: true }); +}; + +export const editTranslatedRuleByRow = (rowNum: number) => { + cy.get(SELECTORS.TRANSLATED_RULES_RESULT_TABLE.RULE_NAME).eq(rowNum).click(); + cy.get(SELECTORS.TRANSLATED_RULE_DETAILS_FLYOUT).should('be.visible'); +}; + +export const reprocessFailedRules = () => { + cy.get(SELECTORS.REPROCESS_FAILED_RULES_BTN).click(); +}; diff --git a/x-pack/test/security_solution_cypress/cypress/urls/navigation.ts b/x-pack/test/security_solution_cypress/cypress/urls/navigation.ts index 6f6f4dc1a108a..97c1fd7f447cf 100644 --- a/x-pack/test/security_solution_cypress/cypress/urls/navigation.ts +++ b/x-pack/test/security_solution_cypress/cypress/urls/navigation.ts @@ -82,3 +82,5 @@ export const FLEET_URL = '/app/fleet'; // Entity Analytics export const ENTITY_ANALYTICS_DASHBOARD_URL = '/app/security/entity_analytics'; + +export const SIEM_MIGRATIONS_TRANSLATED_RULES_URL = 'app/security/siem_migrations/rules'; diff --git a/x-pack/test/security_solution_cypress/es_archives/siem_migrations/rule_migrations/data.json b/x-pack/test/security_solution_cypress/es_archives/siem_migrations/rule_migrations/data.json new file mode 100644 index 0000000000000..99835a05e9046 --- /dev/null +++ b/x-pack/test/security_solution_cypress/es_archives/siem_migrations/rule_migrations/data.json @@ -0,0 +1,11 @@ +{ + "type": "doc", + "value": { + "id": "0e5effd1-f355-469e-8614-74e82ca23f42", + "index": ".kibana-siem-rule-migrations-migrations-default", + "source": { + "created_by": "u_mGBROF_q5bmFCATbLXAcCwKa0k8JvONAwSruelyKA5E_0", + "created_at": "2025-05-06T07:53:47.560Z" + } + } +} diff --git a/x-pack/test/security_solution_cypress/es_archives/siem_migrations/rule_migrations/mappings.json b/x-pack/test/security_solution_cypress/es_archives/siem_migrations/rule_migrations/mappings.json new file mode 100644 index 0000000000000..3bd0d52fa36cc --- /dev/null +++ b/x-pack/test/security_solution_cypress/es_archives/siem_migrations/rule_migrations/mappings.json @@ -0,0 +1,24 @@ +{ + "type": "index", + "value": { + "index": ".kibana-siem-rule-migrations-migrations-default", + "aliases": {}, + "mappings": { + "dynamic": "false", + "_meta": { + "namespace": "default", + "kibana": { + "version": "9.1.0" + } + }, + "properties": { + "created_at": { + "type": "date" + }, + "created_by": { + "type": "keyword" + } + } + } + } +} diff --git a/x-pack/test/security_solution_cypress/es_archives/siem_migrations/rules/data.json b/x-pack/test/security_solution_cypress/es_archives/siem_migrations/rules/data.json new file mode 100644 index 0000000000000..a8cdf1e3800a5 --- /dev/null +++ b/x-pack/test/security_solution_cypress/es_archives/siem_migrations/rules/data.json @@ -0,0 +1,325 @@ + { + "type": "doc", + "value": { + "index": ".kibana-siem-rule-migrations-rules-default", + "id": "RHOWpJYBkBGfeIGVMlcL", + "source": { + "migration_id": "0e5effd1-f355-469e-8614-74e82ca23f42", + "original_rule": { + "id": "https://127.0.0.1:8089/servicesNS/splunk-admin/Splunk_Security_Essentials/saved/searches/Alert%20with%20IP%20Method%20and%20URI%20Filters%20with%20Default%20Severity", + "vendor": "splunk", + "title": "Alert with IP Method and URI Filters with minimum Sev - 1", + "description": "", + "query": "source=\"tutorialdata.zip:*\" clientip=\"198.35.1.75\" method=POST uri_path=\"/cart/error.do\"", + "query_language": "spl", + "severity": "1" + }, + "@timestamp": "2025-05-06T07:53:48.805Z", + "status": "failed", + "created_by": "u_mGBROF_q5bmFCATbLXAcCwKa0k8JvONAwSruelyKA5E_0", + "updated_by": "u_mGBROF_q5bmFCATbLXAcCwKa0k8JvONAwSruelyKA5E_0", + "updated_at": "2025-05-06T07:57:24.929Z", + "comments": [ + { + "created_at": "2025-05-06T07:57:24.929Z", + "message": "Error migrating rule: Tool call arguments for structuredOutput (toolu_bdrk_01RNRCCUgR7CjWqSFW8EUztm) were invalid", + "created_by": "assistant" + } + ] + } + } + } + +{ + "type": "doc", + "value": { + "index": ".kibana-siem-rule-migrations-rules-default", + "id": "RnOWpJYBkBGfeIGVMlcL", + "source": { + "migration_id": "0e5effd1-f355-469e-8614-74e82ca23f42", + "original_rule": { + "id": "https://127.0.0.1:8089/servicesNS/nobody/Splunk_Security_Essentials/saved/searches/Sample%20Alert%20in%20Essentials", + "vendor": "splunk", + "title": "Custom Rule Alert in Essentials Not Prebuild and Severity - 3", + "description": "", + "query": "source=\"tutorialdata.zip:*\"", + "query_language": "spl", + "severity": "3" + }, + "@timestamp": "2025-05-06T07:53:48.805Z", + "status": "completed", + "created_by": "u_mGBROF_q5bmFCATbLXAcCwKa0k8JvONAwSruelyKA5E_0", + "updated_by": "u_mGBROF_q5bmFCATbLXAcCwKa0k8JvONAwSruelyKA5E_0", + "updated_at": "2025-05-06T07:57:27.998Z", + "comments": [ + { + "created_at": "2025-05-06T07:56:39.882Z", + "message": "## Prebuilt Rule Matching Summary\nAfter analyzing the provided Splunk rule and the list of Elastic Prebuilt Rules, no suitable match was found. The Splunk rule titled \"Custom Rule Alert in Essentials Not Prebuild and Severity - 3\" appears to be a custom rule with a generic query that doesn't align with any of the specific use cases covered by the provided Elastic Prebuilt Rules. The Splunk rule's query is simply looking at a specific source, which could be used for various purposes and doesn't provide enough context to match it confidently with any of the Elastic rules. Therefore, no match is suggested.", + "created_by": "assistant" + }, + { + "created_at": "2025-05-06T07:56:49.689Z", + "message": "## Integration Matching Summary.\nNone of the provided Elastic Integrations match the given Splunk rule. The rule titled \"Custom Rule Alert in Essentials Not Prebuild and Severity - 3\" does not provide enough context about its data source or functionality to confidently match it with any of the available integrations. The Splunk integration could potentially be used to ingest this custom rule, but without more specific information about the rule's purpose or data source, it's not possible to make a definitive match.", + "created_by": "assistant" + }, + { + "created_at": "2025-05-06T07:57:04.311Z", + "message": "## Translation Summary\n\nThe original Splunk SPL query was:\n\n```splunk-spl\nsource=\"tutorialdata.zip:*\"\n```\n\nHere's a breakdown of the translation process:\n\n1. The SPL query uses a simple source filter. In ES|QL, we start with the `FROM` command to specify the data source.\n\n2. We use `logs-*` as the index pattern in the `FROM` command. This is a common pattern for log data in Elasticsearch, but you may need to adjust this based on your specific index naming convention.\n\n3. The source filter in SPL is translated to a `WHERE` clause in ES|QL. We use the equality operator `==` to match the `source` field exactly.\n\n4. The wildcard `*` at the end of the source value is kept as-is, as both SPL and ES|QL support wildcards in string matching.\n\n5. No additional processing or aggregation is performed in the original SPL query, so we don't need to add any further commands to the ES|QL query.\n\nThis translation maintains the original intent of the Splunk rule, which is to filter events based on the source field. The resulting ES|QL query will return all documents from the specified index pattern where the `source` field matches the pattern \"tutorialdata.zip:*\".", + "created_by": "assistant" + }, + { + "created_at": "2025-05-06T07:57:27.394Z", + "message": "## Field Mapping Summary\n\n1. Field Mapping:\n - Original field: `source`\n - Target ECS field: `source.original`\n - Reason: In the Elastic Common Schema (ECS), `source.original` is used to represent the original source of the event, which aligns with the Splunk CIM field `source`.\n\n2. Unchanged fields:\n - `id`, `_version`, and `index` were left unchanged as they are already metadata fields in Elasticsearch and do not require mapping to ECS fields.\n\nThe query structure remains the same, with only the `source` field being mapped to its ECS equivalent. The `FROM` command and the `WHERE` clause structure were not modified, as they are already in the correct ES|QL syntax.\n\nNote that the original Splunk query didn't specify a particular datamodel, so we focused on mapping the `source` field, which is a common field used in both Splunk and Elasticsearch for identifying the origin of log data.", + "created_by": "assistant" + } + ], + "translation_result": "partial", + "elastic_rule": { + "severity": "medium", + "risk_score": 47, + "query": "FROM [indexPattern] METADATA id,_version,index\n| WHERE source.original == \"tutorialdata.zip:*\"", + "description": "Custom Rule Alert in Essentials Not Prebuild and Severity - 3", + "query_language": "esql", + "title": "Custom Rule Alert in Essentials Not Prebuild and Severity - 3", + "integrationids": [ + "" + ] + } + } + } + } + +{ + "type": "doc", + "value": { + "index": ".kibana-siem-rule-migrations-rules-default", + "id": "R3OWpJYBkBGfeIGVMlcL", + "source": { + "migration_id": "0e5effd1-f355-469e-8614-74e82ca23f42", + "original_rule": { + "id": "https://127.0.0.1:8089/servicesNS/nobody/Splunk_Security_Essentials/saved/searches/Sample%20Alert%20in%20Essentials", + "vendor": "splunk", + "title": "Custom Rule Alert in Essentials Not Prebuild and Severity - 4", + "description": "", + "query": "source=\"tutorialdata.zip:*\"", + "query_language": "spl", + "severity": "4" + }, + "@timestamp": "2025-05-06T07:53:48.805Z", + "status": "completed", + "created_by": "u_mGBROF_q5bmFCATbLXAcCwKa0k8JvONAwSruelyKA5E_0", + "updated_by": "u_mGBROF_q5bmFCATbLXAcCwKa0k8JvONAwSruelyKA5E_0", + "updated_at": "2025-05-06T07:57:32.348Z", + "comments": [ + { + "created_at": "2025-05-06T07:56:40.766Z", + "message": "## Prebuilt Rule Matching Summary\nAfter analyzing the provided Splunk rule and the list of Elastic Prebuilt Rules, no suitable match was found. The Splunk rule titled \"Custom Rule Alert in Essentials Not Prebuild and Severity - 4\" appears to be a custom rule with a very generic query (\"source=\"tutorialdata.zip:*\"\") that doesn't align with any of the specific use cases covered by the provided Elastic Prebuilt Rules. The Splunk rule's purpose is not clear from its title or query, making it difficult to match with any of the more specific and well-defined Elastic rules. Therefore, no confident match could be made.", + "created_by": "assistant" + }, + { + "created_at": "2025-05-06T07:56:49.911Z", + "message": "## Integration Matching Summary\nNone of the provided Elastic Integrations match the given Splunk rule. The rule titled \"Custom Rule Alert in Essentials Not Prebuild and Severity - 4\" does not provide enough specific information about its data source or functionality to confidently match it with any of the available integrations. The Splunk integration could potentially be used to ingest this custom rule, but without more details about the rule's purpose or data source, it's not possible to make a definitive match.", + "created_by": "assistant" + }, + { + "created_at": "2025-05-06T07:57:03.064Z", + "message": "## Translation Summary\n\nThe original SPL query was:\n\n```splunk-spl\nsource=\"tutorialdata.zip:*\"\n```\n\nThe translation process was straightforward:\n\n1. We started the ES|QL query with the `FROM` command, using the `logs-*` index pattern as a general starting point for log data.\n\n2. We then used the `WHERE` command to filter the data based on the `source` field, which is equivalent to the SPL query's source specification.\n\n3. The field name `source` and its value `tutorialdata.zip:*` were kept exactly the same as in the original SPL query.\n\n4. No additional processing or aggregation was required, as the original SPL query was a simple source filter.\n\nThis translation maintains the core functionality of the original SPL query, filtering logs based on the specified source. The main difference is the explicit use of the `FROM` command in ES|QL to specify the index pattern, which is implicit in Splunk's SPL.", + "created_by": "assistant" + }, + { + "created_at": "2025-05-06T07:57:31.806Z", + "message": "## Field Mapping Summary\n\n1. Field: `source`\n - Original (Splunk CIM): `source`\n - Target (ECS): `source.address`\n - Reason: In the CIM to ECS map, `source` is mapped to `source.address` in the Network_Traffic datamodel. This seems to be the most appropriate mapping for the `source` field in this context.\n\n2. Fields: `id`, `_version`, `index`\n - These fields are metadata fields and are not part of the CIM to ECS mapping. They are left unchanged as they are specific to Elasticsearch and not related to the Splunk CIM fields.\n\nThe structure of the query remains the same, with only the `source` field being translated to its ECS equivalent. The `metadata` fields are kept as is since they are Elasticsearch-specific and not part of the CIM to ECS mapping.\n\nNote: The original Splunk query didn't provide much context about the specific datamodel or fields being used, so the mapping is based on the best match found in the CIM to ECS map. If there's additional context about the data or the intent of the query, a more precise mapping might be possible.", + "created_by": "assistant" + } + ], + "translation_result": "partial", + "elastic_rule": { + "severity": "high", + "risk_score": 73, + "query": "FROM [indexPattern] metadata id,_version,index\n| WHERE source.address == \"tutorialdata.zip:*\"", + "description": "Custom Rule Alert in Essentials Not Prebuild and Severity - 4", + "query_language": "esql", + "title": "Custom Rule Alert in Essentials Not Prebuild and Severity - 4", + "integrationids": [ + "" + ] + } + } + } + } + +{ + "type": "doc", + "value": { + "index": ".kibana-siem-rule-migrations-rules-default", + "id": "SHOWpJYBkBGfeIGVMlcL", + "source": { + "migration_id": "0e5effd1-f355-469e-8614-74e82ca23f42", + "original_rule": { + "id": "https://127.0.0.1:8089/servicesNS/splunk-admin/Splunk_Security_Essentials/saved/searches/Tutorial%20data%20based%20on%20host%20name", + "vendor": "splunk", + "title": "Custom Rule but not prebuilt Tutorial data based on host name and Severity - 5", + "description": "Tutorial data based on host name", + "query": "source=\"tutorialdata.zip:*\" | search host=vendor_sales", + "query_language": "spl", + "severity": "5" + }, + "@timestamp": "2025-05-06T07:53:48.805Z", + "status": "completed", + "created_by": "u_mGBROF_q5bmFCATbLXAcCwKa0k8JvONAwSruelyKA5E_0", + "updated_by": "u_mGBROF_q5bmFCATbLXAcCwKa0k8JvONAwSruelyKA5E_0", + "updated_at": "2025-05-06T07:57:33.042Z", + "comments": [ + { + "created_at": "2025-05-06T07:56:42.514Z", + "message": "## Prebuilt Rule Matching Summary \n After analyzing the provided Splunk rule and the list of Elastic Prebuilt Rules, no suitable match was found. The Splunk rule appears to be a custom rule specifically designed for tutorial data, filtering events based on a specific host name ('vendor_sales'). This rule is highly specific to a particular data source and host, which is not comparable to any of the more general-purpose security rules provided in the Elastic Prebuilt Rules list. The Elastic rules focus on detecting specific security-related behaviors or potential threats, while this Splunk rule seems to be a basic filter for tutorial purposes.", + "created_by": "assistant" + }, + { + "created_at": "2025-05-06T07:56:49.465Z", + "message": "## Integration Matching Summary\nNone of the provided Elastic Integrations match the given Splunk rule. The rule appears to be a custom rule based on tutorial data, and there is no specific integration in the provided list that corresponds to this type of generic or tutorial-based data source. The Splunk integration listed is for collecting logs from Splunk itself, not for matching specific rule types or data sources within Splunk.", + "created_by": "assistant" + }, + { + "created_at": "2025-05-06T07:57:06.039Z", + "message": "## Translation Summary\n\nThe original SPL query was:\n\n```splunk-spl\nsource=\"tutorialdata.zip:*\"\n| search host=vendor_sales\n```\n\nHere's a breakdown of the translation process:\n\n1. The SPL query starts with a source specification. In ES|QL, we start with the `FROM` command to specify the data source. Since we don't have specific index information, we use a generic `logs-*` pattern.\n\n2. The `source=\"tutorialdata.zip:*\"` condition in SPL is translated to a `WHERE` clause in ES|QL. We use the equality operator `==` instead of the single `=` used in SPL.\n\n3. The `search host=vendor_sales` part in SPL is also translated into the `WHERE` clause. In ES|QL, we combine multiple conditions using the `AND` operator.\n\n4. We don't need to use the `SEARCH` command in ES|QL as it doesn't exist. Instead, we use the `WHERE` command to filter the data.\n\n5. The field names (`source` and `host`) are kept the same as in the original SPL query, as per the instructions.\n\nThe resulting ES|QL query achieves the same filtering as the original SPL query, selecting data from the source \"tutorialdata.zip:*\" and where the host is \"vendor_sales\".", + "created_by": "assistant" + }, + { + "created_at": "2025-05-06T07:57:32.519Z", + "message": "## Field Mapping Summary\n\n1. `source` field:\n - Original field: `source`\n - Target ECS field: No change (kept as `source`)\n - Reason: The `source` field is already a common field in both Splunk CIM and ECS, so it doesn't need to be changed.\n\n2. `host` field:\n - Original field: `host`\n - Target ECS field: `host.name`\n - Reason: In the ECS, the `host` field is an object that contains various host-related fields. The `name` subfield is used to represent the hostname, which is equivalent to the Splunk CIM `host` field.\n\nThe other parts of the query, including the `FROM` command and the `metadata` fields, remain unchanged as they are not related to Splunk CIM fields.\n\nNote: The datamodel used in this query is not explicitly mentioned, so we relied on the general mapping between Splunk CIM and ECS fields. If there's a specific datamodel that should be considered, please provide that information for a more accurate mapping.", + "created_by": "assistant" + } + ], + "translation_result": "partial", + "elastic_rule": { + "severity": "critical", + "risk_score": 99, + "query": "FROM [indexPattern] metadata id,_version,index\n| WHERE source == \"tutorialdata.zip:*\" AND host.name == \"vendor_sales\"", + "description": "Tutorial data based on host name", + "query_language": "esql", + "title": "Custom Rule but not prebuilt Tutorial data based on host name and Severity - 5", + "integrationids": [ + "" + ] + } + } + } + } + +{ + "type": "doc", + "value": { + "index": ".kibana-siem-rule-migrations-rules-default", + "id": "RXOWpJYBkBGfeIGVMlcL", + "source": { + "migration_id": "0e5effd1-f355-469e-8614-74e82ca23f42", + "original_rule": { + "id": "https://127.0.0.1:8089/servicesns/splunk-admin/splunk_security_essentials/saved/searches/new%20alert%20with%20index%20filter", + "vendor": "splunk", + "title": "new alert with index filter and severity- 2 ", + "description": "tutorial data based on host name", + "query": "source=\"tutorialdata.zip:*\" | search splunk_server=\"macbookpro.fritz.box\" index=main", + "query_language": "spl", + "severity": "2" + }, + "@timestamp": "2025-05-06T07:53:48.805Z", + "status": "completed", + "created_by": "u_mGBROF_q5bmFCATbLXAcCwKa0k8JvONAwSruelyKA5E_0", + "updated_by": "u_mGBROF_q5bmFCATbLXAcCwKa0k8JvONAwSruelyKA5E_0", + "updated_at": "2025-05-06T07:57:33.676Z", + "comments": [ + { + "created_at": "2025-05-06T07:56:38.968Z", + "message": "## Prebuilt Rule Matching Summary\nAfter analyzing the provided Splunk rule and the list of Elastic Prebuilt Rules, no suitable match was found. The Splunk rule appears to be a custom query focused on filtering data from a specific source, server, and index. It doesn't align with any of the more specific security-focused rules in the provided Elastic Prebuilt Rules list. The Splunk rule seems to be a general data filtering query rather than a targeted security detection, which is why it doesn't correspond to any of the given Elastic rules.", + "created_by": "assistant" + }, + { + "created_at": "2025-05-06T07:56:49.238Z", + "message": "## Integration Matching Summary\nThe Splunk rule \"New Alert with Index filter and Severity- 2\" is matched with the \"Splunk\" integration. This integration is designed to collect logs from Splunk, which aligns with the source of the provided Splunk rule. While the rule itself doesn't provide specific details about the data source, the fact that it's a Splunk rule suggests that using the Splunk integration would be the most appropriate choice for migrating this rule to Elastic Security.", + "created_by": "assistant" + }, + { + "created_at": "2025-05-06T07:57:05.654Z", + "message": "## Translation Summary\n\nLet's break down the translation process:\n\n1. The original SPL query:\n```splunk-spl\nsource=\"tutorialdata.zip:*\"\n| search splunk_server=\"MacBookPro.fritz.box\" index=main\n```\n\n2. Translation steps:\n\n a. We start the ES|QL query with the `FROM` command, using the index pattern `logs-splunk.alert-*` as requested in the guidelines.\n\n b. The `source=\"tutorialdata.zip:*\"` part in SPL is translated to a `WHERE` condition in ES|QL.\n\n c. The `search` command in SPL is essentially a filter, which is equivalent to the `WHERE` command in ES|QL. We combine all the conditions from the original SPL query into a single `WHERE` clause.\n\n d. We use the `AND` operator to combine multiple conditions in the `WHERE` clause.\n\n e. We keep the field names exactly as they are in the original SPL query, as instructed.\n\n3. The resulting ES|QL query combines all these elements into a single pipeline, using the `|` character to separate commands.\n\nThis translation maintains the intent of the original Splunk rule, which is to filter data based on specific source, splunk_server, and index values. The ES|QL query will retrieve data from the specified index pattern and apply the same filtering conditions as the original SPL query.", + "created_by": "assistant" + }, + { + "created_at": "2025-05-06T07:57:33.046Z", + "message": "## Field Mapping Summary\n\n1. `splunk_server` to `observer.hostname`:\n - Reason: In the Splunk CIM to ECS map, there isn't a direct mapping for `splunk_server`. However, in ECS, `observer.hostname` is used to represent the hostname of the system doing the observation, which aligns with the purpose of `splunk_server` in Splunk.\n\n2. `index` to `event.dataset`:\n - Reason: In Splunk, the `index` field is often used to categorize different types of data. In ECS, `event.dataset` serves a similar purpose, representing a high-level classification of the type of data.\n\n3. `source` field:\n - The `source` field was left as is because there isn't a clear one-to-one mapping in the provided CIM to ECS map, and its usage here seems to be specific to the data source rather than a standard field.\n\nThese mappings aim to preserve the intent of the original Splunk query while adapting it to the Elastic Common Schema. The `source` field was left unchanged due to lack of a clear ECS equivalent in this context.", + "created_by": "assistant" + } + ], + "translation_result": "full", + "elastic_rule": { + "severity": "low", + "risk_score": 21, + "query": "FROM logs-splunk.alert-* metadata id,_version,index\n| WHERE source == \"tutorialdata.zip:*\" AND observer.hostname == \"MacBookPro.fritz.box\" AND event.dataset == \"main\"", + "description": "Tutorial data based on host name", + "query_language": "esql", + "title": "New Alert with Index filter and Severity- 2 ", + "integrationids": [ + "splunk" + ] + } + } + } + } + +{ + "type": "doc", + "value": { + "index": ".kibana-siem-rule-migrations-rules-default", + "id": "SXOWpJYBkBGfeIGVMlcL", + "source": { + "migration_id": "0e5effd1-f355-469e-8614-74e82ca23f42", + "original_rule": { + "id": "https://127.0.0.1:8089/servicesNS/splunk-admin/Splunk_Security_Essentials/saved/searches/Tutorial%20data%20based%20on%20host%20name", + "vendor": "splunk", + "title": "Alert with no Severity. Should low in Elastic", + "description": "Tutorial data based on host name", + "query": "source=\"tutorialdata.zip:*\"\n| search host=vendor_sales", + "query_language": "spl" + }, + "@timestamp": "2025-05-06T07:53:48.805Z", + "status": "completed", + "created_by": "u_mGBROF_q5bmFCATbLXAcCwKa0k8JvONAwSruelyKA5E_0", + "updated_by": "u_mGBROF_q5bmFCATbLXAcCwKa0k8JvONAwSruelyKA5E_0", + "updated_at": "2025-05-06T07:57:42.895Z", + "comments": [ + { + "created_at": "2025-05-06T07:56:41.646Z", + "message": "## Prebuilt Rule Matching Summary\nAfter analyzing the provided Splunk rule and the list of Elastic Prebuilt Rules, no suitable match was found. The Splunk rule appears to be a simple tutorial-based alert focusing on a specific host name (vendor_sales) from a tutorial dataset. This rule is too specific and doesn't align with any of the more general security-focused rules in the provided Elastic Prebuilt Rules list. The Elastic rules cover more complex security use cases such as traffic anomalies, encoding/decoding activities, and authentication issues, which are not related to the simple host-based filtering in the Splunk rule.", + "created_by": "assistant" + }, + { + "created_at": "2025-05-06T07:56:50.140Z", + "message": "## Integration Matching Summary\nNone of the provided Elastic Integrations directly match the Splunk rule \"Alert with no Severity. Should low in Elastic\". The rule description mentions \"Tutorial data based on host name\", which doesn't align specifically with any of the given integrations. The Splunk integration could potentially be used to ingest this data, but without more specific information about the data source or type, it's not possible to confidently match it to any of the provided integrations.", + "created_by": "assistant" + }, + { + "created_at": "2025-05-06T07:57:06.805Z", + "message": "## Translation Summary\n\nThe original Splunk SPL query was:\n\n```splunk-spl\nsource=\"tutorialdata.zip:*\"\n| search host=vendor_sales\n```\n\nHere's a breakdown of the translation process:\n\n1. The `FROM` command:\n - In ES|QL, we always start with a `FROM` command to specify the index pattern. Since the original query doesn't specify an index, we use the generic `logs-*` pattern.\n\n2. The `source` filter:\n - In Splunk, the `source` field is often used at the beginning of the query without an explicit `search` command.\n - In ES|QL, we include this condition in the `WHERE` clause.\n\n3. The `host` filter:\n - In the Splunk query, this is specified using the `search` command.\n - In ES|QL, we combine this condition with the `source` condition in the `WHERE` clause using the `AND` operator.\n\n4. Field names:\n - As per the instructions, we kept the field names the same as in the original SPL query.\n\n5. Operator translation:\n - Splunk uses `=` for equality, while ES|QL uses `==`. We made this adjustment in the translation.\n\nThe resulting ES|QL query filters the data to include only events where the `source` field matches \"tutorialdata.zip:*\" and the `host` field is exactly \"vendor_sales\", which is equivalent to the original Splunk query.", + "created_by": "assistant" + }, + { + "created_at": "2025-05-06T07:57:42.383Z", + "message": "## Field Mapping Summary\n\n1. `source` field:\n - Original field: `source`\n - Target ECS field: `source.keyword`\n - Reason: The `source` field is commonly used in Elastic Common Schema to represent the source of the event. However, since we're doing an exact match, we're using the `.keyword` subfield to ensure we're matching on the full string value.\n\n2. `host` field:\n - Original field: `host`\n - Target ECS field: `host.hostname`\n - Reason: In ECS, `host.hostname` is used to represent the hostname of the host where the event originated. This is the most appropriate mapping for the Splunk CIM `host` field in this context.\n\n3. Metadata fields:\n - The metadata fields `id`, `_version`, and `index` were left unchanged as they are already in the correct format for Elasticsearch.\n\n4. Index pattern:\n - The index pattern `logs-*` was left unchanged as it's already in a format compatible with Elasticsearch.\n\nNote: The structure of the query remains the same, with only the field names being updated to their ECS equivalents. The `FROM` and `WHERE` commands were kept as they are valid ES|QL commands according to the provided documentation.", + "created_by": "assistant" + } + ], + "translation_result": "partial", + "elastic_rule": { + "severity": "low", + "risk_score": 21, + "query": "FROM [indexPattern] metadata id,_version,index\n| WHERE source.keyword == \"tutorialdata.zip:*\" AND host.hostname == \"vendor_sales\"", + "description": "Tutorial data based on host name", + "query_language": "esql", + "title": "Alert with no Severity. Should low in Elastic", + "integrationids": [ + "" + ] + } + } + } + } diff --git a/x-pack/test/security_solution_cypress/es_archives/siem_migrations/rules/mappings.json b/x-pack/test/security_solution_cypress/es_archives/siem_migrations/rules/mappings.json new file mode 100644 index 0000000000000..6e632e1e8c647 --- /dev/null +++ b/x-pack/test/security_solution_cypress/es_archives/siem_migrations/rules/mappings.json @@ -0,0 +1,126 @@ +{ + "type": "index", + "value": { + "index": ".kibana-siem-rule-migrations-rules-default", + "aliases": {}, + "mappings": { + "dynamic": "false", + "_meta": { + "namespace": "default", + "kibana": { + "version": "9.1.0" + } + }, + "properties": { + "@timestamp": { + "type": "date", + "ignore_malformed": false + }, + "comments": { + "properties": { + "created_at": { + "type": "date" + }, + "created_by": { + "type": "keyword" + }, + "message": { + "type": "keyword" + } + } + }, + "created_by": { + "type": "keyword" + }, + "elastic_rule": { + "properties": { + "description": { + "type": "text" + }, + "id": { + "type": "keyword" + }, + "integration_ids": { + "type": "keyword" + }, + "prebuilt_rule_id": { + "type": "keyword" + }, + "query": { + "type": "text" + }, + "query_language": { + "type": "keyword" + }, + "risk_score": { + "type": "short" + }, + "severity": { + "type": "keyword" + }, + "title": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword" + } + } + } + } + }, + "migration_id": { + "type": "keyword" + }, + "original_rule": { + "properties": { + "annotations": { + "properties": { + "mitre_attack": { + "type": "keyword" + } + } + }, + "description": { + "type": "text" + }, + "id": { + "type": "keyword" + }, + "query": { + "type": "text" + }, + "query_language": { + "type": "keyword" + }, + "severity": { + "type": "keyword" + }, + "title": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword" + } + } + }, + "vendor": { + "type": "keyword" + } + } + }, + "status": { + "type": "keyword" + }, + "translation_result": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "type": "keyword" + } + } + } + } +}