[Security Solution] Add deprecation warning about rule preview permissions (#116878)

* WIP: Adding basic structure/logic for rule preview deprecation

* Alert users that they need a new privilege for rule preview in 8.0
* If we find existing roles that have read access to the signals index,
  list their names for the user.

* Refactor and unit test rule preview privilege deprecation

* Wire up our deprecation handler

* Fix deprecation translation

Use i18n interpolation instead of a template literal.

* Update signals preview with alerts-as-data index

* Copy: rename

"signals" -> "detection alerts"

* Update tests in response to copy changes

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Ryland Herrick 2021-11-02 15:40:32 -05:00 committed by GitHub
parent 9ecb189d92
commit 8a55fa4fdc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 349 additions and 0 deletions

View file

@ -0,0 +1,8 @@
/*
* 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.
*/
export { registerRulePreviewPrivilegeDeprecations } from './rule_preview_privileges';

View file

@ -0,0 +1,231 @@
/*
* 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 {
deprecationsServiceMock,
elasticsearchServiceMock,
savedObjectsClientMock,
} from 'src/core/server/mocks';
import { RegisterDeprecationsConfig } from 'src/core/server';
import { Role } from '../../../security/common/model';
import {
registerRulePreviewPrivilegeDeprecations,
roleHasSignalsReadAccess,
} from './rule_preview_privileges';
const emptyRole: Role = {
name: 'mockRole',
metadata: { _reserved: false },
elasticsearch: { cluster: [], indices: [], run_as: [] },
kibana: [{ spaces: [], base: [], feature: {} }],
};
const getRoleMock = (
indicesOverrides: Role['elasticsearch']['indices'] = [],
name = 'mockRole'
): Role => ({
...emptyRole,
name,
elasticsearch: {
...emptyRole.elasticsearch,
indices: indicesOverrides,
},
});
const getDependenciesMock = () => ({
deprecationsService: deprecationsServiceMock.createSetupContract(),
getKibanaRoles: jest.fn(),
packageInfo: {
branch: 'some-branch',
buildSha: 'deadbeef',
dist: true,
version: '7.16.0',
buildNum: 1,
},
applicationName: 'kibana-.kibana',
});
const getContextMock = () => ({
esClient: elasticsearchServiceMock.createScopedClusterClient(),
savedObjectsClient: savedObjectsClientMock.create(),
});
describe('rule preview privileges deprecation', () => {
describe('deprecation handler', () => {
let mockDependencies: ReturnType<typeof getDependenciesMock>;
let mockContext: ReturnType<typeof getContextMock>;
let deprecationHandler: RegisterDeprecationsConfig;
beforeEach(() => {
mockContext = getContextMock();
mockDependencies = getDependenciesMock();
registerRulePreviewPrivilegeDeprecations(mockDependencies);
expect(mockDependencies.deprecationsService.registerDeprecations).toHaveBeenCalledTimes(1);
deprecationHandler =
mockDependencies.deprecationsService.registerDeprecations.mock.calls[0][0];
});
it('returns errors from getKibanaRoles', async () => {
const errorResponse = {
errors: [
{
correctiveActions: {
manualSteps: [
"A user with the 'manage_security' cluster privilege is required to perform this check.",
],
},
level: 'fetch_error',
message: 'Error retrieving roles for privilege deprecations: Test error',
title: 'Error in privilege deprecations services',
},
],
};
mockDependencies.getKibanaRoles.mockResolvedValue(errorResponse);
const result = await deprecationHandler.getDeprecations(mockContext);
expect(result).toEqual([
{
correctiveActions: {
manualSteps: [
"A user with the 'manage_security' cluster privilege is required to perform this check.",
],
},
level: 'fetch_error',
message: 'Error retrieving roles for privilege deprecations: Test error',
title: 'Error in privilege deprecations services',
},
]);
});
it('returns an appropriate deprecation if no roles are found', async () => {
mockDependencies.getKibanaRoles.mockResolvedValue({
roles: [],
});
const result = await deprecationHandler.getDeprecations(mockContext);
expect(result).toEqual([
{
correctiveActions: {
manualSteps: [
'Update your roles to include read privileges for the detection alerts preview indices appropriate for that role and space(s).',
'In 8.0, users will be unable to view preview results until those permissions are added.',
],
},
deprecationType: 'feature',
documentationUrl:
'https://www.elastic.co/guide/en/security/some-branch/rules-ui-create.html#preview-rules',
level: 'warning',
message:
'In order to enable a more robust preview, users will need read privileges to new detection alerts preview indices (.alerts-security.preview.alert-<KIBANA_SPACE>), analogous to existing detection alerts indices (.siem-signals-<KIBANA_SPACE>).',
title: 'The Detections Rule Preview feature is changing',
},
]);
});
it('returns an appropriate deprecation if roles are found', async () => {
mockDependencies.getKibanaRoles.mockResolvedValue({
roles: [
getRoleMock(
[
{
names: ['other-index', 'second-index'],
privileges: ['all'],
},
],
'irrelevantRole'
),
getRoleMock(
[
{
names: ['other-index', '.siem-signals-*'],
privileges: ['all'],
},
],
'relevantRole'
),
],
});
const result = await deprecationHandler.getDeprecations(mockContext);
expect(result).toEqual([
{
correctiveActions: {
manualSteps: [
'Update your roles to include read privileges for the detection alerts preview indices appropriate for that role and space(s).',
'In 8.0, users will be unable to view preview results until those permissions are added.',
'The roles that currently have read access to detection alerts indices are: relevantRole',
],
},
deprecationType: 'feature',
documentationUrl:
'https://www.elastic.co/guide/en/security/some-branch/rules-ui-create.html#preview-rules',
level: 'warning',
message:
'In order to enable a more robust preview, users will need read privileges to new detection alerts preview indices (.alerts-security.preview.alert-<KIBANA_SPACE>), analogous to existing detection alerts indices (.siem-signals-<KIBANA_SPACE>).',
title: 'The Detections Rule Preview feature is changing',
},
]);
});
});
describe('utilities', () => {
describe('roleHasSignalsReadAccess', () => {
it('returns true if the role has read privilege to all signals indexes', () => {
const role = getRoleMock([
{
names: ['.siem-signals-*'],
privileges: ['read'],
},
]);
expect(roleHasSignalsReadAccess(role)).toEqual(true);
});
it('returns true if the role has read privilege to a single signals index', () => {
const role = getRoleMock([
{
names: ['.siem-signals-spaceId'],
privileges: ['read'],
},
]);
expect(roleHasSignalsReadAccess(role)).toEqual(true);
});
it('returns true if the role has all privilege to a single signals index', () => {
const role = getRoleMock([
{
names: ['.siem-signals-spaceId', 'other-index'],
privileges: ['all'],
},
]);
expect(roleHasSignalsReadAccess(role)).toEqual(true);
});
it('returns false if the role has read privilege to other indices', () => {
const role = getRoleMock([
{
names: ['other-index'],
privileges: ['read'],
},
]);
expect(roleHasSignalsReadAccess(role)).toEqual(false);
});
it('returns false if the role has all privilege to other indices', () => {
const role = getRoleMock([
{
names: ['other-index', 'second-index'],
privileges: ['all'],
},
]);
expect(roleHasSignalsReadAccess(role)).toEqual(false);
});
it('returns false if the role has no specific privileges', () => {
const role = getRoleMock();
expect(roleHasSignalsReadAccess(role)).toEqual(false);
});
});
});
});

View file

@ -0,0 +1,104 @@
/*
* 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 { i18n } from '@kbn/i18n';
import { DeprecationsServiceSetup, PackageInfo } from 'src/core/server';
import type { PrivilegeDeprecationsService, Role } from '../../../security/common/model';
import { DEFAULT_SIGNALS_INDEX } from '../../common/constants';
const buildManualSteps = (roleNames: string[]): string[] => {
const baseSteps = [
i18n.translate('xpack.securitySolution.deprecations.rulePreviewPrivileges.manualStep1', {
defaultMessage:
'Update your roles to include read privileges for the detection alerts preview indices appropriate for that role and space(s).',
}),
i18n.translate('xpack.securitySolution.deprecations.rulePreviewPrivileges.manualStep2', {
defaultMessage:
'In 8.0, users will be unable to view preview results until those permissions are added.',
}),
];
const informationalStep = i18n.translate(
'xpack.securitySolution.deprecations.rulePreviewPrivileges.manualStep3',
{
defaultMessage:
'The roles that currently have read access to detection alerts indices are: {roles}',
values: {
roles: roleNames.join(', '),
},
}
);
if (roleNames.length === 0) {
return baseSteps;
} else {
return [...baseSteps, informationalStep];
}
};
interface Dependencies {
deprecationsService: DeprecationsServiceSetup;
getKibanaRoles?: PrivilegeDeprecationsService['getKibanaRoles'];
packageInfo: PackageInfo;
}
export const registerRulePreviewPrivilegeDeprecations = ({
deprecationsService,
getKibanaRoles,
packageInfo,
}: Dependencies) => {
deprecationsService.registerDeprecations({
getDeprecations: async (context) => {
let rolesWhichReadSignals: Role[] = [];
if (getKibanaRoles) {
const { roles, errors } = await getKibanaRoles({ context });
if (errors?.length) {
return errors;
}
rolesWhichReadSignals = roles?.filter(roleHasSignalsReadAccess) ?? [];
}
const roleNamesWhichReadSignals = rolesWhichReadSignals.map((role) => role.name);
return [
{
title: i18n.translate('xpack.securitySolution.deprecations.rulePreviewPrivileges.title', {
defaultMessage: 'The Detections Rule Preview feature is changing',
}),
message: i18n.translate(
'xpack.securitySolution.deprecations.rulePreviewPrivileges.message',
{
values: {
previewIndexPrefix: '.alerts-security.preview.alert',
signalsIndexPrefix: DEFAULT_SIGNALS_INDEX,
},
defaultMessage:
'In order to enable a more robust preview, users will need read privileges to new detection alerts preview indices ({previewIndexPrefix}-<KIBANA_SPACE>), analogous to existing detection alerts indices ({signalsIndexPrefix}-<KIBANA_SPACE>).',
}
),
level: 'warning',
deprecationType: 'feature',
documentationUrl: `https://www.elastic.co/guide/en/security/${packageInfo.branch}/rules-ui-create.html#preview-rules`,
correctiveActions: {
manualSteps: buildManualSteps(roleNamesWhichReadSignals),
},
},
];
},
});
};
const READ_PRIVILEGES = ['all', 'read'];
export const roleHasSignalsReadAccess = (role: Role): boolean =>
role.elasticsearch.indices.some(
(index) =>
index.names.some((indexName) => indexName.startsWith(DEFAULT_SIGNALS_INDEX)) &&
index.privileges.some((indexPrivilege) => READ_PRIVILEGES.includes(indexPrivilege))
);

View file

@ -70,6 +70,7 @@ import { EndpointMetadataService } from './endpoint/services/metadata';
import { CreateRuleOptions } from './lib/detection_engine/rule_types/types';
import { ctiFieldMap } from './lib/detection_engine/rule_types/field_maps/cti';
import { registerPrivilegeDeprecations } from './deprecation_privileges';
import { registerRulePreviewPrivilegeDeprecations } from './deprecations';
// eslint-disable-next-line no-restricted-imports
import { legacyRulesNotificationAlertType } from './lib/detection_engine/notifications/legacy_rules_notification_alert_type';
// eslint-disable-next-line no-restricted-imports
@ -335,6 +336,11 @@ export class Plugin implements ISecuritySolutionPlugin {
getKibanaRoles: plugins.security?.privilegeDeprecationsService.getKibanaRoles,
logger: this.logger.get('deprecations'),
});
registerRulePreviewPrivilegeDeprecations({
deprecationsService: core.deprecations,
getKibanaRoles: plugins.security?.privilegeDeprecationsService.getKibanaRoles,
packageInfo: this.pluginContext.env.packageInfo,
});
return {};
}