mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
## 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 <sergi.massaneda@gmail.com>
This commit is contained in:
parent
20aac5c915
commit
819fd7a3e9
5 changed files with 350 additions and 71 deletions
|
@ -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);
|
||||
|
|
|
@ -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] });
|
||||
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);
|
||||
// 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({ 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,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
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,
|
||||
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,
|
||||
})
|
||||
);
|
||||
|
||||
// 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);
|
||||
// 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',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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'));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
|
@ -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);
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue