mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
* [SIEM] [Detections Engine] Import rules unit tests (#57466) * Added unit tests for detection engine import_rules_route and moved out small portion of import_rules_route into a util to be unit tested as well. Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com> * Updating tests to reflect state of 7.6. 7.7 and 8 include code from # 56814 that was not backported to 7.6 Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
0a87f2dfd2
commit
d18b21be48
8 changed files with 669 additions and 26 deletions
|
@ -125,6 +125,28 @@ export const createMockServerWithoutActionOrAlertClientDecoration = (
|
|||
};
|
||||
};
|
||||
|
||||
export const createMockServerWithoutSavedObjectDecoration = (
|
||||
config: Record<string, string> = defaultConfig
|
||||
) => {
|
||||
const serverWithoutSavedObjectClient = new Hapi.Server({
|
||||
port: 0,
|
||||
});
|
||||
|
||||
serverWithoutSavedObjectClient.config = () => createMockKibanaConfig(config);
|
||||
|
||||
const actionsClient = actionsClientMock.create();
|
||||
const alertsClient = alertsClientMock.create();
|
||||
|
||||
serverWithoutSavedObjectClient.decorate('request', 'getAlertsClient', () => alertsClient);
|
||||
serverWithoutSavedObjectClient.decorate('request', 'getActionsClient', () => actionsClient);
|
||||
serverWithoutSavedObjectClient.plugins.spaces = { getSpaceId: () => 'default' };
|
||||
return {
|
||||
serverWithoutSavedObjectClient: serverWithoutSavedObjectClient as ServerFacade & Hapi.Server,
|
||||
alertsClient,
|
||||
actionsClient,
|
||||
};
|
||||
};
|
||||
|
||||
export const getMockIndexName = () =>
|
||||
jest.fn().mockImplementation(() => ({
|
||||
callWithRequest: jest.fn().mockImplementationOnce(() => 'index-name'),
|
||||
|
|
|
@ -19,6 +19,7 @@ import {
|
|||
} from '../../../../../common/constants';
|
||||
import { RuleAlertType, IRuleSavedAttributesSavedObjectAttributes } from '../../rules/types';
|
||||
import { RuleAlertParamsRest, PrepackagedRules } from '../../types';
|
||||
import { TEST_BOUNDARY } from './utils';
|
||||
|
||||
export const mockPrepackagedRule = (): PrepackagedRules => ({
|
||||
rule_id: 'rule-1',
|
||||
|
@ -223,6 +224,24 @@ export const getFindResultWithMultiHits = ({
|
|||
};
|
||||
};
|
||||
|
||||
export const getImportRulesRequest = (payload?: Buffer): ServerInjectOptions => ({
|
||||
method: 'POST',
|
||||
url: `${DETECTION_ENGINE_RULES_URL}/_import`,
|
||||
headers: {
|
||||
'Content-Type': `multipart/form-data; boundary=${TEST_BOUNDARY}`,
|
||||
},
|
||||
payload,
|
||||
});
|
||||
|
||||
export const getImportRulesRequestOverwriteTrue = (payload?: Buffer): ServerInjectOptions => ({
|
||||
method: 'POST',
|
||||
url: `${DETECTION_ENGINE_RULES_URL}/_import?overwrite=true`,
|
||||
headers: {
|
||||
'Content-Type': `multipart/form-data; boundary=${TEST_BOUNDARY}`,
|
||||
},
|
||||
payload,
|
||||
});
|
||||
|
||||
export const getDeleteRequest = (): ServerInjectOptions => ({
|
||||
method: 'DELETE',
|
||||
url: `${DETECTION_ENGINE_RULES_URL}?rule_id=rule-1`,
|
||||
|
|
|
@ -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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { OutputRuleAlertRest } from '../../types';
|
||||
|
||||
export const TEST_BOUNDARY = 'test_multipart_boundary';
|
||||
|
||||
// Not parsable due to extra colon following `name` property - name::
|
||||
export const UNPARSABLE_LINE =
|
||||
'{"name"::"Simple Rule Query","description":"Simple Rule Query","risk_score":1,"rule_id":"rule-1","severity":"high","type":"query","query":"user.name: root or user.name: admin"}';
|
||||
|
||||
/**
|
||||
* This is a typical simple rule for testing that is easy for most basic testing
|
||||
* @param ruleId
|
||||
*/
|
||||
export const getSimpleRule = (ruleId = 'rule-1'): Partial<OutputRuleAlertRest> => ({
|
||||
name: 'Simple Rule Query',
|
||||
description: 'Simple Rule Query',
|
||||
risk_score: 1,
|
||||
rule_id: ruleId,
|
||||
severity: 'high',
|
||||
type: 'query',
|
||||
query: 'user.name: root or user.name: admin',
|
||||
});
|
||||
|
||||
/**
|
||||
* Given an array of rule_id strings this will return a ndjson buffer which is useful
|
||||
* for testing uploads.
|
||||
* @param ruleIds Array of strings of rule_ids
|
||||
* @param isNdjson Boolean to determine file extension
|
||||
*/
|
||||
export const getSimpleRuleAsMultipartContent = (ruleIds: string[], isNdjson = true): Buffer => {
|
||||
const arrayOfRules = ruleIds.map(ruleId => {
|
||||
const simpleRule = getSimpleRule(ruleId);
|
||||
return JSON.stringify(simpleRule);
|
||||
});
|
||||
const stringOfRules = arrayOfRules.join('\r\n');
|
||||
|
||||
const resultingPayload =
|
||||
`--${TEST_BOUNDARY}\r\n` +
|
||||
`Content-Disposition: form-data; name="file"; filename="rules.${
|
||||
isNdjson ? 'ndjson' : 'json'
|
||||
}\r\n` +
|
||||
'Content-Type: application/octet-stream\r\n' +
|
||||
'\r\n' +
|
||||
`${stringOfRules}\r\n` +
|
||||
`--${TEST_BOUNDARY}--\r\n`;
|
||||
|
||||
return Buffer.from(resultingPayload);
|
||||
};
|
|
@ -0,0 +1,415 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import {
|
||||
getSimpleRuleAsMultipartContent,
|
||||
TEST_BOUNDARY,
|
||||
UNPARSABLE_LINE,
|
||||
getSimpleRule,
|
||||
} from '../__mocks__/utils';
|
||||
import {
|
||||
createMockServer,
|
||||
createMockServerWithoutAlertClientDecoration,
|
||||
createMockServerWithoutSavedObjectDecoration,
|
||||
getMockNonEmptyIndex,
|
||||
getMockEmptyIndex,
|
||||
createMockServerWithoutActionClientDecoration,
|
||||
} from '../__mocks__/_mock_server';
|
||||
import { ImportSuccessError } from '../utils';
|
||||
import {
|
||||
getImportRulesRequest,
|
||||
getImportRulesRequestOverwriteTrue,
|
||||
getFindResult,
|
||||
getResult,
|
||||
createActionResult,
|
||||
getFindResultWithSingleHit,
|
||||
getFindResultStatus,
|
||||
} from '../__mocks__/request_responses';
|
||||
|
||||
import { importRulesRoute } from './import_rules_route';
|
||||
|
||||
describe('import_rules_route', () => {
|
||||
let {
|
||||
server,
|
||||
alertsClient,
|
||||
actionsClient,
|
||||
elasticsearch,
|
||||
savedObjectsClient,
|
||||
} = createMockServer();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
({
|
||||
server,
|
||||
alertsClient,
|
||||
actionsClient,
|
||||
elasticsearch,
|
||||
savedObjectsClient,
|
||||
} = createMockServer());
|
||||
elasticsearch.getCluster = getMockNonEmptyIndex();
|
||||
importRulesRoute(server);
|
||||
});
|
||||
|
||||
describe('status codes with savedObjectsClient and alertClient', () => {
|
||||
test('returns 404 if alertClient is not available on the route', async () => {
|
||||
const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration();
|
||||
importRulesRoute(serverWithoutAlertClient);
|
||||
const { statusCode } = await serverWithoutAlertClient.inject(
|
||||
getImportRulesRequest(getSimpleRuleAsMultipartContent(['rule-1']))
|
||||
);
|
||||
expect(statusCode).toEqual(404);
|
||||
});
|
||||
|
||||
test('returns 404 if actionClient is not available on the route', async () => {
|
||||
const { serverWithoutActionClient } = createMockServerWithoutActionClientDecoration();
|
||||
importRulesRoute(serverWithoutActionClient);
|
||||
const { statusCode } = await serverWithoutActionClient.inject(
|
||||
getImportRulesRequest(getSimpleRuleAsMultipartContent(['rule-1']))
|
||||
);
|
||||
expect(statusCode).toBe(404);
|
||||
});
|
||||
|
||||
test('returns 404 if savedObjectsClient is not available on the route', async () => {
|
||||
const { serverWithoutSavedObjectClient } = createMockServerWithoutSavedObjectDecoration();
|
||||
importRulesRoute(serverWithoutSavedObjectClient);
|
||||
const { statusCode } = await serverWithoutSavedObjectClient.inject(
|
||||
getImportRulesRequest(getSimpleRuleAsMultipartContent(['rule-1']))
|
||||
);
|
||||
|
||||
expect(statusCode).toEqual(404);
|
||||
});
|
||||
|
||||
test('returns reported error if index does not exist', async () => {
|
||||
elasticsearch.getCluster = getMockEmptyIndex();
|
||||
alertsClient.find.mockResolvedValue(getFindResult());
|
||||
alertsClient.get.mockResolvedValue(getResult());
|
||||
actionsClient.create.mockResolvedValue(createActionResult());
|
||||
alertsClient.create.mockResolvedValue(getResult());
|
||||
savedObjectsClient.find.mockResolvedValue(getFindResultStatus());
|
||||
|
||||
const requestPayload = getSimpleRuleAsMultipartContent(['rule-1']);
|
||||
const { statusCode, payload } = await server.inject(getImportRulesRequest(requestPayload));
|
||||
const parsed: ImportSuccessError = JSON.parse(payload);
|
||||
|
||||
expect(parsed).toEqual({
|
||||
errors: [
|
||||
{
|
||||
error: {
|
||||
message:
|
||||
'To create a rule, the index must exist first. Index .siem-signals-default does not exist',
|
||||
status_code: 409,
|
||||
},
|
||||
rule_id: 'rule-1',
|
||||
},
|
||||
],
|
||||
success: false,
|
||||
success_count: 0,
|
||||
});
|
||||
expect(statusCode).toEqual(200);
|
||||
});
|
||||
});
|
||||
|
||||
describe('payload', () => {
|
||||
test('returns 400 if file extension type is not .ndjson', async () => {
|
||||
const requestPayload = getSimpleRuleAsMultipartContent(['rule-1'], false);
|
||||
const { statusCode, payload } = await server.inject(getImportRulesRequest(requestPayload));
|
||||
const parsed: ImportSuccessError = JSON.parse(payload);
|
||||
|
||||
expect(parsed).toEqual({
|
||||
message: 'Invalid file extension .json',
|
||||
status_code: 400,
|
||||
});
|
||||
expect(statusCode).toEqual(400);
|
||||
});
|
||||
});
|
||||
|
||||
describe('single rule import', () => {
|
||||
test('returns 200 if rule imported successfully', async () => {
|
||||
alertsClient.find.mockResolvedValue(getFindResult());
|
||||
alertsClient.get.mockResolvedValue(getResult());
|
||||
actionsClient.create.mockResolvedValue(createActionResult());
|
||||
alertsClient.create.mockResolvedValue(getResult());
|
||||
savedObjectsClient.find.mockResolvedValue(getFindResultStatus());
|
||||
|
||||
const requestPayload = getSimpleRuleAsMultipartContent(['rule-1']);
|
||||
const { statusCode, payload } = await server.inject(getImportRulesRequest(requestPayload));
|
||||
const parsed: ImportSuccessError = JSON.parse(payload);
|
||||
|
||||
expect(parsed).toEqual({
|
||||
errors: [],
|
||||
success: true,
|
||||
success_count: 1,
|
||||
});
|
||||
expect(statusCode).toEqual(200);
|
||||
});
|
||||
|
||||
test('returns reported conflict if error parsing rule', async () => {
|
||||
const multipartPayload =
|
||||
`--${TEST_BOUNDARY}\r\n` +
|
||||
`Content-Disposition: form-data; name="file"; filename="rules.ndjson"\r\n` +
|
||||
'Content-Type: application/octet-stream\r\n' +
|
||||
'\r\n' +
|
||||
`${UNPARSABLE_LINE}\r\n` +
|
||||
`--${TEST_BOUNDARY}--\r\n`;
|
||||
|
||||
alertsClient.find.mockResolvedValue(getFindResult());
|
||||
|
||||
const requestPayload = Buffer.from(multipartPayload);
|
||||
const { statusCode, payload } = await server.inject(getImportRulesRequest(requestPayload));
|
||||
const parsed: ImportSuccessError = JSON.parse(payload);
|
||||
|
||||
expect(parsed).toEqual({
|
||||
errors: [
|
||||
{
|
||||
error: {
|
||||
message: 'Unexpected token : in JSON at position 8',
|
||||
status_code: 400,
|
||||
},
|
||||
rule_id: '(unknown)',
|
||||
},
|
||||
],
|
||||
success: false,
|
||||
success_count: 0,
|
||||
});
|
||||
expect(statusCode).toEqual(200);
|
||||
});
|
||||
|
||||
describe('rule with existing rule_id', () => {
|
||||
test('returns with reported conflict if `overwrite` is set to `false`', async () => {
|
||||
alertsClient.find.mockResolvedValue(getFindResult());
|
||||
|
||||
const requestPayload = getSimpleRuleAsMultipartContent(['rule-1']);
|
||||
const { statusCode, payload } = await server.inject(getImportRulesRequest(requestPayload));
|
||||
const parsed: ImportSuccessError = JSON.parse(payload);
|
||||
|
||||
expect(parsed).toEqual({
|
||||
errors: [],
|
||||
success: true,
|
||||
success_count: 1,
|
||||
});
|
||||
expect(statusCode).toEqual(200);
|
||||
|
||||
alertsClient.find.mockResolvedValue(getFindResultWithSingleHit());
|
||||
alertsClient.get.mockResolvedValue(getResult());
|
||||
|
||||
const { statusCode: statusCodeRequest2, payload: payloadRequest2 } = await server.inject(
|
||||
getImportRulesRequest(requestPayload)
|
||||
);
|
||||
const parsedRequest2: ImportSuccessError = JSON.parse(payloadRequest2);
|
||||
|
||||
expect(parsedRequest2).toEqual({
|
||||
errors: [
|
||||
{
|
||||
error: {
|
||||
message: 'rule_id: "rule-1" already exists',
|
||||
status_code: 409,
|
||||
},
|
||||
rule_id: 'rule-1',
|
||||
},
|
||||
],
|
||||
success: false,
|
||||
success_count: 0,
|
||||
});
|
||||
expect(statusCodeRequest2).toEqual(200);
|
||||
});
|
||||
|
||||
test('returns with NO reported conflict if `overwrite` is set to `true`', async () => {
|
||||
alertsClient.find.mockResolvedValue(getFindResult());
|
||||
|
||||
const requestPayload = getSimpleRuleAsMultipartContent(['rule-1']);
|
||||
const { statusCode, payload } = await server.inject(getImportRulesRequest(requestPayload));
|
||||
const parsed: ImportSuccessError = JSON.parse(payload);
|
||||
|
||||
expect(parsed).toEqual({
|
||||
errors: [],
|
||||
success: true,
|
||||
success_count: 1,
|
||||
});
|
||||
expect(statusCode).toEqual(200);
|
||||
|
||||
alertsClient.find.mockResolvedValue(getFindResultWithSingleHit());
|
||||
alertsClient.get.mockResolvedValue(getResult());
|
||||
|
||||
const { statusCode: statusCodeRequest2, payload: payloadRequest2 } = await server.inject(
|
||||
getImportRulesRequestOverwriteTrue(requestPayload)
|
||||
);
|
||||
const parsedRequest2: ImportSuccessError = JSON.parse(payloadRequest2);
|
||||
|
||||
expect(parsedRequest2).toEqual({
|
||||
errors: [],
|
||||
success: true,
|
||||
success_count: 1,
|
||||
});
|
||||
expect(statusCodeRequest2).toEqual(200);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('multi rule import', () => {
|
||||
test('returns 200 if all rules imported successfully', async () => {
|
||||
alertsClient.find.mockResolvedValue(getFindResult());
|
||||
|
||||
const requestPayload = getSimpleRuleAsMultipartContent(['rule-1', 'rule-2']);
|
||||
const { statusCode, payload } = await server.inject(getImportRulesRequest(requestPayload));
|
||||
const parsed: ImportSuccessError = JSON.parse(payload);
|
||||
|
||||
expect(parsed).toEqual({
|
||||
errors: [],
|
||||
success: true,
|
||||
success_count: 2,
|
||||
});
|
||||
expect(statusCode).toEqual(200);
|
||||
});
|
||||
|
||||
test('returns 200 with reported conflict if error parsing rule', async () => {
|
||||
const multipartPayload =
|
||||
`--${TEST_BOUNDARY}\r\n` +
|
||||
`Content-Disposition: form-data; name="file"; filename="rules.ndjson"\r\n` +
|
||||
'Content-Type: application/octet-stream\r\n' +
|
||||
'\r\n' +
|
||||
`${UNPARSABLE_LINE}\r\n` +
|
||||
`${JSON.stringify(getSimpleRule('rule-2'))}\r\n` +
|
||||
`--${TEST_BOUNDARY}--\r\n`;
|
||||
|
||||
alertsClient.find.mockResolvedValue(getFindResult());
|
||||
|
||||
const requestPayload = Buffer.from(multipartPayload);
|
||||
const { statusCode, payload } = await server.inject(getImportRulesRequest(requestPayload));
|
||||
const parsed: ImportSuccessError = JSON.parse(payload);
|
||||
|
||||
expect(parsed).toEqual({
|
||||
errors: [
|
||||
{
|
||||
error: {
|
||||
message: 'Unexpected token : in JSON at position 8',
|
||||
status_code: 400,
|
||||
},
|
||||
rule_id: '(unknown)',
|
||||
},
|
||||
],
|
||||
success: false,
|
||||
success_count: 1,
|
||||
});
|
||||
expect(statusCode).toEqual(200);
|
||||
});
|
||||
|
||||
describe('rules with matching rule_id', () => {
|
||||
test('returns with reported conflict if `overwrite` is set to `false`', async () => {
|
||||
alertsClient.find.mockResolvedValue(getFindResult());
|
||||
alertsClient.get.mockResolvedValue(getResult());
|
||||
|
||||
const requestPayload = getSimpleRuleAsMultipartContent(['rule-1', 'rule-1']);
|
||||
const { statusCode, payload } = await server.inject(getImportRulesRequest(requestPayload));
|
||||
const parsed: ImportSuccessError = JSON.parse(payload);
|
||||
|
||||
expect(parsed).toEqual({
|
||||
errors: [
|
||||
{
|
||||
error: {
|
||||
message: 'More than one rule with rule-id: "rule-1" found',
|
||||
status_code: 400,
|
||||
},
|
||||
rule_id: 'rule-1',
|
||||
},
|
||||
],
|
||||
success: false,
|
||||
success_count: 1,
|
||||
});
|
||||
expect(statusCode).toEqual(200);
|
||||
});
|
||||
|
||||
test('returns with NO reported conflict if `overwrite` is set to `true`', async () => {
|
||||
alertsClient.find.mockResolvedValue(getFindResult());
|
||||
alertsClient.get.mockResolvedValue(getResult());
|
||||
|
||||
const requestPayload = getSimpleRuleAsMultipartContent(['rule-1', 'rule-1']);
|
||||
const { statusCode, payload } = await server.inject(
|
||||
getImportRulesRequestOverwriteTrue(requestPayload)
|
||||
);
|
||||
const parsed: ImportSuccessError = JSON.parse(payload);
|
||||
|
||||
expect(parsed).toEqual({
|
||||
errors: [],
|
||||
success: true,
|
||||
success_count: 1,
|
||||
});
|
||||
expect(statusCode).toEqual(200);
|
||||
});
|
||||
});
|
||||
|
||||
describe('rules with existing rule_id', () => {
|
||||
test('returns with reported conflict if `overwrite` is set to `false`', async () => {
|
||||
alertsClient.find.mockResolvedValue(getFindResult());
|
||||
|
||||
const requestPayload = getSimpleRuleAsMultipartContent(['rule-1']);
|
||||
const { statusCode, payload } = await server.inject(getImportRulesRequest(requestPayload));
|
||||
const parsedResult: ImportSuccessError = JSON.parse(payload);
|
||||
|
||||
expect(parsedResult).toEqual({
|
||||
errors: [],
|
||||
success: true,
|
||||
success_count: 1,
|
||||
});
|
||||
expect(statusCode).toEqual(200);
|
||||
|
||||
alertsClient.find.mockResolvedValueOnce(getFindResultWithSingleHit());
|
||||
alertsClient.get.mockResolvedValue(getResult());
|
||||
|
||||
const requestPayload2 = getSimpleRuleAsMultipartContent(['rule-1', 'rule-2', 'rule-3']);
|
||||
const { statusCode: statusCodeRequest2, payload: payloadRequest2 } = await server.inject(
|
||||
getImportRulesRequest(requestPayload2)
|
||||
);
|
||||
const parsed: ImportSuccessError = JSON.parse(payloadRequest2);
|
||||
|
||||
expect(parsed).toEqual({
|
||||
errors: [
|
||||
{
|
||||
error: {
|
||||
message: 'rule_id: "rule-1" already exists',
|
||||
status_code: 409,
|
||||
},
|
||||
rule_id: 'rule-1',
|
||||
},
|
||||
],
|
||||
success: false,
|
||||
success_count: 2,
|
||||
});
|
||||
expect(statusCodeRequest2).toEqual(200);
|
||||
});
|
||||
|
||||
test('returns 200 with NO reported conflict if `overwrite` is set to `true`', async () => {
|
||||
alertsClient.find.mockResolvedValue(getFindResult());
|
||||
|
||||
const requestPayload = getSimpleRuleAsMultipartContent(['rule-1']);
|
||||
const { statusCode, payload } = await server.inject(getImportRulesRequest(requestPayload));
|
||||
const parsedResult: ImportSuccessError = JSON.parse(payload);
|
||||
|
||||
expect(parsedResult).toEqual({
|
||||
errors: [],
|
||||
success: true,
|
||||
success_count: 1,
|
||||
});
|
||||
expect(statusCode).toEqual(200);
|
||||
|
||||
alertsClient.find.mockResolvedValueOnce(getFindResultWithSingleHit());
|
||||
alertsClient.get.mockResolvedValue(getResult());
|
||||
|
||||
const requestPayload2 = getSimpleRuleAsMultipartContent(['rule-1', 'rule-2', 'rule-3']);
|
||||
const { statusCode: statusCodeRequest2, payload: payloadRequest2 } = await server.inject(
|
||||
getImportRulesRequestOverwriteTrue(requestPayload2)
|
||||
);
|
||||
const parsed: ImportSuccessError = JSON.parse(payloadRequest2);
|
||||
|
||||
expect(parsed).toEqual({
|
||||
errors: [],
|
||||
success: true,
|
||||
success_count: 3,
|
||||
});
|
||||
expect(statusCodeRequest2).toEqual(200);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -7,7 +7,6 @@
|
|||
import Hapi from 'hapi';
|
||||
import { chunk, isEmpty, isFunction } from 'lodash/fp';
|
||||
import { extname } from 'path';
|
||||
import uuid from 'uuid';
|
||||
import { createPromiseFromStreams } from '../../../../../../../../../src/legacy/utils/streams';
|
||||
import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants';
|
||||
import { createRules } from '../../rules/create_rules';
|
||||
|
@ -25,6 +24,7 @@ import { createRulesStreamFromNdJson } from '../../rules/create_rules_stream_fro
|
|||
import { ImportRuleAlertRest } from '../../types';
|
||||
import { patchRules } from '../../rules/patch_rules';
|
||||
import { importRulesQuerySchema, importRulesPayloadSchema } from '../schemas/import_rules_schema';
|
||||
import { getTupleDuplicateErrorsAndUniqueRules } from './utils';
|
||||
|
||||
type PromiseFromStreams = ImportRuleAlertRest | Error;
|
||||
|
||||
|
@ -74,25 +74,9 @@ export const createImportRulesRoute = (server: ServerFacade): Hapi.ServerRoute =
|
|||
const objectLimit = server.config().get<number>('savedObjects.maxImportExportSize');
|
||||
const readStream = createRulesStreamFromNdJson(request.payload.file, objectLimit);
|
||||
const parsedObjects = await createPromiseFromStreams<PromiseFromStreams[]>([readStream]);
|
||||
const uniqueParsedObjects = Array.from(
|
||||
parsedObjects
|
||||
.reduce(
|
||||
(acc, parsedRule) => {
|
||||
if (parsedRule instanceof Error) {
|
||||
acc.set(uuid.v4(), parsedRule);
|
||||
} else {
|
||||
const { rule_id: ruleId } = parsedRule;
|
||||
if (ruleId != null) {
|
||||
acc.set(ruleId, parsedRule);
|
||||
} else {
|
||||
acc.set(uuid.v4(), parsedRule);
|
||||
}
|
||||
}
|
||||
return acc;
|
||||
}, // using map (preserves ordering)
|
||||
new Map()
|
||||
)
|
||||
.values()
|
||||
const [duplicateIdErrors, uniqueParsedObjects] = getTupleDuplicateErrorsAndUniqueRules(
|
||||
parsedObjects,
|
||||
request.query.overwrite
|
||||
);
|
||||
|
||||
const chunkParseObjects = chunk(CHUNK_PARSED_OBJECT_SIZE, uniqueParsedObjects);
|
||||
|
@ -247,7 +231,11 @@ export const createImportRulesRoute = (server: ServerFacade): Hapi.ServerRoute =
|
|||
return [...accum, importsWorkerPromise];
|
||||
}, [])
|
||||
);
|
||||
importRuleResponse = [...importRuleResponse, ...newImportRuleResponse];
|
||||
importRuleResponse = [
|
||||
...duplicateIdErrors,
|
||||
...importRuleResponse,
|
||||
...newImportRuleResponse,
|
||||
];
|
||||
}
|
||||
|
||||
const errorsResp = importRuleResponse.filter(resp => !isEmpty(resp.error));
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Readable } from 'stream';
|
||||
import {
|
||||
transformAlertToRule,
|
||||
getIdError,
|
||||
|
@ -16,12 +16,18 @@ import {
|
|||
transformAlertsToRules,
|
||||
transformOrImportError,
|
||||
getDuplicates,
|
||||
getTupleDuplicateErrorsAndUniqueRules,
|
||||
} from './utils';
|
||||
import { getResult } from '../__mocks__/request_responses';
|
||||
import { INTERNAL_IDENTIFIER } from '../../../../../common/constants';
|
||||
import { OutputRuleAlertRest } from '../../types';
|
||||
import { OutputRuleAlertRest, ImportRuleAlertRest } from '../../types';
|
||||
import { BulkError, ImportSuccessError } from '../utils';
|
||||
import { sampleRule } from '../../signals/__mocks__/es_results';
|
||||
import { getSimpleRule } from '../__mocks__/utils';
|
||||
import { createRulesStreamFromNdJson } from '../../rules/create_rules_stream_from_ndjson';
|
||||
import { createPromiseFromStreams } from '../../../../../../../../../src/legacy/utils/streams';
|
||||
|
||||
type PromiseFromStreams = ImportRuleAlertRest | Error;
|
||||
|
||||
describe('utils', () => {
|
||||
describe('transformAlertToRule', () => {
|
||||
|
@ -1224,4 +1230,95 @@ describe('utils', () => {
|
|||
expect(output).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTupleDuplicateErrorsAndUniqueRules', () => {
|
||||
test('returns tuple of empty duplicate errors array and rule array with instance of Syntax Error when imported rule contains parse error', async () => {
|
||||
const multipartPayload =
|
||||
'{"name"::"Simple Rule Query","description":"Simple Rule Query","risk_score":1,"rule_id":"rule-1","severity":"high","type":"query","query":"user.name: root or user.name: admin"}\n';
|
||||
const ndJsonStream = new Readable({
|
||||
read() {
|
||||
this.push(multipartPayload);
|
||||
this.push(null);
|
||||
},
|
||||
});
|
||||
const rulesObjectsStream = createRulesStreamFromNdJson(ndJsonStream, 1000);
|
||||
const parsedObjects = await createPromiseFromStreams<PromiseFromStreams[]>([
|
||||
rulesObjectsStream,
|
||||
]);
|
||||
const [errors, output] = getTupleDuplicateErrorsAndUniqueRules(parsedObjects, false);
|
||||
const isInstanceOfError = output[0] instanceof Error;
|
||||
|
||||
expect(isInstanceOfError).toEqual(true);
|
||||
expect(errors.length).toEqual(0);
|
||||
});
|
||||
|
||||
test('returns tuple of duplicate conflict error and single rule when rules with matching rule-ids passed in and `overwrite` is false', async () => {
|
||||
const rule = getSimpleRule('rule-1');
|
||||
const rule2 = getSimpleRule('rule-1');
|
||||
const ndJsonStream = new Readable({
|
||||
read() {
|
||||
this.push(`${JSON.stringify(rule)}\n`);
|
||||
this.push(`${JSON.stringify(rule2)}\n`);
|
||||
this.push(null);
|
||||
},
|
||||
});
|
||||
const rulesObjectsStream = createRulesStreamFromNdJson(ndJsonStream, 1000);
|
||||
const parsedObjects = await createPromiseFromStreams<PromiseFromStreams[]>([
|
||||
rulesObjectsStream,
|
||||
]);
|
||||
const [errors, output] = getTupleDuplicateErrorsAndUniqueRules(parsedObjects, false);
|
||||
|
||||
expect(output.length).toEqual(1);
|
||||
expect(errors).toEqual([
|
||||
{
|
||||
error: {
|
||||
message: 'More than one rule with rule-id: "rule-1" found',
|
||||
status_code: 400,
|
||||
},
|
||||
rule_id: 'rule-1',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('returns tuple of empty duplicate errors array and single rule when rules with matching rule-ids passed in and `overwrite` is true', async () => {
|
||||
const rule = getSimpleRule('rule-1');
|
||||
const rule2 = getSimpleRule('rule-1');
|
||||
const ndJsonStream = new Readable({
|
||||
read() {
|
||||
this.push(`${JSON.stringify(rule)}\n`);
|
||||
this.push(`${JSON.stringify(rule2)}\n`);
|
||||
this.push(null);
|
||||
},
|
||||
});
|
||||
const rulesObjectsStream = createRulesStreamFromNdJson(ndJsonStream, 1000);
|
||||
const parsedObjects = await createPromiseFromStreams<PromiseFromStreams[]>([
|
||||
rulesObjectsStream,
|
||||
]);
|
||||
const [errors, output] = getTupleDuplicateErrorsAndUniqueRules(parsedObjects, true);
|
||||
|
||||
expect(output.length).toEqual(1);
|
||||
expect(errors.length).toEqual(0);
|
||||
});
|
||||
|
||||
test('returns tuple of empty duplicate errors array and single rule when rules without a rule-id is passed in', async () => {
|
||||
const simpleRule = getSimpleRule();
|
||||
delete simpleRule.rule_id;
|
||||
const multipartPayload = `${JSON.stringify(simpleRule)}\n`;
|
||||
const ndJsonStream = new Readable({
|
||||
read() {
|
||||
this.push(multipartPayload);
|
||||
this.push(null);
|
||||
},
|
||||
});
|
||||
const rulesObjectsStream = createRulesStreamFromNdJson(ndJsonStream, 1000);
|
||||
const parsedObjects = await createPromiseFromStreams<PromiseFromStreams[]>([
|
||||
rulesObjectsStream,
|
||||
]);
|
||||
const [errors, output] = getTupleDuplicateErrorsAndUniqueRules(parsedObjects, false);
|
||||
const isInstanceOfError = output[0] instanceof Error;
|
||||
|
||||
expect(isInstanceOfError).toEqual(true);
|
||||
expect(errors.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
import { pickBy } from 'lodash/fp';
|
||||
import { Dictionary } from 'lodash';
|
||||
import { SavedObject } from 'kibana/server';
|
||||
import uuid from 'uuid';
|
||||
import { INTERNAL_IDENTIFIER } from '../../../../../common/constants';
|
||||
import {
|
||||
RuleAlertType,
|
||||
|
@ -17,7 +18,7 @@ import {
|
|||
isRuleStatusFindTypes,
|
||||
isRuleStatusSavedObjectType,
|
||||
} from '../../rules/types';
|
||||
import { OutputRuleAlertRest } from '../../types';
|
||||
import { OutputRuleAlertRest, ImportRuleAlertRest } from '../../types';
|
||||
import {
|
||||
createBulkErrorObject,
|
||||
BulkError,
|
||||
|
@ -27,6 +28,8 @@ import {
|
|||
OutputError,
|
||||
} from '../utils';
|
||||
|
||||
type PromiseFromStreams = ImportRuleAlertRest | Error;
|
||||
|
||||
export const getIdError = ({
|
||||
id,
|
||||
ruleId,
|
||||
|
@ -224,3 +227,41 @@ export const getDuplicates = (lodashDict: Dictionary<number>): string[] => {
|
|||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
export const getTupleDuplicateErrorsAndUniqueRules = (
|
||||
rules: PromiseFromStreams[],
|
||||
isOverwrite: boolean
|
||||
): [BulkError[], PromiseFromStreams[]] => {
|
||||
const { errors, rulesAcc } = rules.reduce(
|
||||
(acc, parsedRule) => {
|
||||
if (parsedRule instanceof Error) {
|
||||
acc.rulesAcc.set(uuid.v4(), parsedRule);
|
||||
} else {
|
||||
const { rule_id: ruleId } = parsedRule;
|
||||
if (ruleId != null) {
|
||||
if (acc.rulesAcc.has(ruleId) && !isOverwrite) {
|
||||
acc.errors.set(
|
||||
uuid.v4(),
|
||||
createBulkErrorObject({
|
||||
ruleId,
|
||||
statusCode: 400,
|
||||
message: `More than one rule with rule-id: "${ruleId}" found`,
|
||||
})
|
||||
);
|
||||
}
|
||||
acc.rulesAcc.set(ruleId, parsedRule);
|
||||
} else {
|
||||
acc.rulesAcc.set(uuid.v4(), parsedRule);
|
||||
}
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, // using map (preserves ordering)
|
||||
{
|
||||
errors: new Map<string, BulkError>(),
|
||||
rulesAcc: new Map<string, PromiseFromStreams>(),
|
||||
}
|
||||
);
|
||||
|
||||
return [Array.from(errors.values()), Array.from(rulesAcc.values())];
|
||||
};
|
||||
|
|
|
@ -115,8 +115,16 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
.expect(200);
|
||||
|
||||
expect(body).to.eql({
|
||||
errors: [], // TODO: This should have a conflict within it as an error rather than an empty array
|
||||
success: true,
|
||||
errors: [
|
||||
{
|
||||
error: {
|
||||
message: 'More than one rule with rule-id: "rule-1" found',
|
||||
status_code: 400,
|
||||
},
|
||||
rule_id: 'rule-1',
|
||||
},
|
||||
],
|
||||
success: false,
|
||||
success_count: 1,
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue