diff --git a/x-pack/plugins/security_solution/server/deprecations/alerts_as_data_privileges.test.ts b/x-pack/plugins/security_solution/server/deprecations/alerts_as_data_privileges.test.ts new file mode 100644 index 0000000000000..ea358ae0e66b0 --- /dev/null +++ b/x-pack/plugins/security_solution/server/deprecations/alerts_as_data_privileges.test.ts @@ -0,0 +1,199 @@ +/* + * 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 } from 'src/core/server/mocks'; +import { RegisterDeprecationsConfig } from 'src/core/server'; +import { registerAlertsIndexPrivilegeDeprecations } from './alerts_as_data_privileges'; +import { getRoleMock, getContextMock } from './utils.mock'; + +const getDependenciesMock = () => ({ + deprecationsService: deprecationsServiceMock.createSetupContract(), + getKibanaRoles: jest.fn(), + applicationName: 'kibana-.kibana', +}); + +describe('alerts as data privileges deprecation', () => { + describe('deprecation handler', () => { + let mockDependencies: ReturnType; + let mockContext: ReturnType; + let deprecationHandler: RegisterDeprecationsConfig; + + beforeEach(() => { + mockContext = getContextMock(); + mockDependencies = getDependenciesMock(); + registerAlertsIndexPrivilegeDeprecations(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 no deprecation if no roles are found', async () => { + mockDependencies.getKibanaRoles.mockResolvedValue({ + roles: [], + }); + const result = await deprecationHandler.getDeprecations(mockContext); + expect(result).toEqual([]); + }); + + it('returns no deprecation when a role also has read access to the alerts-as-data index alias and backing index pattern', async () => { + mockDependencies.getKibanaRoles.mockResolvedValue({ + roles: [ + getRoleMock( + [ + { + names: [ + 'other-index', + '.siem-signals-*', + '.alerts-security.alerts-*', + '.internal.alerts-security.alerts-*', + ], + privileges: ['all'], + }, + ], + 'roleWithCorrectAccess' + ), + ], + }); + const result = await deprecationHandler.getDeprecations(mockContext); + expect(result).toEqual([]); + }); + + it('returns no deprecation if all roles found are internal', async () => { + const internalRoleMock = { + ...getRoleMock( + [ + { + names: ['other-index', '.siem-signals-*'], + privileges: ['all'], + }, + ], + 'internalRole' + ), + metadata: { + _reserved: true, + }, + }; + mockDependencies.getKibanaRoles.mockResolvedValue({ + roles: [internalRoleMock], + }); + const result = await deprecationHandler.getDeprecations(mockContext); + expect(result).toEqual([]); + }); + + 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-*', + '.alerts-security.alerts-*', + '.internal.alerts-security.alerts-*', + ], + privileges: ['all'], + }, + ], + 'roleWithCorrectAccess' + ), + getRoleMock( + [ + { + names: ['other-index', '.siem-signals-*', '.alerts-security.alerts-*'], + privileges: ['all'], + }, + ], + 'relevantRole1' + ), + getRoleMock( + [ + { + names: ['other-index', '.siem-signals-*', '.internal.alerts-security.alerts-*'], + privileges: ['all'], + }, + ], + 'relevantRole2' + ), + getRoleMock( + [ + { + names: ['other-index', '.siem-signals-*'], + privileges: ['all'], + }, + ], + 'relevantRole3' + ), + ], + }); + const result = await deprecationHandler.getDeprecations(mockContext); + expect(result).toEqual([ + { + correctiveActions: { + manualSteps: [ + 'Update your roles to include read privileges for the detection alerts indices appropriate for that role and space(s).', + 'In 8.0, users will be unable to view alerts until those permissions are added.', + 'The roles that currently have read access to detection alerts indices are: relevantRole1, relevantRole2, relevantRole3', + ], + }, + deprecationType: 'feature', + documentationUrl: + 'https://www.elastic.co/guide/en/security/8.0/whats-new.html#index-updates-8.0', + level: 'warning', + message: `In order to view detection alerts in 8.0+, users will need read privileges to new detection alerts index aliases \ +(.alerts-security.alerts-) and backing indices (.internal.alerts-security.alerts--*), \ +analogous to existing detection alerts indices (.siem-signals-). \ +In addition, any enabled Detection rules will be automatically disabled during the upgrade and must be manually re-enabled after \ +upgrading. Rules that are automatically disabled will also automatically be tagged to assist in manually re-enabling them post-upgrade. \ +Alerts created after upgrading will use a different schema.`, + title: 'The Detection Alerts index names are changing', + }, + ]); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/deprecations/alerts_as_data_privileges.ts b/x-pack/plugins/security_solution/server/deprecations/alerts_as_data_privileges.ts new file mode 100644 index 0000000000000..ed4301325b9c2 --- /dev/null +++ b/x-pack/plugins/security_solution/server/deprecations/alerts_as_data_privileges.ts @@ -0,0 +1,113 @@ +/* + * 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 } from 'src/core/server'; +import type { PrivilegeDeprecationsService, Role } from '../../../security/common/model'; +import { DEFAULT_SIGNALS_INDEX } from '../../common/constants'; +import { roleHasReadAccess, roleIsExternal } from './utils'; + +const ALERTS_INDEX_PREFIX = '.alerts-security.alerts'; +const INTERNAL_ALERTS_INDEX_PREFIX = '.internal.alerts-security.alerts'; + +const buildManualSteps = (roleNames: string[]): string[] => { + const baseSteps = [ + i18n.translate('xpack.securitySolution.deprecations.alertsIndexPrivileges.manualStep1', { + defaultMessage: + 'Update your roles to include read privileges for the detection alerts indices appropriate for that role and space(s).', + }), + i18n.translate('xpack.securitySolution.deprecations.alertsIndexPrivileges.manualStep2', { + defaultMessage: + 'In 8.0, users will be unable to view alerts until those permissions are added.', + }), + ]; + const informationalStep = i18n.translate( + 'xpack.securitySolution.deprecations.alertsIndexPrivileges.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']; +} + +export const registerAlertsIndexPrivilegeDeprecations = ({ + deprecationsService, + getKibanaRoles, +}: 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( + (role) => + roleIsExternal(role) && + roleHasReadAccess(role) && + (!roleHasReadAccess(role, ALERTS_INDEX_PREFIX) || + !roleHasReadAccess(role, INTERNAL_ALERTS_INDEX_PREFIX)) + ) ?? []; + } + + if (rolesWhichReadSignals.length === 0) { + return []; + } + + const roleNamesWhichReadSignals = rolesWhichReadSignals.map((role) => role.name); + + return [ + { + title: i18n.translate('xpack.securitySolution.deprecations.alertsIndexPrivileges.title', { + defaultMessage: 'The Detection Alerts index names are changing', + }), + message: i18n.translate( + 'xpack.securitySolution.deprecations.alertsIndexPrivileges.message', + { + values: { + alertsIndexPrefix: ALERTS_INDEX_PREFIX, + internalAlertsIndexPrefix: INTERNAL_ALERTS_INDEX_PREFIX, + signalsIndexPrefix: DEFAULT_SIGNALS_INDEX, + }, + defaultMessage: `In order to view detection alerts in 8.0+, users will need read privileges to new detection alerts index aliases \ +({alertsIndexPrefix}-) and backing indices ({internalAlertsIndexPrefix}--*), \ +analogous to existing detection alerts indices ({signalsIndexPrefix}-). \ +In addition, any enabled Detection rules will be automatically disabled during the upgrade and must be manually re-enabled after \ +upgrading. Rules that are automatically disabled will also automatically be tagged to assist in manually re-enabling them post-upgrade. \ +Alerts created after upgrading will use a different schema.`, + } + ), + level: 'warning', + deprecationType: 'feature', + documentationUrl: `https://www.elastic.co/guide/en/security/8.0/whats-new.html#index-updates-8.0`, + correctiveActions: { + manualSteps: buildManualSteps(roleNamesWhichReadSignals), + }, + }, + ]; + }, + }); +}; diff --git a/x-pack/plugins/security_solution/server/deprecations/rule_preview_privileges.test.ts b/x-pack/plugins/security_solution/server/deprecations/rule_preview_privileges.test.ts index d099ac0d8eb88..ad124c2ebfbbc 100644 --- a/x-pack/plugins/security_solution/server/deprecations/rule_preview_privileges.test.ts +++ b/x-pack/plugins/security_solution/server/deprecations/rule_preview_privileges.test.ts @@ -5,36 +5,10 @@ * 2.0. */ -import { - deprecationsServiceMock, - elasticsearchServiceMock, - savedObjectsClientMock, -} from 'src/core/server/mocks'; +import { deprecationsServiceMock } from 'src/core/server/mocks'; import { RegisterDeprecationsConfig } from 'src/core/server'; -import { Role } from '../../../security/common/model'; -import { - registerRulePreviewPrivilegeDeprecations, - roleHasReadAccess, -} 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, - }, -}); +import { registerRulePreviewPrivilegeDeprecations } from './rule_preview_privileges'; +import { getRoleMock, getContextMock } from './utils.mock'; const getDependenciesMock = () => ({ deprecationsService: deprecationsServiceMock.createSetupContract(), @@ -49,11 +23,6 @@ const getDependenciesMock = () => ({ applicationName: 'kibana-.kibana', }); -const getContextMock = () => ({ - esClient: elasticsearchServiceMock.createScopedClusterClient(), - savedObjectsClient: savedObjectsClientMock.create(), -}); - describe('rule preview privileges deprecation', () => { describe('deprecation handler', () => { let mockDependencies: ReturnType; @@ -202,63 +171,4 @@ describe('rule preview privileges deprecation', () => { ]); }); }); - - describe('utilities', () => { - describe('roleHasReadAccess', () => { - it('returns true if the role has read privilege to all signals indexes', () => { - const role = getRoleMock([ - { - names: ['.siem-signals-*'], - privileges: ['read'], - }, - ]); - expect(roleHasReadAccess(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(roleHasReadAccess(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(roleHasReadAccess(role)).toEqual(true); - }); - - it('returns false if the role has read privilege to other indices', () => { - const role = getRoleMock([ - { - names: ['other-index'], - privileges: ['read'], - }, - ]); - expect(roleHasReadAccess(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(roleHasReadAccess(role)).toEqual(false); - }); - - it('returns false if the role has no specific privileges', () => { - const role = getRoleMock(); - expect(roleHasReadAccess(role)).toEqual(false); - }); - }); - }); }); diff --git a/x-pack/plugins/security_solution/server/deprecations/rule_preview_privileges.ts b/x-pack/plugins/security_solution/server/deprecations/rule_preview_privileges.ts index ab74dfdd79d43..af66088a79929 100644 --- a/x-pack/plugins/security_solution/server/deprecations/rule_preview_privileges.ts +++ b/x-pack/plugins/security_solution/server/deprecations/rule_preview_privileges.ts @@ -10,18 +10,9 @@ 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'; +import { roleHasReadAccess, roleIsExternal } from './utils'; const PREVIEW_INDEX_PREFIX = '.preview.alerts-security.alerts'; -const READ_PRIVILEGES = ['all', 'read']; - -export const roleHasReadAccess = (role: Role, indexPrefix = DEFAULT_SIGNALS_INDEX): boolean => - role.elasticsearch.indices.some( - (index) => - index.names.some((indexName) => indexName.startsWith(indexPrefix)) && - index.privileges.some((indexPrivilege) => READ_PRIVILEGES.includes(indexPrivilege)) - ); - -export const roleIsExternal = (role: Role): boolean => role.metadata?._reserved !== true; const buildManualSteps = (roleNames: string[]): string[] => { const baseSteps = [ diff --git a/x-pack/plugins/security_solution/server/deprecations/utils.mock.ts b/x-pack/plugins/security_solution/server/deprecations/utils.mock.ts new file mode 100644 index 0000000000000..b96c6681cf85d --- /dev/null +++ b/x-pack/plugins/security_solution/server/deprecations/utils.mock.ts @@ -0,0 +1,33 @@ +/* + * 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 { Role } from '../../../security/common/model'; +import { elasticsearchServiceMock, savedObjectsClientMock } from 'src/core/server/mocks'; + +const emptyRole: Role = { + name: 'mockRole', + metadata: { _reserved: false }, + elasticsearch: { cluster: [], indices: [], run_as: [] }, + kibana: [{ spaces: [], base: [], feature: {} }], +}; + +export const getRoleMock = ( + indicesOverrides: Role['elasticsearch']['indices'] = [], + name = 'mockRole' +): Role => ({ + ...emptyRole, + name, + elasticsearch: { + ...emptyRole.elasticsearch, + indices: indicesOverrides, + }, +}); + +export const getContextMock = () => ({ + esClient: elasticsearchServiceMock.createScopedClusterClient(), + savedObjectsClient: savedObjectsClientMock.create(), +}); diff --git a/x-pack/plugins/security_solution/server/deprecations/utils.test.ts b/x-pack/plugins/security_solution/server/deprecations/utils.test.ts new file mode 100644 index 0000000000000..04035a28d0379 --- /dev/null +++ b/x-pack/plugins/security_solution/server/deprecations/utils.test.ts @@ -0,0 +1,68 @@ +/* + * 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 { roleHasReadAccess } from './utils'; +import { getRoleMock } from './utils.mock'; + +describe('deprecation utils', () => { + describe('roleHasReadAccess', () => { + it('returns true if the role has read privilege to all signals indexes', () => { + const role = getRoleMock([ + { + names: ['.siem-signals-*'], + privileges: ['read'], + }, + ]); + expect(roleHasReadAccess(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(roleHasReadAccess(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(roleHasReadAccess(role)).toEqual(true); + }); + + it('returns false if the role has read privilege to other indices', () => { + const role = getRoleMock([ + { + names: ['other-index'], + privileges: ['read'], + }, + ]); + expect(roleHasReadAccess(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(roleHasReadAccess(role)).toEqual(false); + }); + + it('returns false if the role has no specific privileges', () => { + const role = getRoleMock(); + expect(roleHasReadAccess(role)).toEqual(false); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/deprecations/utils.ts b/x-pack/plugins/security_solution/server/deprecations/utils.ts new file mode 100644 index 0000000000000..b51452116461a --- /dev/null +++ b/x-pack/plugins/security_solution/server/deprecations/utils.ts @@ -0,0 +1,20 @@ +/* + * 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 type { Role } from '../../../security/common/model'; +import { DEFAULT_SIGNALS_INDEX } from '../../common/constants'; + +const READ_PRIVILEGES = ['all', 'read']; + +export const roleHasReadAccess = (role: Role, indexPrefix = DEFAULT_SIGNALS_INDEX): boolean => + role.elasticsearch.indices.some( + (index) => + index.names.some((indexName) => indexName.startsWith(indexPrefix)) && + index.privileges.some((indexPrivilege) => READ_PRIVILEGES.includes(indexPrivilege)) + ); + +export const roleIsExternal = (role: Role): boolean => role.metadata?._reserved !== true; diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 9efea2ffcb948..ebc96a8b19409 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -89,6 +89,7 @@ import type { SecuritySolutionPluginStart, PluginInitializerContext, } from './plugin_contract'; +import { registerAlertsIndexPrivilegeDeprecations } from './deprecations/alerts_as_data_privileges'; export type { SetupPlugins, StartPlugins, PluginSetup, PluginStart } from './plugin_contract'; @@ -341,6 +342,10 @@ export class Plugin implements ISecuritySolutionPlugin { getKibanaRoles: plugins.security?.privilegeDeprecationsService.getKibanaRoles, packageInfo: this.pluginContext.env.packageInfo, }); + registerAlertsIndexPrivilegeDeprecations({ + deprecationsService: core.deprecations, + getKibanaRoles: plugins.security?.privilegeDeprecationsService.getKibanaRoles, + }); return {}; }