[7.6] [SIEM] [Detections Engine] Import rules unit tests (#57466) (#58019)

* [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:
Yara Tercero 2020-02-20 17:57:22 -05:00 committed by GitHub
parent 0a87f2dfd2
commit d18b21be48
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 669 additions and 26 deletions

View file

@ -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'),

View file

@ -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`,

View file

@ -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);
};

View file

@ -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);
});
});
});
});

View file

@ -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));

View file

@ -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);
});
});
});

View file

@ -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())];
};

View file

@ -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,
});
});