From 644169756631b2221b1b644cc637e01af69760cc Mon Sep 17 00:00:00 2001 From: Ievgen Sorokopud Date: Fri, 14 Feb 2025 18:39:21 +0100 Subject: [PATCH] [Rules migration][Integration test] Update migration API (#11232) (#211196) ## Summary [Internal link](https://github.com/elastic/security-team/issues/10820) to the feature details Part of https://github.com/elastic/security-team/issues/11232 This PR covers SIEM Migrations Update API (route: `PUT /internal/siem_migrations/rules/{migration_id}`) integration test: * Happy path * update migration * ignore attributes that are not eligible for update * Error handling * an empty content response * an error when rule's id is not specified * an error when undefined payload has been passed Also, as part of this PR, I added error handling cases for Create API: * no content error * an error when undefined payload has been passed * an error when original rule id is not specified * error when original rule vendor is not specified * an error when original rule title is not specified * an error when original rule description is not specified * an error when original rule query is not specified * an error when original rule query_language is not specified --------- Co-authored-by: Sergi Massaneda (cherry picked from commit 819fd7a3e9f0e885bc22f382dcc165ea1a4b55e7) --- .../lib/siem_migrations/rules/api/update.ts | 3 + .../trial_license_complete_tier/create.ts | 225 +++++++++++++----- .../trial_license_complete_tier/index.ts | 1 + .../trial_license_complete_tier/update.ts | 156 ++++++++++++ .../siem_migrations/utils/rules.ts | 36 ++- 5 files changed, 350 insertions(+), 71 deletions(-) create mode 100644 x-pack/test/security_solution_api_integration/test_suites/siem_migrations/rules/trial_license_complete_tier/update.ts diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/update.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/update.ts index 1c288578748a6..a1d93eee2c55e 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/update.ts @@ -44,6 +44,9 @@ export const registerSiemRuleMigrationsUpdateRoute = ( 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); 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 4a3230b0162c9..f6aa2b66459a1 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 @@ -28,78 +28,171 @@ export default ({ getService }: FtrProviderContext) => { await deleteAllMigrationRules(es); }); - it('should create migrations with provided id', async () => { - const migrationId = uuidv4(); - await migrationRulesRoutes.create({ migrationId, body: [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, - }) - ); - }); + describe('Happy path', () => { + it('should create migrations with provided id', async () => { + const migrationId = uuidv4(); + await migrationRulesRoutes.create({ migrationId, payload: [defaultOriginalRule] }); - it('should create migrations without provided id', async () => { - const { - body: { migration_id: migrationId }, - } = await migrationRulesRoutes.create({ body: [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, - }) - ); - }); + // 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 () => { + 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); + + 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 () => { + 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, + }) + ); - it('should create migrations with the rules that have resources', async () => { - const migrationId = uuidv4(); - await migrationRulesRoutes.create({ migrationId, body: [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({ - migrationId, + // 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' }, + ]); }); - expect(resourcesResponse.body).toEqual([ - { type: 'macro', name: 'summariesonly' }, - { type: 'macro', name: 'drop_dm_object_name(1)' }, - { type: 'lookup', name: 'malware_tracker' }, - ]); }); - it('should return no content error', async () => { - const migrationId = uuidv4(); - await migrationRulesRoutes.create({ migrationId, body: [], expectStatusCode: 204 }); + 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 }); - // fetch migration rule - const response = await migrationRulesRoutes.get({ migrationId }); - expect(response.body.total).toEqual(0); + 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', + }); + }); }); }); }; 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 4e155a00edbf8..d36f93afb8656 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 @@ -10,5 +10,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { describe('@ess SecuritySolution SIEM Migrations', () => { loadTestFile(require.resolve('./create')); loadTestFile(require.resolve('./get')); + loadTestFile(require.resolve('./update')); }); } 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/update.ts new file mode 100644 index 0000000000000..f114af2de519a --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/siem_migrations/rules/trial_license_complete_tier/update.ts @@ -0,0 +1,156 @@ +/* + * 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 { + createMigrationRules, + deleteAllMigrationRules, + getMigrationRuleDocument, + migrationRulesRouteHelpersFactory, +} from '../../utils'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default ({ getService }: FtrProviderContext) => { + const es = getService('es'); + const supertest = getService('supertest'); + const migrationRulesRoutes = migrationRulesRouteHelpersFactory(supertest); + + describe('@ess @serverless @serverlessQA Update API', () => { + beforeEach(async () => { + await deleteAllMigrationRules(es); + }); + + describe('Happy path', () => { + it('should update migration rules', async () => { + const migrationId = uuidv4(); + const migrationRuleDocument = getMigrationRuleDocument({ migration_id: migrationId }); + const [createdDocumentId] = await createMigrationRules(es, [migrationRuleDocument]); + + const now = new Date().toISOString(); + await migrationRulesRoutes.update({ + migrationId, + payload: [ + { + id: createdDocumentId, + elastic_rule: { title: 'Updated title' }, + comments: [{ message: 'Update comment', created_by: 'ftr test', created_at: now }], + }, + ], + }); + + // fetch migration rule + const response = await migrationRulesRoutes.get({ migrationId }); + expect(response.body.total).toEqual(1); + + const { + '@timestamp': timestamp, + updated_at: updatedAt, + updated_by: updatedBy, + elastic_rule: elasticRule, + ...rest + } = migrationRuleDocument; + + const migrationRule = response.body.data[0]; + expect(migrationRule).toEqual( + expect.objectContaining({ + ...rest, + elastic_rule: { ...elasticRule, title: 'Updated title' }, + comments: [{ message: 'Update comment', created_by: 'ftr test', created_at: now }], + }) + ); + }); + + it('should ignore attributes that are not eligible for update', async () => { + const migrationId = uuidv4(); + const migrationRuleDocument = getMigrationRuleDocument({ migration_id: migrationId }); + const [createdDocumentId] = await createMigrationRules(es, [migrationRuleDocument]); + + const now = new Date().toISOString(); + await migrationRulesRoutes.update({ + migrationId, + payload: [ + { + id: createdDocumentId, + elastic_rule: { title: 'Updated title' }, + comments: [{ message: 'Update comment', created_by: 'ftr test', created_at: now }], + // Should be ignored + migration_id: 'fake_migration_id_1', + original_rule: { description: 'Ignore this description' }, + translation_result: 'ignore this translation result', + status: 'ignore this status', + }, + ], + }); + + const { + '@timestamp': timestamp, + updated_at: updatedAt, + updated_by: updatedBy, + elastic_rule: elasticRule, + ...rest + } = migrationRuleDocument; + const expectedMigrationRule = expect.objectContaining({ + ...rest, + elastic_rule: { ...elasticRule, title: 'Updated title' }, + comments: [{ message: 'Update comment', created_by: 'ftr test', created_at: now }], + }); + + // fetch migration rule + const response = await migrationRulesRoutes.get({ migrationId }); + expect(response.body.total).toEqual(1); + + const migrationRule = response.body.data[0]; + expect(migrationRule).toEqual(expectedMigrationRule); + }); + }); + + describe('Error handling', () => { + it('should return empty content response when no rules passed', async () => { + const migrationId = uuidv4(); + await migrationRulesRoutes.update({ + migrationId, + payload: [], + expectStatusCode: 204, + }); + }); + + it(`should return an error when rule's id is not specified`, async () => { + const migrationId = uuidv4(); + const migrationRuleDocument = getMigrationRuleDocument({ migration_id: migrationId }); + await createMigrationRules(es, [migrationRuleDocument]); + + const response = await migrationRulesRoutes.update({ + migrationId, + payload: [{ elastic_rule: { title: 'Updated title' } }], + expectStatusCode: 400, + }); + expect(response.body).toEqual({ + error: 'Bad Request', + message: '[request body]: 0.id: Required', + statusCode: 400, + }); + }); + + it(`should return an error when undefined payload has been passed`, async () => { + const migrationId = uuidv4(); + const migrationRuleDocument = getMigrationRuleDocument({ migration_id: migrationId }); + await createMigrationRules(es, [migrationRuleDocument]); + + const response = await migrationRulesRoutes.update({ + migrationId, + expectStatusCode: 400, + }); + expect(response.body).toEqual({ + error: 'Bad Request', + message: '[request body]: Expected array, received null', + statusCode: 400, + }); + }); + }); + }); +}; 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 b8d42d7b6d422..a58cbb53a4310 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 @@ -17,10 +17,10 @@ import { SIEM_RULE_MIGRATION_PATH, } from '@kbn/security-solution-plugin/common/siem_migrations/constants'; import { - CreateRuleMigrationRequestBody, CreateRuleMigrationResponse, GetRuleMigrationRequestQuery, GetRuleMigrationResponse, + UpdateRuleMigrationResponse, } 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'; @@ -38,8 +38,17 @@ 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 payload to send */ + payload?: any; + /** Optional expected status code parameter */ + expectStatusCode?: number; +} + +export interface UpdateRulesParams { + /** `id` of the migration to install rules for */ + migrationId: string; + /** Optional payload to send */ + payload?: any; /** Optional expected status code parameter */ expectStatusCode?: number; } @@ -66,7 +75,7 @@ export const migrationRulesRouteHelpersFactory = (supertest: SuperTest.Agent) => create: async ({ migrationId, - body, + payload, expectStatusCode = 200, }: CreateRuleMigrationParams): Promise<{ body: CreateRuleMigrationResponse }> => { const response = await supertest @@ -74,7 +83,24 @@ export const migrationRulesRouteHelpersFactory = (supertest: SuperTest.Agent) => .set('kbn-xsrf', 'true') .set(ELASTIC_HTTP_VERSION_HEADER, API_VERSIONS.internal.v1) .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') - .send(body); + .send(payload); + + assertStatusCode(expectStatusCode, response); + + return response; + }, + + update: async ({ + migrationId, + payload, + expectStatusCode = 200, + }: UpdateRulesParams): Promise<{ body: UpdateRuleMigrationResponse }> => { + const response = await supertest + .put(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(payload); assertStatusCode(expectStatusCode, response);