[Security Solution][Detections] Adds list plugin Saved Objects to Security feature privilege (#90895) (#91075)

## Summary

Add's the list plugins Saved Objects (`exception-list` and `exception-list-agnostic`) to the `Security` feature privilege.

Resolves https://github.com/elastic/kibana/issues/90715

### Test Instructions
Load pre-packaged roles/users, and ensure only those with the Kibana Space privilege `Security:All` have the ability to create/edit rules and exception lists (space-aware/agnostic). Users with `Security:Read` should only be able to view rules/exception lists. Pre-packaged security roles should no longer be granted the `Saved Objects Management` feature privilege, and this feature privilege should no longer be required to use any of the Detections features.

To add test users:

t1_analyst (`"siem": ["read"]`):
``` bash
cd x-pack/plugins/security_solution/server/lib/detection_engine/scripts/
./roles_users/t1_analyst/post_detections_role.sh roles_users/t1_analyst/detections_role.json
./roles_users/t1_analyst/post_detections_user.sh roles_users/t1_analyst/detections_user.json
```

hunter (`"siem": ["all"]`):
``` bash
cd x-pack/plugins/security_solution/server/lib/detection_engine/scripts/
./roles_users/t1_analyst/post_detections_role.sh roles_users/hunter/detections_role.json
./roles_users/t1_analyst/post_detections_user.sh roles_users/hunter/detections_user.json
```

Note: Be sure to remove these users after testing if using a public cluster.

### Checklist

Delete any items that are not applicable to this PR.

- [X] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials -- `docs` label added, will work with @jmikell821 on doc changes
- [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios

Co-authored-by: Garrett Spong <spong@users.noreply.github.com>
This commit is contained in:
Kibana Machine 2021-02-11 00:29:11 -05:00 committed by GitHub
parent 17b2b27765
commit 5950d262eb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 641 additions and 578 deletions

View file

@ -26,8 +26,7 @@
"siem": ["all"],
"actions": ["read"],
"builtInAlerts": ["all"],
"dev_tools": ["all"],
"savedObjectsManagement": ["all"]
"dev_tools": ["all"]
},
"spaces": ["*"]
}

View file

@ -2,7 +2,6 @@ This user can CRUD rules and signals. The main difference here is the user has
```json
"builtInAlerts": ["all"],
"savedObjectsManagement": ["all"]
```
privileges whereas the T1 and T2 have "read" privileges which prevents them from creating rules

View file

@ -30,8 +30,7 @@
"ml": ["read"],
"siem": ["all"],
"actions": ["read"],
"builtInAlerts": ["all"],
"savedObjectsManagement": ["all"]
"builtInAlerts": ["all"]
},
"spaces": ["*"]
}

View file

@ -30,8 +30,7 @@
"ml": ["all"],
"siem": ["all"],
"actions": ["all"],
"builtInAlerts": ["all"],
"savedObjectsManagement": ["all"]
"builtInAlerts": ["all"]
},
"spaces": ["*"]
}

View file

@ -24,8 +24,7 @@
"ml": ["read"],
"siem": ["read"],
"actions": ["read"],
"builtInAlerts": ["read"],
"savedObjectsManagement": ["read"]
"builtInAlerts": ["read"]
},
"spaces": ["*"]
}

View file

@ -28,8 +28,7 @@
"ml": ["read"],
"siem": ["all"],
"actions": ["read"],
"builtInAlerts": ["all"],
"savedObjectsManagement": ["all"]
"builtInAlerts": ["all"]
},
"spaces": ["*"]
}

View file

@ -28,8 +28,7 @@
"ml": ["read"],
"siem": ["all"],
"actions": ["all"],
"builtInAlerts": ["all"],
"savedObjectsManagement": ["all"]
"builtInAlerts": ["all"]
},
"spaces": ["*"]
}

View file

@ -21,10 +21,9 @@
{
"feature": {
"ml": ["read"],
"siem": ["all"],
"siem": ["read"],
"actions": ["read"],
"builtInAlerts": ["read"],
"savedObjectsManagement": ["read"]
"builtInAlerts": ["read"]
},
"spaces": ["*"]
}

View file

@ -23,10 +23,9 @@
{
"feature": {
"ml": ["read"],
"siem": ["all"],
"siem": ["read"],
"actions": ["read"],
"builtInAlerts": ["read"],
"savedObjectsManagement": ["read"]
"builtInAlerts": ["read"]
},
"spaces": ["*"]
}

View file

@ -219,6 +219,8 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
'cases-comments',
'cases-configure',
'cases-user-actions',
'exception-list',
'exception-list-agnostic',
...savedObjectTypes,
],
read: ['config'],
@ -243,6 +245,8 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
'cases-comments',
'cases-configure',
'cases-user-actions',
'exception-list',
'exception-list-agnostic',
...savedObjectTypes,
],
},

View file

@ -100,7 +100,6 @@ interface RoleInterface {
siem: string[];
actions: string[];
builtInAlerts: string[];
savedObjectsManagement: string[];
};
spaces: string[];
}>;

View file

@ -14,7 +14,10 @@ import { deleteAllExceptions } from '../../../lists_api_integration/utils';
import { RulesSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/response';
import { getCreateExceptionListMinimalSchemaMock } from '../../../../plugins/lists/common/schemas/request/create_exception_list_schema.mock';
import { CreateExceptionListItemSchema } from '../../../../plugins/lists/common';
import { EXCEPTION_LIST_URL } from '../../../../plugins/lists/common/constants';
import {
EXCEPTION_LIST_ITEM_URL,
EXCEPTION_LIST_URL,
} from '../../../../plugins/lists/common/constants';
import { DETECTION_ENGINE_RULES_URL } from '../../../../plugins/security_solution/common/constants';
import { FtrProviderContext } from '../../common/ftr_provider_context';
@ -37,10 +40,13 @@ import {
findImmutableRuleById,
getPrePackagedRulesStatus,
} from '../../utils';
import { ROLES } from '../../../../plugins/security_solution/common/test';
import { createUserAndRole, deleteUserAndRole } from '../roles_users_utils';
// eslint-disable-next-line import/no-default-export
export default ({ getService }: FtrProviderContext) => {
const supertest = getService('supertest');
const supertestWithoutAuth = getService('supertestWithoutAuth');
const esArchiver = getService('esArchiver');
const es = getService('es');
@ -58,419 +64,456 @@ export default ({ getService }: FtrProviderContext) => {
await esArchiver.unload('auditbeat/hosts');
});
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);
describe('elastic admin', () => {
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: [
const ruleWithException: CreateRulesSchema = {
...getSimpleRule(),
exceptions_list: [
{
id,
list_id,
namespace_type,
type,
},
],
};
const rule = await createRule(supertest, ruleWithException);
const expected: Partial<RulesSchema> = {
...getSimpleRuleOutput(),
exceptions_list: [
{
id,
list_id,
namespace_type,
type,
},
],
};
const bodyToCompare = removeServerGeneratedProperties(rule);
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(),
enabled: true,
exceptions_list: [
{
id,
list_id,
namespace_type,
type,
},
],
};
const rule = await createRule(supertest, ruleWithException);
await waitForRuleSuccessOrStatus(supertest, rule.id);
const bodyToCompare = removeServerGeneratedProperties(rule);
const expected: Partial<RulesSchema> = {
...getSimpleRuleOutput(),
enabled: true,
exceptions_list: [
{
id,
list_id,
namespace_type,
type,
},
],
};
expect(bodyToCompare).to.eql(expected);
});
it('should allow removing an exception list from an immutable rule through patch', 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
// 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 exceptions_list
// 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 immutableRuleSecondTime = await getRule(
supertest,
'9a1a2dae-0b5f-4c3d-8305-a268d404c306'
);
expect(immutableRuleSecondTime.exceptions_list.length).to.eql(0);
});
it('should allow 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 immutableRuleSecondTime = await getRule(
supertest,
'9a1a2dae-0b5f-4c3d-8305-a268d404c306'
);
expect(immutableRuleSecondTime.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 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
// 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
await supertest
.patch(DETECTION_ENGINE_RULES_URL)
.set('kbn-xsrf', 'true')
.send({ rule_id: '9a1a2dae-0b5f-4c3d-8305-a268d404c306', exceptions_list: [] })
.expect(200);
await downgradeImmutableRule(es, '9a1a2dae-0b5f-4c3d-8305-a268d404c306');
await installPrePackagedRules(supertest);
const immutableRuleSecondTime = await getRule(
supertest,
'9a1a2dae-0b5f-4c3d-8305-a268d404c306'
);
// 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(immutableRuleSecondTime.exceptions_list.length).to.eql(1);
expect(immutableRuleSecondTime.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 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 ensure does not stomp on our existing rule
const immutableRule = await getRule(supertest, '9a1a2dae-0b5f-4c3d-8305-a268d404c306');
expect(immutableRule.exceptions_list.length).greaterThan(0); // make sure we have at least one
// 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);
await downgradeImmutableRule(es, '9a1a2dae-0b5f-4c3d-8305-a268d404c306');
await installPrePackagedRules(supertest);
const immutableRuleSecondTime = await getRule(
supertest,
'9a1a2dae-0b5f-4c3d-8305-a268d404c306'
);
expect(immutableRuleSecondTime.exceptions_list).to.eql([
...immutableRule.exceptions_list,
{
id,
list_id,
namespace_type,
type,
},
],
};
]);
});
const rule = await createRule(supertest, ruleWithException);
const expected: Partial<RulesSchema> = {
...getSimpleRuleOutput(),
exceptions_list: [
it('should NOT add an extra exceptions_list that already exists on a rule during an upgrade', 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
// This rule has an existing exceptions_list that we are going to ensure does not stomp on our existing rule
const immutableRule = await getRule(supertest, '9a1a2dae-0b5f-4c3d-8305-a268d404c306');
expect(immutableRule.exceptions_list.length).greaterThan(0); // make sure we have at least one
await downgradeImmutableRule(es, '9a1a2dae-0b5f-4c3d-8305-a268d404c306');
await installPrePackagedRules(supertest);
const immutableRuleSecondTime = await getRule(
supertest,
'9a1a2dae-0b5f-4c3d-8305-a268d404c306'
);
// The installed rule should have both the original immutable exceptions list back and the
// new list the user added.
expect(immutableRuleSecondTime.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 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 ensure does not stomp on our existing rule
const immutableRule = await getRule(supertest, '9a1a2dae-0b5f-4c3d-8305-a268d404c306');
// 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);
await downgradeImmutableRule(es, '9a1a2dae-0b5f-4c3d-8305-a268d404c306');
await installPrePackagedRules(supertest);
const immutableRuleSecondTime = await getRule(
supertest,
'9a1a2dae-0b5f-4c3d-8305-a268d404c306'
);
// It should be the same as what the user added originally
expect(immutableRuleSecondTime.exceptions_list).to.eql([
...immutableRule.exceptions_list,
{
id,
list_id,
namespace_type,
type,
},
],
};
const bodyToCompare = removeServerGeneratedProperties(rule);
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);
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 () => {
await installPrePackagedRules(supertest);
const ruleWithException: CreateRulesSchema = {
...getSimpleRule(),
enabled: true,
exceptions_list: [
// Create a new exception list
const { id, list_id, namespace_type, type } = await createExceptionList(
supertest,
getCreateExceptionListMinimalSchemaMock()
);
// Rule id of "eb079c62-4481-4d6e-9643-3ca499df7aaa" is from the file:
// x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/external_alerts.json
// since this rule does not have existing exceptions_list that we are going to use for tests
const immutableRule = await getRule(supertest, 'eb079c62-4481-4d6e-9643-3ca499df7aaa');
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: 'eb079c62-4481-4d6e-9643-3ca499df7aaa',
exceptions_list: [
{
id,
list_id,
namespace_type,
type,
},
],
})
.expect(200);
await downgradeImmutableRule(es, 'eb079c62-4481-4d6e-9643-3ca499df7aaa');
await installPrePackagedRules(supertest);
const immutableRuleSecondTime = await getRule(
supertest,
'eb079c62-4481-4d6e-9643-3ca499df7aaa'
);
expect(immutableRuleSecondTime.exceptions_list).to.eql([
{
id,
list_id,
namespace_type,
type,
},
],
};
]);
});
const rule = await createRule(supertest, ruleWithException);
await waitForRuleSuccessOrStatus(supertest, rule.id);
const bodyToCompare = removeServerGeneratedProperties(rule);
it('should not change the immutable tags when adding a second exception list to an immutable rule through patch', async () => {
await installPrePackagedRules(supertest);
const expected: Partial<RulesSchema> = {
...getSimpleRuleOutput(),
enabled: true,
exceptions_list: [
{
id,
list_id,
namespace_type,
type,
},
],
};
expect(bodyToCompare).to.eql(expected);
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);
});
});
it('should allow removing an exception list from an immutable rule through patch', async () => {
await installPrePackagedRules(supertest);
describe('t1_analyst', () => {
const role = ROLES.t1_analyst;
// 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 exceptions_list
beforeEach(async () => {
await createUserAndRole(getService, role);
});
// 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);
afterEach(async () => {
await deleteUserAndRole(getService, role);
});
const immutableRuleSecondTime = await getRule(
supertest,
'9a1a2dae-0b5f-4c3d-8305-a268d404c306'
);
expect(immutableRuleSecondTime.exceptions_list.length).to.eql(0);
});
it('should NOT be able to create an exception list', async () => {
await supertestWithoutAuth
.post(EXCEPTION_LIST_ITEM_URL)
.auth(role, 'changeme')
.set('kbn-xsrf', 'true')
.send(getCreateExceptionListItemMinimalSchemaMock())
.expect(403);
});
it('should allow 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 immutableRuleSecondTime = await getRule(
supertest,
'9a1a2dae-0b5f-4c3d-8305-a268d404c306'
);
expect(immutableRuleSecondTime.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 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
// 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
await supertest
.patch(DETECTION_ENGINE_RULES_URL)
.set('kbn-xsrf', 'true')
.send({ rule_id: '9a1a2dae-0b5f-4c3d-8305-a268d404c306', exceptions_list: [] })
.expect(200);
await downgradeImmutableRule(es, '9a1a2dae-0b5f-4c3d-8305-a268d404c306');
await installPrePackagedRules(supertest);
const immutableRuleSecondTime = await getRule(
supertest,
'9a1a2dae-0b5f-4c3d-8305-a268d404c306'
);
// 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(immutableRuleSecondTime.exceptions_list.length).to.eql(1);
expect(immutableRuleSecondTime.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 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 ensure does not stomp on our existing rule
const immutableRule = await getRule(supertest, '9a1a2dae-0b5f-4c3d-8305-a268d404c306');
expect(immutableRule.exceptions_list.length).greaterThan(0); // make sure we have at least one
// 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);
await downgradeImmutableRule(es, '9a1a2dae-0b5f-4c3d-8305-a268d404c306');
await installPrePackagedRules(supertest);
const immutableRuleSecondTime = await getRule(
supertest,
'9a1a2dae-0b5f-4c3d-8305-a268d404c306'
);
expect(immutableRuleSecondTime.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 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
// This rule has an existing exceptions_list that we are going to ensure does not stomp on our existing rule
const immutableRule = await getRule(supertest, '9a1a2dae-0b5f-4c3d-8305-a268d404c306');
expect(immutableRule.exceptions_list.length).greaterThan(0); // make sure we have at least one
await downgradeImmutableRule(es, '9a1a2dae-0b5f-4c3d-8305-a268d404c306');
await installPrePackagedRules(supertest);
const immutableRuleSecondTime = await getRule(
supertest,
'9a1a2dae-0b5f-4c3d-8305-a268d404c306'
);
// The installed rule should have both the original immutable exceptions list back and the
// new list the user added.
expect(immutableRuleSecondTime.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 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 ensure does not stomp on our existing rule
const immutableRule = await getRule(supertest, '9a1a2dae-0b5f-4c3d-8305-a268d404c306');
// 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);
await downgradeImmutableRule(es, '9a1a2dae-0b5f-4c3d-8305-a268d404c306');
await installPrePackagedRules(supertest);
const immutableRuleSecondTime = await getRule(
supertest,
'9a1a2dae-0b5f-4c3d-8305-a268d404c306'
);
// It should be the same as what the user added originally
expect(immutableRuleSecondTime.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 () => {
await installPrePackagedRules(supertest);
// Create a new exception list
const { id, list_id, namespace_type, type } = await createExceptionList(
supertest,
getCreateExceptionListMinimalSchemaMock()
);
// Rule id of "eb079c62-4481-4d6e-9643-3ca499df7aaa" is from the file:
// x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/external_alerts.json
// since this rule does not have existing exceptions_list that we are going to use for tests
const immutableRule = await getRule(supertest, 'eb079c62-4481-4d6e-9643-3ca499df7aaa');
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: 'eb079c62-4481-4d6e-9643-3ca499df7aaa',
exceptions_list: [
{
id,
list_id,
namespace_type,
type,
},
],
})
.expect(200);
await downgradeImmutableRule(es, 'eb079c62-4481-4d6e-9643-3ca499df7aaa');
await installPrePackagedRules(supertest);
const immutableRuleSecondTime = await getRule(
supertest,
'eb079c62-4481-4d6e-9643-3ca499df7aaa'
);
expect(immutableRuleSecondTime.exceptions_list).to.eql([
{
id,
list_id,
namespace_type,
type,
},
]);
});
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);
it('should NOT be able to create an exception list item', async () => {
await supertestWithoutAuth
.post(EXCEPTION_LIST_ITEM_URL)
.auth(role, 'changeme')
.set('kbn-xsrf', 'true')
.send(getCreateExceptionListItemMinimalSchemaMock())
.expect(403);
});
});
describe('tests with auditbeat data', () => {

View file

@ -30,10 +30,13 @@ import {
getRuleForSignalTesting,
getRuleForSignalTestingWithTimestampOverride,
} from '../../utils';
import { ROLES } from '../../../../plugins/security_solution/common/test';
import { createUserAndRole, deleteUserAndRole } from '../roles_users_utils';
// eslint-disable-next-line import/no-default-export
export default ({ getService }: FtrProviderContext) => {
const supertest = getService('supertest');
const supertestWithoutAuth = getService('supertestWithoutAuth');
const esArchiver = getService('esArchiver');
describe('create_rules', () => {
@ -65,186 +68,209 @@ export default ({ getService }: FtrProviderContext) => {
await esArchiver.unload('auditbeat/hosts');
});
it('should create a single rule with a rule_id', async () => {
const { body } = await supertest
.post(DETECTION_ENGINE_RULES_URL)
.set('kbn-xsrf', 'true')
.send(getSimpleRule())
.expect(200);
describe('elastic admin', () => {
it('should create a single rule with a rule_id', async () => {
const { body } = await supertest
.post(DETECTION_ENGINE_RULES_URL)
.set('kbn-xsrf', 'true')
.send(getSimpleRule())
.expect(200);
const bodyToCompare = removeServerGeneratedProperties(body);
expect(bodyToCompare).to.eql(getSimpleRuleOutput());
const bodyToCompare = removeServerGeneratedProperties(body);
expect(bodyToCompare).to.eql(getSimpleRuleOutput());
});
/*
This test is to ensure no future regressions introduced by the following scenario
a call to updateApiKey was invalidating the api key used by the
rule while the rule was executing, or even before it executed,
on the first rule run.
this pr https://github.com/elastic/kibana/pull/68184
fixed this by finding the true source of a bug that required the manual
api key update, and removed the call to that function.
When the api key is updated before / while the rule is executing, the alert
executor no longer has access to a service to update the rule status
saved object in Elasticsearch. Because of this, we cannot set the rule into
a 'failure' state, so the user ends up seeing 'going to run' as that is the
last status set for the rule before it erupts in an error that cannot be
recorded inside of the executor.
This adds an e2e test for the backend to catch that in case
this pops up again elsewhere.
*/
it('should create a single rule with a rule_id and validate it ran successfully', async () => {
const simpleRule = getRuleForSignalTesting(['auditbeat-*']);
const { body } = await supertest
.post(DETECTION_ENGINE_RULES_URL)
.set('kbn-xsrf', 'true')
.send(simpleRule)
.expect(200);
await waitForRuleSuccessOrStatus(supertest, body.id);
const { body: statusBody } = await supertest
.post(DETECTION_ENGINE_RULES_STATUS_URL)
.set('kbn-xsrf', 'true')
.send({ ids: [body.id] })
.expect(200);
expect(statusBody[body.id].current_status.status).to.eql('succeeded');
});
it('should create a single rule with a rule_id and an index pattern that does not match anything available and fail the rule', async () => {
const simpleRule = getRuleForSignalTesting(['does-not-exist-*']);
const { body } = await supertest
.post(DETECTION_ENGINE_RULES_URL)
.set('kbn-xsrf', 'true')
.send(simpleRule)
.expect(200);
await waitForRuleSuccessOrStatus(supertest, body.id, 'failed');
const { body: statusBody } = await supertest
.post(DETECTION_ENGINE_RULES_STATUS_URL)
.set('kbn-xsrf', 'true')
.send({ ids: [body.id] })
.expect(200);
expect(statusBody[body.id].current_status.status).to.eql('failed');
expect(statusBody[body.id].current_status.last_failure_message).to.eql(
'The following index patterns did not match any indices: ["does-not-exist-*"]'
);
});
it('should create a single rule with a rule_id and an index pattern that does not match anything and an index pattern that does and the rule should be successful', async () => {
const simpleRule = getRuleForSignalTesting(['does-not-exist-*', 'auditbeat-*']);
const { body } = await supertest
.post(DETECTION_ENGINE_RULES_URL)
.set('kbn-xsrf', 'true')
.send(simpleRule)
.expect(200);
await waitForRuleSuccessOrStatus(supertest, body.id, 'succeeded');
const { body: statusBody } = await supertest
.post(DETECTION_ENGINE_RULES_STATUS_URL)
.set('kbn-xsrf', 'true')
.send({ ids: [body.id] })
.expect(200);
expect(statusBody[body.id].current_status.status).to.eql('succeeded');
});
it('should create a single rule without an input index', async () => {
const rule: CreateRulesSchema = {
name: 'Simple Rule Query',
description: 'Simple Rule Query',
enabled: true,
risk_score: 1,
rule_id: 'rule-1',
severity: 'high',
type: 'query',
query: 'user.name: root or user.name: admin',
};
const expected = {
actions: [],
author: [],
created_by: 'elastic',
description: 'Simple Rule Query',
enabled: true,
false_positives: [],
from: 'now-6m',
immutable: false,
interval: '5m',
rule_id: 'rule-1',
language: 'kuery',
output_index: '.siem-signals-default',
max_signals: 100,
risk_score: 1,
risk_score_mapping: [],
name: 'Simple Rule Query',
query: 'user.name: root or user.name: admin',
references: [],
severity: 'high',
severity_mapping: [],
updated_by: 'elastic',
tags: [],
to: 'now',
type: 'query',
threat: [],
throttle: 'no_actions',
exceptions_list: [],
version: 1,
};
const { body } = await supertest
.post(DETECTION_ENGINE_RULES_URL)
.set('kbn-xsrf', 'true')
.send(rule)
.expect(200);
const bodyToCompare = removeServerGeneratedProperties(body);
expect(bodyToCompare).to.eql(expected);
});
it('should create a single rule without a rule_id', async () => {
const { body } = await supertest
.post(DETECTION_ENGINE_RULES_URL)
.set('kbn-xsrf', 'true')
.send(getSimpleRuleWithoutRuleId())
.expect(200);
const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(body);
expect(bodyToCompare).to.eql(getSimpleRuleOutputWithoutRuleId());
});
it('should create a single Machine Learning rule', async () => {
const { body } = await supertest
.post(DETECTION_ENGINE_RULES_URL)
.set('kbn-xsrf', 'true')
.send(getSimpleMlRule())
.expect(200);
const bodyToCompare = removeServerGeneratedProperties(body);
expect(bodyToCompare).to.eql(getSimpleMlRuleOutput());
});
it('should cause a 409 conflict if we attempt to create the same rule_id twice', async () => {
await supertest
.post(DETECTION_ENGINE_RULES_URL)
.set('kbn-xsrf', 'true')
.send(getSimpleRule())
.expect(200);
const { body } = await supertest
.post(DETECTION_ENGINE_RULES_URL)
.set('kbn-xsrf', 'true')
.send(getSimpleRule())
.expect(409);
expect(body).to.eql({
message: 'rule_id: "rule-1" already exists',
status_code: 409,
});
});
});
/*
This test is to ensure no future regressions introduced by the following scenario
a call to updateApiKey was invalidating the api key used by the
rule while the rule was executing, or even before it executed,
on the first rule run.
this pr https://github.com/elastic/kibana/pull/68184
fixed this by finding the true source of a bug that required the manual
api key update, and removed the call to that function.
describe('t1_analyst', () => {
const role = ROLES.t1_analyst;
When the api key is updated before / while the rule is executing, the alert
executor no longer has access to a service to update the rule status
saved object in Elasticsearch. Because of this, we cannot set the rule into
a 'failure' state, so the user ends up seeing 'going to run' as that is the
last status set for the rule before it erupts in an error that cannot be
recorded inside of the executor.
beforeEach(async () => {
await createUserAndRole(getService, role);
});
This adds an e2e test for the backend to catch that in case
this pops up again elsewhere.
*/
it('should create a single rule with a rule_id and validate it ran successfully', async () => {
const simpleRule = getRuleForSignalTesting(['auditbeat-*']);
const { body } = await supertest
.post(DETECTION_ENGINE_RULES_URL)
.set('kbn-xsrf', 'true')
.send(simpleRule)
.expect(200);
afterEach(async () => {
await deleteUserAndRole(getService, role);
});
await waitForRuleSuccessOrStatus(supertest, body.id);
const { body: statusBody } = await supertest
.post(DETECTION_ENGINE_RULES_STATUS_URL)
.set('kbn-xsrf', 'true')
.send({ ids: [body.id] })
.expect(200);
expect(statusBody[body.id].current_status.status).to.eql('succeeded');
});
it('should create a single rule with a rule_id and an index pattern that does not match anything available and fail the rule', async () => {
const simpleRule = getRuleForSignalTesting(['does-not-exist-*']);
const { body } = await supertest
.post(DETECTION_ENGINE_RULES_URL)
.set('kbn-xsrf', 'true')
.send(simpleRule)
.expect(200);
await waitForRuleSuccessOrStatus(supertest, body.id, 'failed');
const { body: statusBody } = await supertest
.post(DETECTION_ENGINE_RULES_STATUS_URL)
.set('kbn-xsrf', 'true')
.send({ ids: [body.id] })
.expect(200);
expect(statusBody[body.id].current_status.status).to.eql('failed');
expect(statusBody[body.id].current_status.last_failure_message).to.eql(
'The following index patterns did not match any indices: ["does-not-exist-*"]'
);
});
it('should create a single rule with a rule_id and an index pattern that does not match anything and an index pattern that does and the rule should be successful', async () => {
const simpleRule = getRuleForSignalTesting(['does-not-exist-*', 'auditbeat-*']);
const { body } = await supertest
.post(DETECTION_ENGINE_RULES_URL)
.set('kbn-xsrf', 'true')
.send(simpleRule)
.expect(200);
await waitForRuleSuccessOrStatus(supertest, body.id, 'succeeded');
const { body: statusBody } = await supertest
.post(DETECTION_ENGINE_RULES_STATUS_URL)
.set('kbn-xsrf', 'true')
.send({ ids: [body.id] })
.expect(200);
expect(statusBody[body.id].current_status.status).to.eql('succeeded');
});
it('should create a single rule without an input index', async () => {
const rule: CreateRulesSchema = {
name: 'Simple Rule Query',
description: 'Simple Rule Query',
enabled: true,
risk_score: 1,
rule_id: 'rule-1',
severity: 'high',
type: 'query',
query: 'user.name: root or user.name: admin',
};
const expected = {
actions: [],
author: [],
created_by: 'elastic',
description: 'Simple Rule Query',
enabled: true,
false_positives: [],
from: 'now-6m',
immutable: false,
interval: '5m',
rule_id: 'rule-1',
language: 'kuery',
output_index: '.siem-signals-default',
max_signals: 100,
risk_score: 1,
risk_score_mapping: [],
name: 'Simple Rule Query',
query: 'user.name: root or user.name: admin',
references: [],
severity: 'high',
severity_mapping: [],
updated_by: 'elastic',
tags: [],
to: 'now',
type: 'query',
threat: [],
throttle: 'no_actions',
exceptions_list: [],
version: 1,
};
const { body } = await supertest
.post(DETECTION_ENGINE_RULES_URL)
.set('kbn-xsrf', 'true')
.send(rule)
.expect(200);
const bodyToCompare = removeServerGeneratedProperties(body);
expect(bodyToCompare).to.eql(expected);
});
it('should create a single rule without a rule_id', async () => {
const { body } = await supertest
.post(DETECTION_ENGINE_RULES_URL)
.set('kbn-xsrf', 'true')
.send(getSimpleRuleWithoutRuleId())
.expect(200);
const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(body);
expect(bodyToCompare).to.eql(getSimpleRuleOutputWithoutRuleId());
});
it('should create a single Machine Learning rule', async () => {
const { body } = await supertest
.post(DETECTION_ENGINE_RULES_URL)
.set('kbn-xsrf', 'true')
.send(getSimpleMlRule())
.expect(200);
const bodyToCompare = removeServerGeneratedProperties(body);
expect(bodyToCompare).to.eql(getSimpleMlRuleOutput());
});
it('should cause a 409 conflict if we attempt to create the same rule_id twice', async () => {
await supertest
.post(DETECTION_ENGINE_RULES_URL)
.set('kbn-xsrf', 'true')
.send(getSimpleRule())
.expect(200);
const { body } = await supertest
.post(DETECTION_ENGINE_RULES_URL)
.set('kbn-xsrf', 'true')
.send(getSimpleRule())
.expect(409);
expect(body).to.eql({
message: 'rule_id: "rule-1" already exists',
status_code: 409,
it('should NOT be able to create a rule', async () => {
await supertestWithoutAuth
.post(DETECTION_ENGINE_RULES_URL)
.auth(role, 'changeme')
.set('kbn-xsrf', 'true')
.send(getSimpleRule())
.expect(403);
});
});
});