mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[8.0] [Security Solution][Lists][Platform] - Fixes import rules modal to work with latest added exceptions import functionality (#120837) (#122175)
* [Security Solution][Lists][Platform] - Fixes import rules modal to work with latest added exceptions import functionality (#120837) ## Summary Without the added overwrite support for exceptions separate from rules, unexpected user behavior experienced. This PR does the following: - Updates the import rules modal text to account for exceptions - Updates the import rules modal logic to account for the exceptions overwrite option - Users can now select to overwrite rules, exceptions or both - Updates the backend logic in the rules import route to batch checking if the exception lists referenced by the rules trying to be imported exist. If the list does not exist, it removes the reference before trying to import the rule. Previously, this check was being done one by one for each rule. - Added effort to try to speed up the import after added exceptions logic from original PR slowed down functionality # Conflicts: # x-pack/plugins/lists/server/services/exception_lists/utils/import/find_all_exception_list_types.ts * fixing merger conflict
This commit is contained in:
parent
07a1d44b49
commit
81513dc074
34 changed files with 1368 additions and 210 deletions
|
@ -12,8 +12,9 @@ import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts
|
|||
|
||||
describe('importQuerySchema', () => {
|
||||
test('it should validate proper schema', () => {
|
||||
const payload = {
|
||||
const payload: ImportQuerySchema = {
|
||||
overwrite: true,
|
||||
overwrite_exceptions: true,
|
||||
};
|
||||
const decoded = importQuerySchema.decode(payload);
|
||||
const checked = exactCheck(payload, decoded);
|
||||
|
@ -26,6 +27,7 @@ describe('importQuerySchema', () => {
|
|||
test('it should NOT validate a non boolean value for "overwrite"', () => {
|
||||
const payload: Omit<ImportQuerySchema, 'overwrite'> & { overwrite: string } = {
|
||||
overwrite: 'wrong',
|
||||
overwrite_exceptions: true,
|
||||
};
|
||||
const decoded = importQuerySchema.decode(payload);
|
||||
const checked = exactCheck(payload, decoded);
|
||||
|
@ -37,12 +39,30 @@ describe('importQuerySchema', () => {
|
|||
expect(message.schema).toEqual({});
|
||||
});
|
||||
|
||||
test('it should NOT validate a non boolean value for "overwrite_exceptions"', () => {
|
||||
const payload: Omit<ImportQuerySchema, 'overwrite_exceptions'> & {
|
||||
overwrite_exceptions: string;
|
||||
} = {
|
||||
overwrite: true,
|
||||
overwrite_exceptions: 'wrong',
|
||||
};
|
||||
const decoded = importQuerySchema.decode(payload);
|
||||
const checked = exactCheck(payload, decoded);
|
||||
const message = foldLeftRight(checked);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([
|
||||
'Invalid value "wrong" supplied to "overwrite_exceptions"',
|
||||
]);
|
||||
expect(message.schema).toEqual({});
|
||||
});
|
||||
|
||||
test('it should NOT allow an extra key to be sent in', () => {
|
||||
const payload: ImportQuerySchema & {
|
||||
extraKey?: string;
|
||||
} = {
|
||||
extraKey: 'extra',
|
||||
overwrite: true,
|
||||
overwrite_exceptions: true,
|
||||
};
|
||||
|
||||
const decoded = importQuerySchema.decode(payload);
|
||||
|
|
|
@ -13,10 +13,15 @@ import { DefaultStringBooleanFalse } from '../default_string_boolean_false';
|
|||
export const importQuerySchema = t.exact(
|
||||
t.partial({
|
||||
overwrite: DefaultStringBooleanFalse,
|
||||
overwrite_exceptions: DefaultStringBooleanFalse,
|
||||
})
|
||||
);
|
||||
|
||||
export type ImportQuerySchema = t.TypeOf<typeof importQuerySchema>;
|
||||
export type ImportQuerySchemaDecoded = Omit<ImportQuerySchema, 'overwrite'> & {
|
||||
export type ImportQuerySchemaDecoded = Omit<
|
||||
ImportQuerySchema,
|
||||
'overwrite' | 'overwrite_exceptions'
|
||||
> & {
|
||||
overwrite: boolean;
|
||||
overwrite_exceptions: boolean;
|
||||
};
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
|
||||
import { savedObjectsClientMock } from '../../../../../../../../src/core/server/mocks';
|
||||
import type { SavedObjectsClientContract } from '../../../../../../../../src/core/server';
|
||||
import { getImportExceptionsListSchemaDecodedMock } from '../../../../../common/schemas/request/import_exceptions_schema.mock';
|
||||
import { findExceptionList } from '../../find_exception_list';
|
||||
|
||||
import { findAllListTypes, getAllListTypes, getListFilter } from './find_all_exception_list_types';
|
||||
|
@ -27,8 +26,8 @@ describe('find_all_exception_list_item_types', () => {
|
|||
const result = getListFilter({
|
||||
namespaceType: 'agnostic',
|
||||
objects: [
|
||||
getImportExceptionsListSchemaDecodedMock('1'),
|
||||
getImportExceptionsListSchemaDecodedMock('2'),
|
||||
{ listId: '1', namespaceType: 'agnostic' },
|
||||
{ listId: '2', namespaceType: 'agnostic' },
|
||||
],
|
||||
});
|
||||
|
||||
|
@ -39,8 +38,8 @@ describe('find_all_exception_list_item_types', () => {
|
|||
const result = getListFilter({
|
||||
namespaceType: 'single',
|
||||
objects: [
|
||||
getImportExceptionsListSchemaDecodedMock('1'),
|
||||
getImportExceptionsListSchemaDecodedMock('2'),
|
||||
{ listId: '1', namespaceType: 'single' },
|
||||
{ listId: '2', namespaceType: 'single' },
|
||||
],
|
||||
});
|
||||
|
||||
|
@ -56,11 +55,7 @@ describe('find_all_exception_list_item_types', () => {
|
|||
});
|
||||
|
||||
it('searches for agnostic lists if no non agnostic lists passed in', async () => {
|
||||
await findAllListTypes(
|
||||
[{ ...getImportExceptionsListSchemaDecodedMock('1'), namespace_type: 'agnostic' }],
|
||||
[],
|
||||
savedObjectsClient
|
||||
);
|
||||
await findAllListTypes([{ listId: '1', namespaceType: 'agnostic' }], [], savedObjectsClient);
|
||||
|
||||
expect(findExceptionList).toHaveBeenCalledWith({
|
||||
filter: 'exception-list-agnostic.attributes.list_id:(1)',
|
||||
|
@ -74,11 +69,7 @@ describe('find_all_exception_list_item_types', () => {
|
|||
});
|
||||
|
||||
it('searches for non agnostic lists if no agnostic lists passed in', async () => {
|
||||
await findAllListTypes(
|
||||
[],
|
||||
[{ ...getImportExceptionsListSchemaDecodedMock('1'), namespace_type: 'single' }],
|
||||
savedObjectsClient
|
||||
);
|
||||
await findAllListTypes([], [{ listId: '1', namespaceType: 'single' }], savedObjectsClient);
|
||||
|
||||
expect(findExceptionList).toHaveBeenCalledWith({
|
||||
filter: 'exception-list.attributes.list_id:(1)',
|
||||
|
@ -93,8 +84,8 @@ describe('find_all_exception_list_item_types', () => {
|
|||
|
||||
it('searches for both agnostic an non agnostic lists if some of both passed in', async () => {
|
||||
await findAllListTypes(
|
||||
[{ ...getImportExceptionsListSchemaDecodedMock('1'), namespace_type: 'agnostic' }],
|
||||
[{ ...getImportExceptionsListSchemaDecodedMock('2'), namespace_type: 'single' }],
|
||||
[{ listId: '1', namespaceType: 'agnostic' }],
|
||||
[{ listId: '2', namespaceType: 'single' }],
|
||||
savedObjectsClient
|
||||
);
|
||||
|
||||
|
@ -140,8 +131,8 @@ describe('find_all_exception_list_item_types', () => {
|
|||
total: 1,
|
||||
});
|
||||
const result = await getAllListTypes(
|
||||
[{ ...getImportExceptionsListSchemaDecodedMock('1'), namespace_type: 'agnostic' }],
|
||||
[{ ...getImportExceptionsListSchemaDecodedMock('2'), namespace_type: 'single' }],
|
||||
[{ listId: '1', namespaceType: 'agnostic' }],
|
||||
[{ listId: '2', namespaceType: 'single' }],
|
||||
savedObjectsClient
|
||||
);
|
||||
|
||||
|
|
|
@ -8,8 +8,6 @@
|
|||
import {
|
||||
ExceptionListSchema,
|
||||
FoundExceptionListSchema,
|
||||
ImportExceptionListItemSchemaDecoded,
|
||||
ImportExceptionListSchemaDecoded,
|
||||
NamespaceType,
|
||||
} from '@kbn/securitysolution-io-ts-list-types';
|
||||
import { getSavedObjectTypes } from '@kbn/securitysolution-list-utils';
|
||||
|
@ -18,6 +16,10 @@ import { SavedObjectsClientContract } from 'kibana/server';
|
|||
import { findExceptionList } from '../../find_exception_list';
|
||||
import { CHUNK_PARSED_OBJECT_SIZE } from '../../import_exception_list_and_items';
|
||||
|
||||
export interface ExceptionListQueryInfo {
|
||||
listId: string;
|
||||
namespaceType: NamespaceType;
|
||||
}
|
||||
/**
|
||||
* Helper to build out a filter using list_id
|
||||
* @param objects {array} - exception lists to add to filter
|
||||
|
@ -28,16 +30,14 @@ export const getListFilter = ({
|
|||
objects,
|
||||
namespaceType,
|
||||
}: {
|
||||
objects: Array<
|
||||
(ImportExceptionListSchemaDecoded | ImportExceptionListItemSchemaDecoded) & { list_id: string }
|
||||
>;
|
||||
objects: ExceptionListQueryInfo[];
|
||||
namespaceType: NamespaceType;
|
||||
}): string => {
|
||||
return `${
|
||||
getSavedObjectTypes({
|
||||
namespaceType: [namespaceType],
|
||||
})[0]
|
||||
}.attributes.list_id:(${objects.map<string>((list) => list.list_id).join(' OR ')})`;
|
||||
}.attributes.list_id:(${objects.map((list) => list.listId).join(' OR ')})`;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -48,8 +48,8 @@ export const getListFilter = ({
|
|||
* @returns {object} results of any found lists
|
||||
*/
|
||||
export const findAllListTypes = async (
|
||||
agnosticListItems: ImportExceptionListSchemaDecoded[] | ImportExceptionListItemSchemaDecoded[],
|
||||
nonAgnosticListItems: ImportExceptionListSchemaDecoded[] | ImportExceptionListItemSchemaDecoded[],
|
||||
agnosticListItems: ExceptionListQueryInfo[],
|
||||
nonAgnosticListItems: ExceptionListQueryInfo[],
|
||||
savedObjectsClient: SavedObjectsClientContract
|
||||
): Promise<FoundExceptionListSchema | null> => {
|
||||
// Agnostic filter
|
||||
|
@ -107,8 +107,8 @@ export const findAllListTypes = async (
|
|||
* @returns {object} results of any found lists
|
||||
*/
|
||||
export const getAllListTypes = async (
|
||||
agnosticListItems: ImportExceptionListSchemaDecoded[] | ImportExceptionListItemSchemaDecoded[],
|
||||
nonAgnosticListItems: ImportExceptionListSchemaDecoded[] | ImportExceptionListItemSchemaDecoded[],
|
||||
agnosticListItems: ExceptionListQueryInfo[],
|
||||
nonAgnosticListItems: ExceptionListQueryInfo[],
|
||||
savedObjectsClient: SavedObjectsClientContract
|
||||
): Promise<Record<string, ExceptionListSchema>> => {
|
||||
// Gather lists referenced
|
||||
|
|
|
@ -5,7 +5,10 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ImportExceptionListItemSchemaDecoded } from '@kbn/securitysolution-io-ts-list-types';
|
||||
import {
|
||||
ImportExceptionListItemSchemaDecoded,
|
||||
NamespaceType,
|
||||
} from '@kbn/securitysolution-io-ts-list-types';
|
||||
import { SavedObjectsClientContract } from 'kibana/server';
|
||||
|
||||
import { ImportDataResponse, ImportResponse } from '../../import_exception_list_and_items';
|
||||
|
@ -42,12 +45,18 @@ export const importExceptionListItems = async ({
|
|||
for await (const itemsChunk of itemsChunks) {
|
||||
// sort by namespaceType
|
||||
const [agnosticListItems, nonAgnosticListItems] = sortItemsImportsByNamespace(itemsChunk);
|
||||
const mapList = (
|
||||
list: ImportExceptionListItemSchemaDecoded
|
||||
): { listId: string; namespaceType: NamespaceType } => ({
|
||||
listId: list.list_id,
|
||||
namespaceType: list.namespace_type,
|
||||
});
|
||||
|
||||
// Gather lists referenced by items
|
||||
// Dictionary of found lists
|
||||
const foundLists = await getAllListTypes(
|
||||
agnosticListItems,
|
||||
nonAgnosticListItems,
|
||||
agnosticListItems.map(mapList),
|
||||
nonAgnosticListItems.map(mapList),
|
||||
savedObjectsClient
|
||||
);
|
||||
|
||||
|
|
|
@ -43,7 +43,14 @@ export const importExceptionLists = async ({
|
|||
|
||||
// Gather lists referenced by items
|
||||
// Dictionary of found lists
|
||||
const foundLists = await getAllListTypes(agnosticLists, nonAgnosticLists, savedObjectsClient);
|
||||
const foundLists = await getAllListTypes(
|
||||
agnosticLists.map((list) => ({ listId: list.list_id, namespaceType: list.namespace_type })),
|
||||
nonAgnosticLists.map((list) => ({
|
||||
listId: list.list_id,
|
||||
namespaceType: list.namespace_type,
|
||||
})),
|
||||
savedObjectsClient
|
||||
);
|
||||
|
||||
// Figure out what lists to bulk create/update
|
||||
const { errors, listItemsToDelete, listsToCreate, listsToUpdate } =
|
||||
|
|
|
@ -9,13 +9,11 @@ import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts
|
|||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
import { left } from 'fp-ts/lib/Either';
|
||||
import {
|
||||
importRulesPayloadSchema,
|
||||
ImportRulesPayloadSchema,
|
||||
ImportRulesSchema,
|
||||
importRulesSchema,
|
||||
ImportRulesSchemaDecoded,
|
||||
importRulesQuerySchema,
|
||||
ImportRulesQuerySchema,
|
||||
importRulesPayloadSchema,
|
||||
ImportRulesPayloadSchema,
|
||||
} from './import_rules_schema';
|
||||
import {
|
||||
getImportRulesSchemaMock,
|
||||
|
@ -1254,46 +1252,6 @@ describe('import rules schema', () => {
|
|||
expect(message.schema).toEqual({});
|
||||
});
|
||||
|
||||
describe('importRulesQuerySchema', () => {
|
||||
test('overwrite gets a default value of false', () => {
|
||||
const payload: ImportRulesQuerySchema = {};
|
||||
|
||||
const decoded = importRulesQuerySchema.decode(payload);
|
||||
const checked = exactCheck(payload, decoded);
|
||||
const message = pipe(checked, foldLeftRight);
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual({
|
||||
overwrite: false,
|
||||
});
|
||||
});
|
||||
|
||||
test('overwrite validates with a boolean true', () => {
|
||||
const payload: ImportRulesQuerySchema = { overwrite: true };
|
||||
|
||||
const decoded = importRulesQuerySchema.decode(payload);
|
||||
const checked = exactCheck(payload, decoded);
|
||||
const message = pipe(checked, foldLeftRight);
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual({
|
||||
overwrite: true,
|
||||
});
|
||||
});
|
||||
|
||||
test('overwrite does not validate with a weird string', () => {
|
||||
const payload: Omit<ImportRulesQuerySchema, 'overwrite'> & { overwrite: string } = {
|
||||
overwrite: 'invalid-string',
|
||||
};
|
||||
|
||||
const decoded = importRulesQuerySchema.decode(payload);
|
||||
const checked = exactCheck(payload, decoded);
|
||||
const message = pipe(checked, foldLeftRight);
|
||||
expect(getPaths(left(message.errors))).toEqual([
|
||||
'Invalid value "invalid-string" supplied to "overwrite"',
|
||||
]);
|
||||
expect(message.schema).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('importRulesPayloadSchema', () => {
|
||||
test('does not validate with an empty object', () => {
|
||||
const payload = {};
|
||||
|
|
|
@ -45,7 +45,6 @@ import {
|
|||
DefaultStringArray,
|
||||
DefaultBooleanTrue,
|
||||
OnlyFalseAllowed,
|
||||
DefaultStringBooleanFalse,
|
||||
} from '@kbn/securitysolution-io-ts-types';
|
||||
import { DefaultListArray, ListArray } from '@kbn/securitysolution-io-ts-list-types';
|
||||
import {
|
||||
|
@ -203,17 +202,6 @@ export type ImportRulesSchemaDecoded = Omit<
|
|||
immutable: false;
|
||||
};
|
||||
|
||||
export const importRulesQuerySchema = t.exact(
|
||||
t.partial({
|
||||
overwrite: DefaultStringBooleanFalse,
|
||||
})
|
||||
);
|
||||
|
||||
export type ImportRulesQuerySchema = t.TypeOf<typeof importRulesQuerySchema>;
|
||||
export type ImportRulesQuerySchemaDecoded = Omit<ImportRulesQuerySchema, 'overwrite'> & {
|
||||
overwrite: boolean;
|
||||
};
|
||||
|
||||
export const importRulesPayloadSchema = t.exact(
|
||||
t.type({
|
||||
file: t.object,
|
||||
|
|
|
@ -14,7 +14,14 @@ import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts
|
|||
|
||||
describe('import_rules_schema', () => {
|
||||
test('it should validate an empty import response with no errors', () => {
|
||||
const payload: ImportRulesSchema = { success: true, success_count: 0, errors: [] };
|
||||
const payload: ImportRulesSchema = {
|
||||
success: true,
|
||||
success_count: 0,
|
||||
errors: [],
|
||||
exceptions_errors: [],
|
||||
exceptions_success: true,
|
||||
exceptions_success_count: 0,
|
||||
};
|
||||
const decoded = importRulesSchema.decode(payload);
|
||||
const checked = exactCheck(payload, decoded);
|
||||
const message = pipe(checked, foldLeftRight);
|
||||
|
@ -28,6 +35,26 @@ describe('import_rules_schema', () => {
|
|||
success: false,
|
||||
success_count: 0,
|
||||
errors: [{ error: { status_code: 400, message: 'some message' } }],
|
||||
exceptions_errors: [],
|
||||
exceptions_success: true,
|
||||
exceptions_success_count: 0,
|
||||
};
|
||||
const decoded = importRulesSchema.decode(payload);
|
||||
const checked = exactCheck(payload, decoded);
|
||||
const message = pipe(checked, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual(payload);
|
||||
});
|
||||
|
||||
test('it should validate an empty import response with a single exceptions error', () => {
|
||||
const payload: ImportRulesSchema = {
|
||||
success: false,
|
||||
success_count: 0,
|
||||
errors: [],
|
||||
exceptions_errors: [{ error: { status_code: 400, message: 'some message' } }],
|
||||
exceptions_success: true,
|
||||
exceptions_success_count: 0,
|
||||
};
|
||||
const decoded = importRulesSchema.decode(payload);
|
||||
const checked = exactCheck(payload, decoded);
|
||||
|
@ -45,6 +72,9 @@ describe('import_rules_schema', () => {
|
|||
{ error: { status_code: 400, message: 'some message' } },
|
||||
{ error: { status_code: 500, message: 'some message' } },
|
||||
],
|
||||
exceptions_errors: [],
|
||||
exceptions_success: true,
|
||||
exceptions_success_count: 0,
|
||||
};
|
||||
const decoded = importRulesSchema.decode(payload);
|
||||
const checked = exactCheck(payload, decoded);
|
||||
|
@ -54,11 +84,34 @@ describe('import_rules_schema', () => {
|
|||
expect(message.schema).toEqual(payload);
|
||||
});
|
||||
|
||||
test('it should NOT validate a status_count that is a negative number', () => {
|
||||
test('it should validate an empty import response with two exception errors', () => {
|
||||
const payload: ImportRulesSchema = {
|
||||
success: false,
|
||||
success_count: 0,
|
||||
errors: [],
|
||||
exceptions_errors: [
|
||||
{ error: { status_code: 400, message: 'some message' } },
|
||||
{ error: { status_code: 500, message: 'some message' } },
|
||||
],
|
||||
exceptions_success: true,
|
||||
exceptions_success_count: 0,
|
||||
};
|
||||
const decoded = importRulesSchema.decode(payload);
|
||||
const checked = exactCheck(payload, decoded);
|
||||
const message = pipe(checked, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual(payload);
|
||||
});
|
||||
|
||||
test('it should NOT validate a success_count that is a negative number', () => {
|
||||
const payload: ImportRulesSchema = {
|
||||
success: false,
|
||||
success_count: -1,
|
||||
errors: [],
|
||||
exceptions_errors: [],
|
||||
exceptions_success: true,
|
||||
exceptions_success_count: 0,
|
||||
};
|
||||
const decoded = importRulesSchema.decode(payload);
|
||||
const checked = exactCheck(payload, decoded);
|
||||
|
@ -70,6 +123,25 @@ describe('import_rules_schema', () => {
|
|||
expect(message.schema).toEqual({});
|
||||
});
|
||||
|
||||
test('it should NOT validate a exceptions_success_count that is a negative number', () => {
|
||||
const payload: ImportRulesSchema = {
|
||||
success: false,
|
||||
success_count: 0,
|
||||
errors: [],
|
||||
exceptions_errors: [],
|
||||
exceptions_success: true,
|
||||
exceptions_success_count: -1,
|
||||
};
|
||||
const decoded = importRulesSchema.decode(payload);
|
||||
const checked = exactCheck(payload, decoded);
|
||||
const message = pipe(checked, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([
|
||||
'Invalid value "-1" supplied to "exceptions_success_count"',
|
||||
]);
|
||||
expect(message.schema).toEqual({});
|
||||
});
|
||||
|
||||
test('it should NOT validate a success that is not a boolean', () => {
|
||||
type UnsafeCastForTest = Either<
|
||||
Errors,
|
||||
|
@ -93,6 +165,9 @@ describe('import_rules_schema', () => {
|
|||
success: 'hello',
|
||||
success_count: 0,
|
||||
errors: [],
|
||||
exceptions_errors: [],
|
||||
exceptions_success: true,
|
||||
exceptions_success_count: 0,
|
||||
};
|
||||
const decoded = importRulesSchema.decode(payload);
|
||||
const checked = exactCheck(payload, decoded as UnsafeCastForTest);
|
||||
|
@ -102,12 +177,54 @@ describe('import_rules_schema', () => {
|
|||
expect(message.schema).toEqual({});
|
||||
});
|
||||
|
||||
test('it should NOT validate a exceptions_success that is not a boolean', () => {
|
||||
type UnsafeCastForTest = Either<
|
||||
Errors,
|
||||
{
|
||||
success: boolean;
|
||||
exceptions_success: string;
|
||||
success_count: number;
|
||||
errors: Array<
|
||||
{
|
||||
id?: string | undefined;
|
||||
rule_id?: string | undefined;
|
||||
} & {
|
||||
error: {
|
||||
status_code: number;
|
||||
message: string;
|
||||
};
|
||||
}
|
||||
>;
|
||||
}
|
||||
>;
|
||||
const payload: Omit<ImportRulesSchema, 'exceptions_success'> & { exceptions_success: string } =
|
||||
{
|
||||
success: true,
|
||||
success_count: 0,
|
||||
errors: [],
|
||||
exceptions_errors: [],
|
||||
exceptions_success: 'hello',
|
||||
exceptions_success_count: 0,
|
||||
};
|
||||
const decoded = importRulesSchema.decode(payload);
|
||||
const checked = exactCheck(payload, decoded as UnsafeCastForTest);
|
||||
const message = pipe(checked, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([
|
||||
'Invalid value "hello" supplied to "exceptions_success"',
|
||||
]);
|
||||
expect(message.schema).toEqual({});
|
||||
});
|
||||
|
||||
test('it should NOT validate a success an extra invalid field', () => {
|
||||
const payload: ImportRulesSchema & { invalid_field: string } = {
|
||||
success: true,
|
||||
success_count: 0,
|
||||
errors: [],
|
||||
invalid_field: 'invalid_data',
|
||||
exceptions_errors: [],
|
||||
exceptions_success: true,
|
||||
exceptions_success_count: 0,
|
||||
};
|
||||
const decoded = importRulesSchema.decode(payload);
|
||||
const checked = exactCheck(payload, decoded);
|
||||
|
@ -117,7 +234,7 @@ describe('import_rules_schema', () => {
|
|||
expect(message.schema).toEqual({});
|
||||
});
|
||||
|
||||
test('it should NOT validate an extra field in the second position of the array', () => {
|
||||
test('it should NOT validate an extra field in the second position of the errors array', () => {
|
||||
type InvalidError = ErrorSchema & { invalid_data?: string };
|
||||
const payload: Omit<ImportRulesSchema, 'errors'> & {
|
||||
errors: InvalidError[];
|
||||
|
@ -128,6 +245,9 @@ describe('import_rules_schema', () => {
|
|||
{ error: { status_code: 400, message: 'some message' } },
|
||||
{ invalid_data: 'something', error: { status_code: 500, message: 'some message' } },
|
||||
],
|
||||
exceptions_errors: [],
|
||||
exceptions_success: true,
|
||||
exceptions_success_count: 0,
|
||||
};
|
||||
const decoded = importRulesSchema.decode(payload);
|
||||
const checked = exactCheck(payload, decoded);
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { PositiveInteger } from '@kbn/securitysolution-io-ts-types';
|
||||
import * as t from 'io-ts';
|
||||
|
||||
import { success, success_count } from '../common/schemas';
|
||||
|
@ -12,6 +13,9 @@ import { errorSchema } from './error_schema';
|
|||
|
||||
export const importRulesSchema = t.exact(
|
||||
t.type({
|
||||
exceptions_success: t.boolean,
|
||||
exceptions_success_count: PositiveInteger,
|
||||
exceptions_errors: t.array(errorSchema),
|
||||
success,
|
||||
success_count,
|
||||
errors: t.array(errorSchema),
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
{"id":"3e284ab0-5862-11ec-b7ea-a3a4654b19e1","updated_at":"2021-12-09T19:21:13.557Z","updated_by":"elastic","created_at":"2021-12-08T20:05:52.385Z","created_by":"elastic","name":"Test Custom Rule","tags":[],"interval":"1m","enabled":true,"description":"Test Custom Rule","risk_score":21,"severity":"low","license":"","output_index":".siem-signals-siem-estc-dev-default","meta":{"from":"100m","kibana_siem_app_url":"https://kibana.siem.estc.dev/app/security"},"author":[],"false_positives":[],"from":"now-6060s","rule_id":"cc646fc2-37a4-4858-af1a-e09b00b15c9e","max_signals":100,"risk_score_mapping":[],"severity_mapping":[],"threat":[],"to":"now","references":[],"version":2,"exceptions_list":[{"id":"2b50cb60-5925-11ec-a0e8-65c8bb7e794f","list_id":"b8dfd17f-1e11-41b0-ae7e-9e7f8237de49","type":"detection","namespace_type":"single"}],"immutable":false,"type":"query","language":"kuery","index":["test*"],"query":"file.hash.md5 : *","filters":[],"throttle":"no_actions","actions":[]}
|
||||
{"_version":"WzM1NTcxLDFd","created_at":"2021-12-09T19:21:11.702Z","created_by":"elastic","description":"Test Custom Rule","id":"2b50cb60-5925-11ec-a0e8-65c8bb7e794f","immutable":false,"list_id":"b8dfd17f-1e11-41b0-ae7e-9e7f8237de49","name":"Test Custom Rule","namespace_type":"single","os_types":[],"tags":[],"tie_breaker_id":"491414df-26f4-4c15-9cfa-0b1e1604330c","type":"detection","updated_at":"2021-12-09T19:21:11.709Z","updated_by":"elastic","version":1}
|
||||
{"_version":"Wzg4Mjc4LDJd","comments":[],"created_at":"2021-12-13T07:01:23.261Z","created_by":"yara.tecero@elastic.co","description":"Test Custom Rule - exception list item","entries":[{"field":"file.hash.md5","operator":"included","type":"exists"}],"id":"7b6536d0-5be2-11ec-bb9b-d70de62f20d7","item_id":"3daf7dbd-3ab9-40a4-b8e4-1ef1546b0950","list_id":"b8dfd17f-1e11-41b0-ae7e-9e7f8237de49","name":"Test Custom Rule - exception list item","namespace_type":"single","os_types":[],"tags":[],"tie_breaker_id":"f302e67e-dc48-492d-bdc3-032051a1ccca","type":"simple","updated_at":"2021-12-13T07:01:23.269Z","updated_by":"yara.tecero@elastic.co"}
|
||||
{"exported_count":3,"exported_rules_count":1,"missing_rules":[],"missing_rules_count":0,"exported_exception_list_count":1,"exported_exception_list_item_count":1,"missing_exception_list_item_count":0,"missing_exception_list_items":[],"missing_exception_lists":[],"missing_exception_lists_count":0}
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import {
|
||||
goToManageAlertsDetectionRules,
|
||||
waitForAlertsIndexToBeCreated,
|
||||
waitForAlertsPanelToBeLoaded,
|
||||
} from '../../tasks/alerts';
|
||||
import {
|
||||
getRulesImportToast,
|
||||
importRules,
|
||||
importRulesWithOverwriteAll,
|
||||
} from '../../tasks/alerts_detection_rules';
|
||||
import { cleanKibana, reload } from '../../tasks/common';
|
||||
import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login';
|
||||
|
||||
import { ALERTS_URL } from '../../urls/navigation';
|
||||
|
||||
describe('Import rules', () => {
|
||||
beforeEach(() => {
|
||||
cleanKibana();
|
||||
cy.intercept('POST', '/api/detection_engine/rules/_import*').as('import');
|
||||
loginAndWaitForPageWithoutDateRange(ALERTS_URL);
|
||||
waitForAlertsPanelToBeLoaded();
|
||||
waitForAlertsIndexToBeCreated();
|
||||
goToManageAlertsDetectionRules();
|
||||
});
|
||||
|
||||
it('Imports a custom rule with exceptions', function () {
|
||||
importRules('7_16_rules.ndjson');
|
||||
|
||||
cy.wait('@import').then(({ response }) => {
|
||||
cy.wrap(response?.statusCode).should('eql', 200);
|
||||
getRulesImportToast(['Successfully imported 1 rule', 'Successfully imported 2 exceptions.']);
|
||||
});
|
||||
});
|
||||
|
||||
it('Shows error toaster when trying to import rule and exception list that already exist', function () {
|
||||
importRules('7_16_rules.ndjson');
|
||||
|
||||
cy.wait('@import').then(({ response }) => {
|
||||
cy.wrap(response?.statusCode).should('eql', 200);
|
||||
});
|
||||
|
||||
reload();
|
||||
importRules('7_16_rules.ndjson');
|
||||
|
||||
cy.wait('@import').then(({ response }) => {
|
||||
cy.wrap(response?.statusCode).should('eql', 200);
|
||||
getRulesImportToast(['Failed to import 1 rule', 'Failed to import 2 exceptions']);
|
||||
});
|
||||
});
|
||||
|
||||
it('Does not show error toaster when trying to import rule and exception list that already exist when overwrite is true', function () {
|
||||
importRules('7_16_rules.ndjson');
|
||||
|
||||
cy.wait('@import').then(({ response }) => {
|
||||
cy.wrap(response?.statusCode).should('eql', 200);
|
||||
});
|
||||
|
||||
reload();
|
||||
importRulesWithOverwriteAll('7_16_rules.ndjson');
|
||||
|
||||
cy.wait('@import').then(({ response }) => {
|
||||
cy.wrap(response?.statusCode).should('eql', 200);
|
||||
getRulesImportToast(['Successfully imported 1 rule', 'Successfully imported 2 exceptions.']);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -99,3 +99,16 @@ export const ALERT_DETAILS_CELLS = '[data-test-subj="dataGridRowCell"]';
|
|||
export const SERVER_SIDE_EVENT_COUNT = '[data-test-subj="server-side-event-count"]';
|
||||
|
||||
export const SELECT_ALL_RULES_ON_PAGE_CHECKBOX = '[data-test-subj="checkboxSelectAll"]';
|
||||
|
||||
export const RULE_IMPORT_MODAL = '[data-test-subj="rules-import-modal-button"]';
|
||||
|
||||
export const RULE_IMPORT_MODAL_BUTTON = '[data-test-subj="import-data-modal-button"]';
|
||||
|
||||
export const INPUT_FILE = 'input[type=file]';
|
||||
|
||||
export const TOASTER = '[data-test-subj="euiToastHeader"]';
|
||||
|
||||
export const RULE_IMPORT_OVERWRITE_CHECKBOX = '[id="import-data-modal-checkbox-label"]';
|
||||
|
||||
export const RULE_IMPORT_OVERWRITE_EXCEPTIONS_CHECKBOX =
|
||||
'[id="import-data-modal-exceptions-checkbox-label"]';
|
||||
|
|
|
@ -41,6 +41,12 @@ import {
|
|||
ACTIVATE_RULE_BULK_BTN,
|
||||
DEACTIVATE_RULE_BULK_BTN,
|
||||
RULE_DETAILS_DELETE_BTN,
|
||||
RULE_IMPORT_MODAL_BUTTON,
|
||||
RULE_IMPORT_MODAL,
|
||||
INPUT_FILE,
|
||||
TOASTER,
|
||||
RULE_IMPORT_OVERWRITE_CHECKBOX,
|
||||
RULE_IMPORT_OVERWRITE_EXCEPTIONS_CHECKBOX,
|
||||
} from '../screens/alerts_detection_rules';
|
||||
import { ALL_ACTIONS } from '../screens/rule_details';
|
||||
import { LOADING_INDICATOR } from '../screens/security_header';
|
||||
|
@ -260,3 +266,51 @@ export const goToPage = (pageNumber: number) => {
|
|||
cy.get(pageSelector(pageNumber)).last().click({ force: true });
|
||||
waitForRulesTableToBeRefreshed();
|
||||
};
|
||||
|
||||
export const importRules = (rulesFile: string) => {
|
||||
cy.get(RULE_IMPORT_MODAL).click();
|
||||
cy.get(INPUT_FILE).should('exist');
|
||||
cy.get(INPUT_FILE).trigger('click', { force: true }).attachFile(rulesFile).trigger('change');
|
||||
cy.get(RULE_IMPORT_MODAL_BUTTON).last().click({ force: true });
|
||||
cy.get(INPUT_FILE).should('not.exist');
|
||||
};
|
||||
|
||||
export const getRulesImportToast = (headers: string[]) => {
|
||||
cy.get(TOASTER)
|
||||
.should('exist')
|
||||
.then(($els) => {
|
||||
const arrayOfText = Cypress.$.makeArray($els).map((el) => el.innerText);
|
||||
|
||||
return headers.reduce((areAllIncluded, header) => {
|
||||
const isContained = arrayOfText.includes(header);
|
||||
if (!areAllIncluded) {
|
||||
return false;
|
||||
} else {
|
||||
return isContained;
|
||||
}
|
||||
}, true);
|
||||
})
|
||||
.should('be.true');
|
||||
};
|
||||
|
||||
export const selectOverwriteRulesImport = () => {
|
||||
cy.get(RULE_IMPORT_OVERWRITE_CHECKBOX)
|
||||
.pipe(($el) => $el.trigger('click'))
|
||||
.should('be.checked');
|
||||
};
|
||||
|
||||
export const selectOverwriteExceptionsRulesImport = () => {
|
||||
cy.get(RULE_IMPORT_OVERWRITE_EXCEPTIONS_CHECKBOX)
|
||||
.pipe(($el) => $el.trigger('click'))
|
||||
.should('be.checked');
|
||||
};
|
||||
|
||||
export const importRulesWithOverwriteAll = (rulesFile: string) => {
|
||||
cy.get(RULE_IMPORT_MODAL).click();
|
||||
cy.get(INPUT_FILE).should('exist');
|
||||
cy.get(INPUT_FILE).trigger('click', { force: true }).attachFile(rulesFile).trigger('change');
|
||||
selectOverwriteRulesImport();
|
||||
selectOverwriteExceptionsRulesImport();
|
||||
cy.get(RULE_IMPORT_MODAL_BUTTON).last().click({ force: true });
|
||||
cy.get(INPUT_FILE).should('not.exist');
|
||||
};
|
||||
|
|
|
@ -51,6 +51,7 @@ exports[`ImportDataModal renders correctly against snapshot 1`] = `
|
|||
Cancel
|
||||
</EuiButtonEmpty>
|
||||
<EuiButton
|
||||
data-test-subj="import-data-modal-button"
|
||||
disabled={true}
|
||||
fill={true}
|
||||
onClick={[Function]}
|
||||
|
|
|
@ -23,6 +23,9 @@ import React, { useCallback, useState } from 'react';
|
|||
import {
|
||||
ImportDataResponse,
|
||||
ImportDataProps,
|
||||
ImportResponseError,
|
||||
ImportRulesResponseError,
|
||||
ExceptionsImportError,
|
||||
} from '../../../detections/containers/detection_engine/rules';
|
||||
import { useAppToasts } from '../../hooks/use_app_toasts';
|
||||
import * as i18n from './translations';
|
||||
|
@ -36,6 +39,7 @@ interface ImportDataModalProps {
|
|||
importComplete: () => void;
|
||||
importData: (arg: ImportDataProps) => Promise<ImportDataResponse>;
|
||||
showCheckBox: boolean;
|
||||
showExceptionsCheckBox?: boolean;
|
||||
showModal: boolean;
|
||||
submitBtnText: string;
|
||||
subtitle: string;
|
||||
|
@ -55,6 +59,7 @@ export const ImportDataModalComponent = ({
|
|||
importComplete,
|
||||
importData,
|
||||
showCheckBox = true,
|
||||
showExceptionsCheckBox = false,
|
||||
showModal,
|
||||
submitBtnText,
|
||||
subtitle,
|
||||
|
@ -64,8 +69,25 @@ export const ImportDataModalComponent = ({
|
|||
const [selectedFiles, setSelectedFiles] = useState<FileList | null>(null);
|
||||
const [isImporting, setIsImporting] = useState(false);
|
||||
const [overwrite, setOverwrite] = useState(false);
|
||||
const [overwriteExceptions, setOverwriteExceptions] = useState(false);
|
||||
const { addError, addSuccess } = useAppToasts();
|
||||
|
||||
const formatError = useCallback(
|
||||
(
|
||||
importResponse: ImportDataResponse,
|
||||
errors: Array<ImportRulesResponseError | ImportResponseError | ExceptionsImportError>
|
||||
) => {
|
||||
const formattedErrors = errors.map((e) => failedDetailed(e.error.message));
|
||||
const error: Error & { raw_network_error?: object } = new Error(formattedErrors.join('. '));
|
||||
error.stack = undefined;
|
||||
error.name = 'Network errors';
|
||||
error.raw_network_error = importResponse;
|
||||
|
||||
return error;
|
||||
},
|
||||
[failedDetailed]
|
||||
);
|
||||
|
||||
const cleanupAndCloseModal = useCallback(() => {
|
||||
setIsImporting(false);
|
||||
setSelectedFiles(null);
|
||||
|
@ -81,23 +103,40 @@ export const ImportDataModalComponent = ({
|
|||
const importResponse = await importData({
|
||||
fileToImport: selectedFiles[0],
|
||||
overwrite,
|
||||
overwriteExceptions,
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
|
||||
// rules response actions
|
||||
if (importResponse.success) {
|
||||
addSuccess(successMessage(importResponse.success_count));
|
||||
}
|
||||
if (importResponse.errors.length > 0) {
|
||||
const formattedErrors = importResponse.errors.map((e) => failedDetailed(e.error.message));
|
||||
const error: Error & { raw_network_error?: object } = new Error(
|
||||
formattedErrors.join('. ')
|
||||
);
|
||||
error.stack = undefined;
|
||||
error.name = 'Network errors';
|
||||
error.raw_network_error = importResponse;
|
||||
const error = formatError(importResponse, importResponse.errors);
|
||||
addError(error, { title: errorMessage(importResponse.errors.length) });
|
||||
}
|
||||
|
||||
// if import includes exceptions
|
||||
if (showExceptionsCheckBox) {
|
||||
// exceptions response actions
|
||||
if (
|
||||
importResponse.exceptions_success &&
|
||||
importResponse.exceptions_success_count != null
|
||||
) {
|
||||
addSuccess(
|
||||
i18n.SUCCESSFULLY_IMPORTED_EXCEPTIONS(importResponse.exceptions_success_count)
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
importResponse.exceptions_errors != null &&
|
||||
importResponse.exceptions_errors.length > 0
|
||||
) {
|
||||
const error = formatError(importResponse, importResponse.exceptions_errors);
|
||||
addError(error, { title: i18n.IMPORT_FAILED(importResponse.exceptions_errors.length) });
|
||||
}
|
||||
}
|
||||
|
||||
importComplete();
|
||||
cleanupAndCloseModal();
|
||||
} catch (error) {
|
||||
|
@ -108,14 +147,16 @@ export const ImportDataModalComponent = ({
|
|||
}, [
|
||||
selectedFiles,
|
||||
overwrite,
|
||||
overwriteExceptions,
|
||||
addError,
|
||||
addSuccess,
|
||||
cleanupAndCloseModal,
|
||||
errorMessage,
|
||||
failedDetailed,
|
||||
importComplete,
|
||||
importData,
|
||||
successMessage,
|
||||
showExceptionsCheckBox,
|
||||
formatError,
|
||||
]);
|
||||
|
||||
const handleCloseModal = useCallback(() => {
|
||||
|
@ -123,6 +164,14 @@ export const ImportDataModalComponent = ({
|
|||
closeModal();
|
||||
}, [closeModal]);
|
||||
|
||||
const handleCheckboxClick = useCallback(() => {
|
||||
setOverwrite((shouldOverwrite) => !shouldOverwrite);
|
||||
}, []);
|
||||
|
||||
const handleExceptionsCheckboxClick = useCallback(() => {
|
||||
setOverwriteExceptions((shouldOverwrite) => !shouldOverwrite);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
{showModal && (
|
||||
|
@ -149,18 +198,29 @@ export const ImportDataModalComponent = ({
|
|||
/>
|
||||
<EuiSpacer size="s" />
|
||||
{showCheckBox && (
|
||||
<EuiCheckbox
|
||||
id="import-data-modal-checkbox-label"
|
||||
label={checkBoxLabel}
|
||||
checked={overwrite}
|
||||
onChange={() => setOverwrite(!overwrite)}
|
||||
/>
|
||||
<>
|
||||
<EuiCheckbox
|
||||
id="import-data-modal-checkbox-label"
|
||||
label={checkBoxLabel}
|
||||
checked={overwrite}
|
||||
onChange={handleCheckboxClick}
|
||||
/>
|
||||
{showExceptionsCheckBox && (
|
||||
<EuiCheckbox
|
||||
id="import-data-modal-exceptions-checkbox-label"
|
||||
label={i18n.OVERWRITE_EXCEPTIONS_LABEL}
|
||||
checked={overwriteExceptions}
|
||||
onChange={handleExceptionsCheckboxClick}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</EuiModalBody>
|
||||
|
||||
<EuiModalFooter>
|
||||
<EuiButtonEmpty onClick={handleCloseModal}>{i18n.CANCEL_BUTTON}</EuiButtonEmpty>
|
||||
<EuiButton
|
||||
data-test-subj="import-data-modal-button"
|
||||
onClick={importDataCallback}
|
||||
disabled={selectedFiles == null || isImporting}
|
||||
fill
|
||||
|
|
|
@ -13,3 +13,30 @@ export const CANCEL_BUTTON = i18n.translate(
|
|||
defaultMessage: 'Cancel',
|
||||
}
|
||||
);
|
||||
|
||||
export const OVERWRITE_EXCEPTIONS_LABEL = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.components.importRuleModal.overwriteExceptionLabel',
|
||||
{
|
||||
defaultMessage: 'Overwrite existing exception lists with conflicting "list_id"',
|
||||
}
|
||||
);
|
||||
|
||||
export const SUCCESSFULLY_IMPORTED_EXCEPTIONS = (totalExceptions: number) =>
|
||||
i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.components.importRuleModal.exceptionsSuccessLabel',
|
||||
{
|
||||
values: { totalExceptions },
|
||||
defaultMessage:
|
||||
'Successfully imported {totalExceptions} {totalExceptions, plural, =1 {exception} other {exceptions}}.',
|
||||
}
|
||||
);
|
||||
|
||||
export const IMPORT_FAILED = (totalExceptions: number) =>
|
||||
i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.components.importRuleModal.importExceptionsFailedLabel',
|
||||
{
|
||||
values: { totalExceptions },
|
||||
defaultMessage:
|
||||
'Failed to import {totalExceptions} {totalExceptions, plural, =1 {exception} other {exceptions}}',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -520,6 +520,7 @@ describe('Detections Rules API', () => {
|
|||
},
|
||||
query: {
|
||||
overwrite: false,
|
||||
overwrite_exceptions: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -535,6 +536,7 @@ describe('Detections Rules API', () => {
|
|||
},
|
||||
query: {
|
||||
overwrite: true,
|
||||
overwrite_exceptions: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -544,12 +546,18 @@ describe('Detections Rules API', () => {
|
|||
success: true,
|
||||
success_count: 33,
|
||||
errors: [],
|
||||
exceptions_errors: [],
|
||||
exceptions_success: true,
|
||||
exceptions_success_count: 0,
|
||||
});
|
||||
const resp = await importRules({ fileToImport, signal: abortCtrl.signal });
|
||||
expect(resp).toEqual({
|
||||
success: true,
|
||||
success_count: 33,
|
||||
errors: [],
|
||||
exceptions_errors: [],
|
||||
exceptions_success: true,
|
||||
exceptions_success_count: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -311,6 +311,7 @@ export const createPrepackagedRules = async ({
|
|||
export const importRules = async ({
|
||||
fileToImport,
|
||||
overwrite = false,
|
||||
overwriteExceptions = false,
|
||||
signal,
|
||||
}: ImportDataProps): Promise<ImportDataResponse> => {
|
||||
const formData = new FormData();
|
||||
|
@ -321,7 +322,7 @@ export const importRules = async ({
|
|||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': undefined },
|
||||
query: { overwrite },
|
||||
query: { overwrite, overwrite_exceptions: overwriteExceptions },
|
||||
body: formData,
|
||||
signal,
|
||||
}
|
||||
|
|
|
@ -244,6 +244,7 @@ export interface BasicFetchProps {
|
|||
export interface ImportDataProps {
|
||||
fileToImport: File;
|
||||
overwrite?: boolean;
|
||||
overwriteExceptions?: boolean;
|
||||
signal: AbortSignal;
|
||||
}
|
||||
|
||||
|
@ -263,10 +264,23 @@ export interface ImportResponseError {
|
|||
};
|
||||
}
|
||||
|
||||
export interface ExceptionsImportError {
|
||||
error: {
|
||||
status_code: number;
|
||||
message: string;
|
||||
};
|
||||
id?: string | undefined;
|
||||
list_id?: string | undefined;
|
||||
item_id?: string | undefined;
|
||||
}
|
||||
|
||||
export interface ImportDataResponse {
|
||||
success: boolean;
|
||||
success_count: number;
|
||||
errors: Array<ImportRulesResponseError | ImportResponseError>;
|
||||
exceptions_success?: boolean;
|
||||
exceptions_success_count?: number;
|
||||
exceptions_errors?: ExceptionsImportError[];
|
||||
}
|
||||
|
||||
export interface ExportDocumentsProps {
|
||||
|
|
|
@ -181,11 +181,12 @@ const RulesPageComponent: React.FC = () => {
|
|||
importComplete={handleRefreshRules}
|
||||
importData={importRules}
|
||||
successMessage={i18n.SUCCESSFULLY_IMPORTED_RULES}
|
||||
showCheckBox={true}
|
||||
showModal={showImportModal}
|
||||
submitBtnText={i18n.IMPORT_RULE_BTN_TITLE}
|
||||
subtitle={i18n.INITIAL_PROMPT_TEXT}
|
||||
title={i18n.IMPORT_RULE}
|
||||
showExceptionsCheckBox
|
||||
showCheckBox
|
||||
/>
|
||||
<SecuritySolutionPageWrapper>
|
||||
<DetectionEngineHeaderPage title={i18n.PAGE_TITLE}>
|
||||
|
@ -210,6 +211,7 @@ const RulesPageComponent: React.FC = () => {
|
|||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
data-test-subj="rules-import-modal-button"
|
||||
iconType="importAction"
|
||||
isDisabled={!userHasPermissions(canUserCRUD) || loading}
|
||||
onClick={() => {
|
||||
|
|
|
@ -544,7 +544,7 @@ export const DELETE = i18n.translate(
|
|||
export const IMPORT_RULE_BTN_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.components.importRuleModal.importRuleTitle',
|
||||
{
|
||||
defaultMessage: 'Import rule',
|
||||
defaultMessage: 'Import',
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -552,7 +552,7 @@ export const SELECT_RULE = i18n.translate(
|
|||
'xpack.securitySolution.detectionEngine.components.importRuleModal.selectRuleDescription',
|
||||
{
|
||||
defaultMessage:
|
||||
'Select rules and actions (as exported from the Security > Rules page) to import',
|
||||
'Select rules to import. Associated rule actions and exceptions can be included.',
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -566,7 +566,7 @@ export const INITIAL_PROMPT_TEXT = i18n.translate(
|
|||
export const OVERWRITE_WITH_SAME_NAME = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.components.importRuleModal.overwriteDescription',
|
||||
{
|
||||
defaultMessage: 'Overwrite existing detection rules with conflicting Rule ID',
|
||||
defaultMessage: 'Overwrite existing detection rules with conflicting "rule_id"',
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -102,6 +102,9 @@ describe.each([
|
|||
],
|
||||
success: false,
|
||||
success_count: 0,
|
||||
exceptions_errors: [],
|
||||
exceptions_success: true,
|
||||
exceptions_success_count: 0,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -172,6 +175,9 @@ describe.each([
|
|||
errors: [],
|
||||
success: true,
|
||||
success_count: 1,
|
||||
exceptions_errors: [],
|
||||
exceptions_success: true,
|
||||
exceptions_success_count: 0,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -193,6 +199,9 @@ describe.each([
|
|||
],
|
||||
success: false,
|
||||
success_count: 0,
|
||||
exceptions_errors: [],
|
||||
exceptions_success: true,
|
||||
exceptions_success_count: 0,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -216,6 +225,9 @@ describe.each([
|
|||
],
|
||||
success: false,
|
||||
success_count: 0,
|
||||
exceptions_errors: [],
|
||||
exceptions_success: true,
|
||||
exceptions_success_count: 0,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -233,6 +245,9 @@ describe.each([
|
|||
errors: [],
|
||||
success: true,
|
||||
success_count: 1,
|
||||
exceptions_errors: [],
|
||||
exceptions_success: true,
|
||||
exceptions_success_count: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -250,6 +265,9 @@ describe.each([
|
|||
errors: [],
|
||||
success: true,
|
||||
success_count: 2,
|
||||
exceptions_errors: [],
|
||||
exceptions_success: true,
|
||||
exceptions_success_count: 0,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -263,6 +281,9 @@ describe.each([
|
|||
errors: [],
|
||||
success: true,
|
||||
success_count: 9999,
|
||||
exceptions_errors: [],
|
||||
exceptions_success: true,
|
||||
exceptions_success_count: 0,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -299,6 +320,9 @@ describe.each([
|
|||
],
|
||||
success: false,
|
||||
success_count: 0,
|
||||
exceptions_errors: [],
|
||||
exceptions_success: true,
|
||||
exceptions_success_count: 0,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -322,6 +346,9 @@ describe.each([
|
|||
],
|
||||
success: false,
|
||||
success_count: 1,
|
||||
exceptions_errors: [],
|
||||
exceptions_success: true,
|
||||
exceptions_success_count: 0,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -336,6 +363,9 @@ describe.each([
|
|||
errors: [],
|
||||
success: true,
|
||||
success_count: 1,
|
||||
exceptions_errors: [],
|
||||
exceptions_success: true,
|
||||
exceptions_success_count: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -365,6 +395,9 @@ describe.each([
|
|||
],
|
||||
success: false,
|
||||
success_count: 2,
|
||||
exceptions_errors: [],
|
||||
exceptions_success: true,
|
||||
exceptions_success_count: 0,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -378,6 +411,9 @@ describe.each([
|
|||
errors: [],
|
||||
success: true,
|
||||
success_count: 3,
|
||||
exceptions_errors: [],
|
||||
exceptions_success: true,
|
||||
exceptions_success_count: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -36,10 +36,11 @@ import { createRulesAndExceptionsStreamFromNdJson } from '../../rules/create_rul
|
|||
import { buildRouteValidation } from '../../../../utils/build_validation/route_validation';
|
||||
import { HapiReadableStream } from '../../rules/types';
|
||||
import {
|
||||
importRuleExceptions,
|
||||
importRules as importRulesHelper,
|
||||
RuleExceptionsPromiseFromStreams,
|
||||
} from './utils/import_rules_utils';
|
||||
import { getReferencedExceptionLists } from './utils/gather_referenced_exceptions';
|
||||
import { importRuleExceptions } from './utils/import_rule_exceptions';
|
||||
|
||||
const CHUNK_PARSED_OBJECT_SIZE = 50;
|
||||
|
||||
|
@ -118,8 +119,7 @@ export const importRulesRoute = (
|
|||
} = await importRuleExceptions({
|
||||
exceptions,
|
||||
exceptionsClient,
|
||||
// TODO: Add option of overwriting exceptions separately
|
||||
overwrite: request.query.overwrite,
|
||||
overwrite: request.query.overwrite_exceptions,
|
||||
maxExceptionsImportSize: objectLimit,
|
||||
});
|
||||
|
||||
|
@ -132,6 +132,12 @@ export const importRulesRoute = (
|
|||
actionsClient
|
||||
);
|
||||
|
||||
// gather all exception lists that the imported rules reference
|
||||
const foundReferencedExceptionLists = await getReferencedExceptionLists({
|
||||
rules: uniqueParsedObjects,
|
||||
savedObjectsClient,
|
||||
});
|
||||
|
||||
const chunkParseObjects = chunk(CHUNK_PARSED_OBJECT_SIZE, uniqueParsedObjects);
|
||||
|
||||
const importRuleResponse: ImportRuleResponse[] = await importRulesHelper({
|
||||
|
@ -146,6 +152,7 @@ export const importRulesRoute = (
|
|||
isRuleRegistryEnabled,
|
||||
spaceId: context.securitySolution.getSpaceId(),
|
||||
signalsIndex,
|
||||
existingLists: foundReferencedExceptionLists,
|
||||
});
|
||||
|
||||
const errorsResp = importRuleResponse.filter((resp) => isBulkError(resp)) as BulkError[];
|
||||
|
@ -157,9 +164,12 @@ export const importRulesRoute = (
|
|||
}
|
||||
});
|
||||
const importRules: ImportRulesResponseSchema = {
|
||||
success: errorsResp.length === 0 && exceptionsSuccess,
|
||||
success_count: successes.length + exceptionsSuccessCount,
|
||||
errors: [...errorsResp, ...exceptionsErrors],
|
||||
success: errorsResp.length === 0,
|
||||
success_count: successes.length,
|
||||
errors: errorsResp,
|
||||
exceptions_errors: exceptionsErrors,
|
||||
exceptions_success: exceptionsSuccess,
|
||||
exceptions_success_count: exceptionsSuccessCount,
|
||||
};
|
||||
|
||||
const [validated, errors] = validate(importRules, importRulesResponseSchema);
|
||||
|
|
|
@ -0,0 +1,142 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { getImportRulesSchemaDecodedMock } from '../../../../../../common/detection_engine/schemas/request/import_rules_schema.mock';
|
||||
import { checkRuleExceptionReferences } from './check_rule_exception_references';
|
||||
import { getExceptionListSchemaMock } from '../../../../../../../lists/common/schemas/response/exception_list_schema.mock';
|
||||
|
||||
describe('checkRuleExceptionReferences', () => {
|
||||
it('returns empty array if rule has no exception list references', () => {
|
||||
const result = checkRuleExceptionReferences({
|
||||
existingLists: {},
|
||||
rule: { ...getImportRulesSchemaDecodedMock(), exceptions_list: [] },
|
||||
});
|
||||
|
||||
expect(result).toEqual([[], []]);
|
||||
});
|
||||
|
||||
it('does not modify exceptions reference array if they match existing lists', () => {
|
||||
const result = checkRuleExceptionReferences({
|
||||
existingLists: {
|
||||
'my-list': {
|
||||
...getExceptionListSchemaMock(),
|
||||
list_id: 'my-list',
|
||||
namespace_type: 'single',
|
||||
type: 'detection',
|
||||
},
|
||||
},
|
||||
rule: {
|
||||
...getImportRulesSchemaDecodedMock(),
|
||||
exceptions_list: [
|
||||
{ id: '123', list_id: 'my-list', namespace_type: 'single', type: 'detection' },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toEqual([
|
||||
[],
|
||||
[
|
||||
{
|
||||
id: '1',
|
||||
list_id: 'my-list',
|
||||
namespace_type: 'single',
|
||||
type: 'detection',
|
||||
},
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
it('removes an exception reference if list not found to exist', () => {
|
||||
const result = checkRuleExceptionReferences({
|
||||
existingLists: {},
|
||||
rule: {
|
||||
...getImportRulesSchemaDecodedMock(),
|
||||
exceptions_list: [
|
||||
{ id: '123', list_id: 'my-list', namespace_type: 'single', type: 'detection' },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toEqual([
|
||||
[
|
||||
{
|
||||
error: {
|
||||
message:
|
||||
'Rule with rule_id: "rule-1" references a non existent exception list of list_id: "my-list". Reference has been removed.',
|
||||
status_code: 400,
|
||||
},
|
||||
rule_id: 'rule-1',
|
||||
},
|
||||
],
|
||||
[],
|
||||
]);
|
||||
});
|
||||
|
||||
it('removes an exception reference if list namespace_type does not match', () => {
|
||||
const result = checkRuleExceptionReferences({
|
||||
existingLists: {
|
||||
'my-list': {
|
||||
...getExceptionListSchemaMock(),
|
||||
list_id: 'my-list',
|
||||
namespace_type: 'agnostic',
|
||||
type: 'detection',
|
||||
},
|
||||
},
|
||||
rule: {
|
||||
...getImportRulesSchemaDecodedMock(),
|
||||
exceptions_list: [
|
||||
{ id: '123', list_id: 'my-list', namespace_type: 'single', type: 'detection' },
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(result).toEqual([
|
||||
[
|
||||
{
|
||||
error: {
|
||||
message:
|
||||
'Rule with rule_id: "rule-1" references a non existent exception list of list_id: "my-list". Reference has been removed.',
|
||||
status_code: 400,
|
||||
},
|
||||
rule_id: 'rule-1',
|
||||
},
|
||||
],
|
||||
[],
|
||||
]);
|
||||
});
|
||||
|
||||
it('removes an exception reference if list type does not match', () => {
|
||||
const result = checkRuleExceptionReferences({
|
||||
existingLists: {
|
||||
'my-list': {
|
||||
...getExceptionListSchemaMock(),
|
||||
list_id: 'my-list',
|
||||
namespace_type: 'single',
|
||||
type: 'endpoint',
|
||||
},
|
||||
},
|
||||
rule: {
|
||||
...getImportRulesSchemaDecodedMock(),
|
||||
exceptions_list: [
|
||||
{ id: '123', list_id: 'my-list', namespace_type: 'single', type: 'detection' },
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(result).toEqual([
|
||||
[
|
||||
{
|
||||
error: {
|
||||
message:
|
||||
'Rule with rule_id: "rule-1" references a non existent exception list of list_id: "my-list". Reference has been removed.',
|
||||
status_code: 400,
|
||||
},
|
||||
rule_id: 'rule-1',
|
||||
},
|
||||
],
|
||||
[],
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { ListArray, ExceptionListSchema } from '@kbn/securitysolution-io-ts-list-types';
|
||||
|
||||
import { ImportRulesSchemaDecoded } from '../../../../../../common/detection_engine/schemas/request/import_rules_schema';
|
||||
import { BulkError, createBulkErrorObject } from '../../utils';
|
||||
|
||||
/**
|
||||
* Helper to check if all the exception lists referenced on a
|
||||
* rule exist. Returns updated exception lists reference array.
|
||||
* This helper assumes that the search for existing exception lists
|
||||
* has been batched and is simply checking if it's within the param
|
||||
* that contains existing lists. This is to avoid doing one call per
|
||||
* rule for this check.
|
||||
* @param rule {object} - rule whose exception references are being checked
|
||||
* @param existingLists {object} - a dictionary of sorts that uses list_id as key
|
||||
* @returns {array} tuple of updated exception references and reported errors
|
||||
*/
|
||||
export const checkRuleExceptionReferences = ({
|
||||
rule,
|
||||
existingLists,
|
||||
}: {
|
||||
rule: ImportRulesSchemaDecoded;
|
||||
existingLists: Record<string, ExceptionListSchema>;
|
||||
}): [BulkError[], ListArray] => {
|
||||
let ruleExceptions: ListArray = [];
|
||||
let errors: BulkError[] = [];
|
||||
const { exceptions_list: exceptionLists, rule_id: ruleId } = rule;
|
||||
|
||||
if (!exceptionLists.length) {
|
||||
return [[], []];
|
||||
}
|
||||
|
||||
for (const exceptionList of exceptionLists) {
|
||||
const matchingList = existingLists[exceptionList.list_id];
|
||||
|
||||
if (
|
||||
matchingList &&
|
||||
matchingList.namespace_type === exceptionList.namespace_type &&
|
||||
matchingList.type === exceptionList.type
|
||||
) {
|
||||
ruleExceptions = [
|
||||
...ruleExceptions,
|
||||
{ ...exceptionList, id: existingLists[exceptionList.list_id].id },
|
||||
];
|
||||
} else {
|
||||
// If exception is not found remove link. Also returns
|
||||
// this error to notify a user of the action taken.
|
||||
errors = [
|
||||
...errors,
|
||||
createBulkErrorObject({
|
||||
ruleId,
|
||||
statusCode: 400,
|
||||
message: `Rule with rule_id: "${ruleId}" references a non existent exception list of list_id: "${exceptionList.list_id}". Reference has been removed.`,
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return [errors, ruleExceptions];
|
||||
};
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { SavedObjectsClientContract } from 'kibana/server';
|
||||
|
||||
import { savedObjectsClientMock } from '../../../../../../../../../src/core/server/mocks';
|
||||
import { findExceptionList } from '../../../../../../../lists/server/services/exception_lists/find_exception_list';
|
||||
import { getExceptionListSchemaMock } from '../../../../../../../lists/common/schemas/response/exception_list_schema.mock';
|
||||
import { getReferencedExceptionLists } from './gather_referenced_exceptions';
|
||||
import { getImportRulesSchemaDecodedMock } from '../../../../../../common/detection_engine/schemas/request/import_rules_schema.mock';
|
||||
|
||||
jest.mock('../../../../../../../lists/server/services/exception_lists/find_exception_list');
|
||||
|
||||
describe('getReferencedExceptionLists', () => {
|
||||
let savedObjectsClient: jest.Mocked<SavedObjectsClientContract>;
|
||||
|
||||
beforeEach(() => {
|
||||
savedObjectsClient = savedObjectsClientMock.create();
|
||||
|
||||
(findExceptionList as jest.Mock).mockResolvedValue({
|
||||
data: [
|
||||
{
|
||||
...getExceptionListSchemaMock(),
|
||||
id: '123',
|
||||
list_id: 'my-list',
|
||||
namespace_type: 'single',
|
||||
type: 'detection',
|
||||
},
|
||||
],
|
||||
page: 1,
|
||||
per_page: 20,
|
||||
total: 1,
|
||||
});
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('returns empty object if no rules to search', async () => {
|
||||
const result = await getReferencedExceptionLists({
|
||||
rules: [],
|
||||
savedObjectsClient,
|
||||
});
|
||||
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
it('returns found referenced exception lists', async () => {
|
||||
const result = await getReferencedExceptionLists({
|
||||
rules: [
|
||||
{
|
||||
...getImportRulesSchemaDecodedMock(),
|
||||
exceptions_list: [
|
||||
{ id: '123', list_id: 'my-list', namespace_type: 'single', type: 'detection' },
|
||||
],
|
||||
},
|
||||
],
|
||||
savedObjectsClient,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
'my-list': {
|
||||
...getExceptionListSchemaMock(),
|
||||
id: '123',
|
||||
list_id: 'my-list',
|
||||
namespace_type: 'single',
|
||||
type: 'detection',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('returns empty object if no referenced exception lists found', async () => {
|
||||
const result = await getReferencedExceptionLists({
|
||||
rules: [],
|
||||
savedObjectsClient,
|
||||
});
|
||||
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { ExceptionListSchema, ListArray } from '@kbn/securitysolution-io-ts-list-types';
|
||||
import { SavedObjectsClientContract } from 'kibana/server';
|
||||
import { ImportRulesSchemaDecoded } from '../../../../../../common/detection_engine/schemas/request';
|
||||
|
||||
import {
|
||||
ExceptionListQueryInfo,
|
||||
getAllListTypes,
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
} from '../../../../../../../lists/server/services/exception_lists/utils/import/find_all_exception_list_types';
|
||||
|
||||
/**
|
||||
* Helper that takes rules, goes through their referenced exception lists and
|
||||
* searches for them, returning an object with all those found, using list_id as keys
|
||||
* @param rules {array}
|
||||
* @param savedObjectsClient {object}
|
||||
* @returns {Promise} an object with all referenced lists found, using list_id as keys
|
||||
*/
|
||||
export const getReferencedExceptionLists = async ({
|
||||
rules,
|
||||
savedObjectsClient,
|
||||
}: {
|
||||
rules: Array<ImportRulesSchemaDecoded | Error>;
|
||||
savedObjectsClient: SavedObjectsClientContract;
|
||||
}): Promise<Record<string, ExceptionListSchema>> => {
|
||||
const [lists] = rules.reduce<ListArray[]>((acc, rule) => {
|
||||
if (!(rule instanceof Error) && rule.exceptions_list != null) {
|
||||
return [...acc, rule.exceptions_list];
|
||||
} else {
|
||||
return acc;
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (lists == null) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const [agnosticLists, nonAgnosticLists] = lists.reduce<
|
||||
[ExceptionListQueryInfo[], ExceptionListQueryInfo[]]
|
||||
>(
|
||||
([agnostic, single], list) => {
|
||||
const listInfo = { listId: list.list_id, namespaceType: list.namespace_type };
|
||||
if (list.namespace_type === 'agnostic') {
|
||||
return [[...agnostic, listInfo], single];
|
||||
} else {
|
||||
return [agnostic, [...single, listInfo]];
|
||||
}
|
||||
},
|
||||
[[], []]
|
||||
);
|
||||
|
||||
return getAllListTypes(agnosticLists, nonAgnosticLists, savedObjectsClient);
|
||||
};
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { getImportExceptionsListSchemaMock } from '../../../../../../../lists/common/schemas/request/import_exceptions_schema.mock';
|
||||
|
||||
import { importRuleExceptions } from './import_rule_exceptions';
|
||||
|
||||
// Other than the tested logic below, this method just returns the result of
|
||||
// calling into exceptionsClient.importExceptionListAndItemsAsArray. Figure it's
|
||||
// more important to test that out well than mock out the results and check it
|
||||
// returns that mock
|
||||
describe('importRuleExceptions', () => {
|
||||
it('reports success and success count 0 if no exception list client passed down', async () => {
|
||||
const result = await importRuleExceptions({
|
||||
exceptions: [getImportExceptionsListSchemaMock()],
|
||||
exceptionsClient: undefined,
|
||||
overwrite: true,
|
||||
maxExceptionsImportSize: 10000,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
success: true,
|
||||
errors: [],
|
||||
successCount: 0,
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import {
|
||||
ImportExceptionsListSchema,
|
||||
ImportExceptionListItemSchema,
|
||||
} from '@kbn/securitysolution-io-ts-list-types';
|
||||
|
||||
import { ExceptionListClient } from '../../../../../../../../plugins/lists/server';
|
||||
|
||||
/**
|
||||
* Util to call into exceptions list client import logic
|
||||
* @param exceptions {array} - exception lists and items to import
|
||||
* @param exceptionsClient {object}
|
||||
* @param overwrite {boolean} - user defined value whether or not to overwrite
|
||||
* any exception lists found to have an existing matching list
|
||||
* @param maxExceptionsImportSize {number} - max number of exception objects allowed to import
|
||||
* @returns {Promise} an object summarizing success and errors during import
|
||||
*/
|
||||
export const importRuleExceptions = async ({
|
||||
exceptions,
|
||||
exceptionsClient,
|
||||
overwrite,
|
||||
maxExceptionsImportSize,
|
||||
}: {
|
||||
exceptions: Array<ImportExceptionsListSchema | ImportExceptionListItemSchema>;
|
||||
exceptionsClient: ExceptionListClient | undefined;
|
||||
overwrite: boolean;
|
||||
maxExceptionsImportSize: number;
|
||||
}) => {
|
||||
if (exceptionsClient == null) {
|
||||
return {
|
||||
success: true,
|
||||
errors: [],
|
||||
successCount: 0,
|
||||
};
|
||||
}
|
||||
|
||||
const {
|
||||
errors,
|
||||
success,
|
||||
success_count: successCount,
|
||||
} = await exceptionsClient.importExceptionListAndItemsAsArray({
|
||||
exceptionsToImport: exceptions,
|
||||
overwrite,
|
||||
maxExceptionsImportSize,
|
||||
});
|
||||
|
||||
return {
|
||||
errors,
|
||||
success,
|
||||
successCount,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,288 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { requestContextMock } from '../../__mocks__';
|
||||
import { importRules } from './import_rules_utils';
|
||||
import {
|
||||
getAlertMock,
|
||||
getEmptyFindResult,
|
||||
getFindResultWithSingleHit,
|
||||
} from '../../__mocks__/request_responses';
|
||||
import { getQueryRuleParams } from '../../../schemas/rule_schemas.mock';
|
||||
import { getImportRulesSchemaDecodedMock } from '../../../../../../common/detection_engine/schemas/request/import_rules_schema.mock';
|
||||
import { createRules } from '../../../rules/create_rules';
|
||||
import { patchRules } from '../../../rules/patch_rules';
|
||||
|
||||
jest.mock('../../../rules/create_rules');
|
||||
jest.mock('../../../rules/patch_rules');
|
||||
|
||||
describe('importRules', () => {
|
||||
const mlAuthz = {
|
||||
validateRuleType: jest
|
||||
.fn()
|
||||
.mockResolvedValue({ valid: true, message: 'mocked validation message' }),
|
||||
};
|
||||
const { clients, context } = requestContextMock.createTools();
|
||||
|
||||
beforeEach(() => {
|
||||
clients.rulesClient.find.mockResolvedValue(getEmptyFindResult());
|
||||
clients.rulesClient.update.mockResolvedValue(getAlertMock(true, getQueryRuleParams()));
|
||||
clients.actionsClient.getAll.mockResolvedValue([]);
|
||||
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('returns rules response if no rules to import', async () => {
|
||||
const result = await importRules({
|
||||
ruleChunks: [],
|
||||
rulesResponseAcc: [],
|
||||
mlAuthz,
|
||||
overwriteRules: false,
|
||||
isRuleRegistryEnabled: true,
|
||||
savedObjectsClient: context.core.savedObjects.client,
|
||||
rulesClient: context.alerting.getRulesClient(),
|
||||
ruleStatusClient: context.securitySolution.getExecutionLogClient(),
|
||||
exceptionsClient: context.lists?.getExceptionListClient(),
|
||||
spaceId: 'default',
|
||||
signalsIndex: '.signals-index',
|
||||
existingLists: {},
|
||||
});
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('returns 400 error if "ruleChunks" includes Error', async () => {
|
||||
const result = await importRules({
|
||||
ruleChunks: [[new Error('error importing')]],
|
||||
rulesResponseAcc: [],
|
||||
mlAuthz,
|
||||
overwriteRules: false,
|
||||
isRuleRegistryEnabled: true,
|
||||
savedObjectsClient: context.core.savedObjects.client,
|
||||
rulesClient: context.alerting.getRulesClient(),
|
||||
ruleStatusClient: context.securitySolution.getExecutionLogClient(),
|
||||
exceptionsClient: context.lists?.getExceptionListClient(),
|
||||
spaceId: 'default',
|
||||
signalsIndex: '.signals-index',
|
||||
existingLists: {},
|
||||
});
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
error: {
|
||||
message: 'error importing',
|
||||
status_code: 400,
|
||||
},
|
||||
rule_id: '(unknown id)',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('creates rule if no matching existing rule found', async () => {
|
||||
const result = await importRules({
|
||||
ruleChunks: [
|
||||
[
|
||||
{
|
||||
...getImportRulesSchemaDecodedMock(),
|
||||
rule_id: 'rule-1',
|
||||
},
|
||||
],
|
||||
],
|
||||
rulesResponseAcc: [],
|
||||
mlAuthz,
|
||||
overwriteRules: false,
|
||||
isRuleRegistryEnabled: true,
|
||||
savedObjectsClient: context.core.savedObjects.client,
|
||||
rulesClient: context.alerting.getRulesClient(),
|
||||
ruleStatusClient: context.securitySolution.getExecutionLogClient(),
|
||||
exceptionsClient: context.lists?.getExceptionListClient(),
|
||||
spaceId: 'default',
|
||||
signalsIndex: '.signals-index',
|
||||
existingLists: {},
|
||||
});
|
||||
|
||||
expect(result).toEqual([{ rule_id: 'rule-1', status_code: 200 }]);
|
||||
expect(createRules).toHaveBeenCalled();
|
||||
expect(patchRules).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('reports error if "overwriteRules" is "false" and matching rule found', async () => {
|
||||
clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit(true));
|
||||
|
||||
const result = await importRules({
|
||||
ruleChunks: [
|
||||
[
|
||||
{
|
||||
...getImportRulesSchemaDecodedMock(),
|
||||
rule_id: 'rule-1',
|
||||
},
|
||||
],
|
||||
],
|
||||
rulesResponseAcc: [],
|
||||
mlAuthz,
|
||||
overwriteRules: false,
|
||||
isRuleRegistryEnabled: true,
|
||||
savedObjectsClient: context.core.savedObjects.client,
|
||||
rulesClient: context.alerting.getRulesClient(),
|
||||
ruleStatusClient: context.securitySolution.getExecutionLogClient(),
|
||||
exceptionsClient: context.lists?.getExceptionListClient(),
|
||||
spaceId: 'default',
|
||||
signalsIndex: '.signals-index',
|
||||
existingLists: {},
|
||||
});
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
error: { message: 'rule_id: "rule-1" already exists', status_code: 409 },
|
||||
rule_id: 'rule-1',
|
||||
},
|
||||
]);
|
||||
expect(createRules).not.toHaveBeenCalled();
|
||||
expect(patchRules).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('patches rule if "overwriteRules" is "true" and matching rule found', async () => {
|
||||
clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit(true));
|
||||
|
||||
const result = await importRules({
|
||||
ruleChunks: [
|
||||
[
|
||||
{
|
||||
...getImportRulesSchemaDecodedMock(),
|
||||
rule_id: 'rule-1',
|
||||
},
|
||||
],
|
||||
],
|
||||
rulesResponseAcc: [],
|
||||
mlAuthz,
|
||||
overwriteRules: true,
|
||||
isRuleRegistryEnabled: true,
|
||||
savedObjectsClient: context.core.savedObjects.client,
|
||||
rulesClient: context.alerting.getRulesClient(),
|
||||
ruleStatusClient: context.securitySolution.getExecutionLogClient(),
|
||||
exceptionsClient: context.lists?.getExceptionListClient(),
|
||||
spaceId: 'default',
|
||||
signalsIndex: '.signals-index',
|
||||
existingLists: {},
|
||||
});
|
||||
|
||||
expect(result).toEqual([{ rule_id: 'rule-1', status_code: 200 }]);
|
||||
expect(createRules).not.toHaveBeenCalled();
|
||||
expect(patchRules).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('reports error if rulesClient throws', async () => {
|
||||
clients.rulesClient.find.mockRejectedValue(new Error('error reading rule'));
|
||||
|
||||
const result = await importRules({
|
||||
ruleChunks: [
|
||||
[
|
||||
{
|
||||
...getImportRulesSchemaDecodedMock(),
|
||||
rule_id: 'rule-1',
|
||||
},
|
||||
],
|
||||
],
|
||||
rulesResponseAcc: [],
|
||||
mlAuthz,
|
||||
overwriteRules: true,
|
||||
isRuleRegistryEnabled: true,
|
||||
savedObjectsClient: context.core.savedObjects.client,
|
||||
rulesClient: context.alerting.getRulesClient(),
|
||||
ruleStatusClient: context.securitySolution.getExecutionLogClient(),
|
||||
exceptionsClient: context.lists?.getExceptionListClient(),
|
||||
spaceId: 'default',
|
||||
signalsIndex: '.signals-index',
|
||||
existingLists: {},
|
||||
});
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
error: {
|
||||
message: 'error reading rule',
|
||||
status_code: 400,
|
||||
},
|
||||
rule_id: 'rule-1',
|
||||
},
|
||||
]);
|
||||
expect(createRules).not.toHaveBeenCalled();
|
||||
expect(patchRules).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('reports error if "createRules" throws', async () => {
|
||||
(createRules as jest.Mock).mockRejectedValue(new Error('error creating rule'));
|
||||
|
||||
const result = await importRules({
|
||||
ruleChunks: [
|
||||
[
|
||||
{
|
||||
...getImportRulesSchemaDecodedMock(),
|
||||
rule_id: 'rule-1',
|
||||
},
|
||||
],
|
||||
],
|
||||
rulesResponseAcc: [],
|
||||
mlAuthz,
|
||||
overwriteRules: false,
|
||||
isRuleRegistryEnabled: true,
|
||||
savedObjectsClient: context.core.savedObjects.client,
|
||||
rulesClient: context.alerting.getRulesClient(),
|
||||
ruleStatusClient: context.securitySolution.getExecutionLogClient(),
|
||||
exceptionsClient: context.lists?.getExceptionListClient(),
|
||||
spaceId: 'default',
|
||||
signalsIndex: '.signals-index',
|
||||
existingLists: {},
|
||||
});
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
error: {
|
||||
message: 'error creating rule',
|
||||
status_code: 400,
|
||||
},
|
||||
rule_id: 'rule-1',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('reports error if "patchRules" throws', async () => {
|
||||
(patchRules as jest.Mock).mockRejectedValue(new Error('error patching rule'));
|
||||
clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit(true));
|
||||
|
||||
const result = await importRules({
|
||||
ruleChunks: [
|
||||
[
|
||||
{
|
||||
...getImportRulesSchemaDecodedMock(),
|
||||
rule_id: 'rule-1',
|
||||
},
|
||||
],
|
||||
],
|
||||
rulesResponseAcc: [],
|
||||
mlAuthz,
|
||||
overwriteRules: true,
|
||||
isRuleRegistryEnabled: true,
|
||||
savedObjectsClient: context.core.savedObjects.client,
|
||||
rulesClient: context.alerting.getRulesClient(),
|
||||
ruleStatusClient: context.securitySolution.getExecutionLogClient(),
|
||||
exceptionsClient: context.lists?.getExceptionListClient(),
|
||||
spaceId: 'default',
|
||||
signalsIndex: '.signals-index',
|
||||
existingLists: {},
|
||||
});
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
error: {
|
||||
message: 'error patching rule',
|
||||
status_code: 400,
|
||||
},
|
||||
rule_id: 'rule-1',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -9,12 +9,12 @@ import { SavedObjectsClientContract } from 'kibana/server';
|
|||
import {
|
||||
ImportExceptionsListSchema,
|
||||
ImportExceptionListItemSchema,
|
||||
ListArray,
|
||||
ExceptionListSchema,
|
||||
} from '@kbn/securitysolution-io-ts-list-types';
|
||||
|
||||
import { legacyMigrate } from '../../../rules/utils';
|
||||
import { PartialFilter } from '../../../types';
|
||||
import { createBulkErrorObject, ImportRuleResponse, BulkError } from '../../utils';
|
||||
import { createBulkErrorObject, ImportRuleResponse } from '../../utils';
|
||||
import { isMlRule } from '../../../../../../common/machine_learning/helpers';
|
||||
import { createRules } from '../../../rules/create_rules';
|
||||
import { readRules } from '../../../rules/read_rules';
|
||||
|
@ -25,6 +25,7 @@ import { throwHttpError } from '../../../../machine_learning/validation';
|
|||
import { RulesClient } from '../../../../../../../../plugins/alerting/server';
|
||||
import { IRuleExecutionLogClient } from '../../../rule_execution_log';
|
||||
import { ExceptionListClient } from '../../../../../../../../plugins/lists/server';
|
||||
import { checkRuleExceptionReferences } from './check_rule_exception_references';
|
||||
|
||||
export type PromiseFromStreams = ImportRulesSchemaDecoded | Error;
|
||||
export interface RuleExceptionsPromiseFromStreams {
|
||||
|
@ -35,6 +36,23 @@ export interface RuleExceptionsPromiseFromStreams {
|
|||
/**
|
||||
* Takes rules to be imported and either creates or updates rules
|
||||
* based on user overwrite preferences
|
||||
* @param ruleChunks {array} - rules being imported
|
||||
* @param rulesResponseAcc {array} - the accumulation of success and
|
||||
* error messages gathered through the rules import logic
|
||||
* @param mlAuthz {object}
|
||||
* @param overwriteRules {boolean} - whether to overwrite existing rules
|
||||
* with imported rules if their rule_id matches
|
||||
* @param isRuleRegistryEnabled {boolean} - feature flag that should be
|
||||
* removed as this is now on and no going back
|
||||
* @param rulesClient {object}
|
||||
* @param ruleStatusClient {object}
|
||||
* @param savedObjectsClient {object}
|
||||
* @param exceptionsClient {object}
|
||||
* @param spaceId {string} - space being used during import
|
||||
* @param signalsIndex {string} - the signals index name
|
||||
* @param existingLists {object} - all exception lists referenced by
|
||||
* rules that were found to exist
|
||||
* @returns {Promise} an array of error and success messages from import
|
||||
*/
|
||||
export const importRules = async ({
|
||||
ruleChunks,
|
||||
|
@ -48,6 +66,7 @@ export const importRules = async ({
|
|||
exceptionsClient,
|
||||
spaceId,
|
||||
signalsIndex,
|
||||
existingLists,
|
||||
}: {
|
||||
ruleChunks: PromiseFromStreams[][];
|
||||
rulesResponseAcc: ImportRuleResponse[];
|
||||
|
@ -60,6 +79,7 @@ export const importRules = async ({
|
|||
exceptionsClient: ExceptionListClient | undefined;
|
||||
spaceId: string;
|
||||
signalsIndex: string;
|
||||
existingLists: Record<string, ExceptionListSchema>;
|
||||
}) => {
|
||||
let importRuleResponse: ImportRuleResponse[] = [...rulesResponseAcc];
|
||||
|
||||
|
@ -134,15 +154,13 @@ export const importRules = async ({
|
|||
timeline_title: timelineTitle,
|
||||
throttle,
|
||||
version,
|
||||
exceptions_list: exceptionsList,
|
||||
actions,
|
||||
} = parsedRule;
|
||||
|
||||
try {
|
||||
const [exceptionErrors, exceptions] = await checkExceptions({
|
||||
ruleId,
|
||||
exceptionsClient,
|
||||
exceptions: exceptionsList,
|
||||
const [exceptionErrors, exceptions] = checkRuleExceptionReferences({
|
||||
rule: parsedRule,
|
||||
existingLists,
|
||||
});
|
||||
|
||||
importRuleResponse = [...importRuleResponse, ...exceptionErrors];
|
||||
|
@ -312,81 +330,3 @@ export const importRules = async ({
|
|||
return importRuleResponse;
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: Batch this upfront and send down to check against
|
||||
export const checkExceptions = async ({
|
||||
ruleId,
|
||||
exceptions,
|
||||
exceptionsClient,
|
||||
}: {
|
||||
ruleId: string;
|
||||
exceptions: ListArray;
|
||||
exceptionsClient: ExceptionListClient | undefined;
|
||||
}): Promise<[BulkError[], ListArray]> => {
|
||||
let ruleExceptions: ListArray = [];
|
||||
let errors: BulkError[] = [];
|
||||
|
||||
if (!exceptions.length || exceptionsClient == null) {
|
||||
return [[], exceptions];
|
||||
}
|
||||
for await (const exception of exceptions) {
|
||||
const { list_id: listId, namespace_type: namespaceType } = exception;
|
||||
const list = await exceptionsClient.getExceptionList({
|
||||
id: undefined,
|
||||
listId,
|
||||
namespaceType,
|
||||
});
|
||||
|
||||
if (list != null) {
|
||||
ruleExceptions = [...ruleExceptions, { ...exception, id: list.id }];
|
||||
} else {
|
||||
// if exception is not found remove link
|
||||
errors = [
|
||||
...errors,
|
||||
createBulkErrorObject({
|
||||
ruleId,
|
||||
statusCode: 400,
|
||||
message: `Rule with rule_id: "${ruleId}" references a non existent exception list of list_id: "${listId}". Reference has been removed.`,
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return [errors, ruleExceptions];
|
||||
};
|
||||
|
||||
export const importRuleExceptions = async ({
|
||||
exceptions,
|
||||
exceptionsClient,
|
||||
overwrite,
|
||||
maxExceptionsImportSize,
|
||||
}: {
|
||||
exceptions: Array<ImportExceptionsListSchema | ImportExceptionListItemSchema>;
|
||||
exceptionsClient: ExceptionListClient | undefined;
|
||||
overwrite: boolean;
|
||||
maxExceptionsImportSize: number;
|
||||
}) => {
|
||||
if (exceptionsClient == null) {
|
||||
return {
|
||||
success: true,
|
||||
errors: [],
|
||||
successCount: 0,
|
||||
};
|
||||
}
|
||||
|
||||
const {
|
||||
errors,
|
||||
success,
|
||||
success_count: successCount,
|
||||
} = await exceptionsClient.importExceptionListAndItemsAsArray({
|
||||
exceptionsToImport: exceptions,
|
||||
overwrite,
|
||||
maxExceptionsImportSize,
|
||||
});
|
||||
|
||||
return {
|
||||
errors,
|
||||
success,
|
||||
successCount,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -69,6 +69,9 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
errors: [],
|
||||
success: true,
|
||||
success_count: 1,
|
||||
exceptions_errors: [],
|
||||
exceptions_success: true,
|
||||
exceptions_success_count: 0,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -136,6 +139,9 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
errors: [],
|
||||
success: true,
|
||||
success_count: 2,
|
||||
exceptions_errors: [],
|
||||
exceptions_success: true,
|
||||
exceptions_success_count: 0,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -155,6 +161,9 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
errors: [],
|
||||
success: true,
|
||||
success_count: 10,
|
||||
exceptions_errors: [],
|
||||
exceptions_success: true,
|
||||
exceptions_success_count: 0,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -208,6 +217,9 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
],
|
||||
success: false,
|
||||
success_count: 1,
|
||||
exceptions_errors: [],
|
||||
exceptions_success: true,
|
||||
exceptions_success_count: 0,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -222,6 +234,9 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
errors: [],
|
||||
success: true,
|
||||
success_count: 1,
|
||||
exceptions_errors: [],
|
||||
exceptions_success: true,
|
||||
exceptions_success_count: 0,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -250,6 +265,9 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
],
|
||||
success: false,
|
||||
success_count: 0,
|
||||
exceptions_errors: [],
|
||||
exceptions_success: true,
|
||||
exceptions_success_count: 0,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -270,6 +288,9 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
errors: [],
|
||||
success: true,
|
||||
success_count: 1,
|
||||
exceptions_errors: [],
|
||||
exceptions_success: true,
|
||||
exceptions_success_count: 0,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -327,6 +348,9 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
],
|
||||
success: false,
|
||||
success_count: 2,
|
||||
exceptions_errors: [],
|
||||
exceptions_success: true,
|
||||
exceptions_success_count: 0,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -362,6 +386,9 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
],
|
||||
success: false,
|
||||
success_count: 1,
|
||||
exceptions_errors: [],
|
||||
exceptions_success: true,
|
||||
exceptions_success_count: 0,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -78,6 +78,9 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
errors: [],
|
||||
success: true,
|
||||
success_count: 1,
|
||||
exceptions_errors: [],
|
||||
exceptions_success: true,
|
||||
exceptions_success_count: 0,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -108,6 +111,9 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
errors: [],
|
||||
success: true,
|
||||
success_count: 2,
|
||||
exceptions_errors: [],
|
||||
exceptions_success: true,
|
||||
exceptions_success_count: 0,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -130,6 +136,9 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
],
|
||||
success: false,
|
||||
success_count: 1,
|
||||
exceptions_errors: [],
|
||||
exceptions_success: true,
|
||||
exceptions_success_count: 0,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -144,6 +153,9 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
errors: [],
|
||||
success: true,
|
||||
success_count: 1,
|
||||
exceptions_errors: [],
|
||||
exceptions_success: true,
|
||||
exceptions_success_count: 0,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -172,6 +184,9 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
],
|
||||
success: false,
|
||||
success_count: 0,
|
||||
exceptions_errors: [],
|
||||
exceptions_success: true,
|
||||
exceptions_success_count: 0,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -192,6 +207,9 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
errors: [],
|
||||
success: true,
|
||||
success_count: 1,
|
||||
exceptions_errors: [],
|
||||
exceptions_success: true,
|
||||
exceptions_success_count: 0,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -249,6 +267,9 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
],
|
||||
success: false,
|
||||
success_count: 2,
|
||||
exceptions_errors: [],
|
||||
exceptions_success: true,
|
||||
exceptions_success_count: 0,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -284,6 +305,9 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
],
|
||||
success: false,
|
||||
success_count: 1,
|
||||
exceptions_errors: [],
|
||||
exceptions_success: true,
|
||||
exceptions_success_count: 0,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -356,6 +380,9 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
},
|
||||
},
|
||||
],
|
||||
exceptions_errors: [],
|
||||
exceptions_success: true,
|
||||
exceptions_success_count: 0,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -382,7 +409,14 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
.set('kbn-xsrf', 'true')
|
||||
.attach('file', ruleToNdjson(simpleRule), 'rules.ndjson')
|
||||
.expect(200);
|
||||
expect(body).to.eql({ success: true, success_count: 1, errors: [] });
|
||||
expect(body).to.eql({
|
||||
success: true,
|
||||
success_count: 1,
|
||||
errors: [],
|
||||
exceptions_errors: [],
|
||||
exceptions_success: true,
|
||||
exceptions_success_count: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('should be able to import 2 rules with action connectors that exist', async () => {
|
||||
|
@ -426,7 +460,14 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
.attach('file', buffer, 'rules.ndjson')
|
||||
.expect(200);
|
||||
|
||||
expect(body).to.eql({ success: true, success_count: 2, errors: [] });
|
||||
expect(body).to.eql({
|
||||
success: true,
|
||||
success_count: 2,
|
||||
errors: [],
|
||||
exceptions_errors: [],
|
||||
exceptions_success: true,
|
||||
exceptions_success_count: 0,
|
||||
});
|
||||
});
|
||||
|
||||
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 () => {
|
||||
|
@ -482,6 +523,9 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
},
|
||||
},
|
||||
],
|
||||
exceptions_errors: [],
|
||||
exceptions_success: true,
|
||||
exceptions_success_count: 0,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -508,15 +552,27 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
'rules.ndjson'
|
||||
)
|
||||
.expect(200);
|
||||
expect(body).to.eql({ success: true, success_count: 3, errors: [] });
|
||||
expect(body).to.eql({
|
||||
success: true,
|
||||
success_count: 1,
|
||||
errors: [],
|
||||
exceptions_errors: [],
|
||||
exceptions_success: true,
|
||||
exceptions_success_count: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it('should should only remove non existent exception list references from rule', async () => {
|
||||
it('should only remove non existent exception list references from rule', async () => {
|
||||
// create an exception list
|
||||
const { body: exceptionBody } = await supertest
|
||||
.post(EXCEPTION_LIST_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({ ...getCreateExceptionListMinimalSchemaMock(), list_id: 'i_exist' })
|
||||
.send({
|
||||
...getCreateExceptionListMinimalSchemaMock(),
|
||||
list_id: 'i_exist',
|
||||
namespace_type: 'single',
|
||||
type: 'detection',
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const simpleRule: ReturnType<typeof getSimpleRule> = {
|
||||
|
@ -570,6 +626,9 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
},
|
||||
},
|
||||
],
|
||||
exceptions_errors: [],
|
||||
exceptions_success: true,
|
||||
exceptions_success_count: 0,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -637,8 +696,11 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
|
||||
expect(body).to.eql({
|
||||
success: true,
|
||||
success_count: 3,
|
||||
success_count: 1,
|
||||
errors: [],
|
||||
exceptions_errors: [],
|
||||
exceptions_success: true,
|
||||
exceptions_success_count: 2,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue