mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Security Solution] [Security Platform] Allow users without any actions privileges to still import rules (#126203)
allow users without any actions privileges to still import rules, adds tests to cover this case Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
bdc08fbf4f
commit
ed7b51ffd6
16 changed files with 301 additions and 18 deletions
|
@ -22,6 +22,7 @@ export type {
|
|||
ActionType,
|
||||
PreConfiguredAction,
|
||||
ActionsApiRequestHandlerContext,
|
||||
FindActionResult,
|
||||
} from './types';
|
||||
|
||||
export type {
|
||||
|
|
|
@ -12,6 +12,7 @@ export enum ROLES {
|
|||
t1_analyst = 't1_analyst',
|
||||
t2_analyst = 't2_analyst',
|
||||
hunter = 'hunter',
|
||||
hunter_no_actions = 'hunter_no_actions',
|
||||
rule_author = 'rule_author',
|
||||
platform_engineer = 'platform_engineer',
|
||||
detections_admin = 'detections_admin',
|
||||
|
|
|
@ -45,6 +45,7 @@ import {
|
|||
} from './utils/import_rules_utils';
|
||||
import { getReferencedExceptionLists } from './utils/gather_referenced_exceptions';
|
||||
import { importRuleExceptions } from './utils/import_rule_exceptions';
|
||||
import { ImportRulesSchemaDecoded } from '../../../../../common/detection_engine/schemas/request';
|
||||
|
||||
const CHUNK_PARSED_OBJECT_SIZE = 50;
|
||||
|
||||
|
@ -138,22 +139,33 @@ export const importRulesRoute = (
|
|||
actionSOClient
|
||||
);
|
||||
|
||||
const [nonExistentActionErrors, uniqueParsedObjects] = await getInvalidConnectors(
|
||||
migratedParsedObjectsWithoutDuplicateErrors,
|
||||
actionsClient
|
||||
let parsedRules;
|
||||
let actionErrors: BulkError[] = [];
|
||||
const actualRules = rules.filter(
|
||||
(rule): rule is ImportRulesSchemaDecoded => !(rule instanceof Error)
|
||||
);
|
||||
|
||||
if (actualRules.some((rule) => rule.actions.length > 0)) {
|
||||
const [nonExistentActionErrors, uniqueParsedObjects] = await getInvalidConnectors(
|
||||
migratedParsedObjectsWithoutDuplicateErrors,
|
||||
actionsClient
|
||||
);
|
||||
parsedRules = uniqueParsedObjects;
|
||||
actionErrors = nonExistentActionErrors;
|
||||
} else {
|
||||
parsedRules = migratedParsedObjectsWithoutDuplicateErrors;
|
||||
}
|
||||
// gather all exception lists that the imported rules reference
|
||||
const foundReferencedExceptionLists = await getReferencedExceptionLists({
|
||||
rules: uniqueParsedObjects,
|
||||
rules: parsedRules,
|
||||
savedObjectsClient,
|
||||
});
|
||||
|
||||
const chunkParseObjects = chunk(CHUNK_PARSED_OBJECT_SIZE, uniqueParsedObjects);
|
||||
const chunkParseObjects = chunk(CHUNK_PARSED_OBJECT_SIZE, parsedRules);
|
||||
|
||||
const importRuleResponse: ImportRuleResponse[] = await importRulesHelper({
|
||||
ruleChunks: chunkParseObjects,
|
||||
rulesResponseAcc: [...nonExistentActionErrors, ...duplicateIdErrors],
|
||||
rulesResponseAcc: [...actionErrors, ...duplicateIdErrors],
|
||||
mlAuthz,
|
||||
overwriteRules: request.query.overwrite,
|
||||
rulesClient,
|
||||
|
|
|
@ -16,7 +16,7 @@ import { RulesSchema } from '../../../../../common/detection_engine/schemas/resp
|
|||
import { ImportRulesSchemaDecoded } from '../../../../../common/detection_engine/schemas/request/import_rules_schema';
|
||||
import { CreateRulesBulkSchema } from '../../../../../common/detection_engine/schemas/request/create_rules_bulk_schema';
|
||||
import { PartialAlert, FindResult } from '../../../../../../alerting/server';
|
||||
import { ActionsClient } from '../../../../../../actions/server';
|
||||
import { ActionsClient, FindActionResult } from '../../../../../../actions/server';
|
||||
import { INTERNAL_IDENTIFIER } from '../../../../../common/constants';
|
||||
import { RuleAlertType, isAlertType } from '../../rules/types';
|
||||
import { createBulkErrorObject, BulkError, OutputError } from '../utils';
|
||||
|
@ -305,7 +305,32 @@ export const getInvalidConnectors = async (
|
|||
rules: PromiseFromStreams[],
|
||||
actionsClient: ActionsClient
|
||||
): Promise<[BulkError[], PromiseFromStreams[]]> => {
|
||||
const actionsFind = await actionsClient.getAll();
|
||||
let actionsFind: FindActionResult[] = [];
|
||||
const reducerAccumulator = {
|
||||
errors: new Map<string, BulkError>(),
|
||||
rulesAcc: new Map<string, PromiseFromStreams>(),
|
||||
};
|
||||
try {
|
||||
actionsFind = await actionsClient.getAll();
|
||||
} catch (exc) {
|
||||
if (exc?.output?.statusCode === 403) {
|
||||
reducerAccumulator.errors.set(
|
||||
uuid.v4(),
|
||||
createBulkErrorObject({
|
||||
statusCode: exc.output.statusCode,
|
||||
message: `You may not have actions privileges required to import rules with actions: ${exc.output.payload.message}`,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
reducerAccumulator.errors.set(
|
||||
uuid.v4(),
|
||||
createBulkErrorObject({
|
||||
statusCode: 404,
|
||||
message: JSON.stringify(exc),
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
const actionIds = new Set(actionsFind.map((action) => action.id));
|
||||
const { errors, rulesAcc } = rules.reduce(
|
||||
(acc, parsedRule) => {
|
||||
|
@ -339,10 +364,7 @@ export const getInvalidConnectors = async (
|
|||
}
|
||||
return acc;
|
||||
}, // using map (preserves ordering)
|
||||
{
|
||||
errors: new Map<string, BulkError>(),
|
||||
rulesAcc: new Map<string, PromiseFromStreams>(),
|
||||
}
|
||||
reducerAccumulator
|
||||
);
|
||||
|
||||
return [Array.from(errors.values()), Array.from(rulesAcc.values())];
|
||||
|
|
|
@ -24,11 +24,7 @@
|
|||
"privileges": ["read", "write"]
|
||||
},
|
||||
{
|
||||
"names": [
|
||||
"metrics-endpoint.metadata_current_*",
|
||||
".fleet-agents*",
|
||||
".fleet-actions*"
|
||||
],
|
||||
"names": ["metrics-endpoint.metadata_current_*", ".fleet-agents*", ".fleet-actions*"],
|
||||
"privileges": ["read"]
|
||||
}
|
||||
]
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
This user can CRUD rules and signals. The main difference here is the user has
|
||||
|
||||
```json
|
||||
"builtInAlerts": ["all"],
|
||||
```
|
||||
|
||||
privileges whereas the T1 and T2 have "read" privileges which prevents them from creating rules
|
||||
|
||||
| Role | Data Sources | Security Solution ML Jobs/Results | Lists | Rules/Exceptions | Action Connectors | Signals/Alerts |
|
||||
| :-----------------: | :----------: | :------------------: | :---: | :--------------: | :---------------: | :------------: |
|
||||
| Hunter / T3 Analyst | read, write | read | read | read, write | none | read, write |
|
|
@ -0,0 +1,11 @@
|
|||
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
curl -v -H 'Content-Type: application/json' -H 'kbn-xsrf: 123'\
|
||||
-u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \
|
||||
-XDELETE ${ELASTICSEARCH_URL}/_security/user/hunter_no_actions
|
|
@ -0,0 +1,43 @@
|
|||
{
|
||||
"elasticsearch": {
|
||||
"cluster": [],
|
||||
"indices": [
|
||||
{
|
||||
"names": [
|
||||
"apm-*-transaction*",
|
||||
"traces-apm*",
|
||||
"auditbeat-*",
|
||||
"endgame-*",
|
||||
"filebeat-*",
|
||||
"logs-*",
|
||||
"packetbeat-*",
|
||||
"winlogbeat-*"
|
||||
],
|
||||
"privileges": ["read", "write"]
|
||||
},
|
||||
{
|
||||
"names": [".alerts-security*", ".siem-signals-*"],
|
||||
"privileges": ["read", "write"]
|
||||
},
|
||||
{
|
||||
"names": [".lists*", ".items*"],
|
||||
"privileges": ["read", "write"]
|
||||
},
|
||||
{
|
||||
"names": ["metrics-endpoint.metadata_current_*", ".fleet-agents*", ".fleet-actions*"],
|
||||
"privileges": ["read"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"kibana": [
|
||||
{
|
||||
"feature": {
|
||||
"ml": ["read"],
|
||||
"siem": ["all", "read_alerts", "crud_alerts"],
|
||||
"securitySolutionCases": ["all"],
|
||||
"builtInAlerts": ["all"]
|
||||
},
|
||||
"spaces": ["*"]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"password": "changeme",
|
||||
"roles": ["hunter_no_actions"],
|
||||
"full_name": "Hunter No Actions",
|
||||
"email": "detections-reader@example.com"
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
curl -H 'Content-Type: application/json' -H 'kbn-xsrf: 123'\
|
||||
-u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \
|
||||
-XGET ${KIBANA_URL}/api/security/role/hunter_no_actions | jq -S .
|
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* 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 * as hunterNoActionsUser from './detections_user.json';
|
||||
import * as hunterNoActionsRole from './detections_role.json';
|
||||
export { hunterNoActionsUser, hunterNoActionsRole };
|
|
@ -0,0 +1,14 @@
|
|||
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
ROLE=(${@:-./detections_role.json})
|
||||
|
||||
curl -H 'Content-Type: application/json' -H 'kbn-xsrf: 123'\
|
||||
-u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \
|
||||
-XPUT ${KIBANA_URL}/api/security/role/hunter_no_actions \
|
||||
-d @${ROLE}
|
|
@ -0,0 +1,14 @@
|
|||
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
USER=(${@:-./detections_user.json})
|
||||
|
||||
curl -H 'Content-Type: application/json' -H 'kbn-xsrf: 123'\
|
||||
-u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \
|
||||
${ELASTICSEARCH_URL}/_security/user/hunter_no_actions \
|
||||
-d @${USER}
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
export * from './detections_admin';
|
||||
export * from './hunter';
|
||||
export * from './hunter_no_actions';
|
||||
export * from './platform_engineer';
|
||||
export * from './reader';
|
||||
export * from './rule_author';
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
t1AnalystUser,
|
||||
t2AnalystUser,
|
||||
hunterUser,
|
||||
hunterNoActionsUser,
|
||||
ruleAuthorUser,
|
||||
socManagerUser,
|
||||
platformEngineerUser,
|
||||
|
@ -19,6 +20,7 @@ import {
|
|||
t1AnalystRole,
|
||||
t2AnalystRole,
|
||||
hunterRole,
|
||||
hunterNoActionsRole,
|
||||
ruleAuthorRole,
|
||||
socManagerRole,
|
||||
platformEngineerRole,
|
||||
|
@ -53,6 +55,13 @@ export const createUserAndRole = async (
|
|||
return postRoleAndUser(ROLES.t2_analyst, t2AnalystRole, t2AnalystUser, getService);
|
||||
case ROLES.hunter:
|
||||
return postRoleAndUser(ROLES.hunter, hunterRole, hunterUser, getService);
|
||||
case ROLES.hunter_no_actions:
|
||||
return postRoleAndUser(
|
||||
ROLES.hunter_no_actions,
|
||||
hunterNoActionsRole,
|
||||
hunterNoActionsUser,
|
||||
getService
|
||||
);
|
||||
case ROLES.rule_author:
|
||||
return postRoleAndUser(ROLES.rule_author, ruleAuthorRole, ruleAuthorUser, getService);
|
||||
case ROLES.soc_manager:
|
||||
|
@ -105,7 +114,7 @@ interface RoleInterface {
|
|||
feature: {
|
||||
ml: string[];
|
||||
siem: string[];
|
||||
actions: string[];
|
||||
actions?: string[];
|
||||
builtInAlerts: string[];
|
||||
};
|
||||
spaces: string[];
|
||||
|
|
|
@ -28,6 +28,8 @@ import {
|
|||
getImportExceptionsListSchemaMock,
|
||||
} from '../../../../plugins/lists/common/schemas/request/import_exceptions_schema.mock';
|
||||
import { deleteAllExceptions } from '../../../lists_api_integration/utils';
|
||||
import { createUserAndRole, deleteUserAndRole } from '../../../common/services/security_solution';
|
||||
import { ROLES } from '../../../../plugins/security_solution/common/test';
|
||||
|
||||
const getImportRuleBuffer = (connectorId: string) => {
|
||||
const rule1 = {
|
||||
|
@ -95,8 +97,127 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
const supertest = getService('supertest');
|
||||
const log = getService('log');
|
||||
const esArchiver = getService('esArchiver');
|
||||
const supertestWithoutAuth = getService('supertestWithoutAuth');
|
||||
|
||||
describe('import_rules', () => {
|
||||
describe('importing rules with different roles', () => {
|
||||
before(async () => {
|
||||
await createUserAndRole(getService, ROLES.hunter_no_actions);
|
||||
await createUserAndRole(getService, ROLES.hunter);
|
||||
});
|
||||
after(async () => {
|
||||
await deleteUserAndRole(getService, ROLES.hunter_no_actions);
|
||||
await deleteUserAndRole(getService, ROLES.hunter);
|
||||
});
|
||||
beforeEach(async () => {
|
||||
await createSignalsIndex(supertest, log);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await deleteSignalsIndex(supertest, log);
|
||||
await deleteAllAlerts(supertest, log);
|
||||
});
|
||||
it('should successfully import rules without actions when user has no actions privileges', async () => {
|
||||
const { body } = await supertestWithoutAuth
|
||||
.post(`${DETECTION_ENGINE_RULES_URL}/_import`)
|
||||
.auth(ROLES.hunter_no_actions, 'changeme')
|
||||
.set('kbn-xsrf', 'true')
|
||||
.attach('file', getSimpleRuleAsNdjson(['rule-1']), 'rules.ndjson')
|
||||
.expect(200);
|
||||
|
||||
expect(body).to.eql({
|
||||
errors: [],
|
||||
success: true,
|
||||
success_count: 1,
|
||||
exceptions_errors: [],
|
||||
exceptions_success: true,
|
||||
exceptions_success_count: 0,
|
||||
});
|
||||
});
|
||||
it('should successfully import rules with actions when user has "read" actions privileges', async () => {
|
||||
// create a new action
|
||||
const { body: hookAction } = await supertest
|
||||
.post('/api/actions/action')
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getWebHookAction())
|
||||
.expect(200);
|
||||
const simpleRule: ReturnType<typeof getSimpleRule> = {
|
||||
...getSimpleRule('rule-1'),
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
id: hookAction.id,
|
||||
action_type_id: hookAction.actionTypeId,
|
||||
params: {},
|
||||
},
|
||||
],
|
||||
};
|
||||
const { body } = await supertestWithoutAuth
|
||||
.post(`${DETECTION_ENGINE_RULES_URL}/_import`)
|
||||
.auth(ROLES.hunter, 'changeme')
|
||||
.set('kbn-xsrf', 'true')
|
||||
.attach('file', ruleToNdjson(simpleRule), 'rules.ndjson')
|
||||
.expect(200);
|
||||
|
||||
expect(body).to.eql({
|
||||
errors: [],
|
||||
success: true,
|
||||
success_count: 1,
|
||||
exceptions_errors: [],
|
||||
exceptions_success: true,
|
||||
exceptions_success_count: 0,
|
||||
});
|
||||
});
|
||||
it('should not import rules with actions when a user has no actions privileges', async () => {
|
||||
// create a new action
|
||||
const { body: hookAction } = await supertest
|
||||
.post('/api/actions/action')
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getWebHookAction())
|
||||
.expect(200);
|
||||
const simpleRule: ReturnType<typeof getSimpleRule> = {
|
||||
...getSimpleRule('rule-1'),
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
id: hookAction.id,
|
||||
action_type_id: hookAction.actionTypeId,
|
||||
params: {},
|
||||
},
|
||||
],
|
||||
};
|
||||
const { body } = await supertestWithoutAuth
|
||||
.post(`${DETECTION_ENGINE_RULES_URL}/_import`)
|
||||
.auth(ROLES.hunter_no_actions, 'changeme')
|
||||
.set('kbn-xsrf', 'true')
|
||||
.attach('file', ruleToNdjson(simpleRule), 'rules.ndjson')
|
||||
.expect(200);
|
||||
expect(body).to.eql({
|
||||
success: false,
|
||||
success_count: 0,
|
||||
errors: [
|
||||
{
|
||||
error: {
|
||||
message:
|
||||
'You may not have actions privileges required to import rules with actions: Unauthorized to get actions',
|
||||
status_code: 403,
|
||||
},
|
||||
rule_id: '(unknown id)',
|
||||
},
|
||||
{
|
||||
error: {
|
||||
message: `1 connector is missing. Connector id missing is: ${hookAction.id}`,
|
||||
status_code: 404,
|
||||
},
|
||||
rule_id: 'rule-1',
|
||||
},
|
||||
],
|
||||
exceptions_errors: [],
|
||||
exceptions_success: true,
|
||||
exceptions_success_count: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('importing rules with an index', () => {
|
||||
beforeEach(async () => {
|
||||
await createSignalsIndex(supertest, log);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue