mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Security Solutions] Critical bug fix to make error messages about missing connections clearer for the end user. (#116490) (#116803)
## Summary Fixes issue see on this comment: https://github.com/elastic/kibana/issues/116336#issuecomment-952159636 * Removes legacy toaster component * Adds newer toaster component * Removes issue with the deps array within ReactJS * Adds utility to give a better network error message to the end user. * This does effect the timeline component since it shares the same import common component. * Adds a count of how many rules/timeline items have failed imports * These error toasters mimic Kibana core's error toaster error message and UI/UX * Adds e2e tests for imports with actions and error messages for them. ## Rules import error messages now Before for small toaster: <img width="417" alt="Screen Shot 2021-10-26 at 6 03 25 PM" src="https://user-images.githubusercontent.com/1151048/139132586-3cf77c73-53ac-4066-b01f-2e91ef2da111.png"> After for small toaster for different error conditions: <img width="358" alt="Screen Shot 2021-10-26 at 6 00 24 PM" src="https://user-images.githubusercontent.com/1151048/139132679-2eeb1ed3-9f6e-4766-a8ed-8804ce3e6963.png"> <img width="396" alt="Screen Shot 2021-10-26 at 6 01 00 PM" src="https://user-images.githubusercontent.com/1151048/139132742-750cd937-f401-44e8-9a10-c21410073b5d.png"> <img width="379" alt="Screen Shot 2021-10-26 at 6 02 29 PM" src="https://user-images.githubusercontent.com/1151048/139132766-21b58bea-7f46-43a6-a0e9-f01632958eab.png"> Before for when you click "See the full error": <img width="817" alt="Screen Shot 2021-10-26 at 5 58 47 PM" src="https://user-images.githubusercontent.com/1151048/139132980-de1942d6-7b03-4c08-b34a-1fc4a22d5207.png"> After for when you click "See the full error": <img width="838" alt="Screen Shot 2021-10-27 at 1 48 16 PM" src="https://user-images.githubusercontent.com/1151048/139136581-af1e331e-ed77-4338-8fb0-c2457acd135f.png"> <img width="802" alt="Screen Shot 2021-10-27 at 1 26 31 PM" src="https://user-images.githubusercontent.com/1151048/139135083-9ca56940-30a8-4f83-9355-312307172834.png"> ## timeline Before: <img width="441" alt="Screen Shot 2021-10-27 at 1 19 00 PM" src="https://user-images.githubusercontent.com/1151048/139136614-8360d6a6-d182-413e-b5d9-b18e3d70dc24.png"> <img width="827" alt="Screen Shot 2021-10-27 at 1 19 08 PM" src="https://user-images.githubusercontent.com/1151048/139136637-f9203ac2-0eea-4a77-9c53-ac2c20ab32e0.png"> After: <img width="408" alt="Screen Shot 2021-10-27 at 1 49 45 PM" src="https://user-images.githubusercontent.com/1151048/139136758-7532a8ba-6d73-45e2-adbb-6756ee997289.png"> <img width="820" alt="Screen Shot 2021-10-27 at 1 49 50 PM" src="https://user-images.githubusercontent.com/1151048/139136774-26d4a8a2-caf0-4c6f-94d3-a6cd92b79f5f.png"> ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios Co-authored-by: Frank Hassanabad <frank.hassanabad@elastic.co>
This commit is contained in:
parent
4e9b77906c
commit
e4c93a87a0
17 changed files with 866 additions and 87 deletions
|
@ -19,7 +19,7 @@ describe('ImportDataModal', () => {
|
|||
importComplete={jest.fn()}
|
||||
checkBoxLabel="checkBoxLabel"
|
||||
description="description"
|
||||
errorMessage="errorMessage"
|
||||
errorMessage={jest.fn()}
|
||||
failedDetailed={jest.fn()}
|
||||
importData={jest.fn()}
|
||||
showCheckBox={true}
|
||||
|
|
|
@ -23,23 +23,16 @@ import React, { useCallback, useState } from 'react';
|
|||
import {
|
||||
ImportDataResponse,
|
||||
ImportDataProps,
|
||||
ImportRulesResponseError,
|
||||
ImportResponseError,
|
||||
} from '../../../detections/containers/detection_engine/rules';
|
||||
import {
|
||||
displayErrorToast,
|
||||
displaySuccessToast,
|
||||
useStateToaster,
|
||||
errorToToaster,
|
||||
} from '../toasters';
|
||||
import { useAppToasts } from '../../hooks/use_app_toasts';
|
||||
import * as i18n from './translations';
|
||||
|
||||
interface ImportDataModalProps {
|
||||
checkBoxLabel: string;
|
||||
closeModal: () => void;
|
||||
description: string;
|
||||
errorMessage: string;
|
||||
failedDetailed: (id: string, statusCode: number, message: string) => string;
|
||||
errorMessage: (totalCount: number) => string;
|
||||
failedDetailed: (message: string) => string;
|
||||
importComplete: () => void;
|
||||
importData: (arg: ImportDataProps) => Promise<ImportDataResponse>;
|
||||
showCheckBox: boolean;
|
||||
|
@ -50,12 +43,6 @@ interface ImportDataModalProps {
|
|||
title: string;
|
||||
}
|
||||
|
||||
const isImportRulesResponseError = (
|
||||
error: ImportRulesResponseError | ImportResponseError
|
||||
): error is ImportRulesResponseError => {
|
||||
return (error as ImportRulesResponseError).rule_id !== undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Modal component for importing Rules from a json file
|
||||
*/
|
||||
|
@ -77,7 +64,7 @@ export const ImportDataModalComponent = ({
|
|||
const [selectedFiles, setSelectedFiles] = useState<FileList | null>(null);
|
||||
const [isImporting, setIsImporting] = useState(false);
|
||||
const [overwrite, setOverwrite] = useState(false);
|
||||
const [, dispatchToaster] = useStateToaster();
|
||||
const { addError, addSuccess } = useAppToasts();
|
||||
|
||||
const cleanupAndCloseModal = useCallback(() => {
|
||||
setIsImporting(false);
|
||||
|
@ -97,31 +84,39 @@ export const ImportDataModalComponent = ({
|
|||
signal: abortCtrl.signal,
|
||||
});
|
||||
|
||||
// TODO: Improve error toast details for better debugging failed imports
|
||||
// e.g. When success == true && success_count === 0 that means no rules were overwritten, etc
|
||||
if (importResponse.success) {
|
||||
displaySuccessToast(successMessage(importResponse.success_count), dispatchToaster);
|
||||
addSuccess(successMessage(importResponse.success_count));
|
||||
}
|
||||
if (importResponse.errors.length > 0) {
|
||||
const formattedErrors = importResponse.errors.map((e) =>
|
||||
failedDetailed(
|
||||
isImportRulesResponseError(e) ? e.rule_id : e.id,
|
||||
e.error.status_code,
|
||||
e.error.message
|
||||
)
|
||||
const formattedErrors = importResponse.errors.map((e) => failedDetailed(e.error.message));
|
||||
const error: Error & { raw_network_error?: object } = new Error(
|
||||
formattedErrors.join('. ')
|
||||
);
|
||||
displayErrorToast(errorMessage, formattedErrors, dispatchToaster);
|
||||
error.stack = undefined;
|
||||
error.name = 'Network errors';
|
||||
error.raw_network_error = importResponse;
|
||||
addError(error, { title: errorMessage(importResponse.errors.length) });
|
||||
}
|
||||
|
||||
importComplete();
|
||||
cleanupAndCloseModal();
|
||||
} catch (error) {
|
||||
cleanupAndCloseModal();
|
||||
errorToToaster({ title: errorMessage, error, dispatchToaster });
|
||||
addError(error, { title: errorMessage(1) });
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedFiles, overwrite]);
|
||||
}, [
|
||||
selectedFiles,
|
||||
overwrite,
|
||||
addError,
|
||||
addSuccess,
|
||||
cleanupAndCloseModal,
|
||||
errorMessage,
|
||||
failedDetailed,
|
||||
importComplete,
|
||||
importData,
|
||||
successMessage,
|
||||
]);
|
||||
|
||||
const handleCloseModal = useCallback(() => {
|
||||
setSelectedFiles(null);
|
||||
|
|
|
@ -579,19 +579,21 @@ export const SUCCESSFULLY_IMPORTED_RULES = (totalRules: number) =>
|
|||
}
|
||||
);
|
||||
|
||||
export const IMPORT_FAILED = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.components.importRuleModal.importFailedTitle',
|
||||
{
|
||||
defaultMessage: 'Failed to import rules',
|
||||
}
|
||||
);
|
||||
export const IMPORT_FAILED = (totalRules: number) =>
|
||||
i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.components.importRuleModal.importFailedTitle',
|
||||
{
|
||||
values: { totalRules },
|
||||
defaultMessage: 'Failed to import {totalRules} {totalRules, plural, =1 {rule} other {rules}}',
|
||||
}
|
||||
);
|
||||
|
||||
export const IMPORT_FAILED_DETAILED = (ruleId: string, statusCode: number, message: string) =>
|
||||
export const IMPORT_FAILED_DETAILED = (message: string) =>
|
||||
i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.components.importRuleModal.importFailedDetailedTitle',
|
||||
{
|
||||
values: { ruleId, statusCode, message },
|
||||
defaultMessage: 'Rule ID: {ruleId}\n Status Code: {statusCode}\n Message: {message}',
|
||||
values: { message },
|
||||
defaultMessage: '{message}',
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -373,12 +373,15 @@ export const SUCCESSFULLY_IMPORTED_TIMELINES = (totalCount: number) =>
|
|||
}
|
||||
);
|
||||
|
||||
export const IMPORT_FAILED = i18n.translate(
|
||||
'xpack.securitySolution.timelines.components.importTimelineModal.importFailedTitle',
|
||||
{
|
||||
defaultMessage: 'Failed to import',
|
||||
}
|
||||
);
|
||||
export const IMPORT_FAILED = (totalTimelines: number) =>
|
||||
i18n.translate(
|
||||
'xpack.securitySolution.timelines.components.importTimelineModal.importFailedTitle',
|
||||
{
|
||||
values: { totalTimelines },
|
||||
defaultMessage:
|
||||
'Failed to import {totalTimelines} {totalTimelines, plural, =1 {rule} other {rules}}',
|
||||
}
|
||||
);
|
||||
|
||||
export const IMPORT_TIMELINE = i18n.translate(
|
||||
'xpack.securitySolution.timelines.components.importTimelineModal.importTitle',
|
||||
|
@ -387,11 +390,11 @@ export const IMPORT_TIMELINE = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const IMPORT_FAILED_DETAILED = (id: string, statusCode: number, message: string) =>
|
||||
export const IMPORT_FAILED_DETAILED = (message: string) =>
|
||||
i18n.translate(
|
||||
'xpack.securitySolution.timelines.components.importTimelineModal.importFailedDetailedTitle',
|
||||
{
|
||||
values: { id, statusCode, message },
|
||||
defaultMessage: 'Timeline ID: {id}\n Status Code: {statusCode}\n Message: {message}',
|
||||
values: { message },
|
||||
defaultMessage: '{message}',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -11,6 +11,7 @@ import { coreMock } from 'src/core/server/mocks';
|
|||
import { ActionsApiRequestHandlerContext } from '../../../../../../actions/server';
|
||||
import { AlertingApiRequestHandlerContext } from '../../../../../../alerting/server';
|
||||
import { rulesClientMock } from '../../../../../../alerting/server/mocks';
|
||||
import { actionsClientMock } from '../../../../../../actions/server/mocks';
|
||||
import { licensingMock } from '../../../../../../licensing/server/mocks';
|
||||
import { listMock } from '../../../../../../lists/server/mocks';
|
||||
import { ruleRegistryMocks } from '../../../../../../rule_registry/server/mocks';
|
||||
|
@ -44,6 +45,7 @@ const createMockClients = () => {
|
|||
exceptionListClient: listMock.getExceptionListClient(core.savedObjects.client),
|
||||
},
|
||||
rulesClient: rulesClientMock.create(),
|
||||
actionsClient: actionsClientMock.create(),
|
||||
ruleDataService: ruleRegistryMocks.createRuleDataService(),
|
||||
|
||||
config: createMockConfig(),
|
||||
|
@ -65,7 +67,9 @@ const createRequestContextMock = (
|
|||
return {
|
||||
core: clients.core,
|
||||
securitySolution: createSecuritySolutionRequestContextMock(clients),
|
||||
actions: {} as unknown as jest.Mocked<ActionsApiRequestHandlerContext>,
|
||||
actions: {
|
||||
getActionsClient: jest.fn(() => clients.actionsClient),
|
||||
} as unknown as jest.Mocked<ActionsApiRequestHandlerContext>,
|
||||
alerting: {
|
||||
getRulesClient: jest.fn(() => clients.rulesClient),
|
||||
} as unknown as jest.Mocked<AlertingApiRequestHandlerContext>,
|
||||
|
|
|
@ -53,6 +53,7 @@ describe.each([
|
|||
clients.rulesClient.update.mockResolvedValue(
|
||||
getAlertMock(isRuleRegistryEnabled, getQueryRuleParams())
|
||||
);
|
||||
clients.actionsClient.getAll.mockResolvedValue([]);
|
||||
context.core.elasticsearch.client.asCurrentUser.search.mockResolvedValue(
|
||||
elasticsearchClientMock.createSuccessTransportRequestPromise(getBasicEmptySearchResponse())
|
||||
);
|
||||
|
@ -77,21 +78,6 @@ describe.each([
|
|||
status_code: 500,
|
||||
});
|
||||
});
|
||||
|
||||
test('returns 404 if alertClient is not available on the route', async () => {
|
||||
context.alerting.getRulesClient = jest.fn();
|
||||
const response = await server.inject(request, context);
|
||||
expect(response.status).toEqual(404);
|
||||
expect(response.body).toEqual({ message: 'Not Found', status_code: 404 });
|
||||
});
|
||||
|
||||
it('returns 404 if siem client is unavailable', async () => {
|
||||
const { securitySolution, ...contextWithoutSecuritySolution } = context;
|
||||
// @ts-expect-error
|
||||
const response = await server.inject(request, contextWithoutSecuritySolution);
|
||||
expect(response.status).toEqual(404);
|
||||
expect(response.body).toEqual({ message: 'Not Found', status_code: 404 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('unhappy paths', () => {
|
||||
|
|
|
@ -41,7 +41,7 @@ import {
|
|||
|
||||
import { patchRules } from '../../rules/patch_rules';
|
||||
import { legacyMigrate } from '../../rules/utils';
|
||||
import { getTupleDuplicateErrorsAndUniqueRules } from './utils';
|
||||
import { getTupleDuplicateErrorsAndUniqueRules, getInvalidConnectors } from './utils';
|
||||
import { createRulesStreamFromNdJson } from '../../rules/create_rules_stream_from_ndjson';
|
||||
import { buildRouteValidation } from '../../../../utils/build_validation/route_validation';
|
||||
import { HapiReadableStream } from '../../rules/types';
|
||||
|
@ -78,14 +78,11 @@ export const importRulesRoute = (
|
|||
const siemResponse = buildSiemResponse(response);
|
||||
|
||||
try {
|
||||
const rulesClient = context.alerting?.getRulesClient();
|
||||
const rulesClient = context.alerting.getRulesClient();
|
||||
const actionsClient = context.actions.getActionsClient();
|
||||
const esClient = context.core.elasticsearch.client;
|
||||
const savedObjectsClient = context.core.savedObjects.client;
|
||||
const siemClient = context.securitySolution?.getAppClient();
|
||||
|
||||
if (!siemClient || !rulesClient) {
|
||||
return siemResponse.error({ statusCode: 404 });
|
||||
}
|
||||
const siemClient = context.securitySolution.getAppClient();
|
||||
|
||||
const mlAuthz = buildMlAuthz({
|
||||
license: context.licensing.license,
|
||||
|
@ -103,6 +100,7 @@ export const importRulesRoute = (
|
|||
body: `Invalid file extension ${fileExtension}`,
|
||||
});
|
||||
}
|
||||
|
||||
const signalsIndex = siemClient.getSignalsIndex();
|
||||
const indexExists = await getIndexExists(esClient.asCurrentUser, signalsIndex);
|
||||
if (!isRuleRegistryEnabled && !indexExists) {
|
||||
|
@ -118,14 +116,24 @@ export const importRulesRoute = (
|
|||
request.body.file as HapiReadableStream,
|
||||
...readStream,
|
||||
]);
|
||||
const [duplicateIdErrors, uniqueParsedObjects] = getTupleDuplicateErrorsAndUniqueRules(
|
||||
parsedObjects,
|
||||
request.query.overwrite
|
||||
|
||||
const [duplicateIdErrors, parsedObjectsWithoutDuplicateErrors] =
|
||||
getTupleDuplicateErrorsAndUniqueRules(parsedObjects, request.query.overwrite);
|
||||
|
||||
const [nonExistentActionErrors, uniqueParsedObjects] = await getInvalidConnectors(
|
||||
parsedObjectsWithoutDuplicateErrors,
|
||||
actionsClient
|
||||
);
|
||||
|
||||
const chunkParseObjects = chunk(CHUNK_PARSED_OBJECT_SIZE, uniqueParsedObjects);
|
||||
let importRuleResponse: ImportRuleResponse[] = [];
|
||||
|
||||
// If we had 100% errors and no successful rule could be imported we still have to output an error.
|
||||
// otherwise we would output we are success importing 0 rules.
|
||||
if (chunkParseObjects.length === 0) {
|
||||
importRuleResponse = [...nonExistentActionErrors, ...duplicateIdErrors];
|
||||
}
|
||||
|
||||
while (chunkParseObjects.length) {
|
||||
const batchParseObjects = chunkParseObjects.shift() ?? [];
|
||||
const newImportRuleResponse = await Promise.all(
|
||||
|
@ -362,6 +370,7 @@ export const importRulesRoute = (
|
|||
}, [])
|
||||
);
|
||||
importRuleResponse = [
|
||||
...nonExistentActionErrors,
|
||||
...duplicateIdErrors,
|
||||
...importRuleResponse,
|
||||
...newImportRuleResponse,
|
||||
|
|
|
@ -88,7 +88,6 @@ export const updateRulesBulkRoute = (
|
|||
spaceId: context.securitySolution.getSpaceId(),
|
||||
rulesClient,
|
||||
ruleStatusClient,
|
||||
savedObjectsClient,
|
||||
defaultOutputIndex: siemClient.getSignalsIndex(),
|
||||
ruleUpdate: payloadRule,
|
||||
isRuleRegistryEnabled,
|
||||
|
|
|
@ -79,7 +79,6 @@ export const updateRulesRoute = (
|
|||
isRuleRegistryEnabled,
|
||||
rulesClient,
|
||||
ruleStatusClient,
|
||||
savedObjectsClient,
|
||||
ruleUpdate: request.body,
|
||||
spaceId: context.securitySolution.getSpaceId(),
|
||||
});
|
||||
|
|
|
@ -18,6 +18,7 @@ import {
|
|||
transformAlertsToRules,
|
||||
getDuplicates,
|
||||
getTupleDuplicateErrorsAndUniqueRules,
|
||||
getInvalidConnectors,
|
||||
} from './utils';
|
||||
import { getAlertMock } from '../__mocks__/request_responses';
|
||||
import { INTERNAL_IDENTIFIER } from '../../../../../common/constants';
|
||||
|
@ -36,6 +37,8 @@ import {
|
|||
getQueryRuleParams,
|
||||
getThreatRuleParams,
|
||||
} from '../../schemas/rule_schemas.mock';
|
||||
import { requestContextMock } from '../__mocks__';
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { LegacyRulesActionsSavedObject } from '../../rule_actions/legacy_get_rule_actions_saved_object';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
|
@ -47,6 +50,8 @@ describe.each([
|
|||
['Legacy', false],
|
||||
['RAC', true],
|
||||
])('utils - %s', (_, isRuleRegistryEnabled) => {
|
||||
const { clients } = requestContextMock.createTools();
|
||||
|
||||
describe('transformAlertToRule', () => {
|
||||
test('should work with a full data set', () => {
|
||||
const fullRule = getAlertMock(isRuleRegistryEnabled, getQueryRuleParams());
|
||||
|
@ -645,4 +650,468 @@ describe.each([
|
|||
expect(errors.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getInvalidConnectors', () => {
|
||||
beforeEach(() => {
|
||||
clients.actionsClient.getAll.mockReset();
|
||||
});
|
||||
|
||||
test('returns empty 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(1000);
|
||||
const parsedObjects = await createPromiseFromStreams<PromiseFromStreams[]>([
|
||||
ndJsonStream,
|
||||
...rulesObjectsStream,
|
||||
]);
|
||||
clients.actionsClient.getAll.mockResolvedValue([]);
|
||||
const [errors, output] = await getInvalidConnectors(parsedObjects, clients.actionsClient);
|
||||
const isInstanceOfError = output[0] instanceof Error;
|
||||
|
||||
expect(isInstanceOfError).toEqual(true);
|
||||
expect(errors.length).toEqual(0);
|
||||
});
|
||||
|
||||
test('creates error with a rule has an action that does not exist within the actions client', async () => {
|
||||
const rule: ReturnType<typeof getCreateRulesSchemaMock> = {
|
||||
...getCreateRulesSchemaMock('rule-1'),
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
id: '123',
|
||||
action_type_id: '456',
|
||||
params: {},
|
||||
},
|
||||
],
|
||||
};
|
||||
const ndJsonStream = new Readable({
|
||||
read() {
|
||||
this.push(`${JSON.stringify(rule)}\n`);
|
||||
this.push(null);
|
||||
},
|
||||
});
|
||||
const rulesObjectsStream = createRulesStreamFromNdJson(1000);
|
||||
const parsedObjects = await createPromiseFromStreams<PromiseFromStreams[]>([
|
||||
ndJsonStream,
|
||||
...rulesObjectsStream,
|
||||
]);
|
||||
clients.actionsClient.getAll.mockResolvedValue([]);
|
||||
const [errors, output] = await getInvalidConnectors(parsedObjects, clients.actionsClient);
|
||||
expect(output.length).toEqual(0);
|
||||
expect(errors).toEqual<BulkError[]>([
|
||||
{
|
||||
error: {
|
||||
message: '1 connector is missing. Connector id missing is: 123',
|
||||
status_code: 404,
|
||||
},
|
||||
rule_id: 'rule-1',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('creates output with no errors if 1 rule with an action exists within the actions client', async () => {
|
||||
const rule: ReturnType<typeof getCreateRulesSchemaMock> = {
|
||||
...getCreateRulesSchemaMock('rule-1'),
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
id: '123',
|
||||
action_type_id: '456',
|
||||
params: {},
|
||||
},
|
||||
],
|
||||
};
|
||||
const ndJsonStream = new Readable({
|
||||
read() {
|
||||
this.push(`${JSON.stringify(rule)}\n`);
|
||||
this.push(null);
|
||||
},
|
||||
});
|
||||
const rulesObjectsStream = createRulesStreamFromNdJson(1000);
|
||||
const parsedObjects = await createPromiseFromStreams<PromiseFromStreams[]>([
|
||||
ndJsonStream,
|
||||
...rulesObjectsStream,
|
||||
]);
|
||||
clients.actionsClient.getAll.mockResolvedValue([
|
||||
{
|
||||
id: '123',
|
||||
referencedByCount: 1,
|
||||
actionTypeId: 'default',
|
||||
name: 'name',
|
||||
isPreconfigured: false,
|
||||
},
|
||||
]);
|
||||
const [errors, output] = await getInvalidConnectors(parsedObjects, clients.actionsClient);
|
||||
expect(errors.length).toEqual(0);
|
||||
expect(output.length).toEqual(1);
|
||||
expect(output[0]).toEqual<PromiseFromStreams[]>(expect.objectContaining(rule));
|
||||
});
|
||||
|
||||
test('creates output with no errors if 1 rule with 2 actions exists within the actions client', async () => {
|
||||
const rule: ReturnType<typeof getCreateRulesSchemaMock> = {
|
||||
...getCreateRulesSchemaMock('rule-1'),
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
id: '123',
|
||||
action_type_id: '456',
|
||||
params: {},
|
||||
},
|
||||
{
|
||||
group: 'default',
|
||||
id: '789',
|
||||
action_type_id: '101112',
|
||||
params: {},
|
||||
},
|
||||
],
|
||||
};
|
||||
const ndJsonStream = new Readable({
|
||||
read() {
|
||||
this.push(`${JSON.stringify(rule)}\n`);
|
||||
this.push(null);
|
||||
},
|
||||
});
|
||||
const rulesObjectsStream = createRulesStreamFromNdJson(1000);
|
||||
const parsedObjects = await createPromiseFromStreams<PromiseFromStreams[]>([
|
||||
ndJsonStream,
|
||||
...rulesObjectsStream,
|
||||
]);
|
||||
clients.actionsClient.getAll.mockResolvedValue([
|
||||
{
|
||||
id: '123',
|
||||
referencedByCount: 1,
|
||||
actionTypeId: 'default',
|
||||
name: 'name',
|
||||
isPreconfigured: false,
|
||||
},
|
||||
{
|
||||
id: '789',
|
||||
referencedByCount: 1,
|
||||
actionTypeId: 'default',
|
||||
name: 'name',
|
||||
isPreconfigured: false,
|
||||
},
|
||||
]);
|
||||
const [errors, output] = await getInvalidConnectors(parsedObjects, clients.actionsClient);
|
||||
expect(errors.length).toEqual(0);
|
||||
expect(output.length).toEqual(1);
|
||||
expect(output[0]).toEqual<PromiseFromStreams[]>(expect.objectContaining(rule));
|
||||
});
|
||||
|
||||
test('creates output with no errors if 2 rules with 1 action each exists within the actions client', async () => {
|
||||
const rule1: ReturnType<typeof getCreateRulesSchemaMock> = {
|
||||
...getCreateRulesSchemaMock('rule-1'),
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
id: '123',
|
||||
action_type_id: '456',
|
||||
params: {},
|
||||
},
|
||||
],
|
||||
};
|
||||
const rule2: ReturnType<typeof getCreateRulesSchemaMock> = {
|
||||
...getCreateRulesSchemaMock('rule-2'),
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
id: '123',
|
||||
action_type_id: '456',
|
||||
params: {},
|
||||
},
|
||||
],
|
||||
};
|
||||
const ndJsonStream = new Readable({
|
||||
read() {
|
||||
this.push(`${JSON.stringify(rule1)}\n`);
|
||||
this.push(`${JSON.stringify(rule2)}\n`);
|
||||
this.push(null);
|
||||
},
|
||||
});
|
||||
const rulesObjectsStream = createRulesStreamFromNdJson(1000);
|
||||
const parsedObjects = await createPromiseFromStreams<PromiseFromStreams[]>([
|
||||
ndJsonStream,
|
||||
...rulesObjectsStream,
|
||||
]);
|
||||
clients.actionsClient.getAll.mockResolvedValue([
|
||||
{
|
||||
id: '123',
|
||||
referencedByCount: 1,
|
||||
actionTypeId: 'default',
|
||||
name: 'name',
|
||||
isPreconfigured: false,
|
||||
},
|
||||
{
|
||||
id: '789',
|
||||
referencedByCount: 1,
|
||||
actionTypeId: 'default',
|
||||
name: 'name',
|
||||
isPreconfigured: false,
|
||||
},
|
||||
]);
|
||||
const [errors, output] = await getInvalidConnectors(parsedObjects, clients.actionsClient);
|
||||
expect(errors.length).toEqual(0);
|
||||
expect(output.length).toEqual(2);
|
||||
expect(output[0]).toEqual<PromiseFromStreams[]>(expect.objectContaining(rule1));
|
||||
expect(output[1]).toEqual<PromiseFromStreams[]>(expect.objectContaining(rule2));
|
||||
});
|
||||
|
||||
test('creates output with 1 error if 2 rules with 1 action each exists within the actions client but 1 has a nonexistent action', async () => {
|
||||
const rule1: ReturnType<typeof getCreateRulesSchemaMock> = {
|
||||
...getCreateRulesSchemaMock('rule-1'),
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
id: '123',
|
||||
action_type_id: '456',
|
||||
params: {},
|
||||
},
|
||||
],
|
||||
};
|
||||
const rule2: ReturnType<typeof getCreateRulesSchemaMock> = {
|
||||
...getCreateRulesSchemaMock('rule-2'),
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
id: '123',
|
||||
action_type_id: '456',
|
||||
params: {},
|
||||
},
|
||||
{
|
||||
group: 'default',
|
||||
id: '456', // <--- Non-existent that triggers the error.
|
||||
action_type_id: '456',
|
||||
params: {},
|
||||
},
|
||||
],
|
||||
};
|
||||
const ndJsonStream = new Readable({
|
||||
read() {
|
||||
this.push(`${JSON.stringify(rule1)}\n`);
|
||||
this.push(`${JSON.stringify(rule2)}\n`);
|
||||
this.push(null);
|
||||
},
|
||||
});
|
||||
const rulesObjectsStream = createRulesStreamFromNdJson(1000);
|
||||
const parsedObjects = await createPromiseFromStreams<PromiseFromStreams[]>([
|
||||
ndJsonStream,
|
||||
...rulesObjectsStream,
|
||||
]);
|
||||
clients.actionsClient.getAll.mockResolvedValue([
|
||||
{
|
||||
id: '123',
|
||||
referencedByCount: 1,
|
||||
actionTypeId: 'default',
|
||||
name: 'name',
|
||||
isPreconfigured: false,
|
||||
},
|
||||
{
|
||||
id: '789',
|
||||
referencedByCount: 1,
|
||||
actionTypeId: 'default',
|
||||
name: 'name',
|
||||
isPreconfigured: false,
|
||||
},
|
||||
]);
|
||||
const [errors, output] = await getInvalidConnectors(parsedObjects, clients.actionsClient);
|
||||
expect(errors.length).toEqual(1);
|
||||
expect(output.length).toEqual(1);
|
||||
expect(output[0]).toEqual<PromiseFromStreams[]>(expect.objectContaining(rule1));
|
||||
expect(errors).toEqual<BulkError[]>([
|
||||
{
|
||||
error: {
|
||||
message: '1 connector is missing. Connector id missing is: 456',
|
||||
status_code: 404,
|
||||
},
|
||||
rule_id: 'rule-2',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('creates output with error if 1 rule with 2 actions but 1 action does not exist within the actions client', async () => {
|
||||
const rule: ReturnType<typeof getCreateRulesSchemaMock> = {
|
||||
...getCreateRulesSchemaMock('rule-1'),
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
id: '123',
|
||||
action_type_id: '456',
|
||||
params: {},
|
||||
},
|
||||
{
|
||||
group: 'default',
|
||||
id: '789',
|
||||
action_type_id: '101112',
|
||||
params: {},
|
||||
},
|
||||
{
|
||||
group: 'default',
|
||||
id: '101112', // <-- Does not exist
|
||||
action_type_id: '101112',
|
||||
params: {},
|
||||
},
|
||||
],
|
||||
};
|
||||
const ndJsonStream = new Readable({
|
||||
read() {
|
||||
this.push(`${JSON.stringify(rule)}\n`);
|
||||
this.push(null);
|
||||
},
|
||||
});
|
||||
const rulesObjectsStream = createRulesStreamFromNdJson(1000);
|
||||
const parsedObjects = await createPromiseFromStreams<PromiseFromStreams[]>([
|
||||
ndJsonStream,
|
||||
...rulesObjectsStream,
|
||||
]);
|
||||
clients.actionsClient.getAll.mockResolvedValue([
|
||||
{
|
||||
id: '123',
|
||||
referencedByCount: 1,
|
||||
actionTypeId: 'default',
|
||||
name: 'name',
|
||||
isPreconfigured: false,
|
||||
},
|
||||
{
|
||||
id: '789',
|
||||
referencedByCount: 1,
|
||||
actionTypeId: 'default',
|
||||
name: 'name',
|
||||
isPreconfigured: false,
|
||||
},
|
||||
]);
|
||||
const [errors, output] = await getInvalidConnectors(parsedObjects, clients.actionsClient);
|
||||
expect(errors.length).toEqual(1);
|
||||
expect(output.length).toEqual(0);
|
||||
expect(errors).toEqual<BulkError[]>([
|
||||
{
|
||||
error: {
|
||||
message: '1 connector is missing. Connector id missing is: 101112',
|
||||
status_code: 404,
|
||||
},
|
||||
rule_id: 'rule-1',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('creates output with 2 errors if 3 rules with actions but 1 action does not exist within the actions client', async () => {
|
||||
const rule1: ReturnType<typeof getCreateRulesSchemaMock> = {
|
||||
...getCreateRulesSchemaMock('rule-1'),
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
id: '123',
|
||||
action_type_id: '456',
|
||||
params: {},
|
||||
},
|
||||
{
|
||||
group: 'default',
|
||||
id: '789',
|
||||
action_type_id: '101112',
|
||||
params: {},
|
||||
},
|
||||
{
|
||||
group: 'default',
|
||||
id: '101112', // <-- Does not exist
|
||||
action_type_id: '101112',
|
||||
params: {},
|
||||
},
|
||||
],
|
||||
};
|
||||
const rule2: ReturnType<typeof getCreateRulesSchemaMock> = {
|
||||
...getCreateRulesSchemaMock('rule-1'),
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
id: '123',
|
||||
action_type_id: '456',
|
||||
params: {},
|
||||
},
|
||||
{
|
||||
group: 'default',
|
||||
id: '789',
|
||||
action_type_id: '101112',
|
||||
params: {},
|
||||
},
|
||||
],
|
||||
};
|
||||
const rule3: ReturnType<typeof getCreateRulesSchemaMock> = {
|
||||
...getCreateRulesSchemaMock('rule-1'),
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
id: '123',
|
||||
action_type_id: '456',
|
||||
params: {},
|
||||
},
|
||||
{
|
||||
group: 'default',
|
||||
id: '789',
|
||||
action_type_id: '101112',
|
||||
params: {},
|
||||
},
|
||||
{
|
||||
group: 'default',
|
||||
id: '101112', // <-- Does not exist
|
||||
action_type_id: '101112',
|
||||
params: {},
|
||||
},
|
||||
],
|
||||
};
|
||||
const ndJsonStream = new Readable({
|
||||
read() {
|
||||
this.push(`${JSON.stringify(rule1)}\n`);
|
||||
this.push(`${JSON.stringify(rule2)}\n`);
|
||||
this.push(`${JSON.stringify(rule3)}\n`);
|
||||
this.push(null);
|
||||
},
|
||||
});
|
||||
const rulesObjectsStream = createRulesStreamFromNdJson(1000);
|
||||
const parsedObjects = await createPromiseFromStreams<PromiseFromStreams[]>([
|
||||
ndJsonStream,
|
||||
...rulesObjectsStream,
|
||||
]);
|
||||
clients.actionsClient.getAll.mockResolvedValue([
|
||||
{
|
||||
id: '123',
|
||||
referencedByCount: 1,
|
||||
actionTypeId: 'default',
|
||||
name: 'name',
|
||||
isPreconfigured: false,
|
||||
},
|
||||
{
|
||||
id: '789',
|
||||
referencedByCount: 1,
|
||||
actionTypeId: 'default',
|
||||
name: 'name',
|
||||
isPreconfigured: false,
|
||||
},
|
||||
]);
|
||||
const [errors, output] = await getInvalidConnectors(parsedObjects, clients.actionsClient);
|
||||
expect(errors.length).toEqual(2);
|
||||
expect(output.length).toEqual(1);
|
||||
expect(output[0]).toEqual<PromiseFromStreams[]>(expect.objectContaining(rule2));
|
||||
expect(errors).toEqual<BulkError[]>([
|
||||
{
|
||||
error: {
|
||||
message: '1 connector is missing. Connector id missing is: 101112',
|
||||
status_code: 404,
|
||||
},
|
||||
rule_id: 'rule-1',
|
||||
},
|
||||
{
|
||||
error: {
|
||||
message: '1 connector is missing. Connector id missing is: 101112',
|
||||
status_code: 404,
|
||||
},
|
||||
rule_id: 'rule-1',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -13,6 +13,7 @@ import { RulesSchema } from '../../../../../common/detection_engine/schemas/resp
|
|||
import { ImportRulesSchemaDecoded } from '../../../../../common/detection_engine/schemas/request/import_rules_schema';
|
||||
import { CreateRulesBulkSchema } from '../../../../../common/detection_engine/schemas/request/create_rules_bulk_schema';
|
||||
import { PartialAlert, FindResult } from '../../../../../../alerting/server';
|
||||
import { ActionsClient } from '../../../../../../actions/server';
|
||||
import { INTERNAL_IDENTIFIER } from '../../../../../common/constants';
|
||||
import {
|
||||
RuleAlertType,
|
||||
|
@ -194,3 +195,57 @@ export const getTupleDuplicateErrorsAndUniqueRules = (
|
|||
|
||||
return [Array.from(errors.values()), Array.from(rulesAcc.values())];
|
||||
};
|
||||
|
||||
/**
|
||||
* Given a set of rules and an actions client this will return connectors that are invalid
|
||||
* such as missing connectors and filter out the rules that have invalid connectors.
|
||||
* @param rules The rules to check for invalid connectors
|
||||
* @param actionsClient The actions client to get all the connectors.
|
||||
* @returns An array of connector errors if it found any and then the promise stream of valid and invalid connectors.
|
||||
*/
|
||||
export const getInvalidConnectors = async (
|
||||
rules: PromiseFromStreams[],
|
||||
actionsClient: ActionsClient
|
||||
): Promise<[BulkError[], PromiseFromStreams[]]> => {
|
||||
const actionsFind = await actionsClient.getAll();
|
||||
const actionIds = actionsFind.map((action) => action.id);
|
||||
const { errors, rulesAcc } = rules.reduce(
|
||||
(acc, parsedRule) => {
|
||||
if (parsedRule instanceof Error) {
|
||||
acc.rulesAcc.set(uuid.v4(), parsedRule);
|
||||
} else {
|
||||
const { rule_id: ruleId, actions } = parsedRule;
|
||||
const missingActionIds = actions.flatMap((action) => {
|
||||
if (actionIds.find((actionsId) => actionsId === action.id) == null) {
|
||||
return [action.id];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
if (missingActionIds.length === 0) {
|
||||
acc.rulesAcc.set(ruleId, parsedRule);
|
||||
} else {
|
||||
const errorMessage =
|
||||
missingActionIds.length > 1
|
||||
? 'connectors are missing. Connector ids missing are:'
|
||||
: 'connector is missing. Connector id missing is:';
|
||||
acc.errors.set(
|
||||
uuid.v4(),
|
||||
createBulkErrorObject({
|
||||
ruleId,
|
||||
statusCode: 404,
|
||||
message: `${missingActionIds.length} ${errorMessage} ${missingActionIds.join(', ')}`,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
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())];
|
||||
};
|
||||
|
|
|
@ -275,7 +275,6 @@ export interface UpdateRulesOptions {
|
|||
rulesClient: RulesClient;
|
||||
defaultOutputIndex: string;
|
||||
ruleUpdate: UpdateRulesSchema;
|
||||
savedObjectsClient: SavedObjectsClientContract;
|
||||
}
|
||||
|
||||
export interface PatchRulesOptions {
|
||||
|
|
|
@ -33,7 +33,6 @@ export const updateRules = async ({
|
|||
ruleStatusClient,
|
||||
defaultOutputIndex,
|
||||
ruleUpdate,
|
||||
savedObjectsClient,
|
||||
}: UpdateRulesOptions): Promise<PartialAlert<RuleParams> | null> => {
|
||||
const existingRule = await readRules({
|
||||
isRuleRegistryEnabled,
|
||||
|
|
|
@ -20556,8 +20556,6 @@
|
|||
"xpack.securitySolution.detectionEngine.components.allRules.refreshPromptConfirm": "続行",
|
||||
"xpack.securitySolution.detectionEngine.components.allRules.refreshPromptTitle": "応答してください。",
|
||||
"xpack.securitySolution.detectionEngine.components.importRuleModal.cancelTitle": "キャンセル",
|
||||
"xpack.securitySolution.detectionEngine.components.importRuleModal.importFailedDetailedTitle": "ルールID:{ruleId}\n ステータスコード:{statusCode}\n メッセージ:{message}",
|
||||
"xpack.securitySolution.detectionEngine.components.importRuleModal.importFailedTitle": "ルールをインポートできませんでした",
|
||||
"xpack.securitySolution.detectionEngine.components.importRuleModal.importRuleTitle": "ルールのインポート",
|
||||
"xpack.securitySolution.detectionEngine.components.importRuleModal.initialPromptTextDescription": "有効なrules_export.ndjsonファイルを選択するか、ドラッグしてドロップします",
|
||||
"xpack.securitySolution.detectionEngine.components.importRuleModal.overwriteDescription": "競合するルールIDで既存の検出を上書き",
|
||||
|
@ -22820,8 +22818,6 @@
|
|||
"xpack.securitySolution.timelines.allTimelines.errorFetchingTimelinesTitle": "すべてのタイムラインデータをクエリできませんでした",
|
||||
"xpack.securitySolution.timelines.allTimelines.importTimelineTitle": "インポート",
|
||||
"xpack.securitySolution.timelines.allTimelines.panelTitle": "すべてのタイムライン",
|
||||
"xpack.securitySolution.timelines.components.importTimelineModal.importFailedDetailedTitle": "タイムライン ID:{id}\n ステータスコード:{statusCode}\n メッセージ:{message}",
|
||||
"xpack.securitySolution.timelines.components.importTimelineModal.importFailedTitle": "インポートできませんでした",
|
||||
"xpack.securitySolution.timelines.components.importTimelineModal.importTimelineTitle": "インポート",
|
||||
"xpack.securitySolution.timelines.components.importTimelineModal.importTitle": "インポート…",
|
||||
"xpack.securitySolution.timelines.components.importTimelineModal.initialPromptTextDescription": "有効な timelines_export.ndjson ファイルを選択するか、またはドラッグアンドドロップします",
|
||||
|
|
|
@ -20871,8 +20871,6 @@
|
|||
"xpack.securitySolution.detectionEngine.components.allRules.refreshPromptConfirm": "继续",
|
||||
"xpack.securitySolution.detectionEngine.components.allRules.refreshPromptTitle": "您还在吗?",
|
||||
"xpack.securitySolution.detectionEngine.components.importRuleModal.cancelTitle": "取消",
|
||||
"xpack.securitySolution.detectionEngine.components.importRuleModal.importFailedDetailedTitle": "规则 ID:{ruleId}\n 状态代码:{statusCode}\n 消息:{message}",
|
||||
"xpack.securitySolution.detectionEngine.components.importRuleModal.importFailedTitle": "无法导入规则",
|
||||
"xpack.securitySolution.detectionEngine.components.importRuleModal.importRuleTitle": "导入规则",
|
||||
"xpack.securitySolution.detectionEngine.components.importRuleModal.initialPromptTextDescription": "选择或拖放有效 rules_export.ndjson 文件",
|
||||
"xpack.securitySolution.detectionEngine.components.importRuleModal.overwriteDescription": "覆盖具有冲突规则 ID 的现有检测规则",
|
||||
|
@ -23193,8 +23191,6 @@
|
|||
"xpack.securitySolution.timelines.allTimelines.errorFetchingTimelinesTitle": "无法查询所有时间线数据",
|
||||
"xpack.securitySolution.timelines.allTimelines.importTimelineTitle": "导入",
|
||||
"xpack.securitySolution.timelines.allTimelines.panelTitle": "所有时间线",
|
||||
"xpack.securitySolution.timelines.components.importTimelineModal.importFailedDetailedTitle": "时间线 ID:{id}\n 状态代码:{statusCode}\n 消息:{message}",
|
||||
"xpack.securitySolution.timelines.components.importTimelineModal.importFailedTitle": "无法导入",
|
||||
"xpack.securitySolution.timelines.components.importTimelineModal.importTimelineTitle": "导入",
|
||||
"xpack.securitySolution.timelines.components.importTimelineModal.importTitle": "导入……",
|
||||
"xpack.securitySolution.timelines.components.importTimelineModal.initialPromptTextDescription": "搜索或拖放有效的 timelines_export.ndjson 文件",
|
||||
|
|
|
@ -17,6 +17,7 @@ import {
|
|||
deleteSignalsIndex,
|
||||
getSimpleRule,
|
||||
getSimpleRuleOutput,
|
||||
getWebHookAction,
|
||||
removeServerGeneratedProperties,
|
||||
} from '../../utils';
|
||||
|
||||
|
@ -109,6 +110,113 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
getSimpleRuleOutput('rule-1'),
|
||||
]);
|
||||
});
|
||||
|
||||
it('should export multiple actions attached to 1 rule', async () => {
|
||||
// 1st action
|
||||
const { body: hookAction1 } = await supertest
|
||||
.post('/api/actions/action')
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getWebHookAction())
|
||||
.expect(200);
|
||||
|
||||
// 2nd action
|
||||
const { body: hookAction2 } = await supertest
|
||||
.post('/api/actions/action')
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getWebHookAction())
|
||||
.expect(200);
|
||||
|
||||
const action1 = {
|
||||
group: 'default',
|
||||
id: hookAction1.id,
|
||||
action_type_id: hookAction1.actionTypeId,
|
||||
params: {},
|
||||
};
|
||||
const action2 = {
|
||||
group: 'default',
|
||||
id: hookAction2.id,
|
||||
action_type_id: hookAction2.actionTypeId,
|
||||
params: {},
|
||||
};
|
||||
|
||||
const rule1: ReturnType<typeof getSimpleRule> = {
|
||||
...getSimpleRule('rule-1'),
|
||||
actions: [action1, action2],
|
||||
};
|
||||
|
||||
await createRule(supertest, rule1);
|
||||
|
||||
const { body } = await supertest
|
||||
.post(`${DETECTION_ENGINE_RULES_URL}/_export`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send()
|
||||
.expect(200)
|
||||
.parse(binaryToString);
|
||||
|
||||
const firstRuleParsed = JSON.parse(body.toString().split(/\n/)[0]);
|
||||
const firstRule = removeServerGeneratedProperties(firstRuleParsed);
|
||||
|
||||
const outputRule1: ReturnType<typeof getSimpleRuleOutput> = {
|
||||
...getSimpleRuleOutput('rule-1'),
|
||||
actions: [action1, action2],
|
||||
throttle: 'rule',
|
||||
};
|
||||
expect(firstRule).to.eql(outputRule1);
|
||||
});
|
||||
|
||||
it('should export actions attached to 2 rules', async () => {
|
||||
// create a new action
|
||||
const { body: hookAction } = await supertest
|
||||
.post('/api/actions/action')
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getWebHookAction())
|
||||
.expect(200);
|
||||
|
||||
const action = {
|
||||
group: 'default',
|
||||
id: hookAction.id,
|
||||
action_type_id: hookAction.actionTypeId,
|
||||
params: {},
|
||||
};
|
||||
|
||||
const rule1: ReturnType<typeof getSimpleRule> = {
|
||||
...getSimpleRule('rule-1'),
|
||||
actions: [action],
|
||||
};
|
||||
|
||||
const rule2: ReturnType<typeof getSimpleRule> = {
|
||||
...getSimpleRule('rule-2'),
|
||||
actions: [action],
|
||||
};
|
||||
|
||||
await createRule(supertest, rule1);
|
||||
await createRule(supertest, rule2);
|
||||
|
||||
const { body } = await supertest
|
||||
.post(`${DETECTION_ENGINE_RULES_URL}/_export`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send()
|
||||
.expect(200)
|
||||
.parse(binaryToString);
|
||||
|
||||
const firstRuleParsed = JSON.parse(body.toString().split(/\n/)[0]);
|
||||
const secondRuleParsed = JSON.parse(body.toString().split(/\n/)[1]);
|
||||
const firstRule = removeServerGeneratedProperties(firstRuleParsed);
|
||||
const secondRule = removeServerGeneratedProperties(secondRuleParsed);
|
||||
|
||||
const outputRule1: ReturnType<typeof getSimpleRuleOutput> = {
|
||||
...getSimpleRuleOutput('rule-2'),
|
||||
actions: [action],
|
||||
throttle: 'rule',
|
||||
};
|
||||
const outputRule2: ReturnType<typeof getSimpleRuleOutput> = {
|
||||
...getSimpleRuleOutput('rule-1'),
|
||||
actions: [action],
|
||||
throttle: 'rule',
|
||||
};
|
||||
expect(firstRule).to.eql(outputRule1);
|
||||
expect(secondRule).to.eql(outputRule2);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
getSimpleRule,
|
||||
getSimpleRuleAsNdjson,
|
||||
getSimpleRuleOutput,
|
||||
getWebHookAction,
|
||||
removeServerGeneratedProperties,
|
||||
ruleToNdjson,
|
||||
} from '../../utils';
|
||||
|
@ -315,6 +316,165 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
getSimpleRuleOutput('rule-3'),
|
||||
]);
|
||||
});
|
||||
|
||||
it('should give single connector error back if we have a single connector error message', async () => {
|
||||
const simpleRule: ReturnType<typeof getSimpleRule> = {
|
||||
...getSimpleRule('rule-1'),
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
id: '123',
|
||||
action_type_id: '456',
|
||||
params: {},
|
||||
},
|
||||
],
|
||||
};
|
||||
const { body } = await supertest
|
||||
.post(`${DETECTION_ENGINE_RULES_URL}/_import`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.attach('file', ruleToNdjson(simpleRule), 'rules.ndjson')
|
||||
.expect(200);
|
||||
|
||||
expect(body).to.eql({
|
||||
success: false,
|
||||
success_count: 0,
|
||||
errors: [
|
||||
{
|
||||
rule_id: 'rule-1',
|
||||
error: {
|
||||
status_code: 404,
|
||||
message: '1 connector is missing. Connector id missing is: 123',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should be able to import a rule with an action connector that exists', async () => {
|
||||
// create a new action
|
||||
const { body: hookAction } = await supertest
|
||||
.post('/api/actions/action')
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getWebHookAction())
|
||||
.expect(200);
|
||||
const simpleRule: ReturnType<typeof getSimpleRule> = {
|
||||
...getSimpleRule('rule-1'),
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
id: hookAction.id,
|
||||
action_type_id: hookAction.actionTypeId,
|
||||
params: {},
|
||||
},
|
||||
],
|
||||
};
|
||||
const { body } = await supertest
|
||||
.post(`${DETECTION_ENGINE_RULES_URL}/_import`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.attach('file', ruleToNdjson(simpleRule), 'rules.ndjson')
|
||||
.expect(200);
|
||||
expect(body).to.eql({ success: true, success_count: 1, errors: [] });
|
||||
});
|
||||
|
||||
it('should be able to import 2 rules with action connectors that exist', async () => {
|
||||
// create a new action
|
||||
const { body: hookAction } = await supertest
|
||||
.post('/api/actions/action')
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getWebHookAction())
|
||||
.expect(200);
|
||||
|
||||
const rule1: ReturnType<typeof getSimpleRule> = {
|
||||
...getSimpleRule('rule-1'),
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
id: hookAction.id,
|
||||
action_type_id: hookAction.actionTypeId,
|
||||
params: {},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const rule2: ReturnType<typeof getSimpleRule> = {
|
||||
...getSimpleRule('rule-2'),
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
id: hookAction.id,
|
||||
action_type_id: hookAction.actionTypeId,
|
||||
params: {},
|
||||
},
|
||||
],
|
||||
};
|
||||
const rule1String = JSON.stringify(rule1);
|
||||
const rule2String = JSON.stringify(rule2);
|
||||
const buffer = Buffer.from(`${rule1String}\n${rule2String}\n`);
|
||||
|
||||
const { body } = await supertest
|
||||
.post(`${DETECTION_ENGINE_RULES_URL}/_import`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.attach('file', buffer, 'rules.ndjson')
|
||||
.expect(200);
|
||||
|
||||
expect(body).to.eql({ success: true, success_count: 2, errors: [] });
|
||||
});
|
||||
|
||||
it('should be able to import 1 rule with an action connector that exists and get 1 other error back for a second rule that does not have the connector', async () => {
|
||||
// create a new action
|
||||
const { body: hookAction } = await supertest
|
||||
.post('/api/actions/action')
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getWebHookAction())
|
||||
.expect(200);
|
||||
|
||||
const rule1: ReturnType<typeof getSimpleRule> = {
|
||||
...getSimpleRule('rule-1'),
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
id: hookAction.id,
|
||||
action_type_id: hookAction.actionTypeId,
|
||||
params: {},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const rule2: ReturnType<typeof getSimpleRule> = {
|
||||
...getSimpleRule('rule-2'),
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
id: '123', // <-- This does not exist
|
||||
action_type_id: hookAction.actionTypeId,
|
||||
params: {},
|
||||
},
|
||||
],
|
||||
};
|
||||
const rule1String = JSON.stringify(rule1);
|
||||
const rule2String = JSON.stringify(rule2);
|
||||
const buffer = Buffer.from(`${rule1String}\n${rule2String}\n`);
|
||||
|
||||
const { body } = await supertest
|
||||
.post(`${DETECTION_ENGINE_RULES_URL}/_import`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.attach('file', buffer, 'rules.ndjson')
|
||||
.expect(200);
|
||||
|
||||
expect(body).to.eql({
|
||||
success: false,
|
||||
success_count: 1,
|
||||
errors: [
|
||||
{
|
||||
rule_id: 'rule-2',
|
||||
error: {
|
||||
status_code: 404,
|
||||
message: '1 connector is missing. Connector id missing is: 123',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue