Adds tests for issue with immutable (#90372)

## Summary

Adds e2e tests for https://github.com/elastic/kibana/pull/90326

* Adds e2 tests and backfills for updating actions and expected behaviors
* Adds two tests that would fail without the fix and if a regression happens this will trigger on the regression
* Adds two tests to the PATCH for exception lists even though there is no regression there. Reason is to prevent an accidental issue there.
* Adds tests to ensure the version number does not accidentally get bumped if PATCH or UPDATE is called on actions or exceptions for immutable rules.
* Adds utilities for cutting down noise.

### Checklist

- [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios
This commit is contained in:
Frank Hassanabad 2021-02-04 19:07:14 -07:00 committed by GitHub
parent a971c251e9
commit 0c5fb85bfd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 332 additions and 13 deletions

View file

@ -34,6 +34,8 @@ import {
createExceptionListItem,
waitForSignalsToBePresent,
getSignalsByIds,
findImmutableRuleById,
getPrePackagedRulesStatus,
} from '../../utils';
// eslint-disable-next-line import/no-default-export
@ -394,6 +396,83 @@ export default ({ getService }: FtrProviderContext) => {
]);
});
it('should not change the immutable tags when adding a second exception list to an immutable rule through patch', async () => {
await installPrePackagedRules(supertest);
const { id, list_id, namespace_type, type } = await createExceptionList(
supertest,
getCreateExceptionListMinimalSchemaMock()
);
// 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 immutableRule = await getRule(supertest, '9a1a2dae-0b5f-4c3d-8305-a268d404c306');
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 findImmutableRuleById(supertest, '9a1a2dae-0b5f-4c3d-8305-a268d404c306');
expect(body.data.length).to.eql(1); // should have only one length to the data set, otherwise we have duplicates or the tags were removed and that is incredibly bad.
const bodyToCompare = removeServerGeneratedProperties(body.data[0]);
expect(bodyToCompare.rule_id).to.eql(immutableRule.rule_id); // Rule id should not change with a a patch
expect(bodyToCompare.immutable).to.eql(immutableRule.immutable); // Immutable should always stay the same which is true and never flip to false.
expect(bodyToCompare.version).to.eql(immutableRule.version); // The version should never update on a patch
});
it('should not change count of prepacked rules when adding a second exception list to an immutable rule through patch. If this fails, suspect the immutable tags are not staying on the rule correctly.', async () => {
await installPrePackagedRules(supertest);
const { id, list_id, namespace_type, type } = await createExceptionList(
supertest,
getCreateExceptionListMinimalSchemaMock()
);
// 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 immutableRule = await getRule(supertest, '9a1a2dae-0b5f-4c3d-8305-a268d404c306');
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 status = await getPrePackagedRulesStatus(supertest);
expect(status.rules_not_installed).to.eql(0);
});
describe('tests with auditbeat data', () => {
beforeEach(async () => {
await createSignalsIndex(supertest);

View file

@ -14,6 +14,7 @@ export default ({ loadTestFile }: FtrProviderContext): void => {
this.tags('ciGroup11');
loadTestFile(require.resolve('./add_actions'));
loadTestFile(require.resolve('./update_actions'));
loadTestFile(require.resolve('./add_prepackaged_rules'));
loadTestFile(require.resolve('./create_rules'));
loadTestFile(require.resolve('./create_rules_bulk'));

View file

@ -0,0 +1,158 @@
/*
* 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 expect from '@kbn/expect';
import { CreateRulesSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/request';
import { DETECTION_ENGINE_RULES_URL } from '../../../../plugins/security_solution/common/constants';
import { FtrProviderContext } from '../../common/ftr_provider_context';
import {
createSignalsIndex,
deleteAllAlerts,
deleteSignalsIndex,
removeServerGeneratedProperties,
getRuleWithWebHookAction,
getSimpleRuleOutputWithWebHookAction,
waitForRuleSuccessOrStatus,
createRule,
getSimpleRule,
updateRule,
installPrePackagedRules,
getRule,
createNewAction,
findImmutableRuleById,
getPrePackagedRulesStatus,
} from '../../utils';
// eslint-disable-next-line import/no-default-export
export default ({ getService }: FtrProviderContext) => {
const supertest = getService('supertest');
const esArchiver = getService('esArchiver');
describe('update_actions', () => {
describe('updating actions', () => {
beforeEach(async () => {
await esArchiver.load('auditbeat/hosts');
await createSignalsIndex(supertest);
});
afterEach(async () => {
await deleteSignalsIndex(supertest);
await deleteAllAlerts(supertest);
await esArchiver.unload('auditbeat/hosts');
});
it('should be able to create a new webhook action and update a rule with the webhook action', async () => {
const hookAction = await createNewAction(supertest);
const rule = getSimpleRule();
await createRule(supertest, rule);
const ruleToUpdate = getRuleWithWebHookAction(hookAction.id, false, rule);
const updatedRule = await updateRule(supertest, ruleToUpdate);
const bodyToCompare = removeServerGeneratedProperties(updatedRule);
const expected = {
...getSimpleRuleOutputWithWebHookAction(`${bodyToCompare.actions?.[0].id}`),
version: 2, // version bump is required since this is an updated rule and this is part of the testing that we do bump the version number on update
};
expect(bodyToCompare).to.eql(expected);
});
it('should be able to create a new webhook action and attach it to a rule without a meta field and run it correctly', async () => {
const hookAction = await createNewAction(supertest);
const rule = getSimpleRule();
await createRule(supertest, rule);
const ruleToUpdate = getRuleWithWebHookAction(hookAction.id, true, rule);
const updatedRule = await updateRule(supertest, ruleToUpdate);
await waitForRuleSuccessOrStatus(supertest, updatedRule.id);
// expected result for status should be 'succeeded'
const { body } = await supertest
.post(`${DETECTION_ENGINE_RULES_URL}/_find_statuses`)
.set('kbn-xsrf', 'true')
.send({ ids: [updatedRule.id] })
.expect(200);
expect(body[updatedRule.id].current_status.status).to.eql('succeeded');
});
it('should be able to create a new webhook action and attach it to a rule with a meta field and run it correctly', async () => {
const hookAction = await createNewAction(supertest);
const rule = getSimpleRule();
await createRule(supertest, rule);
const ruleToUpdate: CreateRulesSchema = {
...getRuleWithWebHookAction(hookAction.id, true, rule),
meta: {}, // create a rule with the action attached and a meta field
};
const updatedRule = await updateRule(supertest, ruleToUpdate);
await waitForRuleSuccessOrStatus(supertest, updatedRule.id);
// expected result for status should be 'succeeded'
const { body } = await supertest
.post(`${DETECTION_ENGINE_RULES_URL}/_find_statuses`)
.set('kbn-xsrf', 'true')
.send({ ids: [updatedRule.id] })
.expect(200);
expect(body[updatedRule.id].current_status.status).to.eql('succeeded');
});
it('should be able to create a new webhook action and attach it to an immutable rule', async () => {
await installPrePackagedRules(supertest);
// 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
const immutableRule = await getRule(supertest, '9a1a2dae-0b5f-4c3d-8305-a268d404c306');
const hookAction = await createNewAction(supertest);
const newRuleToUpdate = getSimpleRule(immutableRule.rule_id);
const ruleToUpdate = getRuleWithWebHookAction(hookAction.id, false, newRuleToUpdate);
const updatedRule = await updateRule(supertest, ruleToUpdate);
const bodyToCompare = removeServerGeneratedProperties(updatedRule);
const expected = {
...getSimpleRuleOutputWithWebHookAction(`${bodyToCompare.actions?.[0].id}`),
rule_id: immutableRule.rule_id, // Rule id should match the same as the immutable rule
version: immutableRule.version, // This version number should not change when an immutable rule is updated
immutable: true, // It should stay immutable true when returning
};
expect(bodyToCompare).to.eql(expected);
});
it('should be able to create a new webhook action, attach it to an immutable rule and the count of prepackaged rules should not increase. If this fails, suspect the immutable tags are not staying on the rule correctly.', async () => {
await installPrePackagedRules(supertest);
// 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
const immutableRule = await getRule(supertest, '9a1a2dae-0b5f-4c3d-8305-a268d404c306');
const hookAction = await createNewAction(supertest);
const newRuleToUpdate = getSimpleRule(immutableRule.rule_id);
const ruleToUpdate = getRuleWithWebHookAction(hookAction.id, false, newRuleToUpdate);
await updateRule(supertest, ruleToUpdate);
const status = await getPrePackagedRulesStatus(supertest);
expect(status.rules_not_installed).to.eql(0);
});
it('should be able to create a new webhook action, attach it to an immutable rule and the rule should stay immutable when searching against immutable tags', async () => {
await installPrePackagedRules(supertest);
// 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
const immutableRule = await getRule(supertest, '9a1a2dae-0b5f-4c3d-8305-a268d404c306');
const hookAction = await createNewAction(supertest);
const newRuleToUpdate = getSimpleRule(immutableRule.rule_id);
const ruleToUpdate = getRuleWithWebHookAction(hookAction.id, false, newRuleToUpdate);
await updateRule(supertest, ruleToUpdate);
const body = await findImmutableRuleById(supertest, '9a1a2dae-0b5f-4c3d-8305-a268d404c306');
expect(body.data.length).to.eql(1); // should have only one length to the data set, otherwise we have duplicates or the tags were removed and that is incredibly bad.
const bodyToCompare = removeServerGeneratedProperties(body.data[0]);
const expected = {
...getSimpleRuleOutputWithWebHookAction(`${bodyToCompare.actions?.[0].id}`),
rule_id: immutableRule.rule_id, // Rule id should match the same as the immutable rule
version: immutableRule.version, // This version number should not change when an immutable rule is updated
immutable: true, // It should stay immutable true when returning
};
expect(bodyToCompare).to.eql(expected);
});
});
});
};

View file

@ -11,6 +11,7 @@ import { SuperTest } from 'supertest';
import supertestAsPromised from 'supertest-as-promised';
import { Context } from '@elastic/elasticsearch/lib/Transport';
import { SearchResponse } from 'elasticsearch';
import { PrePackagedRulesAndTimelinesStatusSchema } from '../../plugins/security_solution/common/detection_engine/schemas/response';
import { NonEmptyEntriesArray } from '../../plugins/lists/common/schemas';
import { getCreateExceptionListDetectionSchemaMock } from '../../plugins/lists/common/schemas/request/create_exception_list_schema.mock';
import {
@ -38,6 +39,7 @@ import {
DETECTION_ENGINE_PREPACKAGED_URL,
DETECTION_ENGINE_QUERY_SIGNALS_URL,
DETECTION_ENGINE_RULES_URL,
INTERNAL_IMMUTABLE_KEY,
INTERNAL_RULE_ID_KEY,
} from '../../plugins/security_solution/common/constants';
import { getCreateExceptionListItemMinimalSchemaMockWithoutId } from '../../plugins/lists/common/schemas/request/create_exception_list_item_schema.mock';
@ -674,20 +676,27 @@ export const getWebHookAction = () => ({
name: 'Some connector',
});
export const getRuleWithWebHookAction = (id: string, enabled = false): CreateRulesSchema => ({
...getSimpleRule('rule-1', enabled),
throttle: 'rule',
actions: [
{
group: 'default',
id,
params: {
body: '{}',
export const getRuleWithWebHookAction = (
id: string,
enabled = false,
rule?: QueryCreateSchema
): CreateRulesSchema | UpdateRulesSchema => {
const finalRule = rule != null ? { ...rule, enabled } : getSimpleRule('rule-1', enabled);
return {
...finalRule,
throttle: 'rule',
actions: [
{
group: 'default',
id,
params: {
body: '{}',
},
action_type_id: '.webhook',
},
action_type_id: '.webhook',
},
],
});
],
};
};
export const getSimpleRuleOutputWithWebHookAction = (actionId: string): Partial<RulesSchema> => ({
...getSimpleRuleOutput(),
@ -830,6 +839,78 @@ export const createRule = async (
return body;
};
/**
* Helper to cut down on the noise in some of the tests. This checks for
* an expected 200 still and does not do any retries.
* @param supertest The supertest deps
* @param rule The rule to create
*/
export const updateRule = async (
supertest: SuperTest<supertestAsPromised.Test>,
updatedRule: UpdateRulesSchema
): Promise<FullResponseSchema> => {
const { body } = await supertest
.put(DETECTION_ENGINE_RULES_URL)
.set('kbn-xsrf', 'true')
.send(updatedRule)
.expect(200);
return body;
};
/**
* Helper to cut down on the noise in some of the tests. This
* creates a new action and expects a 200 and does not do any retries.
* @param supertest The supertest deps
*/
export const createNewAction = async (supertest: SuperTest<supertestAsPromised.Test>) => {
const { body } = await supertest
.post('/api/actions/action')
.set('kbn-xsrf', 'true')
.send(getWebHookAction())
.expect(200);
return body;
};
/**
* Helper to cut down on the noise in some of the tests. This
* creates a new action and expects a 200 and does not do any retries.
* @param supertest The supertest deps
*/
export const findImmutableRuleById = async (
supertest: SuperTest<supertestAsPromised.Test>,
ruleId: string
): Promise<{
page: number;
perPage: number;
total: number;
data: FullResponseSchema[];
}> => {
const { body } = await supertest
.get(
`${DETECTION_ENGINE_RULES_URL}/_find?filter=alert.attributes.tags: "${INTERNAL_IMMUTABLE_KEY}:true" AND alert.attributes.tags: "${INTERNAL_RULE_ID_KEY}:${ruleId}"`
)
.set('kbn-xsrf', 'true')
.send()
.expect(200);
return body;
};
/**
* Helper to cut down on the noise in some of the tests. This
* creates a new action and expects a 200 and does not do any retries.
* @param supertest The supertest deps
*/
export const getPrePackagedRulesStatus = async (
supertest: SuperTest<supertestAsPromised.Test>
): Promise<PrePackagedRulesAndTimelinesStatusSchema> => {
const { body } = await supertest
.get(`${DETECTION_ENGINE_PREPACKAGED_URL}/_status`)
.set('kbn-xsrf', 'true')
.send()
.expect(200);
return body;
};
/**
* Helper to cut down on the noise in some of the tests. This checks for
* an expected 200 still and does not try to any retries. Creates exception lists