[Security Solutions][Detection Engine] Fixes pre-packaged rules which contain exception lists to not overwrite user defined lists (#80592)

## Summary

Fixes a bug where when you update your pre-packaged rules you could end up removing any existing exception lists the user might have already added. See: https://github.com/elastic/kibana/issues/80417

* Fixes the merge logic so that any exception lists from pre-packaged rules will be additive if they do not already exist on the rule. User based exception lists will not be lost.
* Added new backend integration tests for exception lists that did not exist before including ones that test the functionality of exception lists
* Refactored some of the code in the `get_rules_to_update.ts`
* Refactored some of the integration tests to use helper utils of `countDownES`, and `countDownTest` which simplify the retry logic within the integration tests
* Added unit tests to exercise the bug and then the fix.
* Added integration tests that fail this logic and then fixed the logic

### Checklist

Delete any items that are not applicable to this PR.

- [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
This commit is contained in:
Frank Hassanabad 2020-10-15 13:21:03 -06:00 committed by GitHub
parent 20edc75276
commit e1aec17910
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 1381 additions and 220 deletions

View file

@ -4,102 +4,470 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { getRulesToUpdate } from './get_rules_to_update';
import { filterInstalledRules, getRulesToUpdate, mergeExceptionLists } from './get_rules_to_update';
import { getResult } from '../routes/__mocks__/request_responses';
import { getAddPrepackagedRulesSchemaDecodedMock } from '../../../../common/detection_engine/schemas/request/add_prepackaged_rules_schema.mock';
describe('get_rules_to_update', () => {
test('should return empty array if both rule sets are empty', () => {
const update = getRulesToUpdate([], []);
expect(update).toEqual([]);
describe('get_rules_to_update', () => {
test('should return empty array if both rule sets are empty', () => {
const update = getRulesToUpdate([], []);
expect(update).toEqual([]);
});
test('should return empty array if the rule_id of the two rules do not match', () => {
const ruleFromFileSystem = getAddPrepackagedRulesSchemaDecodedMock();
ruleFromFileSystem.rule_id = 'rule-1';
ruleFromFileSystem.version = 2;
const installedRule = getResult();
installedRule.params.ruleId = 'rule-2';
installedRule.params.version = 1;
const update = getRulesToUpdate([ruleFromFileSystem], [installedRule]);
expect(update).toEqual([]);
});
test('should return empty array if the version of file system rule is less than the installed version', () => {
const ruleFromFileSystem = getAddPrepackagedRulesSchemaDecodedMock();
ruleFromFileSystem.rule_id = 'rule-1';
ruleFromFileSystem.version = 1;
const installedRule = getResult();
installedRule.params.ruleId = 'rule-1';
installedRule.params.version = 2;
const update = getRulesToUpdate([ruleFromFileSystem], [installedRule]);
expect(update).toEqual([]);
});
test('should return empty array if the version of file system rule is the same as the installed version', () => {
const ruleFromFileSystem = getAddPrepackagedRulesSchemaDecodedMock();
ruleFromFileSystem.rule_id = 'rule-1';
ruleFromFileSystem.version = 1;
const installedRule = getResult();
installedRule.params.ruleId = 'rule-1';
installedRule.params.version = 1;
const update = getRulesToUpdate([ruleFromFileSystem], [installedRule]);
expect(update).toEqual([]);
});
test('should return the rule to update if the version of file system rule is greater than the installed version', () => {
const ruleFromFileSystem = getAddPrepackagedRulesSchemaDecodedMock();
ruleFromFileSystem.rule_id = 'rule-1';
ruleFromFileSystem.version = 2;
const installedRule = getResult();
installedRule.params.ruleId = 'rule-1';
installedRule.params.version = 1;
installedRule.params.exceptionsList = [];
const update = getRulesToUpdate([ruleFromFileSystem], [installedRule]);
expect(update).toEqual([ruleFromFileSystem]);
});
test('should return 1 rule out of 2 to update if the version of file system rule is greater than the installed version of just one', () => {
const ruleFromFileSystem = getAddPrepackagedRulesSchemaDecodedMock();
ruleFromFileSystem.rule_id = 'rule-1';
ruleFromFileSystem.version = 2;
const installedRule1 = getResult();
installedRule1.params.ruleId = 'rule-1';
installedRule1.params.version = 1;
installedRule1.params.exceptionsList = [];
const installedRule2 = getResult();
installedRule2.params.ruleId = 'rule-2';
installedRule2.params.version = 1;
installedRule2.params.exceptionsList = [];
const update = getRulesToUpdate([ruleFromFileSystem], [installedRule1, installedRule2]);
expect(update).toEqual([ruleFromFileSystem]);
});
test('should return 2 rules out of 2 to update if the version of file system rule is greater than the installed version of both', () => {
const ruleFromFileSystem1 = getAddPrepackagedRulesSchemaDecodedMock();
ruleFromFileSystem1.rule_id = 'rule-1';
ruleFromFileSystem1.version = 2;
const ruleFromFileSystem2 = getAddPrepackagedRulesSchemaDecodedMock();
ruleFromFileSystem2.rule_id = 'rule-2';
ruleFromFileSystem2.version = 2;
const installedRule1 = getResult();
installedRule1.params.ruleId = 'rule-1';
installedRule1.params.version = 1;
installedRule1.params.exceptionsList = [];
const installedRule2 = getResult();
installedRule2.params.ruleId = 'rule-2';
installedRule2.params.version = 1;
installedRule2.params.exceptionsList = [];
const update = getRulesToUpdate(
[ruleFromFileSystem1, ruleFromFileSystem2],
[installedRule1, installedRule2]
);
expect(update).toEqual([ruleFromFileSystem1, ruleFromFileSystem2]);
});
test('should add back an exception_list if it was removed by the end user on an immutable rule during an upgrade', () => {
const ruleFromFileSystem1 = getAddPrepackagedRulesSchemaDecodedMock();
ruleFromFileSystem1.exceptions_list = [
{
id: 'endpoint_list',
list_id: 'endpoint_list',
namespace_type: 'agnostic',
type: 'endpoint',
},
];
ruleFromFileSystem1.rule_id = 'rule-1';
ruleFromFileSystem1.version = 2;
const installedRule1 = getResult();
installedRule1.params.ruleId = 'rule-1';
installedRule1.params.version = 1;
installedRule1.params.exceptionsList = [];
const [update] = getRulesToUpdate([ruleFromFileSystem1], [installedRule1]);
expect(update.exceptions_list).toEqual(ruleFromFileSystem1.exceptions_list);
});
test('should not remove an additional exception_list if an additional one was added by the end user on an immutable rule during an upgrade', () => {
const ruleFromFileSystem1 = getAddPrepackagedRulesSchemaDecodedMock();
ruleFromFileSystem1.exceptions_list = [
{
id: 'endpoint_list',
list_id: 'endpoint_list',
namespace_type: 'agnostic',
type: 'endpoint',
},
];
ruleFromFileSystem1.rule_id = 'rule-1';
ruleFromFileSystem1.version = 2;
const installedRule1 = getResult();
installedRule1.params.ruleId = 'rule-1';
installedRule1.params.version = 1;
installedRule1.params.exceptionsList = [
{
id: 'second_exception_list',
list_id: 'some-other-id',
namespace_type: 'single',
type: 'detection',
},
];
const [update] = getRulesToUpdate([ruleFromFileSystem1], [installedRule1]);
expect(update.exceptions_list).toEqual([
...ruleFromFileSystem1.exceptions_list,
...installedRule1.params.exceptionsList,
]);
});
test('should not remove an existing exception_list if they are the same between the current installed one and the upgraded one', () => {
const ruleFromFileSystem1 = getAddPrepackagedRulesSchemaDecodedMock();
ruleFromFileSystem1.exceptions_list = [
{
id: 'endpoint_list',
list_id: 'endpoint_list',
namespace_type: 'agnostic',
type: 'endpoint',
},
];
ruleFromFileSystem1.rule_id = 'rule-1';
ruleFromFileSystem1.version = 2;
const installedRule1 = getResult();
installedRule1.params.ruleId = 'rule-1';
installedRule1.params.version = 1;
installedRule1.params.exceptionsList = [
{
id: 'endpoint_list',
list_id: 'endpoint_list',
namespace_type: 'agnostic',
type: 'endpoint',
},
];
const [update] = getRulesToUpdate([ruleFromFileSystem1], [installedRule1]);
expect(update.exceptions_list).toEqual(ruleFromFileSystem1.exceptions_list);
});
test('should not remove an existing exception_list if the rule has an empty exceptions list', () => {
const ruleFromFileSystem1 = getAddPrepackagedRulesSchemaDecodedMock();
ruleFromFileSystem1.exceptions_list = [];
ruleFromFileSystem1.rule_id = 'rule-1';
ruleFromFileSystem1.version = 2;
const installedRule1 = getResult();
installedRule1.params.ruleId = 'rule-1';
installedRule1.params.version = 1;
installedRule1.params.exceptionsList = [
{
id: 'endpoint_list',
list_id: 'endpoint_list',
namespace_type: 'agnostic',
type: 'endpoint',
},
];
const [update] = getRulesToUpdate([ruleFromFileSystem1], [installedRule1]);
expect(update.exceptions_list).toEqual(installedRule1.params.exceptionsList);
});
test('should not remove an existing exception_list if the rule has an empty exceptions list for multiple rules', () => {
const ruleFromFileSystem1 = getAddPrepackagedRulesSchemaDecodedMock();
ruleFromFileSystem1.exceptions_list = [];
ruleFromFileSystem1.rule_id = 'rule-1';
ruleFromFileSystem1.version = 2;
const ruleFromFileSystem2 = getAddPrepackagedRulesSchemaDecodedMock();
ruleFromFileSystem2.exceptions_list = [];
ruleFromFileSystem2.rule_id = 'rule-2';
ruleFromFileSystem2.version = 2;
const installedRule1 = getResult();
installedRule1.params.ruleId = 'rule-1';
installedRule1.params.version = 1;
installedRule1.params.exceptionsList = [
{
id: 'endpoint_list',
list_id: 'endpoint_list',
namespace_type: 'agnostic',
type: 'endpoint',
},
];
const installedRule2 = getResult();
installedRule2.params.ruleId = 'rule-2';
installedRule2.params.version = 1;
installedRule2.params.exceptionsList = [
{
id: 'endpoint_list',
list_id: 'endpoint_list',
namespace_type: 'agnostic',
type: 'endpoint',
},
];
const [update1, update2] = getRulesToUpdate(
[ruleFromFileSystem1, ruleFromFileSystem2],
[installedRule1, installedRule2]
);
expect(update1.exceptions_list).toEqual(installedRule1.params.exceptionsList);
expect(update2.exceptions_list).toEqual(installedRule2.params.exceptionsList);
});
test('should not remove an existing exception_list if the rule has an empty exceptions list for mixed rules', () => {
const ruleFromFileSystem1 = getAddPrepackagedRulesSchemaDecodedMock();
ruleFromFileSystem1.exceptions_list = [];
ruleFromFileSystem1.rule_id = 'rule-1';
ruleFromFileSystem1.version = 2;
const ruleFromFileSystem2 = getAddPrepackagedRulesSchemaDecodedMock();
ruleFromFileSystem2.exceptions_list = [];
ruleFromFileSystem2.rule_id = 'rule-2';
ruleFromFileSystem2.version = 2;
ruleFromFileSystem2.exceptions_list = [
{
id: 'second_list',
list_id: 'second_list',
namespace_type: 'single',
type: 'detection',
},
];
const installedRule1 = getResult();
installedRule1.params.ruleId = 'rule-1';
installedRule1.params.version = 1;
installedRule1.params.exceptionsList = [
{
id: 'endpoint_list',
list_id: 'endpoint_list',
namespace_type: 'agnostic',
type: 'endpoint',
},
];
const installedRule2 = getResult();
installedRule2.params.ruleId = 'rule-2';
installedRule2.params.version = 1;
installedRule2.params.exceptionsList = [
{
id: 'endpoint_list',
list_id: 'endpoint_list',
namespace_type: 'agnostic',
type: 'endpoint',
},
];
const [update1, update2] = getRulesToUpdate(
[ruleFromFileSystem1, ruleFromFileSystem2],
[installedRule1, installedRule2]
);
expect(update1.exceptions_list).toEqual(installedRule1.params.exceptionsList);
expect(update2.exceptions_list).toEqual([
...ruleFromFileSystem2.exceptions_list,
...installedRule2.params.exceptionsList,
]);
});
});
test('should return empty array if the id of the two rules do not match', () => {
const ruleFromFileSystem = getAddPrepackagedRulesSchemaDecodedMock();
ruleFromFileSystem.rule_id = 'rule-1';
ruleFromFileSystem.version = 2;
describe('filterInstalledRules', () => {
test('should return "false" if the id of the two rules do not match', () => {
const ruleFromFileSystem = getAddPrepackagedRulesSchemaDecodedMock();
ruleFromFileSystem.rule_id = 'rule-1';
ruleFromFileSystem.version = 2;
const installedRule = getResult();
installedRule.params.ruleId = 'rule-2';
installedRule.params.version = 1;
const update = getRulesToUpdate([ruleFromFileSystem], [installedRule]);
expect(update).toEqual([]);
const installedRule = getResult();
installedRule.params.ruleId = 'rule-2';
installedRule.params.version = 1;
const shouldUpdate = filterInstalledRules(ruleFromFileSystem, [installedRule]);
expect(shouldUpdate).toEqual(false);
});
test('should return "false" if the version of file system rule is less than the installed version', () => {
const ruleFromFileSystem = getAddPrepackagedRulesSchemaDecodedMock();
ruleFromFileSystem.rule_id = 'rule-1';
ruleFromFileSystem.version = 1;
const installedRule = getResult();
installedRule.params.ruleId = 'rule-1';
installedRule.params.version = 2;
const shouldUpdate = filterInstalledRules(ruleFromFileSystem, [installedRule]);
expect(shouldUpdate).toEqual(false);
});
test('should return "false" if the version of file system rule is the same as the installed version', () => {
const ruleFromFileSystem = getAddPrepackagedRulesSchemaDecodedMock();
ruleFromFileSystem.rule_id = 'rule-1';
ruleFromFileSystem.version = 1;
const installedRule = getResult();
installedRule.params.ruleId = 'rule-1';
installedRule.params.version = 1;
const shouldUpdate = filterInstalledRules(ruleFromFileSystem, [installedRule]);
expect(shouldUpdate).toEqual(false);
});
test('should return "true" to update if the version of file system rule is greater than the installed version', () => {
const ruleFromFileSystem = getAddPrepackagedRulesSchemaDecodedMock();
ruleFromFileSystem.rule_id = 'rule-1';
ruleFromFileSystem.version = 2;
const installedRule = getResult();
installedRule.params.ruleId = 'rule-1';
installedRule.params.version = 1;
installedRule.params.exceptionsList = [];
const shouldUpdate = filterInstalledRules(ruleFromFileSystem, [installedRule]);
expect(shouldUpdate).toEqual(true);
});
});
test('should return empty array if the id of file system rule is less than the installed version', () => {
const ruleFromFileSystem = getAddPrepackagedRulesSchemaDecodedMock();
ruleFromFileSystem.rule_id = 'rule-1';
ruleFromFileSystem.version = 1;
describe('mergeExceptionLists', () => {
test('should add back an exception_list if it was removed by the end user on an immutable rule during an upgrade', () => {
const ruleFromFileSystem1 = getAddPrepackagedRulesSchemaDecodedMock();
ruleFromFileSystem1.exceptions_list = [
{
id: 'endpoint_list',
list_id: 'endpoint_list',
namespace_type: 'agnostic',
type: 'endpoint',
},
];
ruleFromFileSystem1.rule_id = 'rule-1';
ruleFromFileSystem1.version = 2;
const installedRule = getResult();
installedRule.params.ruleId = 'rule-1';
installedRule.params.version = 2;
const update = getRulesToUpdate([ruleFromFileSystem], [installedRule]);
expect(update).toEqual([]);
});
const installedRule1 = getResult();
installedRule1.params.ruleId = 'rule-1';
installedRule1.params.version = 1;
installedRule1.params.exceptionsList = [];
test('should return empty array if the id of file system rule is the same as the installed version', () => {
const ruleFromFileSystem = getAddPrepackagedRulesSchemaDecodedMock();
ruleFromFileSystem.rule_id = 'rule-1';
ruleFromFileSystem.version = 1;
const update = mergeExceptionLists(ruleFromFileSystem1, [installedRule1]);
expect(update.exceptions_list).toEqual(ruleFromFileSystem1.exceptions_list);
});
const installedRule = getResult();
installedRule.params.ruleId = 'rule-1';
installedRule.params.version = 1;
const update = getRulesToUpdate([ruleFromFileSystem], [installedRule]);
expect(update).toEqual([]);
});
test('should not remove an additional exception_list if an additional one was added by the end user on an immutable rule during an upgrade', () => {
const ruleFromFileSystem1 = getAddPrepackagedRulesSchemaDecodedMock();
ruleFromFileSystem1.exceptions_list = [
{
id: 'endpoint_list',
list_id: 'endpoint_list',
namespace_type: 'agnostic',
type: 'endpoint',
},
];
ruleFromFileSystem1.rule_id = 'rule-1';
ruleFromFileSystem1.version = 2;
test('should return the rule to update if the id of file system rule is greater than the installed version', () => {
const ruleFromFileSystem = getAddPrepackagedRulesSchemaDecodedMock();
ruleFromFileSystem.rule_id = 'rule-1';
ruleFromFileSystem.version = 2;
const installedRule1 = getResult();
installedRule1.params.ruleId = 'rule-1';
installedRule1.params.version = 1;
installedRule1.params.exceptionsList = [
{
id: 'second_exception_list',
list_id: 'some-other-id',
namespace_type: 'single',
type: 'detection',
},
];
const installedRule = getResult();
installedRule.params.ruleId = 'rule-1';
installedRule.params.version = 1;
const update = getRulesToUpdate([ruleFromFileSystem], [installedRule]);
expect(update).toEqual([ruleFromFileSystem]);
});
const update = mergeExceptionLists(ruleFromFileSystem1, [installedRule1]);
expect(update.exceptions_list).toEqual([
...ruleFromFileSystem1.exceptions_list,
...installedRule1.params.exceptionsList,
]);
});
test('should return 1 rule out of 2 to update if the id of file system rule is greater than the installed version of just one', () => {
const ruleFromFileSystem = getAddPrepackagedRulesSchemaDecodedMock();
ruleFromFileSystem.rule_id = 'rule-1';
ruleFromFileSystem.version = 2;
test('should not remove an existing exception_list if they are the same between the current installed one and the upgraded one', () => {
const ruleFromFileSystem1 = getAddPrepackagedRulesSchemaDecodedMock();
ruleFromFileSystem1.exceptions_list = [
{
id: 'endpoint_list',
list_id: 'endpoint_list',
namespace_type: 'agnostic',
type: 'endpoint',
},
];
ruleFromFileSystem1.rule_id = 'rule-1';
ruleFromFileSystem1.version = 2;
const installedRule1 = getResult();
installedRule1.params.ruleId = 'rule-1';
installedRule1.params.version = 1;
const installedRule1 = getResult();
installedRule1.params.ruleId = 'rule-1';
installedRule1.params.version = 1;
installedRule1.params.exceptionsList = [
{
id: 'endpoint_list',
list_id: 'endpoint_list',
namespace_type: 'agnostic',
type: 'endpoint',
},
];
const installedRule2 = getResult();
installedRule2.params.ruleId = 'rule-2';
installedRule2.params.version = 1;
const update = mergeExceptionLists(ruleFromFileSystem1, [installedRule1]);
expect(update.exceptions_list).toEqual(ruleFromFileSystem1.exceptions_list);
});
const update = getRulesToUpdate([ruleFromFileSystem], [installedRule1, installedRule2]);
expect(update).toEqual([ruleFromFileSystem]);
});
test('should not remove an existing exception_list if the rule has an empty exceptions list', () => {
const ruleFromFileSystem1 = getAddPrepackagedRulesSchemaDecodedMock();
ruleFromFileSystem1.exceptions_list = [];
ruleFromFileSystem1.rule_id = 'rule-1';
ruleFromFileSystem1.version = 2;
test('should return 2 rules out of 2 to update if the id of file system rule is greater than the installed version of both', () => {
const ruleFromFileSystem1 = getAddPrepackagedRulesSchemaDecodedMock();
ruleFromFileSystem1.rule_id = 'rule-1';
ruleFromFileSystem1.version = 2;
const installedRule1 = getResult();
installedRule1.params.ruleId = 'rule-1';
installedRule1.params.version = 1;
installedRule1.params.exceptionsList = [
{
id: 'endpoint_list',
list_id: 'endpoint_list',
namespace_type: 'agnostic',
type: 'endpoint',
},
];
const ruleFromFileSystem2 = getAddPrepackagedRulesSchemaDecodedMock();
ruleFromFileSystem2.rule_id = 'rule-2';
ruleFromFileSystem2.version = 2;
const installedRule1 = getResult();
installedRule1.params.ruleId = 'rule-1';
installedRule1.params.version = 1;
const installedRule2 = getResult();
installedRule2.params.ruleId = 'rule-2';
installedRule2.params.version = 1;
const update = getRulesToUpdate(
[ruleFromFileSystem1, ruleFromFileSystem2],
[installedRule1, installedRule2]
);
expect(update).toEqual([ruleFromFileSystem1, ruleFromFileSystem2]);
const update = mergeExceptionLists(ruleFromFileSystem1, [installedRule1]);
expect(update.exceptions_list).toEqual(installedRule1.params.exceptionsList);
});
});
});

View file

@ -7,15 +7,67 @@
import { AddPrepackagedRulesSchemaDecoded } from '../../../../common/detection_engine/schemas/request/add_prepackaged_rules_schema';
import { RuleAlertType } from './types';
/**
* Returns the rules to update by doing a compare to the rules from the file system against
* the installed rules already. This also merges exception list items between the two since
* exception list items can exist on both rules to update and already installed rules.
* @param rulesFromFileSystem The rules on the file system to check against installed
* @param installedRules The installed rules
*/
export const getRulesToUpdate = (
rulesFromFileSystem: AddPrepackagedRulesSchemaDecoded[],
installedRules: RuleAlertType[]
): AddPrepackagedRulesSchemaDecoded[] => {
return rulesFromFileSystem.filter((rule) =>
installedRules.some((installedRule) => {
return (
rule.rule_id === installedRule.params.ruleId && rule.version > installedRule.params.version
);
})
);
return rulesFromFileSystem
.filter((ruleFromFileSystem) => filterInstalledRules(ruleFromFileSystem, installedRules))
.map((ruleFromFileSystem) => mergeExceptionLists(ruleFromFileSystem, installedRules));
};
/**
* Filters rules from the file system that do not match the installed rules so you only
* get back rules that are going to be updated
* @param ruleFromFileSystem The rules from the file system to check if any are updates
* @param installedRules The installed rules to compare against for updates
*/
export const filterInstalledRules = (
ruleFromFileSystem: AddPrepackagedRulesSchemaDecoded,
installedRules: RuleAlertType[]
): boolean => {
return installedRules.some((installedRule) => {
return (
ruleFromFileSystem.rule_id === installedRule.params.ruleId &&
ruleFromFileSystem.version > installedRule.params.version
);
});
};
/**
* Given a rule from the file system and the set of installed rules this will merge the exception lists
* from the installed rules onto the rules from the file system.
* @param ruleFromFileSystem The rules from the file system that might have exceptions_lists
* @param installedRules The installed rules which might have user driven exceptions_lists
*/
export const mergeExceptionLists = (
ruleFromFileSystem: AddPrepackagedRulesSchemaDecoded,
installedRules: RuleAlertType[]
): AddPrepackagedRulesSchemaDecoded => {
if (ruleFromFileSystem.exceptions_list != null) {
const installedRule = installedRules.find(
(ruleToFind) => ruleToFind.params.ruleId === ruleFromFileSystem.rule_id
);
if (installedRule != null && installedRule.params.exceptionsList != null) {
const installedExceptionList = installedRule.params.exceptionsList;
const fileSystemExceptions = ruleFromFileSystem.exceptions_list.filter((potentialDuplicate) =>
installedExceptionList.every((item) => item.list_id !== potentialDuplicate.list_id)
);
return {
...ruleFromFileSystem,
exceptions_list: [...fileSystemExceptions, ...installedRule.params.exceptionsList],
};
} else {
return ruleFromFileSystem;
}
} else {
return ruleFromFileSystem;
}
};

View file

@ -0,0 +1,704 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
/* eslint-disable @typescript-eslint/naming-convention */
import expect from '@kbn/expect';
import { SearchResponse } from 'elasticsearch';
import { Signal } from '../../../../plugins/security_solution/server/lib/detection_engine/signals/types';
import { getCreateExceptionListItemMinimalSchemaMock } from '../../../../plugins/lists/common/schemas/request/create_exception_list_item_schema.mock';
import { deleteAllExceptions } from '../../../lists_api_integration/utils';
import { RulesSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/response';
import { CreateRulesSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/request';
import { getCreateExceptionListMinimalSchemaMock } from '../../../../plugins/lists/common/schemas/request/create_exception_list_schema.mock';
import { CreateExceptionListItemSchema } from '../../../../plugins/lists/common';
import {
EXCEPTION_LIST_ITEM_URL,
EXCEPTION_LIST_URL,
} from '../../../../plugins/lists/common/constants';
import {
DETECTION_ENGINE_RULES_URL,
DETECTION_ENGINE_RULES_STATUS_URL,
DETECTION_ENGINE_QUERY_SIGNALS_URL,
DETECTION_ENGINE_PREPACKAGED_URL,
} from '../../../../plugins/security_solution/common/constants';
import { FtrProviderContext } from '../../common/ftr_provider_context';
import {
createSignalsIndex,
deleteAllAlerts,
deleteSignalsIndex,
getSimpleRule,
getSimpleRuleOutput,
removeServerGeneratedProperties,
waitFor,
getQueryAllSignals,
downgradeImmutableRule,
} from '../../utils';
// eslint-disable-next-line import/no-default-export
export default ({ getService }: FtrProviderContext) => {
const supertest = getService('supertest');
const esArchiver = getService('esArchiver');
const es = getService('es');
describe('create_rules_with_exceptions', () => {
describe('creating rules with exceptions', () => {
beforeEach(async () => {
await createSignalsIndex(supertest);
});
afterEach(async () => {
await deleteSignalsIndex(supertest);
await deleteAllAlerts(es);
await deleteAllExceptions(es);
});
it('should create a single rule with a rule_id and add an exception list to the rule', async () => {
const {
body: { id, list_id, namespace_type, type },
} = await supertest
.post(EXCEPTION_LIST_URL)
.set('kbn-xsrf', 'true')
.send(getCreateExceptionListMinimalSchemaMock())
.expect(200);
const ruleWithException: CreateRulesSchema = {
...getSimpleRule(),
exceptions_list: [
{
id,
list_id,
namespace_type,
type,
},
],
};
const { body } = await supertest
.post(DETECTION_ENGINE_RULES_URL)
.set('kbn-xsrf', 'true')
.send(ruleWithException)
.expect(200);
const expected: Partial<RulesSchema> = {
...getSimpleRuleOutput(),
exceptions_list: [
{
id,
list_id,
namespace_type,
type,
},
],
};
const bodyToCompare = removeServerGeneratedProperties(body);
expect(bodyToCompare).to.eql(expected);
});
it('should create a single rule with an exception list and validate it ran successfully', async () => {
const {
body: { id, list_id, namespace_type, type },
} = await supertest
.post(EXCEPTION_LIST_URL)
.set('kbn-xsrf', 'true')
.send(getCreateExceptionListMinimalSchemaMock())
.expect(200);
const ruleWithException: CreateRulesSchema = {
...getSimpleRule(),
exceptions_list: [
{
id,
list_id,
namespace_type,
type,
},
],
};
const { body } = await supertest
.post(DETECTION_ENGINE_RULES_URL)
.set('kbn-xsrf', 'true')
.send(ruleWithException)
.expect(200);
// wait for Task Manager to execute the rule and update status
await waitFor(async () => {
const { body: statusBody } = await supertest
.post(DETECTION_ENGINE_RULES_STATUS_URL)
.set('kbn-xsrf', 'true')
.send({ ids: [body.id] })
.expect(200);
return statusBody[body.id]?.current_status?.status === 'succeeded';
});
const { body: statusBody } = await supertest
.post(DETECTION_ENGINE_RULES_STATUS_URL)
.set('kbn-xsrf', 'true')
.send({ ids: [body.id] })
.expect(200);
const bodyToCompare = removeServerGeneratedProperties(body);
const expected: Partial<RulesSchema> = {
...getSimpleRuleOutput(),
exceptions_list: [
{
id,
list_id,
namespace_type,
type,
},
],
};
expect(bodyToCompare).to.eql(expected);
expect(statusBody[body.id].current_status.status).to.eql('succeeded');
});
it('should allow removing an exception list from an immutable rule through patch', async () => {
// add all the immutable rules from the pre-packaged url
await supertest
.put(DETECTION_ENGINE_PREPACKAGED_URL)
.set('kbn-xsrf', 'true')
.send()
.expect(200);
// Rule id of "9a1a2dae-0b5f-4c3d-8305-a268d404c306" is from the file:
// x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint.json
// This rule has an existing exceptions_list that we are going to use
const { body: immutableRule } = await supertest
.get(`${DETECTION_ENGINE_RULES_URL}?rule_id=9a1a2dae-0b5f-4c3d-8305-a268d404c306`)
.set('kbn-xsrf', 'true')
.send(getSimpleRule())
.expect(200);
expect(immutableRule.exceptions_list.length).greaterThan(0); // make sure we have at least one
// remove the exceptions list as a user is allowed to remove it from an immutable rule
await supertest
.patch(DETECTION_ENGINE_RULES_URL)
.set('kbn-xsrf', 'true')
.send({ rule_id: '9a1a2dae-0b5f-4c3d-8305-a268d404c306', exceptions_list: [] })
.expect(200);
const { body } = await supertest
.get(`${DETECTION_ENGINE_RULES_URL}?rule_id=9a1a2dae-0b5f-4c3d-8305-a268d404c306`)
.set('kbn-xsrf', 'true')
.send(getSimpleRule())
.expect(200);
expect(body.exceptions_list.length).to.eql(0);
});
it('should allow adding a second exception list to an immutable rule through patch', async () => {
// add all the immutable rules from the pre-packaged url
await supertest
.put(DETECTION_ENGINE_PREPACKAGED_URL)
.set('kbn-xsrf', 'true')
.send()
.expect(200);
// Create a new exception list
const {
body: { id, list_id, namespace_type, type },
} = await supertest
.post(EXCEPTION_LIST_URL)
.set('kbn-xsrf', 'true')
.send(getCreateExceptionListMinimalSchemaMock())
.expect(200);
// Rule id of "9a1a2dae-0b5f-4c3d-8305-a268d404c306" is from the file:
// x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint.json
// This rule has an existing exceptions_list that we are going to use
const { body: immutableRule } = await supertest
.get(`${DETECTION_ENGINE_RULES_URL}?rule_id=9a1a2dae-0b5f-4c3d-8305-a268d404c306`)
.set('kbn-xsrf', 'true')
.send(getSimpleRule())
.expect(200);
expect(immutableRule.exceptions_list.length).greaterThan(0); // make sure we have at least one
// add a second exceptions list as a user is allowed to add a second list to an immutable rule
await supertest
.patch(DETECTION_ENGINE_RULES_URL)
.set('kbn-xsrf', 'true')
.send({
rule_id: '9a1a2dae-0b5f-4c3d-8305-a268d404c306',
exceptions_list: [
...immutableRule.exceptions_list,
{
id,
list_id,
namespace_type,
type,
},
],
})
.expect(200);
const { body } = await supertest
.get(`${DETECTION_ENGINE_RULES_URL}?rule_id=9a1a2dae-0b5f-4c3d-8305-a268d404c306`)
.set('kbn-xsrf', 'true')
.send(getSimpleRule())
.expect(200);
expect(body.exceptions_list.length).to.eql(2);
});
it('should override any updates to pre-packaged rules if the user removes the exception list through the API but the new version of a rule has an exception list again', async () => {
await supertest
.put(DETECTION_ENGINE_PREPACKAGED_URL)
.set('kbn-xsrf', 'true')
.send()
.expect(200);
// Rule id of "9a1a2dae-0b5f-4c3d-8305-a268d404c306" is from the file:
// x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint.json
// This rule has an existing exceptions_list that we are going to use
const { body: immutableRule } = await supertest
.get(`${DETECTION_ENGINE_RULES_URL}?rule_id=9a1a2dae-0b5f-4c3d-8305-a268d404c306`)
.set('kbn-xsrf', 'true')
.send(getSimpleRule())
.expect(200);
expect(immutableRule.exceptions_list.length).greaterThan(0); // make sure we have at least one exception list
// Rule id of "9a1a2dae-0b5f-4c3d-8305-a268d404c306" is from the file:
// x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint.json
// This rule has an existing exceptions_list that we are going to ensure does not stomp on our existing rule
// remove the exceptions list as a user is allowed to remove it
await supertest
.patch(DETECTION_ENGINE_RULES_URL)
.set('kbn-xsrf', 'true')
.send({ rule_id: '9a1a2dae-0b5f-4c3d-8305-a268d404c306', exceptions_list: [] })
.expect(200);
// downgrade the version number of the rule
await downgradeImmutableRule(es, '9a1a2dae-0b5f-4c3d-8305-a268d404c306');
// re-add the pre-packaged rule to get the single upgrade to happen
await supertest
.put(DETECTION_ENGINE_PREPACKAGED_URL)
.set('kbn-xsrf', 'true')
.send()
.expect(200);
// get the pre-packaged rule after we upgraded it
const { body } = await supertest
.get(`${DETECTION_ENGINE_RULES_URL}?rule_id=9a1a2dae-0b5f-4c3d-8305-a268d404c306`)
.set('kbn-xsrf', 'true')
.send(getSimpleRule())
.expect(200);
// We should have a length of 1 and it should be the same as our original before we tried to remove it using patch
expect(body.exceptions_list.length).to.eql(1);
expect(body.exceptions_list).to.eql(immutableRule.exceptions_list);
});
it('should merge back an exceptions_list if it was removed from the immutable rule through PATCH', async () => {
await supertest
.put(DETECTION_ENGINE_PREPACKAGED_URL)
.set('kbn-xsrf', 'true')
.send()
.expect(200);
// Create a new exception list
const {
body: { id, list_id, namespace_type, type },
} = await supertest
.post(EXCEPTION_LIST_URL)
.set('kbn-xsrf', 'true')
.send(getCreateExceptionListMinimalSchemaMock())
.expect(200);
// Rule id of "9a1a2dae-0b5f-4c3d-8305-a268d404c306" is from the file:
// x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint.json
// This rule has an existing exceptions_list that we are going to ensure does not stomp on our existing rule
const { body: immutableRule } = await supertest
.get(`${DETECTION_ENGINE_RULES_URL}?rule_id=9a1a2dae-0b5f-4c3d-8305-a268d404c306`)
.set('kbn-xsrf', 'true')
.send(getSimpleRule())
.expect(200);
// remove the exception list and only have a single list that is not an endpoint_list
await supertest
.patch(DETECTION_ENGINE_RULES_URL)
.set('kbn-xsrf', 'true')
.send({
rule_id: '9a1a2dae-0b5f-4c3d-8305-a268d404c306',
exceptions_list: [
{
id,
list_id,
namespace_type,
type,
},
],
})
.expect(200);
// downgrade the version number of the rule
await downgradeImmutableRule(es, '9a1a2dae-0b5f-4c3d-8305-a268d404c306');
// re-add the pre-packaged rule to get the single upgrade to happen
await supertest
.put(DETECTION_ENGINE_PREPACKAGED_URL)
.set('kbn-xsrf', 'true')
.send()
.expect(200);
// get the immutable rule after we installed it a second time
const { body } = await supertest
.get(`${DETECTION_ENGINE_RULES_URL}?rule_id=9a1a2dae-0b5f-4c3d-8305-a268d404c306`)
.set('kbn-xsrf', 'true')
.send(getSimpleRule())
.expect(200);
// The installed rule should have both the original immutable exceptions list back and the
// new list the user added.
expect(body.exceptions_list).to.eql([
...immutableRule.exceptions_list,
{
id,
list_id,
namespace_type,
type,
},
]);
});
it('should NOT add an extra exceptions_list that already exists on a rule during an upgrade', async () => {
await supertest
.put(DETECTION_ENGINE_PREPACKAGED_URL)
.set('kbn-xsrf', 'true')
.send()
.expect(200);
// Rule id of "9a1a2dae-0b5f-4c3d-8305-a268d404c306" is from the file:
// x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint.json
// This rule has an existing exceptions_list that we are going to ensure does not stomp on our existing rule
const { body: immutableRule } = await supertest
.get(`${DETECTION_ENGINE_RULES_URL}?rule_id=9a1a2dae-0b5f-4c3d-8305-a268d404c306`)
.set('kbn-xsrf', 'true')
.send(getSimpleRule())
.expect(200);
// downgrade the version number of the rule
await downgradeImmutableRule(es, '9a1a2dae-0b5f-4c3d-8305-a268d404c306');
// re-add the pre-packaged rule to get the single upgrade to happen
await supertest
.put(DETECTION_ENGINE_PREPACKAGED_URL)
.set('kbn-xsrf', 'true')
.send()
.expect(200);
// get the immutable rule after we installed it a second time
const { body } = await supertest
.get(`${DETECTION_ENGINE_RULES_URL}?rule_id=9a1a2dae-0b5f-4c3d-8305-a268d404c306`)
.set('kbn-xsrf', 'true')
.send(getSimpleRule())
.expect(200);
// The installed rule should have both the original immutable exceptions list back and the
// new list the user added.
expect(body.exceptions_list).to.eql([...immutableRule.exceptions_list]);
});
it('should NOT allow updates to pre-packaged rules to overwrite existing exception based rules when the user adds an additional exception list', async () => {
await supertest
.put(DETECTION_ENGINE_PREPACKAGED_URL)
.set('kbn-xsrf', 'true')
.send()
.expect(200);
// Create a new exception list
const {
body: { id, list_id, namespace_type, type },
} = await supertest
.post(EXCEPTION_LIST_URL)
.set('kbn-xsrf', 'true')
.send(getCreateExceptionListMinimalSchemaMock())
.expect(200);
// Rule id of "9a1a2dae-0b5f-4c3d-8305-a268d404c306" is from the file:
// x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint.json
// This rule has an existing exceptions_list that we are going to ensure does not stomp on our existing rule
const { body: immutableRule } = await supertest
.get(`${DETECTION_ENGINE_RULES_URL}?rule_id=9a1a2dae-0b5f-4c3d-8305-a268d404c306`)
.set('kbn-xsrf', 'true')
.send(getSimpleRule())
.expect(200);
// add a second exceptions list as a user is allowed to add a second list to an immutable rule
await supertest
.patch(DETECTION_ENGINE_RULES_URL)
.set('kbn-xsrf', 'true')
.send({
rule_id: '9a1a2dae-0b5f-4c3d-8305-a268d404c306',
exceptions_list: [
...immutableRule.exceptions_list,
{
id,
list_id,
namespace_type,
type,
},
],
})
.expect(200);
// downgrade the version number of the rule
await downgradeImmutableRule(es, '9a1a2dae-0b5f-4c3d-8305-a268d404c306');
// re-add the pre-packaged rule to get the single upgrade to happen
await supertest
.put(DETECTION_ENGINE_PREPACKAGED_URL)
.set('kbn-xsrf', 'true')
.send()
.expect(200);
const { body } = await supertest
.get(`${DETECTION_ENGINE_RULES_URL}?rule_id=9a1a2dae-0b5f-4c3d-8305-a268d404c306`)
.set('kbn-xsrf', 'true')
.send(getSimpleRule())
.expect(200);
// It should be the same as what the user added originally
expect(body.exceptions_list).to.eql([
...immutableRule.exceptions_list,
{
id,
list_id,
namespace_type,
type,
},
]);
});
it('should not remove any exceptions added to a pre-packaged/immutable rule during an update if that rule has no existing exception lists', async () => {
// add all the immutable rules from the pre-packaged url
await supertest
.put(DETECTION_ENGINE_PREPACKAGED_URL)
.set('kbn-xsrf', 'true')
.send()
.expect(200);
// Create a new exception list
const {
body: { id, list_id, namespace_type, type },
} = await supertest
.post(EXCEPTION_LIST_URL)
.set('kbn-xsrf', 'true')
.send(getCreateExceptionListMinimalSchemaMock())
.expect(200);
// Rule id of "6d3456a5-4a42-49d1-aaf2-7b1fd475b2c6" is from the file:
// x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/c2_reg_beacon.json
// since this rule does not have existing exceptions_list that we are going to use for tests
const { body: immutableRule } = await supertest
.get(`${DETECTION_ENGINE_RULES_URL}?rule_id=6d3456a5-4a42-49d1-aaf2-7b1fd475b2c6`)
.set('kbn-xsrf', 'true')
.send(getSimpleRule())
.expect(200);
expect(immutableRule.exceptions_list.length).eql(0); // make sure we have no exceptions_list
// add a second exceptions list as a user is allowed to add a second list to an immutable rule
await supertest
.patch(DETECTION_ENGINE_RULES_URL)
.set('kbn-xsrf', 'true')
.send({
rule_id: '6d3456a5-4a42-49d1-aaf2-7b1fd475b2c6',
exceptions_list: [
{
id,
list_id,
namespace_type,
type,
},
],
})
.expect(200);
// downgrade the version number of the rule
await downgradeImmutableRule(es, '9a1a2dae-0b5f-4c3d-8305-a268d404c306');
// re-add the pre-packaged rule to get the single upgrade of the rule to happen
await supertest
.put(DETECTION_ENGINE_PREPACKAGED_URL)
.set('kbn-xsrf', 'true')
.send()
.expect(200);
// ensure that the same exception is still on the rule
const { body } = await supertest
.get(`${DETECTION_ENGINE_RULES_URL}?rule_id=6d3456a5-4a42-49d1-aaf2-7b1fd475b2c6`)
.set('kbn-xsrf', 'true')
.send(getSimpleRule())
.expect(200);
expect(body.exceptions_list).to.eql([
{
id,
list_id,
namespace_type,
type,
},
]);
});
describe('tests with auditbeat data', () => {
beforeEach(async () => {
await createSignalsIndex(supertest);
await esArchiver.load('auditbeat/hosts');
});
afterEach(async () => {
await deleteSignalsIndex(supertest);
await deleteAllAlerts(es);
await deleteAllExceptions(es);
await esArchiver.unload('auditbeat/hosts');
});
it('should be able to execute against an exception list that does not include valid entries and get back 10 signals', async () => {
const {
body: { id, list_id, namespace_type, type },
} = await supertest
.post(EXCEPTION_LIST_URL)
.set('kbn-xsrf', 'true')
.send(getCreateExceptionListMinimalSchemaMock())
.expect(200);
const exceptionListItem: CreateExceptionListItemSchema = {
...getCreateExceptionListItemMinimalSchemaMock(),
entries: [
{
field: 'some.none.existent.field', // non-existent field where we should not exclude anything
operator: 'included',
type: 'match',
value: 'some value',
},
],
};
await supertest
.post(EXCEPTION_LIST_ITEM_URL)
.set('kbn-xsrf', 'true')
.send(exceptionListItem)
.expect(200);
const ruleWithException: CreateRulesSchema = {
...getSimpleRule(),
from: '1900-01-01T00:00:00.000Z',
query: 'host.name: "suricata-sensor-amsterdam"',
exceptions_list: [
{
id,
list_id,
namespace_type,
type,
},
],
};
await supertest
.post(DETECTION_ENGINE_RULES_URL)
.set('kbn-xsrf', 'true')
.send(ruleWithException)
.expect(200);
// wait until rules show up and are present
await waitFor(async () => {
const {
body: signalsOpen,
}: { body: SearchResponse<{ signal: Signal }> } = await supertest
.post(DETECTION_ENGINE_QUERY_SIGNALS_URL)
.set('kbn-xsrf', 'true')
.send(getQueryAllSignals())
.expect(200);
return signalsOpen.hits.hits.length > 0;
});
const {
body: signalsOpen,
}: { body: SearchResponse<{ signal: Signal }> } = await supertest
.post(DETECTION_ENGINE_QUERY_SIGNALS_URL)
.set('kbn-xsrf', 'true')
.send(getQueryAllSignals())
.expect(200);
// expect there to be 10
expect(signalsOpen.hits.hits.length).equal(10);
});
it('should be able to execute against an exception list that does include valid entries and get back 0 signals', async () => {
const {
body: { id, list_id, namespace_type, type },
} = await supertest
.post(EXCEPTION_LIST_URL)
.set('kbn-xsrf', 'true')
.send(getCreateExceptionListMinimalSchemaMock())
.expect(200);
const exceptionListItem: CreateExceptionListItemSchema = {
...getCreateExceptionListItemMinimalSchemaMock(),
entries: [
{
field: 'host.name', // This matches the query below which will exclude everything
operator: 'included',
type: 'match',
value: 'suricata-sensor-amsterdam',
},
],
};
await supertest
.post(EXCEPTION_LIST_ITEM_URL)
.set('kbn-xsrf', 'true')
.send(exceptionListItem)
.expect(200);
const ruleWithException: CreateRulesSchema = {
...getSimpleRule(),
from: '1900-01-01T00:00:00.000Z',
query: 'host.name: "suricata-sensor-amsterdam"', // this matches all the exceptions we should exclude
exceptions_list: [
{
id,
list_id,
namespace_type,
type,
},
],
};
const { body: resBody } = await supertest
.post(DETECTION_ENGINE_RULES_URL)
.set('kbn-xsrf', 'true')
.send(ruleWithException)
.expect(200);
// wait for Task Manager to finish executing the rule
await waitFor(async () => {
const { body } = await supertest
.post(`${DETECTION_ENGINE_RULES_URL}/_find_statuses`)
.set('kbn-xsrf', 'true')
.send({ ids: [resBody.id] })
.expect(200);
return body[resBody.id]?.current_status?.status === 'succeeded';
});
// Get the signals now that we are done running and expect the result to always be zero
const {
body: signalsOpen,
}: { body: SearchResponse<{ signal: Signal }> } = await supertest
.post(DETECTION_ENGINE_QUERY_SIGNALS_URL)
.set('kbn-xsrf', 'true')
.send(getQueryAllSignals())
.expect(200);
// expect there to be 10
expect(signalsOpen.hits.hits.length).equal(0);
});
});
});
});
};

View file

@ -170,7 +170,7 @@ export default ({ getService }: FtrProviderContext) => {
expect(signalsOpen.hits.hits.length).equal(10);
});
it('should be return zero matches if the mapping does not match against anything in the mapping', async () => {
it('should return zero matches if the mapping does not match against anything in the mapping', async () => {
const rule: CreateRulesSchema = {
...getCreateThreatMatchRulesSchemaMock(),
from: '1900-01-01T00:00:00.000Z',

View file

@ -16,6 +16,7 @@ export default ({ loadTestFile }: FtrProviderContext): void => {
loadTestFile(require.resolve('./create_rules'));
loadTestFile(require.resolve('./create_rules_bulk'));
loadTestFile(require.resolve('./create_threat_matching'));
loadTestFile(require.resolve('./create_exceptions'));
loadTestFile(require.resolve('./delete_rules'));
loadTestFile(require.resolve('./delete_rules_bulk'));
loadTestFile(require.resolve('./export_rules'));

View file

@ -4,9 +4,10 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { Client } from '@elastic/elasticsearch';
import { ApiResponse, Client } from '@elastic/elasticsearch';
import { SuperTest } from 'supertest';
import supertestAsPromised from 'supertest-as-promised';
import { Context } from '@elastic/elasticsearch/lib/Transport';
import {
Status,
SignalIds,
@ -14,7 +15,10 @@ import {
import { CreateRulesSchema } from '../../plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema';
import { UpdateRulesSchema } from '../../plugins/security_solution/common/detection_engine/schemas/request/update_rules_schema';
import { RulesSchema } from '../../plugins/security_solution/common/detection_engine/schemas/response/rules_schema';
import { DETECTION_ENGINE_INDEX_URL } from '../../plugins/security_solution/common/constants';
import {
DETECTION_ENGINE_INDEX_URL,
INTERNAL_RULE_ID_KEY,
} from '../../plugins/security_solution/common/constants';
/**
* This will remove server generated properties such as date times, etc...
@ -245,34 +249,38 @@ export const getSimpleMlRuleOutput = (ruleId = 'rule-1'): Partial<RulesSchema> =
* This will retry 20 times before giving up and hopefully still not interfere with other tests
* @param es The ElasticSearch handle
*/
export const deleteAllAlerts = async (es: Client, retryCount = 20): Promise<void> => {
if (retryCount > 0) {
try {
const result = await es.deleteByQuery({
index: '.kibana',
q: 'type:alert',
wait_for_completion: true,
refresh: true,
conflicts: 'proceed',
body: {},
});
// deleteByQuery will cause version conflicts as alerts are being updated
// by background processes; the code below accounts for that
if (result.body.version_conflicts !== 0) {
throw new Error(`Version conflicts for ${result.body.version_conflicts} alerts`);
}
} catch (err) {
// eslint-disable-next-line no-console
console.log(`Error in deleteAllAlerts(), retries left: ${retryCount - 1}`, err);
export const deleteAllAlerts = async (es: Client): Promise<void> => {
return countDownES(async () => {
return es.deleteByQuery({
index: '.kibana',
q: 'type:alert',
wait_for_completion: true,
refresh: true,
conflicts: 'proceed',
body: {},
});
}, 'deleteAllAlerts');
};
// retry, counting down, and delay a bit before
await new Promise((resolve) => setTimeout(resolve, 250));
await deleteAllAlerts(es, retryCount - 1);
}
} else {
// eslint-disable-next-line no-console
console.log('Could not deleteAllAlerts, no retries are left');
}
export const downgradeImmutableRule = async (es: Client, ruleId: string): Promise<void> => {
return countDownES(async () => {
return es.updateByQuery({
index: '.kibana',
refresh: true,
wait_for_completion: true,
body: {
script: {
lang: 'painless',
source: 'ctx._source.alert.params.version--',
},
query: {
term: {
'alert.tags': `${INTERNAL_RULE_ID_KEY}:${ruleId}`,
},
},
},
});
}, 'downgradeImmutableRule');
};
/**
@ -295,27 +303,15 @@ export const deleteAllTimelines = async (es: Client): Promise<void> => {
* @param es The ElasticSearch handle
*/
export const deleteAllRulesStatuses = async (es: Client, retryCount = 20): Promise<void> => {
if (retryCount > 0) {
try {
await es.deleteByQuery({
index: '.kibana',
q: 'type:siem-detection-engine-rule-status',
wait_for_completion: true,
refresh: true,
body: {},
});
} catch (err) {
// eslint-disable-next-line no-console
console.log(
`Failure trying to deleteAllRulesStatuses, retries left are: ${retryCount - 1}`,
err
);
await deleteAllRulesStatuses(es, retryCount - 1);
}
} else {
// eslint-disable-next-line no-console
console.log('Could not deleteAllRulesStatuses, no retries are left');
}
return countDownES(async () => {
return es.deleteByQuery({
index: '.kibana',
q: 'type:siem-detection-engine-rule-status',
wait_for_completion: true,
refresh: true,
body: {},
});
}, 'deleteAllRulesStatuses');
};
/**
@ -324,24 +320,12 @@ export const deleteAllRulesStatuses = async (es: Client, retryCount = 20): Promi
* @param supertest The supertest client library
*/
export const createSignalsIndex = async (
supertest: SuperTest<supertestAsPromised.Test>,
retryCount = 20
supertest: SuperTest<supertestAsPromised.Test>
): Promise<void> => {
if (retryCount > 0) {
try {
await supertest.post(DETECTION_ENGINE_INDEX_URL).set('kbn-xsrf', 'true').send();
} catch (err) {
// eslint-disable-next-line no-console
console.log(
`Failure trying to create the signals index, retries left are: ${retryCount - 1}`,
err
);
await createSignalsIndex(supertest, retryCount - 1);
}
} else {
// eslint-disable-next-line no-console
console.log('Could not createSignalsIndex, no retries are left');
}
await countDownTest(async () => {
await supertest.post(DETECTION_ENGINE_INDEX_URL).set('kbn-xsrf', 'true').send();
return true;
}, 'createSignalsIndex');
};
/**
@ -349,21 +333,12 @@ export const createSignalsIndex = async (
* @param supertest The supertest client library
*/
export const deleteSignalsIndex = async (
supertest: SuperTest<supertestAsPromised.Test>,
retryCount = 20
supertest: SuperTest<supertestAsPromised.Test>
): Promise<void> => {
if (retryCount > 0) {
try {
await supertest.delete(DETECTION_ENGINE_INDEX_URL).set('kbn-xsrf', 'true').send();
} catch (err) {
// eslint-disable-next-line no-console
console.log(`Failure trying to deleteSignalsIndex, retries left are: ${retryCount - 1}`, err);
await deleteSignalsIndex(supertest, retryCount - 1);
}
} else {
// eslint-disable-next-line no-console
console.log('Could not deleteSignalsIndex, no retries are left');
}
await countDownTest(async () => {
await supertest.delete(DETECTION_ENGINE_INDEX_URL).set('kbn-xsrf', 'true').send();
return true;
}, 'deleteSignalsIndex');
};
/**
@ -616,7 +591,7 @@ export const waitFor = async (
functionToTest: () => Promise<boolean>,
maxTimeout: number = 5000,
timeoutWait: number = 10
) => {
): Promise<void> => {
await new Promise(async (resolve, reject) => {
let found = false;
let numberOfTries = 0;
@ -636,3 +611,82 @@ export const waitFor = async (
}
});
};
/**
* Does a plain countdown and checks against es queries for either conflicts in the error
* or for any over the wire issues such as timeouts or temp 404's to make the tests more
* reliant.
* @param esFunction The function to test against
* @param esFunctionName The name of the function to print if we encounter errors
* @param retryCount The number of times to retry before giving up (has default)
* @param timeoutWait Time to wait before trying again (has default)
*/
export const countDownES = async (
esFunction: () => Promise<ApiResponse<Record<string, any>, Context>>,
esFunctionName: string,
retryCount: number = 20,
timeoutWait = 250
): Promise<void> => {
await countDownTest(
async () => {
const result = await esFunction();
if (result.body.version_conflicts !== 0) {
// eslint-disable-next-line no-console
console.log(`Version conflicts for ${result.body.version_conflicts}`);
return false;
} else {
return true;
}
},
esFunctionName,
retryCount,
timeoutWait
);
};
/**
* Does a plain countdown and checks against a boolean to determine if to wait and try again.
* This is useful for over the wire things that can cause issues such as conflict or timeouts
* for testing resiliency.
* @param functionToTest The function to test against
* @param name The name of the function to print if we encounter errors
* @param retryCount The number of times to retry before giving up (has default)
* @param timeoutWait Time to wait before trying again (has default)
*/
export const countDownTest = async (
functionToTest: () => Promise<boolean>,
name: string,
retryCount: number = 20,
timeoutWait = 250,
ignoreThrow: boolean = false
) => {
if (retryCount > 0) {
try {
const passed = await functionToTest();
if (!passed) {
// eslint-disable-next-line no-console
console.log(`Failure trying to ${name}, retries left are: ${retryCount - 1}`);
// retry, counting down, and delay a bit before
await new Promise((resolve) => setTimeout(resolve, timeoutWait));
await countDownTest(functionToTest, name, retryCount - 1, timeoutWait, ignoreThrow);
}
} catch (err) {
if (ignoreThrow) {
throw err;
} else {
// eslint-disable-next-line no-console
console.log(
`Failure trying to ${name}, with exception message of:`,
err.message,
`retries left are: ${retryCount - 1}`
);
// retry, counting down, and delay a bit before
await new Promise((resolve) => setTimeout(resolve, timeoutWait));
await countDownTest(functionToTest, name, retryCount - 1, timeoutWait, ignoreThrow);
}
}
} else {
// eslint-disable-next-line no-console
console.log(`Could not ${name}, no retries are left`);
}
};

View file

@ -15,6 +15,7 @@ import {
} from '../../plugins/lists/common/schemas';
import { ListSchema } from '../../plugins/lists/common';
import { LIST_INDEX } from '../../plugins/lists/common/constants';
import { countDownES, countDownTest } from '../detection_engine_api_integration/utils';
/**
* Creates the lists and lists items index for use inside of beforeEach blocks of tests
@ -22,24 +23,12 @@ import { LIST_INDEX } from '../../plugins/lists/common/constants';
* @param supertest The supertest client library
*/
export const createListsIndex = async (
supertest: SuperTest<supertestAsPromised.Test>,
retryCount = 20
supertest: SuperTest<supertestAsPromised.Test>
): Promise<void> => {
if (retryCount > 0) {
try {
await supertest.post(LIST_INDEX).set('kbn-xsrf', 'true').send();
} catch (err) {
// eslint-disable-next-line no-console
console.log(
`Failure trying to create the lists index, retries left are: ${retryCount - 1}`,
err
);
await createListsIndex(supertest, retryCount - 1);
}
} else {
// eslint-disable-next-line no-console
console.log('Could not createListsIndex, no retries are left');
}
return countDownTest(async () => {
await supertest.post(LIST_INDEX).set('kbn-xsrf', 'true').send();
return true;
}, 'createListsIndex');
};
/**
@ -47,21 +36,26 @@ export const createListsIndex = async (
* @param supertest The supertest client library
*/
export const deleteListsIndex = async (
supertest: SuperTest<supertestAsPromised.Test>,
retryCount = 20
supertest: SuperTest<supertestAsPromised.Test>
): Promise<void> => {
if (retryCount > 0) {
try {
await supertest.delete(LIST_INDEX).set('kbn-xsrf', 'true').send();
} catch (err) {
// eslint-disable-next-line no-console
console.log(`Failure trying to deleteListsIndex, retries left are: ${retryCount - 1}`, err);
await deleteListsIndex(supertest, retryCount - 1);
}
} else {
// eslint-disable-next-line no-console
console.log('Could not deleteListsIndex, no retries are left');
}
return countDownTest(async () => {
await supertest.delete(LIST_INDEX).set('kbn-xsrf', 'true').send();
return true;
}, 'deleteListsIndex');
};
/**
* Creates the exception lists and lists items index for use inside of beforeEach blocks of tests
* This will retry 20 times before giving up and hopefully still not interfere with other tests
* @param supertest The supertest client library
*/
export const createExceptionListsIndex = async (
supertest: SuperTest<supertestAsPromised.Test>
): Promise<void> => {
return countDownTest(async () => {
await supertest.post(LIST_INDEX).set('kbn-xsrf', 'true').send();
return true;
}, 'createListsIndex');
};
/**
@ -159,26 +153,14 @@ export const binaryToString = (res: any, callback: any): void => {
* This will retry 20 times before giving up and hopefully still not interfere with other tests
* @param es The ElasticSearch handle
*/
export const deleteAllExceptions = async (es: Client, retryCount = 20): Promise<void> => {
if (retryCount > 0) {
try {
await es.deleteByQuery({
index: '.kibana',
q: 'type:exception-list or type:exception-list-agnostic',
wait_for_completion: true,
refresh: true,
body: {},
});
} catch (err) {
// eslint-disable-next-line no-console
console.log(
`Failure trying to deleteAllExceptions, retries left are: ${retryCount - 1}`,
err
);
await deleteAllExceptions(es, retryCount - 1);
}
} else {
// eslint-disable-next-line no-console
console.log('Could not deleteAllExceptions, no retries are left');
}
export const deleteAllExceptions = async (es: Client): Promise<void> => {
return countDownES(async () => {
return es.deleteByQuery({
index: '.kibana',
q: 'type:exception-list or type:exception-list-agnostic',
wait_for_completion: true,
refresh: true,
body: {},
});
}, 'deleteAllExceptions');
};