diff --git a/packages/kbn-rule-data-utils/src/technical_field_names.ts b/packages/kbn-rule-data-utils/src/technical_field_names.ts index 31779c9f08e81..6c45403fc0a13 100644 --- a/packages/kbn-rule-data-utils/src/technical_field_names.ts +++ b/packages/kbn-rule-data-utils/src/technical_field_names.ts @@ -19,6 +19,7 @@ const RULE_NAME = 'rule.name' as const; const RULE_CATEGORY = 'rule.category' as const; const TAGS = 'tags' as const; const PRODUCER = `${ALERT_NAMESPACE}.producer` as const; +const OWNER = `${ALERT_NAMESPACE}.owner` as const; const ALERT_ID = `${ALERT_NAMESPACE}.id` as const; const ALERT_UUID = `${ALERT_NAMESPACE}.uuid` as const; const ALERT_START = `${ALERT_NAMESPACE}.start` as const; @@ -40,6 +41,7 @@ const fields = { RULE_CATEGORY, TAGS, PRODUCER, + OWNER, ALERT_ID, ALERT_UUID, ALERT_START, @@ -62,6 +64,7 @@ export { RULE_CATEGORY, TAGS, PRODUCER, + OWNER, ALERT_ID, ALERT_UUID, ALERT_START, diff --git a/src/plugins/data/server/search/search_service.ts b/src/plugins/data/server/search/search_service.ts index a651d7b3bf105..b7d4c00f32535 100644 --- a/src/plugins/data/server/search/search_service.ts +++ b/src/plugins/data/server/search/search_service.ts @@ -108,6 +108,7 @@ export class SearchService implements Plugin { private searchStrategies: StrategyMap = {}; private sessionService: ISearchSessionService; private asScoped!: ISearchStart['asScoped']; + private searchAsInternalUser!: ISearchStrategy; constructor( private initializerContext: PluginInitializerContext, @@ -155,6 +156,17 @@ export class SearchService implements Plugin { ) ); + // We don't want to register this because we don't want the client to be able to access this + // strategy, but we do want to expose it to other server-side plugins + // see x-pack/plugins/security_solution/server/search_strategy/timeline/index.ts + // for example use case + this.searchAsInternalUser = enhancedEsSearchStrategyProvider( + this.initializerContext.config.legacy.globalConfig$, + this.logger, + usage, + true + ); + this.registerSearchStrategy(EQL_SEARCH_STRATEGY, eqlSearchStrategyProvider(this.logger)); registerBsearchRoute(bfetch, (request: KibanaRequest) => this.asScoped(request)); @@ -215,6 +227,7 @@ export class SearchService implements Plugin { uiSettings, indexPatterns, }), + searchAsInternalUser: this.searchAsInternalUser, getSearchStrategy: this.getSearchStrategy, asScoped: this.asScoped, searchSource: { diff --git a/src/plugins/data/server/search/types.ts b/src/plugins/data/server/search/types.ts index 6192045fa04c7..26e0416b9a4b0 100644 --- a/src/plugins/data/server/search/types.ts +++ b/src/plugins/data/server/search/types.ts @@ -102,6 +102,11 @@ export interface ISearchStart< SearchStrategyResponse extends IKibanaSearchResponse = IEsSearchResponse > { aggs: AggsStart; + /** + * Search as the internal Kibana system user. This is not a registered search strategy as we don't + * want to allow access from the client. + */ + searchAsInternalUser: ISearchStrategy; /** * Get other registered search strategies by name (or, by default, the Elasticsearch strategy). * For example, if a new strategy needs to use the already-registered ES search strategy, it can diff --git a/x-pack/plugins/alerting/server/authorization/alerting_authorization.mock.ts b/x-pack/plugins/alerting/server/authorization/alerting_authorization.mock.ts index 4e4cd4419a5a2..283f1f1b46ff9 100644 --- a/x-pack/plugins/alerting/server/authorization/alerting_authorization.mock.ts +++ b/x-pack/plugins/alerting/server/authorization/alerting_authorization.mock.ts @@ -21,7 +21,7 @@ const createAlertingAuthorizationMock = () => { }; export const alertingAuthorizationMock: { - create: () => AlertingAuthorizationMock; + create: () => jest.Mocked>; // AlertingAuthorizationMock; } = { create: createAlertingAuthorizationMock, }; diff --git a/x-pack/plugins/alerting/server/index.ts b/x-pack/plugins/alerting/server/index.ts index 72e3325107f31..957bd89f52f36 100644 --- a/x-pack/plugins/alerting/server/index.ts +++ b/x-pack/plugins/alerting/server/index.ts @@ -34,6 +34,13 @@ export { FindResult } from './alerts_client'; export { PublicAlertInstance as AlertInstance } from './alert_instance'; export { parseDuration } from './lib'; export { getEsErrorMessage } from './lib/errors'; +export { + ReadOperations, + AlertingAuthorizationFilterType, + AlertingAuthorization, + WriteOperations, + AlertingAuthorizationEntity, +} from './authorization'; export const plugin = (initContext: PluginInitializerContext) => new AlertingPlugin(initContext); diff --git a/x-pack/plugins/apm/common/alert_types.ts b/x-pack/plugins/apm/common/alert_types.ts index 12df93d54b296..19a429a0c1a7a 100644 --- a/x-pack/plugins/apm/common/alert_types.ts +++ b/x-pack/plugins/apm/common/alert_types.ts @@ -10,6 +10,8 @@ import type { ValuesType } from 'utility-types'; import type { ActionGroup } from '../../alerting/common'; import { ANOMALY_SEVERITY, ANOMALY_THRESHOLD } from './ml_constants'; +export const APM_SERVER_FEATURE_ID = 'apm'; + export enum AlertType { ErrorCount = 'apm.error_rate', // ErrorRate was renamed to ErrorCount but the key is kept as `error_rate` for backwards-compat. TransactionErrorRate = 'apm.transaction_error_rate', @@ -43,7 +45,7 @@ export const ALERT_TYPES_CONFIG: Record< actionGroups: [THRESHOLD_MET_GROUP], defaultActionGroupId: THRESHOLD_MET_GROUP_ID, minimumLicenseRequired: 'basic', - producer: 'apm', + producer: APM_SERVER_FEATURE_ID, }, [AlertType.TransactionDuration]: { name: i18n.translate('xpack.apm.transactionDurationAlert.name', { @@ -52,7 +54,7 @@ export const ALERT_TYPES_CONFIG: Record< actionGroups: [THRESHOLD_MET_GROUP], defaultActionGroupId: THRESHOLD_MET_GROUP_ID, minimumLicenseRequired: 'basic', - producer: 'apm', + producer: APM_SERVER_FEATURE_ID, }, [AlertType.TransactionDurationAnomaly]: { name: i18n.translate('xpack.apm.transactionDurationAnomalyAlert.name', { @@ -61,7 +63,7 @@ export const ALERT_TYPES_CONFIG: Record< actionGroups: [THRESHOLD_MET_GROUP], defaultActionGroupId: THRESHOLD_MET_GROUP_ID, minimumLicenseRequired: 'basic', - producer: 'apm', + producer: APM_SERVER_FEATURE_ID, }, [AlertType.TransactionErrorRate]: { name: i18n.translate('xpack.apm.transactionErrorRateAlert.name', { @@ -70,7 +72,7 @@ export const ALERT_TYPES_CONFIG: Record< actionGroups: [THRESHOLD_MET_GROUP], defaultActionGroupId: THRESHOLD_MET_GROUP_ID, minimumLicenseRequired: 'basic', - producer: 'apm', + producer: APM_SERVER_FEATURE_ID, }, }; diff --git a/x-pack/plugins/apm/public/components/alerting/alerting_flyout/index.tsx b/x-pack/plugins/apm/public/components/alerting/alerting_flyout/index.tsx index 35863d8099394..b87298c5fe8a0 100644 --- a/x-pack/plugins/apm/public/components/alerting/alerting_flyout/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/alerting_flyout/index.tsx @@ -8,7 +8,10 @@ import React, { useCallback, useMemo } from 'react'; import { useParams } from 'react-router-dom'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; -import { AlertType } from '../../../../common/alert_types'; +import { + AlertType, + APM_SERVER_FEATURE_ID, +} from '../../../../common/alert_types'; import { getInitialAlertValues } from '../get_initial_alert_values'; import { ApmPluginStartDeps } from '../../../plugin'; interface Props { @@ -31,7 +34,7 @@ export function AlertingFlyout(props: Props) { () => alertType && services.triggersActionsUi.getAddAlertFlyout({ - consumer: 'apm', + consumer: APM_SERVER_FEATURE_ID, onClose: onCloseAddFlyout, alertTypeId: alertType, canChangeTrigger: false, diff --git a/x-pack/plugins/apm/server/feature.ts b/x-pack/plugins/apm/server/feature.ts index fb0610dffb92e..7d8b0dcff36cf 100644 --- a/x-pack/plugins/apm/server/feature.ts +++ b/x-pack/plugins/apm/server/feature.ts @@ -6,8 +6,9 @@ */ import { i18n } from '@kbn/i18n'; +import { SubFeaturePrivilegeGroupType } from '../../features/common'; import { LicenseType } from '../../licensing/common/types'; -import { AlertType } from '../common/alert_types'; +import { AlertType, APM_SERVER_FEATURE_ID } from '../common/alert_types'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server'; import { LicensingPluginSetup, @@ -15,14 +16,14 @@ import { } from '../../licensing/server'; export const APM_FEATURE = { - id: 'apm', + id: APM_SERVER_FEATURE_ID, name: i18n.translate('xpack.apm.featureRegistry.apmFeatureName', { defaultMessage: 'APM and User Experience', }), order: 900, category: DEFAULT_APP_CATEGORIES.observability, - app: ['apm', 'ux', 'kibana'], - catalogue: ['apm'], + app: [APM_SERVER_FEATURE_ID, 'ux', 'kibana'], + catalogue: [APM_SERVER_FEATURE_ID], management: { insightsAndAlerting: ['triggersActions'], }, @@ -30,9 +31,9 @@ export const APM_FEATURE = { // see x-pack/plugins/features/common/feature_kibana_privileges.ts privileges: { all: { - app: ['apm', 'ux', 'kibana'], - api: ['apm', 'apm_write'], - catalogue: ['apm'], + app: [APM_SERVER_FEATURE_ID, 'ux', 'kibana'], + api: [APM_SERVER_FEATURE_ID, 'apm_write', 'rac'], + catalogue: [APM_SERVER_FEATURE_ID], savedObject: { all: [], read: [], @@ -42,7 +43,7 @@ export const APM_FEATURE = { all: Object.values(AlertType), }, alert: { - all: Object.values(AlertType), + read: Object.values(AlertType), }, }, management: { @@ -51,9 +52,9 @@ export const APM_FEATURE = { ui: ['show', 'save', 'alerting:show', 'alerting:save'], }, read: { - app: ['apm', 'ux', 'kibana'], - api: ['apm'], - catalogue: ['apm'], + app: [APM_SERVER_FEATURE_ID, 'ux', 'kibana'], + api: [APM_SERVER_FEATURE_ID, 'rac'], + catalogue: [APM_SERVER_FEATURE_ID], savedObject: { all: [], read: [], @@ -72,6 +73,40 @@ export const APM_FEATURE = { ui: ['show', 'alerting:show', 'alerting:save'], }, }, + subFeatures: [ + { + name: i18n.translate('xpack.apm.featureRegistry.manageAlerts', { + defaultMessage: 'Manage Alerts', + }), + privilegeGroups: [ + { + groupType: 'independent' as SubFeaturePrivilegeGroupType, + privileges: [ + { + id: 'alert_manage', + name: i18n.translate( + 'xpack.apm.featureRegistry.subfeature.apmFeatureName', + { + defaultMessage: 'Manage Alerts', + } + ), + includeIn: 'all' as 'all', + alerting: { + alert: { + all: Object.values(AlertType), + }, + }, + savedObject: { + all: [], + read: [], + }, + ui: [], + }, + ], + }, + ], + }, + ], }; interface Feature { diff --git a/x-pack/plugins/apm/server/index.ts b/x-pack/plugins/apm/server/index.ts index 9ab56c1a303ea..f603249b8d1e2 100644 --- a/x-pack/plugins/apm/server/index.ts +++ b/x-pack/plugins/apm/server/index.ts @@ -120,6 +120,7 @@ export function mergeConfigs( export const plugin = (initContext: PluginInitializerContext) => new APMPlugin(initContext); +export { APM_SERVER_FEATURE_ID } from '../common/alert_types'; export { APMPlugin } from './plugin'; export { APMPluginSetup } from './types'; export { APMServerRouteRepository } from './routes/get_global_apm_server_route_repository'; diff --git a/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts index 885b22ae343d8..f99d04132aff4 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts @@ -14,7 +14,11 @@ import { import { createLifecycleRuleTypeFactory } from '../../../../rule_registry/server'; import { ENVIRONMENT_NOT_DEFINED } from '../../../common/environment_filter_values'; import { asMutableArray } from '../../../common/utils/as_mutable_array'; -import { AlertType, ALERT_TYPES_CONFIG } from '../../../common/alert_types'; +import { + AlertType, + ALERT_TYPES_CONFIG, + APM_SERVER_FEATURE_ID, +} from '../../../common/alert_types'; import { PROCESSOR_EVENT, SERVICE_ENVIRONMENT, @@ -66,7 +70,7 @@ export function registerErrorCountAlertType({ apmActionVariables.interval, ], }, - producer: 'apm', + producer: APM_SERVER_FEATURE_ID, minimumLicenseRequired: 'basic', executor: async ({ services, params }) => { const config = await config$.pipe(take(1)).toPromise(); diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts index f77cc3ee930b1..565673936ba1a 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts @@ -14,7 +14,11 @@ import { } from '@kbn/rule-data-utils/target/technical_field_names'; import { createLifecycleRuleTypeFactory } from '../../../../rule_registry/server'; import { parseEnvironmentUrlParam } from '../../../common/environment_filter_values'; -import { AlertType, ALERT_TYPES_CONFIG } from '../../../common/alert_types'; +import { + AlertType, + ALERT_TYPES_CONFIG, + APM_SERVER_FEATURE_ID, +} from '../../../common/alert_types'; import { PROCESSOR_EVENT, SERVICE_ENVIRONMENT, @@ -75,7 +79,7 @@ export function registerTransactionDurationAlertType({ apmActionVariables.interval, ], }, - producer: 'apm', + producer: APM_SERVER_FEATURE_ID, minimumLicenseRequired: 'basic', executor: async ({ services, params }) => { const config = await config$.pipe(take(1)).toPromise(); diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts index 4d6a0685fd379..41b32985f7c21 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts @@ -12,7 +12,11 @@ import { ALERT_EVALUATION_VALUE, } from '@kbn/rule-data-utils/target/technical_field_names'; import { createLifecycleRuleTypeFactory } from '../../../../rule_registry/server'; -import { AlertType, ALERT_TYPES_CONFIG } from '../../../common/alert_types'; +import { + AlertType, + ALERT_TYPES_CONFIG, + APM_SERVER_FEATURE_ID, +} from '../../../common/alert_types'; import { EVENT_OUTCOME, PROCESSOR_EVENT, @@ -70,7 +74,7 @@ export function registerTransactionErrorRateAlertType({ apmActionVariables.interval, ], }, - producer: 'apm', + producer: APM_SERVER_FEATURE_ID, minimumLicenseRequired: 'basic', executor: async ({ services, params: alertParams }) => { const config = await config$.pipe(take(1)).toPromise(); diff --git a/x-pack/plugins/apm/server/lib/alerts/test_utils/index.ts b/x-pack/plugins/apm/server/lib/alerts/test_utils/index.ts index ce1466bff01a9..e4c61457f1738 100644 --- a/x-pack/plugins/apm/server/lib/alerts/test_utils/index.ts +++ b/x-pack/plugins/apm/server/lib/alerts/test_utils/index.ts @@ -38,6 +38,9 @@ export const createRuleTypeMocks = () => { const services = { scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), + savedObjectsClient: { + get: () => ({ attributes: { consumer: 'apm' } }), + }, alertInstanceFactory: jest.fn(() => ({ scheduleActions })), alertWithLifecycle: jest.fn(), logger: loggerMock, @@ -66,6 +69,7 @@ export const createRuleTypeMocks = () => { executor: async ({ params }: { params: Record }) => { return alertExecutor({ services, + rule: { consumer: 'apm' }, params, startedAt: new Date(), }); diff --git a/x-pack/plugins/monitoring/server/types.ts b/x-pack/plugins/monitoring/server/types.ts index 3dcf6862b7232..0b837fc00127a 100644 --- a/x-pack/plugins/monitoring/server/types.ts +++ b/x-pack/plugins/monitoring/server/types.ts @@ -21,6 +21,7 @@ import type { ActionsApiRequestHandlerContext, } from '../../actions/server'; import type { AlertingApiRequestHandlerContext } from '../../alerting/server'; +import type { RacApiRequestHandlerContext } from '../../rule_registry/server'; import { PluginStartContract as AlertingPluginStartContract, PluginSetupContract as AlertingPluginSetupContract, @@ -58,6 +59,7 @@ export interface RequestHandlerContextMonitoringPlugin extends RequestHandlerCon actions?: ActionsApiRequestHandlerContext; alerting?: AlertingApiRequestHandlerContext; infra: InfraRequestHandlerContext; + ruleRegistry?: RacApiRequestHandlerContext; } export interface PluginsStart { diff --git a/x-pack/plugins/rule_registry/common/assets/field_maps/technical_rule_field_map.ts b/x-pack/plugins/rule_registry/common/assets/field_maps/technical_rule_field_map.ts index a946e9523548c..6d70c581802c1 100644 --- a/x-pack/plugins/rule_registry/common/assets/field_maps/technical_rule_field_map.ts +++ b/x-pack/plugins/rule_registry/common/assets/field_maps/technical_rule_field_map.ts @@ -18,6 +18,7 @@ import { ALERT_UUID, EVENT_ACTION, EVENT_KIND, + OWNER, PRODUCER, RULE_CATEGORY, RULE_ID, @@ -40,6 +41,7 @@ export const technicalRuleFieldMap = { RULE_CATEGORY, TAGS ), + [OWNER]: { type: 'keyword' }, [PRODUCER]: { type: 'keyword' }, [ALERT_UUID]: { type: 'keyword' }, [ALERT_ID]: { type: 'keyword' }, diff --git a/x-pack/plugins/rule_registry/common/constants.ts b/x-pack/plugins/rule_registry/common/constants.ts new file mode 100644 index 0000000000000..d9aba65e4373e --- /dev/null +++ b/x-pack/plugins/rule_registry/common/constants.ts @@ -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 const BASE_RAC_ALERTS_API_PATH = '/api/rac/alerts'; diff --git a/x-pack/plugins/rule_registry/common/mapping_from_field_map.ts b/x-pack/plugins/rule_registry/common/mapping_from_field_map.ts index 17eb5ae8967af..fc4cab3ef985e 100644 --- a/x-pack/plugins/rule_registry/common/mapping_from_field_map.ts +++ b/x-pack/plugins/rule_registry/common/mapping_from_field_map.ts @@ -11,7 +11,7 @@ import { FieldMap } from './field_map/types'; export function mappingFromFieldMap(fieldMap: FieldMap): TypeMapping { const mappings = { - dynamic: 'strict' as const, + dynamic: true, properties: {}, }; diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.mock.ts b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.mock.ts new file mode 100644 index 0000000000000..26da30ce3c194 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.mock.ts @@ -0,0 +1,27 @@ +/* + * 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 { PublicMethodsOf } from '@kbn/utility-types'; +import { AlertsClient } from './alerts_client'; + +type Schema = PublicMethodsOf; +export type AlertsClientMock = jest.Mocked; + +const createAlertsClientMock = () => { + const mocked: AlertsClientMock = { + get: jest.fn(), + getAlertsIndex: jest.fn(), + update: jest.fn(), + }; + return mocked; +}; + +export const alertsClientMock: { + create: () => AlertsClientMock; +} = { + create: createAlertsClientMock, +}; diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts new file mode 100644 index 0000000000000..617c369da1f9c --- /dev/null +++ b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts @@ -0,0 +1,193 @@ +/* + * 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 { PublicMethodsOf } from '@kbn/utility-types'; +import { AlertTypeParams } from '../../../alerting/server'; +import { + ReadOperations, + AlertingAuthorization, + WriteOperations, + AlertingAuthorizationEntity, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../alerting/server/authorization'; +import { Logger, ElasticsearchClient } from '../../../../../src/core/server'; +import { alertAuditEvent, AlertAuditAction } from './audit_events'; +import { RuleDataPluginService } from '../rule_data_plugin_service'; +import { AuditLogger } from '../../../security/server'; +import { ParsedTechnicalFields } from '../../common/parse_technical_fields'; + +export interface ConstructorOptions { + logger: Logger; + authorization: PublicMethodsOf; + auditLogger?: AuditLogger; + esClient: ElasticsearchClient; + ruleDataService: PublicMethodsOf; +} + +export interface UpdateOptions { + id: string; + data: { + status: string; + }; + // observability-apm see here: x-pack/plugins/apm/server/plugin.ts:191 + assetName: string; +} + +interface GetAlertParams { + id: string; + // observability-apm see here: x-pack/plugins/apm/server/plugin.ts:191 + assetName: string; +} + +export class AlertsClient { + private readonly logger: Logger; + private readonly auditLogger?: AuditLogger; + private readonly authorization: PublicMethodsOf; + private readonly esClient: ElasticsearchClient; + private readonly ruleDataService: PublicMethodsOf; + + constructor({ + auditLogger, + authorization, + logger, + esClient, + ruleDataService, + }: ConstructorOptions) { + this.logger = logger; + this.authorization = authorization; + this.esClient = esClient; + this.auditLogger = auditLogger; + this.ruleDataService = ruleDataService; + } + + /** + * we are "hard coding" this string similar to how rule registry is doing it + * x-pack/plugins/apm/server/plugin.ts:191 + */ + public getAlertsIndex(assetName: string) { + return this.ruleDataService?.getFullAssetName(assetName); + } + + private async fetchAlert({ id, assetName }: GetAlertParams): Promise { + try { + const result = await this.esClient.get({ + index: this.getAlertsIndex(assetName), + id, + }); + + if ( + result == null || + result.body == null || + result.body._source == null || + result.body._source['rule.id'] == null || + result.body._source['kibana.rac.alert.owner'] == null + ) { + const errorMessage = `[rac] - Unable to retrieve alert details for alert with id of "${id}".`; + this.logger.debug(errorMessage); + throw new Error(errorMessage); + } + + return result.body._source; + } catch (error) { + const errorMessage = `[rac] - Unable to retrieve alert with id of "${id}".`; + this.logger.debug(errorMessage); + throw error; + } + } + + public async get({ id, assetName }: GetAlertParams): Promise { + try { + // first search for the alert by id, then use the alert info to check if user has access to it + const alert = await this.fetchAlert({ + id, + assetName, + }); + + // this.authorization leverages the alerting plugin's authorization + // client exposed to us for reuse + await this.authorization.ensureAuthorized({ + ruleTypeId: alert['rule.id']!, // we assert in fetchAlert that these values are non-null + consumer: alert['kibana.rac.alert.owner']!, // we assert in fetchAlert that these values are non-null + operation: ReadOperations.Get, + entity: AlertingAuthorizationEntity.Alert, + }); + + this.auditLogger?.log( + alertAuditEvent({ + action: AlertAuditAction.GET, + id, + }) + ); + + return alert; + } catch (error) { + this.logger.debug(`[rac] - Error fetching alert with id of "${id}"`); + this.auditLogger?.log( + alertAuditEvent({ + action: AlertAuditAction.GET, + id, + error, + }) + ); + throw error; + } + } + + public async update({ + id, + data, + assetName, + }: UpdateOptions): Promise { + try { + // TODO: use MGET + const alert = await this.fetchAlert({ + id, + assetName, + }); + + await this.authorization.ensureAuthorized({ + ruleTypeId: alert['rule.id']!, // we assert in fetchAlert that these values are non-null + consumer: alert['kibana.rac.alert.owner']!, // we assert in fetchAlert that these values are non-null + operation: WriteOperations.Update, + entity: AlertingAuthorizationEntity.Alert, + }); + + const index = this.getAlertsIndex(assetName); + + const updateParameters = { + id, + index, + body: { + doc: { + 'kibana.rac.alert.status': data.status, + }, + }, + }; + + const res = await this.esClient.update( + updateParameters + ); + + this.auditLogger?.log( + alertAuditEvent({ + action: AlertAuditAction.UPDATE, + id, + }) + ); + + return res.body.get?._source; + } catch (error) { + this.auditLogger?.log( + alertAuditEvent({ + action: AlertAuditAction.UPDATE, + id, + error, + }) + ); + throw error; + } + } +} diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client_factory.test.ts b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client_factory.test.ts new file mode 100644 index 0000000000000..1b5fc6322fa94 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client_factory.test.ts @@ -0,0 +1,94 @@ +/* + * 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 { Request } from '@hapi/hapi'; + +import { AlertsClientFactory, AlertsClientFactoryProps } from './alerts_client_factory'; +import { ElasticsearchClient, KibanaRequest } from 'src/core/server'; +import { loggingSystemMock } from 'src/core/server/mocks'; +import { securityMock } from '../../../security/server/mocks'; +import { AuditLogger } from '../../../security/server'; +import { ruleDataPluginServiceMock } from '../rule_data_plugin_service/rule_data_plugin_service.mock'; +import { alertingAuthorizationMock } from '../../../alerting/server/authorization/alerting_authorization.mock'; +import { RuleDataPluginServiceConstructorOptions } from '../rule_data_plugin_service'; + +jest.mock('./alerts_client'); + +const securityPluginSetup = securityMock.createSetup(); +const ruleDataServiceMock = ruleDataPluginServiceMock.create( + {} as RuleDataPluginServiceConstructorOptions +); +const alertingAuthMock = alertingAuthorizationMock.create(); + +const alertsClientFactoryParams: AlertsClientFactoryProps = { + logger: loggingSystemMock.create().get(), + getAlertingAuthorization: (_: KibanaRequest) => alertingAuthMock, + securityPluginSetup, + esClient: {} as ElasticsearchClient, + ruleDataService: ruleDataServiceMock, +}; + +const fakeRequest = ({ + app: {}, + headers: {}, + getBasePath: () => '', + path: '/', + route: { settings: {} }, + url: { + href: '/', + }, + raw: { + req: { + url: '/', + }, + }, +} as unknown) as Request; + +const auditLogger = { + log: jest.fn(), +} as jest.Mocked; + +beforeEach(() => { + jest.resetAllMocks(); + + securityPluginSetup.audit.asScoped.mockReturnValue(auditLogger); +}); + +test('creates an alerts client with proper constructor arguments', async () => { + const factory = new AlertsClientFactory(); + factory.initialize({ ...alertsClientFactoryParams }); + const request = KibanaRequest.from(fakeRequest); + await factory.create(request); + + expect(jest.requireMock('./alerts_client').AlertsClient).toHaveBeenCalledWith({ + authorization: alertingAuthMock, + logger: alertsClientFactoryParams.logger, + auditLogger, + esClient: {}, + ruleDataService: ruleDataServiceMock, + }); +}); + +test('throws an error if already initialized', () => { + const factory = new AlertsClientFactory(); + factory.initialize({ ...alertsClientFactoryParams }); + + expect(() => + factory.initialize({ ...alertsClientFactoryParams }) + ).toThrowErrorMatchingInlineSnapshot(`"AlertsClientFactory (RAC) already initialized"`); +}); + +test('throws an error if ruleDataService not available', () => { + const factory = new AlertsClientFactory(); + + expect(() => + factory.initialize({ + ...alertsClientFactoryParams, + ruleDataService: null, + }) + ).toThrowErrorMatchingInlineSnapshot(`"Rule registry data service required for alerts client"`); +}); diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client_factory.ts b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client_factory.ts new file mode 100644 index 0000000000000..8909fc71f38c5 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client_factory.ts @@ -0,0 +1,65 @@ +/* + * 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 { ElasticsearchClient, KibanaRequest, Logger } from 'src/core/server'; +import { PublicMethodsOf } from '@kbn/utility-types'; +import { SecurityPluginSetup } from '../../../security/server'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { AlertingAuthorization } from '../../../alerting/server/authorization'; +import { AlertsClient } from './alerts_client'; +import { RuleDataPluginService } from '../rule_data_plugin_service'; + +export interface AlertsClientFactoryProps { + logger: Logger; + esClient: ElasticsearchClient; + getAlertingAuthorization: (request: KibanaRequest) => PublicMethodsOf; + securityPluginSetup: SecurityPluginSetup | undefined; + ruleDataService: PublicMethodsOf | null; +} + +export class AlertsClientFactory { + private isInitialized = false; + private logger!: Logger; + private esClient!: ElasticsearchClient; + private getAlertingAuthorization!: ( + request: KibanaRequest + ) => PublicMethodsOf; + private securityPluginSetup!: SecurityPluginSetup | undefined; + private ruleDataService!: PublicMethodsOf; + + public initialize(options: AlertsClientFactoryProps) { + /** + * This should be called by the plugin's start() method. + */ + if (this.isInitialized) { + throw new Error('AlertsClientFactory (RAC) already initialized'); + } + + if (options.ruleDataService == null) { + throw new Error('Rule registry data service required for alerts client'); + } + + this.getAlertingAuthorization = options.getAlertingAuthorization; + this.isInitialized = true; + this.logger = options.logger; + this.esClient = options.esClient; + this.securityPluginSetup = options.securityPluginSetup; + this.ruleDataService = options.ruleDataService; + } + + public async create(request: KibanaRequest): Promise { + const { securityPluginSetup, getAlertingAuthorization, logger } = this; + + return new AlertsClient({ + logger, + authorization: getAlertingAuthorization(request), + auditLogger: securityPluginSetup?.audit.asScoped(request), + esClient: this.esClient, + ruleDataService: this.ruleDataService!, + }); + } +} diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/audit_events.test.ts b/x-pack/plugins/rule_registry/server/alert_data_client/audit_events.test.ts new file mode 100644 index 0000000000000..9536a9a640a00 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/alert_data_client/audit_events.test.ts @@ -0,0 +1,87 @@ +/* + * 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 { AlertAuditAction, alertAuditEvent } from './audit_events'; + +describe('#alertAuditEvent', () => { + test('creates event with `unknown` outcome', () => { + expect( + alertAuditEvent({ + action: AlertAuditAction.GET, + outcome: 'unknown', + id: '123', + }) + ).toMatchInlineSnapshot(` + Object { + "error": undefined, + "event": Object { + "action": "alert_get", + "category": Array [ + "database", + ], + "outcome": "unknown", + "type": Array [ + "access", + ], + }, + "message": "User is accessing alert [id=123]", + } + `); + }); + + test('creates event with `success` outcome', () => { + expect( + alertAuditEvent({ + action: AlertAuditAction.GET, + id: '123', + }) + ).toMatchInlineSnapshot(` + Object { + "error": undefined, + "event": Object { + "action": "alert_get", + "category": Array [ + "database", + ], + "outcome": "success", + "type": Array [ + "access", + ], + }, + "message": "User has accessed alert [id=123]", + } + `); + }); + + test('creates event with `failure` outcome', () => { + expect( + alertAuditEvent({ + action: AlertAuditAction.GET, + id: '123', + error: new Error('ERROR_MESSAGE'), + }) + ).toMatchInlineSnapshot(` + Object { + "error": Object { + "code": "Error", + "message": "ERROR_MESSAGE", + }, + "event": Object { + "action": "alert_get", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "access", + ], + }, + "message": "Failed attempt to access alert [id=123]", + } + `); + }); +}); diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/audit_events.ts b/x-pack/plugins/rule_registry/server/alert_data_client/audit_events.ts new file mode 100644 index 0000000000000..d07c23c7fbe9f --- /dev/null +++ b/x-pack/plugins/rule_registry/server/alert_data_client/audit_events.ts @@ -0,0 +1,61 @@ +/* + * 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 { EcsEventOutcome, EcsEventType } from 'src/core/server'; +import { AuditEvent } from '../../../security/server'; + +export enum AlertAuditAction { + GET = 'alert_get', + UPDATE = 'alert_update', + FIND = 'alert_find', +} + +type VerbsTuple = [string, string, string]; + +const eventVerbs: Record = { + alert_get: ['access', 'accessing', 'accessed'], + alert_update: ['update', 'updating', 'updated'], + alert_find: ['access', 'accessing', 'accessed'], +}; + +const eventTypes: Record = { + alert_get: 'access', + alert_update: 'change', + alert_find: 'access', +}; + +export interface AlertAuditEventParams { + action: AlertAuditAction; + outcome?: EcsEventOutcome; + id?: string; + error?: Error; +} + +export function alertAuditEvent({ action, id, outcome, error }: AlertAuditEventParams): AuditEvent { + const doc = id ? `alert [id=${id}]` : 'an alert'; + const [present, progressive, past] = eventVerbs[action]; + const message = error + ? `Failed attempt to ${present} ${doc}` + : outcome === 'unknown' + ? `User is ${progressive} ${doc}` + : `User has ${past} ${doc}`; + const type = eventTypes[action]; + + return { + message, + event: { + action, + category: ['database'], + type: type ? [type] : undefined, + outcome: outcome ?? (error ? 'failure' : 'success'), + }, + error: error && { + code: error.name, + message: error.message, + }, + }; +} diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/tests/get.test.ts b/x-pack/plugins/rule_registry/server/alert_data_client/tests/get.test.ts new file mode 100644 index 0000000000000..03796a57facab --- /dev/null +++ b/x-pack/plugins/rule_registry/server/alert_data_client/tests/get.test.ts @@ -0,0 +1,160 @@ +/* + * 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 { AlertsClient, ConstructorOptions } from '../alerts_client'; +import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; +import { ruleDataPluginServiceMock } from '../../rule_data_plugin_service/rule_data_plugin_service.mock'; +import { RuleDataPluginServiceConstructorOptions } from '../../rule_data_plugin_service'; +import { alertingAuthorizationMock } from '../../../../alerting/server/authorization/alerting_authorization.mock'; +import { AuditLogger } from '../../../../security/server'; + +const ruleDataServiceMock = ruleDataPluginServiceMock.create( + {} as RuleDataPluginServiceConstructorOptions +); +const alertingAuthMock = alertingAuthorizationMock.create(); +const esClientMock = elasticsearchClientMock.createElasticsearchClient(); +const auditLogger = { + log: jest.fn(), +} as jest.Mocked; + +const alertsClientParams: jest.Mocked = { + logger: loggingSystemMock.create().get(), + ruleDataService: { + ...ruleDataServiceMock, + getFullAssetName: (_?: string | undefined) => '.alerts-observability-apm', + }, + authorization: alertingAuthMock, + esClient: esClientMock, + auditLogger, +}; + +beforeEach(() => { + jest.resetAllMocks(); +}); + +describe('get()', () => { + test('calls ES client with given params', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + esClientMock.get.mockResolvedValueOnce( + elasticsearchClientMock.createApiResponse({ + body: { + found: true, + _type: 'alert', + _index: '.alerts-observability-apm', + _id: 'NoxgpHkBqbdrfX07MqXV', + _source: { + 'rule.id': 'apm.error_rate', + message: 'hello world 1', + 'kibana.rac.alert.owner': 'apm', + 'kibana.rac.alert.status': 'open', + }, + }, + }) + ); + const result = await alertsClient.get({ id: '1', assetName: 'observability-apm' }); + expect(result).toMatchInlineSnapshot(` + Object { + "kibana.rac.alert.owner": "apm", + "kibana.rac.alert.status": "open", + "message": "hello world 1", + "rule.id": "apm.error_rate", + } + `); + expect(esClientMock.get).toHaveBeenCalledTimes(1); + expect(esClientMock.get.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "id": "1", + "index": ".alerts-observability-apm", + }, + ] + `); + expect(auditLogger.log).toHaveBeenCalledWith({ + error: undefined, + event: { action: 'alert_get', category: ['database'], outcome: 'success', type: ['access'] }, + message: 'User has accessed alert [id=1]', + }); + }); + + test(`throws an error if ES client get fails`, async () => { + const error = new Error('something when wrong'); + const alertsClient = new AlertsClient(alertsClientParams); + esClientMock.get.mockRejectedValue(error); + + await expect( + alertsClient.get({ id: '1', assetName: 'observability-apm' }) + ).rejects.toThrowErrorMatchingInlineSnapshot(`"something when wrong"`); + expect(auditLogger.log).toHaveBeenCalledWith({ + error: { code: 'Error', message: 'something when wrong' }, + event: { action: 'alert_get', category: ['database'], outcome: 'failure', type: ['access'] }, + message: 'Failed attempt to access alert [id=1]', + }); + }); + + describe('authorization', () => { + beforeEach(() => { + esClientMock.get.mockResolvedValueOnce( + elasticsearchClientMock.createApiResponse({ + body: { + found: true, + _type: 'alert', + _index: '.alerts-observability-apm', + _id: 'NoxgpHkBqbdrfX07MqXV', + _source: { + 'rule.id': 'apm.error_rate', + message: 'hello world 1', + 'kibana.rac.alert.owner': 'apm', + 'kibana.rac.alert.status': 'open', + }, + }, + }) + ); + }); + + test('returns alert if user is authorized to read alert under the consumer', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + const result = await alertsClient.get({ id: '1', assetName: 'observability-apm' }); + + expect(alertingAuthMock.ensureAuthorized).toHaveBeenCalledWith({ + entity: 'alert', + consumer: 'apm', + operation: 'get', + ruleTypeId: 'apm.error_rate', + }); + expect(result).toMatchInlineSnapshot(` + Object { + "kibana.rac.alert.owner": "apm", + "kibana.rac.alert.status": "open", + "message": "hello world 1", + "rule.id": "apm.error_rate", + } + `); + }); + + test('throws when user is not authorized to get this type of alert', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + alertingAuthMock.ensureAuthorized.mockRejectedValue( + new Error(`Unauthorized to get a "apm.error_rate" alert for "apm"`) + ); + + await expect( + alertsClient.get({ id: '1', assetName: 'observability-apm' }) + ).rejects.toMatchInlineSnapshot( + `[Error: Unauthorized to get a "apm.error_rate" alert for "apm"]` + ); + + expect(alertingAuthMock.ensureAuthorized).toHaveBeenCalledWith({ + entity: 'alert', + consumer: 'apm', + operation: 'get', + ruleTypeId: 'apm.error_rate', + }); + }); + }); +}); diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/tests/update.test.ts b/x-pack/plugins/rule_registry/server/alert_data_client/tests/update.test.ts new file mode 100644 index 0000000000000..e1edba023406d --- /dev/null +++ b/x-pack/plugins/rule_registry/server/alert_data_client/tests/update.test.ts @@ -0,0 +1,288 @@ +/* + * 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 { AlertsClient, ConstructorOptions } from '../alerts_client'; +import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; +import { ruleDataPluginServiceMock } from '../../rule_data_plugin_service/rule_data_plugin_service.mock'; +import { RuleDataPluginServiceConstructorOptions } from '../../rule_data_plugin_service'; +import { alertingAuthorizationMock } from '../../../../alerting/server/authorization/alerting_authorization.mock'; +import { AuditLogger } from '../../../../security/server'; + +const ruleDataServiceMock = ruleDataPluginServiceMock.create( + {} as RuleDataPluginServiceConstructorOptions +); +const alertingAuthMock = alertingAuthorizationMock.create(); +const esClientMock = elasticsearchClientMock.createElasticsearchClient(); +const auditLogger = { + log: jest.fn(), +} as jest.Mocked; + +const alertsClientParams: jest.Mocked = { + logger: loggingSystemMock.create().get(), + ruleDataService: { + ...ruleDataServiceMock, + getFullAssetName: (_?: string | undefined) => '.alerts-observability-apm', + }, + authorization: alertingAuthMock, + esClient: esClientMock, + auditLogger, +}; + +beforeEach(() => { + jest.resetAllMocks(); +}); + +describe('update()', () => { + test('calls ES client with given params', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + esClientMock.get.mockResolvedValueOnce( + elasticsearchClientMock.createApiResponse({ + body: { + found: true, + _type: 'alert', + _index: '.alerts-observability-apm', + _id: 'NoxgpHkBqbdrfX07MqXV', + _source: { + 'rule.id': 'apm.error_rate', + message: 'hello world 1', + 'kibana.rac.alert.owner': 'apm', + 'kibana.rac.alert.status': 'open', + }, + }, + }) + ); + esClientMock.update.mockResolvedValueOnce( + elasticsearchClientMock.createApiResponse({ + body: { + _primary_term: 2, + result: 'updated', + _seq_no: 1, + _shards: { + failed: 0, + successful: 1, + total: 1, + }, + _version: 1, + _index: '.alerts-observability-apm', + _id: 'NoxgpHkBqbdrfX07MqXV', + get: { + found: true, + _seq_no: 1, + _primary_term: 2, + _source: { + 'rule.id': 'apm.error_rate', + message: 'hello world 1', + 'kibana.rac.alert.owner': 'apm', + 'kibana.rac.alert.status': 'closed', + }, + }, + }, + }) + ); + const result = await alertsClient.update({ + id: '1', + data: { status: 'closed' }, + assetName: 'observability-apm', + }); + expect(result).toMatchInlineSnapshot(` + Object { + "kibana.rac.alert.owner": "apm", + "kibana.rac.alert.status": "closed", + "message": "hello world 1", + "rule.id": "apm.error_rate", + } + `); + expect(esClientMock.update).toHaveBeenCalledTimes(1); + expect(esClientMock.update.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "body": Object { + "doc": Object { + "kibana.rac.alert.status": "closed", + }, + }, + "id": "1", + "index": ".alerts-observability-apm", + }, + ] + `); + expect(auditLogger.log).toHaveBeenCalledWith({ + error: undefined, + event: { + action: 'alert_update', + category: ['database'], + outcome: 'success', + type: ['change'], + }, + message: 'User has updated alert [id=1]', + }); + }); + + test(`throws an error if ES client get fails`, async () => { + const error = new Error('something when wrong on get'); + const alertsClient = new AlertsClient(alertsClientParams); + esClientMock.get.mockRejectedValue(error); + + await expect( + alertsClient.update({ + id: '1', + data: { status: 'closed' }, + assetName: 'observability-apm', + }) + ).rejects.toThrowErrorMatchingInlineSnapshot(`"something when wrong on get"`); + expect(auditLogger.log).toHaveBeenCalledWith({ + error: { code: 'Error', message: 'something when wrong on get' }, + event: { + action: 'alert_update', + category: ['database'], + outcome: 'failure', + type: ['change'], + }, + message: 'Failed attempt to update alert [id=1]', + }); + }); + + test(`throws an error if ES client update fails`, async () => { + const error = new Error('something when wrong on update'); + const alertsClient = new AlertsClient(alertsClientParams); + esClientMock.get.mockResolvedValueOnce( + elasticsearchClientMock.createApiResponse({ + body: { + found: true, + _type: 'alert', + _index: '.alerts-observability-apm', + _id: 'NoxgpHkBqbdrfX07MqXV', + _source: { + 'rule.id': 'apm.error_rate', + message: 'hello world 1', + 'kibana.rac.alert.owner': 'apm', + 'kibana.rac.alert.status': 'open', + }, + }, + }) + ); + esClientMock.update.mockRejectedValue(error); + + await expect( + alertsClient.update({ + id: '1', + data: { status: 'closed' }, + assetName: 'observability-apm', + }) + ).rejects.toThrowErrorMatchingInlineSnapshot(`"something when wrong on update"`); + expect(auditLogger.log).toHaveBeenCalledWith({ + error: { code: 'Error', message: 'something when wrong on update' }, + event: { + action: 'alert_update', + category: ['database'], + outcome: 'failure', + type: ['change'], + }, + message: 'Failed attempt to update alert [id=1]', + }); + }); + + describe('authorization', () => { + beforeEach(() => { + esClientMock.get.mockResolvedValueOnce( + elasticsearchClientMock.createApiResponse({ + body: { + found: true, + _type: 'alert', + _index: '.alerts-observability-apm', + _id: 'NoxgpHkBqbdrfX07MqXV', + _source: { + 'rule.id': 'apm.error_rate', + message: 'hello world 1', + 'kibana.rac.alert.owner': 'apm', + 'kibana.rac.alert.status': 'open', + }, + }, + }) + ); + + esClientMock.update.mockResolvedValueOnce( + elasticsearchClientMock.createApiResponse({ + body: { + _primary_term: 2, + result: 'updated', + _seq_no: 1, + _shards: { + failed: 0, + successful: 1, + total: 1, + }, + _version: 1, + _index: '.alerts-observability-apm', + _id: 'NoxgpHkBqbdrfX07MqXV', + get: { + found: true, + _seq_no: 1, + _primary_term: 2, + _source: { + 'rule.id': 'apm.error_rate', + message: 'hello world 1', + 'kibana.rac.alert.owner': 'apm', + 'kibana.rac.alert.status': 'closed', + }, + }, + }, + }) + ); + }); + + test('returns alert if user is authorized to update alert under the consumer', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + const result = await alertsClient.update({ + id: '1', + data: { status: 'closed' }, + assetName: 'observability-apm', + }); + + expect(alertingAuthMock.ensureAuthorized).toHaveBeenCalledWith({ + entity: 'alert', + consumer: 'apm', + operation: 'update', + ruleTypeId: 'apm.error_rate', + }); + expect(result).toMatchInlineSnapshot(` + Object { + "kibana.rac.alert.owner": "apm", + "kibana.rac.alert.status": "closed", + "message": "hello world 1", + "rule.id": "apm.error_rate", + } + `); + }); + + test('throws when user is not authorized to update this type of alert', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + alertingAuthMock.ensureAuthorized.mockRejectedValue( + new Error(`Unauthorized to get a "apm.error_rate" alert for "apm"`) + ); + + await expect( + alertsClient.update({ + id: '1', + data: { status: 'closed' }, + assetName: 'observability-apm', + }) + ).rejects.toMatchInlineSnapshot( + `[Error: Unauthorized to get a "apm.error_rate" alert for "apm"]` + ); + + expect(alertingAuthMock.ensureAuthorized).toHaveBeenCalledWith({ + entity: 'alert', + consumer: 'apm', + operation: 'update', + ruleTypeId: 'apm.error_rate', + }); + }); + }); +}); diff --git a/x-pack/plugins/rule_registry/server/event_log/log/utils/mapping_from_field_map.ts b/x-pack/plugins/rule_registry/server/event_log/log/utils/mapping_from_field_map.ts index fd5dc3ae02288..9d9482ee69ba3 100644 --- a/x-pack/plugins/rule_registry/server/event_log/log/utils/mapping_from_field_map.ts +++ b/x-pack/plugins/rule_registry/server/event_log/log/utils/mapping_from_field_map.ts @@ -11,7 +11,7 @@ import { IndexMappings } from '../../elasticsearch'; export function mappingFromFieldMap(fieldMap: FieldMap): IndexMappings { const mappings = { - dynamic: 'strict' as const, + dynamic: true, properties: {}, }; diff --git a/x-pack/plugins/rule_registry/server/index.ts b/x-pack/plugins/rule_registry/server/index.ts index 9eefc19f34670..b6fd6b9a605c0 100644 --- a/x-pack/plugins/rule_registry/server/index.ts +++ b/x-pack/plugins/rule_registry/server/index.ts @@ -10,6 +10,7 @@ import { RuleRegistryPlugin } from './plugin'; export * from './config'; export type { RuleRegistryPluginSetupContract, RuleRegistryPluginStartContract } from './plugin'; +export type { RacRequestHandlerContext, RacApiRequestHandlerContext } from './types'; export { RuleDataClient } from './rule_data_client'; export { IRuleDataClient } from './rule_data_client/types'; export { getRuleExecutorData, RuleExecutorData } from './utils/get_rule_executor_data'; diff --git a/x-pack/plugins/rule_registry/server/plugin.ts b/x-pack/plugins/rule_registry/server/plugin.ts index 043b07f9d67c1..9e63b0a39230b 100644 --- a/x-pack/plugins/rule_registry/server/plugin.ts +++ b/x-pack/plugins/rule_registry/server/plugin.ts @@ -4,19 +4,42 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - -import { PluginInitializerContext, Plugin, CoreSetup, Logger } from 'src/core/server'; +import { + PluginInitializerContext, + Plugin, + CoreSetup, + Logger, + KibanaRequest, + CoreStart, + IContextProvider, +} from 'src/core/server'; +import { SecurityPluginSetup } from '../../security/server'; +import { AlertsClientFactory } from './alert_data_client/alerts_client_factory'; +import { PluginStartContract as AlertingStart } from '../../alerting/server'; +import { RacApiRequestHandlerContext, RacRequestHandlerContext } from './types'; +import { defineRoutes } from './routes'; import { SpacesPluginStart } from '../../spaces/server'; import { RuleRegistryPluginConfig } from './config'; import { RuleDataPluginService } from './rule_data_plugin_service'; import { EventLogService, IEventLogService } from './event_log'; -// eslint-disable-next-line @typescript-eslint/no-empty-interface -interface RuleRegistryPluginSetupDependencies {} +interface RuleRegistryPluginSetupDependencies { + security: SecurityPluginSetup; +} interface RuleRegistryPluginStartDependencies { spaces: SpacesPluginStart; + alerting: AlertingStart; +} + +export interface RuleRegistryPluginsStart { + alerting: AlertingStart; + spaces?: SpacesPluginStart; +} + +export interface RuleRegistryPluginsSetup { + security?: SecurityPluginSetup; } export interface RuleRegistryPluginSetupContract { @@ -37,17 +60,23 @@ export class RuleRegistryPlugin private readonly config: RuleRegistryPluginConfig; private readonly logger: Logger; private eventLogService: EventLogService | null; + private readonly alertsClientFactory: AlertsClientFactory; + private ruleDataService: RuleDataPluginService | null; + private security: SecurityPluginSetup | undefined; constructor(initContext: PluginInitializerContext) { this.config = initContext.config.get(); this.logger = initContext.logger.get(); this.eventLogService = null; + this.ruleDataService = null; + this.alertsClientFactory = new AlertsClientFactory(); } public setup( - core: CoreSetup + core: CoreSetup, + plugins: RuleRegistryPluginsSetup ): RuleRegistryPluginSetupContract { - const { config, logger } = this; + const { logger } = this; const startDependencies = core.getStartServices().then(([coreStart, pluginStart]) => { return { @@ -56,21 +85,40 @@ export class RuleRegistryPlugin }; }); - const ruleDataService = new RuleDataPluginService({ - logger, - isWriteEnabled: config.write.enabled, - index: config.index, + this.security = plugins.security; + + const service = new RuleDataPluginService({ + logger: this.logger, + isWriteEnabled: this.config.write.enabled, + index: this.config.index, getClusterClient: async () => { const deps = await startDependencies; return deps.core.elasticsearch.client.asInternalUser; }, }); - ruleDataService.init().catch((originalError) => { + service.init().catch((originalError) => { const error = new Error('Failed installing assets'); // @ts-ignore error.stack = originalError.stack; - logger.error(error); + this.logger.error(error); + }); + + this.ruleDataService = service; + + // ALERTS ROUTES + const router = core.http.createRouter(); + core.http.registerRouteHandlerContext( + 'rac', + this.createRouteHandlerContext() + ); + + defineRoutes(router); + // handler is called when '/path' resource is requested with `GET` method + router.get({ path: '/rac-myfakepath', validate: false }, async (context, req, res) => { + const racClient = await context.rac.getAlertsClient(); + racClient?.get({ id: 'hello world', assetName: 'observability-apm' }); + return res.ok(); }); const eventLogService = new EventLogService({ @@ -86,10 +134,48 @@ export class RuleRegistryPlugin }); this.eventLogService = eventLogService; - return { ruleDataService, eventLogService }; + + return { ruleDataService: this.ruleDataService, eventLogService }; } - public start(): RuleRegistryPluginStartContract {} + public start(core: CoreStart, plugins: RuleRegistryPluginsStart) { + const { logger, alertsClientFactory, security } = this; + + alertsClientFactory.initialize({ + logger, + esClient: core.elasticsearch.client.asInternalUser, + // NOTE: Alerts share the authorization client with the alerting plugin + getAlertingAuthorization(request: KibanaRequest) { + return plugins.alerting.getAlertingAuthorizationWithRequest(request); + }, + securityPluginSetup: security, + ruleDataService: this.ruleDataService!, + }); + + const getRacClientWithRequest = (request: KibanaRequest) => { + return alertsClientFactory.create(request); + }; + + return { + getRacClientWithRequest, + alerting: plugins.alerting, + }; + } + + private createRouteHandlerContext = (): IContextProvider => { + const { alertsClientFactory } = this; + return async function alertsRouteHandlerContext( + context, + request + ): Promise { + return { + getAlertsClient: async () => { + const createdClient = alertsClientFactory.create(request); + return createdClient; + }, + }; + }; + }; public stop() { const { eventLogService, logger } = this; diff --git a/x-pack/plugins/rule_registry/server/routes/get_alert_by_id.ts b/x-pack/plugins/rule_registry/server/routes/get_alert_by_id.ts new file mode 100644 index 0000000000000..039c10d4c37a1 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/routes/get_alert_by_id.ts @@ -0,0 +1,66 @@ +/* + * 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 { IRouter } from 'kibana/server'; +import * as t from 'io-ts'; +import { id as _id } from '@kbn/securitysolution-io-ts-list-types'; +import { transformError } from '@kbn/securitysolution-es-utils'; + +import { RacRequestHandlerContext } from '../types'; +import { BASE_RAC_ALERTS_API_PATH } from '../../common/constants'; +import { buildRouteValidation } from './utils/route_validation'; + +export const getAlertByIdRoute = (router: IRouter) => { + router.get( + { + path: BASE_RAC_ALERTS_API_PATH, + validate: { + query: buildRouteValidation( + t.exact( + t.type({ + id: _id, + assetName: t.string, + }) + ) + ), + }, + options: { + tags: ['access:rac'], + }, + }, + async (context, request, response) => { + try { + const alertsClient = await context.rac.getAlertsClient(); + const { id, assetName } = request.query; + const alert = await alertsClient.get({ id, assetName }); + return response.ok({ + body: alert, + }); + } catch (exc) { + const err = transformError(exc); + const contentType = { + 'content-type': 'application/json', + }; + const defaultedHeaders = { + ...contentType, + }; + + return response.custom({ + headers: defaultedHeaders, + statusCode: err.statusCode, + body: Buffer.from( + JSON.stringify({ + message: err.message, + status_code: err.statusCode, + }) + ), + }); + // return response.custom; + } + } + ); +}; diff --git a/x-pack/plugins/rule_registry/server/routes/index.ts b/x-pack/plugins/rule_registry/server/routes/index.ts new file mode 100644 index 0000000000000..4cc7881bf94e0 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/routes/index.ts @@ -0,0 +1,16 @@ +/* + * 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 { IRouter } from 'kibana/server'; +import { RacRequestHandlerContext } from '../types'; +import { getAlertByIdRoute } from './get_alert_by_id'; +import { updateAlertByIdRoute } from './update_alert_by_id'; + +export function defineRoutes(router: IRouter) { + getAlertByIdRoute(router); + updateAlertByIdRoute(router); +} diff --git a/x-pack/plugins/rule_registry/server/routes/update_alert_by_id.ts b/x-pack/plugins/rule_registry/server/routes/update_alert_by_id.ts new file mode 100644 index 0000000000000..66f89d02d5a2e --- /dev/null +++ b/x-pack/plugins/rule_registry/server/routes/update_alert_by_id.ts @@ -0,0 +1,65 @@ +/* + * 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 { IRouter } from 'kibana/server'; +import { id as _id } from '@kbn/securitysolution-io-ts-list-types'; +import { transformError } from '@kbn/securitysolution-es-utils'; +import { schema } from '@kbn/config-schema'; + +import { RacRequestHandlerContext } from '../types'; +import { BASE_RAC_ALERTS_API_PATH } from '../../common/constants'; + +export const updateAlertByIdRoute = (router: IRouter) => { + router.post( + { + path: BASE_RAC_ALERTS_API_PATH, + validate: { + body: schema.object({ + status: schema.string(), + ids: schema.arrayOf(schema.string()), + assetName: schema.string(), + }), + }, + options: { + tags: ['access:rac'], + }, + }, + async (context, req, response) => { + try { + const racClient = await context.rac.getAlertsClient(); + const { status, ids, assetName } = req.body; + + const thing = await racClient?.update({ + id: ids[0], + data: { status }, + assetName, + }); + return response.ok({ body: { success: true, alerts: thing } }); + } catch (exc) { + const err = transformError(exc); + + const contentType = { + 'content-type': 'application/json', + }; + const defaultedHeaders = { + ...contentType, + }; + + return response.custom({ + headers: defaultedHeaders, + statusCode: err.statusCode, + body: Buffer.from( + JSON.stringify({ + message: err.message, + status_code: err.statusCode, + }) + ), + }); + } + } + ); +}; diff --git a/x-pack/plugins/rule_registry/server/routes/utils/route_validation.ts b/x-pack/plugins/rule_registry/server/routes/utils/route_validation.ts new file mode 100644 index 0000000000000..8e74760d6d15f --- /dev/null +++ b/x-pack/plugins/rule_registry/server/routes/utils/route_validation.ts @@ -0,0 +1,56 @@ +/* + * 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. + */ +/* + * 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 { fold } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; +import * as rt from 'io-ts'; +import { exactCheck, formatErrors } from '@kbn/securitysolution-io-ts-utils'; + +import { + RouteValidationError, + RouteValidationFunction, + RouteValidationResultFactory, +} from '../../../../../../src/core/server'; + +type RequestValidationResult = + | { + value: T; + error?: undefined; + } + | { + value?: undefined; + error: RouteValidationError; + }; + +/** + * Copied from x-pack/plugins/security_solution/server/utils/build_validation/route_validation.ts + * This really should be in @kbn/securitysolution-io-ts-utils rather than copied yet again, however, this has types + * from a lot of places such as RouteValidationResultFactory from core/server which in turn can pull in @kbn/schema + * which cannot work on the front end and @kbn/securitysolution-io-ts-utils works on both front and backend. + * + * TODO: Figure out a way to move this function into a package rather than copying it/forking it within plugins + */ +export const buildRouteValidation = >( + schema: T +): RouteValidationFunction => ( + inputValue: unknown, + validationResult: RouteValidationResultFactory +): RequestValidationResult => + pipe( + schema.decode(inputValue), + (decoded) => exactCheck(inputValue, decoded), + fold>( + (errors: rt.Errors) => validationResult.badRequest(formatErrors(errors).join()), + (validatedInput: A) => validationResult.ok(validatedInput) + ) + ); diff --git a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/index.ts b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/index.ts index 159e9b8152597..91b4294861982 100644 --- a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/index.ts +++ b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/index.ts @@ -19,7 +19,7 @@ import { ClusterPutComponentTemplateBody, PutIndexTemplateRequest } from '../../ const BOOTSTRAP_TIMEOUT = 60000; -interface RuleDataPluginServiceConstructorOptions { +export interface RuleDataPluginServiceConstructorOptions { getClusterClient: () => Promise; logger: Logger; isWriteEnabled: boolean; diff --git a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/rule_data_plugin_service.mock.ts b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/rule_data_plugin_service.mock.ts new file mode 100644 index 0000000000000..d5f89ad8b7889 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/rule_data_plugin_service.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 type { PublicMethodsOf } from '@kbn/utility-types'; +import { RuleDataPluginService, RuleDataPluginServiceConstructorOptions } from './'; + +type Schema = PublicMethodsOf; + +const createRuleDataPluginServiceMock = (_: RuleDataPluginServiceConstructorOptions) => { + const mocked: jest.Mocked = { + init: jest.fn(), + isReady: jest.fn(), + wait: jest.fn(), + isWriteEnabled: jest.fn(), + getFullAssetName: jest.fn(), + createOrUpdateComponentTemplate: jest.fn(), + createOrUpdateIndexTemplate: jest.fn(), + createOrUpdateLifecyclePolicy: jest.fn(), + }; + return mocked; +}; + +export const ruleDataPluginServiceMock: { + create: ( + _: RuleDataPluginServiceConstructorOptions + ) => jest.Mocked>; +} = { + create: createRuleDataPluginServiceMock, +}; diff --git a/x-pack/plugins/rule_registry/server/scripts/README.md b/x-pack/plugins/rule_registry/server/scripts/README.md new file mode 100644 index 0000000000000..2b3f01f3c4d6b --- /dev/null +++ b/x-pack/plugins/rule_registry/server/scripts/README.md @@ -0,0 +1,24 @@ +Users with roles granting them access to monitoring (observability) and siem (security solution) should only be able to access alerts with those roles + +```bash +myterminal~$ ./get_security_solution_alert.sh observer +{ + "statusCode": 404, + "error": "Not Found", + "message": "Unauthorized to get \"rac:8.0.0:securitySolution/get\" alert\"" +} +myterminal~$ ./get_security_solution_alert.sh +{ + "success": true +} +myterminal~$ ./get_observability_alert.sh +{ + "success": true +} +myterminal~$ ./get_observability_alert.sh hunter +{ + "statusCode": 404, + "error": "Not Found", + "message": "Unauthorized to get \"rac:8.0.0:observability/get\" alert\"" +} +``` diff --git a/x-pack/plugins/rule_registry/server/scripts/get_alert_by_id.sh b/x-pack/plugins/rule_registry/server/scripts/get_alert_by_id.sh new file mode 100755 index 0000000000000..fc4bb6e2a0c42 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/scripts/get_alert_by_id.sh @@ -0,0 +1,15 @@ +#!/bin/sh + +# +# 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. +# + +set -e + +# Example: ./get_alert_by_id.sh {id} +curl -s -k \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X GET ${KIBANA_URL}${SPACE_URL}/api/rac/alerts?id="$1" | jq . diff --git a/x-pack/plugins/rule_registry/server/scripts/get_observability_alert.sh b/x-pack/plugins/rule_registry/server/scripts/get_observability_alert.sh new file mode 100755 index 0000000000000..45c38bc1ea574 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/scripts/get_observability_alert.sh @@ -0,0 +1,21 @@ +#!/bin/sh + +# +# 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. +# + +set -e + +USER=${1:-'observer'} + +cd ./hunter && sh ./post_detections_role.sh && sh ./post_detections_user.sh +cd ../observer && sh ./post_detections_role.sh && sh ./post_detections_user.sh +cd .. + +# Example: ./find_rules.sh +curl -v -k \ + -u $USER:changeme \ + -X GET "${KIBANA_URL}${SPACE_URL}/api/rac/alerts?id=NoxgpHkBqbdrfX07MqXV&assetName=observability-apm" | jq . diff --git a/x-pack/plugins/rule_registry/server/scripts/get_security_solution_alert.sh b/x-pack/plugins/rule_registry/server/scripts/get_security_solution_alert.sh new file mode 100755 index 0000000000000..b4348266c9634 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/scripts/get_security_solution_alert.sh @@ -0,0 +1,22 @@ +#!/bin/sh + +# +# 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. +# + +set -e + +cd ./hunter && sh ./post_detections_role.sh && sh ./post_detections_user.sh +cd ../observer && sh ./post_detections_role.sh && sh ./post_detections_user.sh +cd .. + + +USER=${1:-'hunter'} + +# Example: ./find_rules.sh +curl -s -k \ + -u $USER:changeme \ + -X GET ${KIBANA_URL}${SPACE_URL}/security-myfakepath | jq . diff --git a/x-pack/plugins/rule_registry/server/scripts/hunter/README.md b/x-pack/plugins/rule_registry/server/scripts/hunter/README.md new file mode 100644 index 0000000000000..a0269d5b060a3 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/scripts/hunter/README.md @@ -0,0 +1,5 @@ +This user can access the monitoring route at http://localhost:5601/security-myfakepath + +| Role | Data Sources | Security Solution ML Jobs/Results | Lists | Rules/Exceptions | Action Connectors | Signals/Alerts | +| :-----------------: | :----------: | :-------------------------------: | :---: | :--------------: | :---------------: | :------------: | +| Hunter / T3 Analyst | read, write | read | read | read, write | read | read, write | diff --git a/x-pack/plugins/rule_registry/server/scripts/hunter/delete_detections_user.sh b/x-pack/plugins/rule_registry/server/scripts/hunter/delete_detections_user.sh new file mode 100755 index 0000000000000..595f0a49282d8 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/scripts/hunter/delete_detections_user.sh @@ -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 diff --git a/x-pack/plugins/rule_registry/server/scripts/hunter/detections_role.json b/x-pack/plugins/rule_registry/server/scripts/hunter/detections_role.json new file mode 100644 index 0000000000000..119fe5421c86c --- /dev/null +++ b/x-pack/plugins/rule_registry/server/scripts/hunter/detections_role.json @@ -0,0 +1,38 @@ +{ + "elasticsearch": { + "cluster": [], + "indices": [ + { + "names": [ + "apm-*-transaction*", + "auditbeat-*", + "endgame-*", + "filebeat-*", + "logs-*", + "packetbeat-*", + "winlogbeat-*" + ], + "privileges": ["read", "write"] + }, + { + "names": [".siem-signals-*"], + "privileges": ["read", "write"] + }, + { + "names": [".lists*", ".items*"], + "privileges": ["read", "write"] + } + ] + }, + "kibana": [ + { + "feature": { + "ml": ["read"], + "siem": ["all"], + "actions": ["read"], + "builtInAlerts": ["all"] + }, + "spaces": ["*"] + } + ] +} diff --git a/x-pack/plugins/rule_registry/server/scripts/hunter/detections_user.json b/x-pack/plugins/rule_registry/server/scripts/hunter/detections_user.json new file mode 100644 index 0000000000000..f9454cc0ad2fe --- /dev/null +++ b/x-pack/plugins/rule_registry/server/scripts/hunter/detections_user.json @@ -0,0 +1,6 @@ +{ + "password": "changeme", + "roles": ["hunter"], + "full_name": "Hunter", + "email": "detections-reader@example.com" +} diff --git a/x-pack/plugins/rule_registry/server/scripts/hunter/get_detections_role.sh b/x-pack/plugins/rule_registry/server/scripts/hunter/get_detections_role.sh new file mode 100755 index 0000000000000..7ec850ce220bb --- /dev/null +++ b/x-pack/plugins/rule_registry/server/scripts/hunter/get_detections_role.sh @@ -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 | jq -S . diff --git a/x-pack/plugins/rule_registry/server/scripts/hunter/index.ts b/x-pack/plugins/rule_registry/server/scripts/hunter/index.ts new file mode 100644 index 0000000000000..3411589de7721 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/scripts/hunter/index.ts @@ -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 hunterUser from './detections_user.json'; +import * as hunterRole from './detections_role.json'; +export { hunterUser, hunterRole }; diff --git a/x-pack/plugins/rule_registry/server/scripts/hunter/post_detections_role.sh b/x-pack/plugins/rule_registry/server/scripts/hunter/post_detections_role.sh new file mode 100755 index 0000000000000..debffe0fcac4c --- /dev/null +++ b/x-pack/plugins/rule_registry/server/scripts/hunter/post_detections_role.sh @@ -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 \ +-d @${ROLE} diff --git a/x-pack/plugins/rule_registry/server/scripts/hunter/post_detections_user.sh b/x-pack/plugins/rule_registry/server/scripts/hunter/post_detections_user.sh new file mode 100755 index 0000000000000..ab2a053081394 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/scripts/hunter/post_detections_user.sh @@ -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 \ +-d @${USER} diff --git a/x-pack/plugins/rule_registry/server/scripts/observer/README.md b/x-pack/plugins/rule_registry/server/scripts/observer/README.md new file mode 100644 index 0000000000000..dc7e989ba4635 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/scripts/observer/README.md @@ -0,0 +1,5 @@ +This user can access the monitoring route at http://localhost:5601/monitoring-myfakepath + +| Role | Data Sources | Security Solution ML Jobs/Results | Lists | Rules/Exceptions | Action Connectors | Signals/Alerts | +| :------: | :----------: | :-------------------------------: | :---: | :--------------: | :---------------: | :------------: | +| observer | read, write | read | read | read, write | read | read, write | \ No newline at end of file diff --git a/x-pack/plugins/rule_registry/server/scripts/observer/delete_detections_user.sh b/x-pack/plugins/rule_registry/server/scripts/observer/delete_detections_user.sh new file mode 100755 index 0000000000000..017d8904a51e1 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/scripts/observer/delete_detections_user.sh @@ -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/observer diff --git a/x-pack/plugins/rule_registry/server/scripts/observer/detections_role.json b/x-pack/plugins/rule_registry/server/scripts/observer/detections_role.json new file mode 100644 index 0000000000000..aebb5ecc6df6c --- /dev/null +++ b/x-pack/plugins/rule_registry/server/scripts/observer/detections_role.json @@ -0,0 +1,41 @@ +{ + "elasticsearch": { + "cluster": [], + "indices": [ + { + "names": [ + "apm-*-transaction*", + "auditbeat-*", + "endgame-*", + "filebeat-*", + "logs-*", + "packetbeat-*", + "winlogbeat-*" + ], + "privileges": ["read", "write"] + }, + { + "names": [".siem-signals-*"], + "privileges": ["read", "write"] + }, + { + "names": [".lists*", ".items*"], + "privileges": ["read", "write"] + } + ] + }, + "kibana": [ + { + "feature": { + "ml": ["read"], + "monitoring": ["all"], + "apm": ["all"], + "ruleRegistry": ["all"], + "actions": ["read"], + "builtInAlerts": ["all"], + "alerting": ["all"] + }, + "spaces": ["*"] + } + ] +} diff --git a/x-pack/plugins/rule_registry/server/scripts/observer/detections_user.json b/x-pack/plugins/rule_registry/server/scripts/observer/detections_user.json new file mode 100644 index 0000000000000..9f06e7dcc29f1 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/scripts/observer/detections_user.json @@ -0,0 +1,6 @@ +{ + "password": "changeme", + "roles": ["observer"], + "full_name": "Observer", + "email": "monitoring-observer@example.com" +} \ No newline at end of file diff --git a/x-pack/plugins/rule_registry/server/scripts/observer/get_detections_role.sh b/x-pack/plugins/rule_registry/server/scripts/observer/get_detections_role.sh new file mode 100755 index 0000000000000..7ec850ce220bb --- /dev/null +++ b/x-pack/plugins/rule_registry/server/scripts/observer/get_detections_role.sh @@ -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 | jq -S . diff --git a/x-pack/plugins/rule_registry/server/scripts/observer/get_observability_alert.sh b/x-pack/plugins/rule_registry/server/scripts/observer/get_observability_alert.sh new file mode 100755 index 0000000000000..dd71e9dc6af43 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/scripts/observer/get_observability_alert.sh @@ -0,0 +1,21 @@ +#!/bin/sh + +# +# 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. +# + +set -e + +USER=${1:-'observer'} + +cd ./hunter && sh ./post_detections_role.sh && sh ./post_detections_user.sh +cd ../observer && sh ./post_detections_role.sh && sh ./post_detections_user.sh +cd .. + +# Example: ./find_rules.sh +curl -s -k \ + -u $USER:changeme \ + -X GET ${KIBANA_URL}${SPACE_URL}/monitoring-myfakepath | jq . diff --git a/x-pack/plugins/rule_registry/server/scripts/observer/get_security_solution_alert.sh b/x-pack/plugins/rule_registry/server/scripts/observer/get_security_solution_alert.sh new file mode 100755 index 0000000000000..b4348266c9634 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/scripts/observer/get_security_solution_alert.sh @@ -0,0 +1,22 @@ +#!/bin/sh + +# +# 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. +# + +set -e + +cd ./hunter && sh ./post_detections_role.sh && sh ./post_detections_user.sh +cd ../observer && sh ./post_detections_role.sh && sh ./post_detections_user.sh +cd .. + + +USER=${1:-'hunter'} + +# Example: ./find_rules.sh +curl -s -k \ + -u $USER:changeme \ + -X GET ${KIBANA_URL}${SPACE_URL}/security-myfakepath | jq . diff --git a/x-pack/plugins/rule_registry/server/scripts/observer/index.ts b/x-pack/plugins/rule_registry/server/scripts/observer/index.ts new file mode 100644 index 0000000000000..5feebc1caeed1 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/scripts/observer/index.ts @@ -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 observerUser from './detections_user.json'; +import * as observerRole from './detections_role.json'; +export { observerUser, observerRole }; diff --git a/x-pack/plugins/rule_registry/server/scripts/observer/post_detections_role.sh b/x-pack/plugins/rule_registry/server/scripts/observer/post_detections_role.sh new file mode 100755 index 0000000000000..4dddb64befc6b --- /dev/null +++ b/x-pack/plugins/rule_registry/server/scripts/observer/post_detections_role.sh @@ -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/observer \ +-d @${ROLE} \ No newline at end of file diff --git a/x-pack/plugins/rule_registry/server/scripts/observer/post_detections_user.sh b/x-pack/plugins/rule_registry/server/scripts/observer/post_detections_user.sh new file mode 100755 index 0000000000000..8a897c0d28142 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/scripts/observer/post_detections_user.sh @@ -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/observer \ +-d @${USER} \ No newline at end of file diff --git a/x-pack/plugins/rule_registry/server/scripts/update_observability_alert.sh b/x-pack/plugins/rule_registry/server/scripts/update_observability_alert.sh new file mode 100755 index 0000000000000..49fca89017484 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/scripts/update_observability_alert.sh @@ -0,0 +1,19 @@ +set -e + +IDS=${1} +STATUS=${2} + +echo $IDS +echo "'"$STATUS"'" + +cd ./hunter && sh ./post_detections_role.sh && sh ./post_detections_user.sh +cd ../observer && sh ./post_detections_role.sh && sh ./post_detections_user.sh +cd .. + +# Example: ./update_observability_alert.sh [\"my-alert-id\",\"another-alert-id\"] +curl -s -k \ + -H 'Content-Type: application/json' \ + -H 'kbn-xsrf: 123' \ + -u observer:changeme \ + -X POST ${KIBANA_URL}${SPACE_URL}/api/rac/alerts \ + -d "{\"ids\": $IDS, \"status\":\"$STATUS\", \"assetName\":\"observability-apm\"}" | jq . diff --git a/x-pack/plugins/rule_registry/server/types.ts b/x-pack/plugins/rule_registry/server/types.ts index 959c05fd1334e..f8bd1940b10a8 100644 --- a/x-pack/plugins/rule_registry/server/types.ts +++ b/x-pack/plugins/rule_registry/server/types.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { RequestHandlerContext } from 'kibana/server'; import { AlertInstanceContext, AlertInstanceState, @@ -12,6 +13,7 @@ import { AlertTypeState, } from '../../alerting/common'; import { AlertType } from '../../alerting/server'; +import { AlertsClient } from './alert_data_client/alerts_client'; type SimpleAlertType< TParams extends AlertTypeParams = {}, @@ -38,3 +40,17 @@ export type AlertTypeWithExecutor< > & { executor: AlertTypeExecutor; }; + +/** + * @public + */ +export interface RacApiRequestHandlerContext { + getAlertsClient: () => Promise; +} + +/** + * @internal + */ +export interface RacRequestHandlerContext extends RequestHandlerContext { + rac: RacApiRequestHandlerContext; +} diff --git a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type_factory.ts b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type_factory.ts index b523dd6770b9f..71afe684c61fb 100644 --- a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type_factory.ts +++ b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type_factory.ts @@ -25,6 +25,7 @@ import { ALERT_UUID, EVENT_ACTION, EVENT_KIND, + OWNER, RULE_UUID, TIMESTAMP, } from '../../common/technical_rule_data_field_names'; @@ -69,10 +70,13 @@ export const createLifecycleRuleTypeFactory: CreateLifecycleRuleTypeFactory = ({ const { services: { alertInstanceFactory }, state: previousState, + rule, } = options; const ruleExecutorData = getRuleExecutorData(type, options); + logger.debug(`LOGGER RULE REGISTRY CONSUMER ${rule.consumer}`); + const decodedState = wrappedStateRt.decode(previousState); const state = isLeft(decodedState) @@ -180,6 +184,7 @@ export const createLifecycleRuleTypeFactory: CreateLifecycleRuleTypeFactory = ({ ...ruleExecutorData, [TIMESTAMP]: timestamp, [EVENT_KIND]: 'state', + [OWNER]: rule.consumer, [ALERT_ID]: alertId, }; @@ -220,7 +225,11 @@ export const createLifecycleRuleTypeFactory: CreateLifecycleRuleTypeFactory = ({ return event; }); + logger.debug(`LOGGER EVENTSTOINDEX: ${JSON.stringify(eventsToIndex, null, 2)}`); + if (eventsToIndex.length) { + logger.debug('LOGGER ABOUT TO INDEX ALERTS'); + await ruleDataClient.getWriter().bulk({ body: eventsToIndex.flatMap((event) => [{ index: {} }, event]), }); diff --git a/x-pack/plugins/rule_registry/server/utils/create_persistence_rule_type_factory.ts b/x-pack/plugins/rule_registry/server/utils/create_persistence_rule_type_factory.ts index 0e244fbaa2ee3..e25a9a088d253 100644 --- a/x-pack/plugins/rule_registry/server/utils/create_persistence_rule_type_factory.ts +++ b/x-pack/plugins/rule_registry/server/utils/create_persistence_rule_type_factory.ts @@ -77,19 +77,21 @@ export const createPersistenceRuleTypeFactory: CreatePersistenceRuleTypeFactory const alertUuid = event['kibana.rac.alert.uuid']; const isAlert = alertUuid != null; return { - ...event, + // ...event, 'event.kind': 'signal', - 'kibana.rac.alert.id': '???', + 'rule.id': 'apm.error_rate', + // 'kibana.rac.alert.id': '???', 'kibana.rac.alert.status': 'open', - 'kibana.rac.alert.uuid': v4(), - 'kibana.rac.alert.ancestors': isAlert - ? ((event['kibana.rac.alert.ancestors'] as string[]) ?? []).concat([ - alertUuid!, - ] as string[]) - : [], - 'kibana.rac.alert.depth': isAlert - ? ((event['kibana.rac.alert.depth'] as number) ?? 0) + 1 - : 0, + 'kibana.rac.alert.owner': 'apm', + // 'kibana.rac.alert.uuid': v4(), + // 'kibana.rac.alert.ancestors': isAlert + // ? ((event['kibana.rac.alert.ancestors'] as string[]) ?? []).concat([ + // alertUuid!, + // ] as string[]) + // : [], + // 'kibana.rac.alert.depth': isAlert + // ? ((event['kibana.rac.alert.depth'] as number) ?? 0) + 1 + // : 0, '@timestamp': timestamp, }; }); diff --git a/x-pack/plugins/rule_registry/tsconfig.json b/x-pack/plugins/rule_registry/tsconfig.json index 5aefe9769da22..5174a4168ca7f 100644 --- a/x-pack/plugins/rule_registry/tsconfig.json +++ b/x-pack/plugins/rule_registry/tsconfig.json @@ -7,7 +7,14 @@ "declaration": true, "declarationMap": true }, - "include": ["common/**/*", "server/**/*", "public/**/*", "../../../typings/**/*"], + "include": [ + "common/**/*", + "server/**/*", + // have to declare *.json explicitly due to https://github.com/microsoft/TypeScript/issues/25636 + "server/**/*.json", + "public/**/*", + "../../../typings/**/*" + ], "references": [ { "path": "../../../src/core/tsconfig.json" }, { "path": "../../../src/plugins/data/tsconfig.json" }, diff --git a/x-pack/plugins/security_solution/common/search_strategy/timeline/events/all/index.ts b/x-pack/plugins/security_solution/common/search_strategy/timeline/events/all/index.ts index d747758640fab..044ac04274e26 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/timeline/events/all/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/timeline/events/all/index.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { JsonObject } from 'src/plugins/kibana_utils/common'; import { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common'; import { Ecs } from '../../../../ecs'; import { CursorType, Inspect, Maybe, PaginationInputPaginated } from '../../../common'; @@ -38,4 +39,5 @@ export interface TimelineEventsAllRequestOptions extends TimelineRequestOptionsP fields: string[] | Array<{ field: string; include_unmapped: boolean }>; fieldRequested: string[]; language: 'eql' | 'kuery' | 'lucene'; + authFilter?: JsonObject; } diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts index aeee7077ec9c0..d5d8cb7d9af20 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts @@ -51,7 +51,7 @@ describe('Alert details with unmapped fields', () => { it('Displays the unmapped field on the table', () => { const expectedUnmmappedField = { - row: 55, + row: 56, field: 'unmapped', text: 'This is the unmapped field', }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/scripts/create_reference_rule_query.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/scripts/create_reference_rule_query.sh index c34af7dee4044..456cb0005e0e8 100755 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/scripts/create_reference_rule_query.sh +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/scripts/create_reference_rule_query.sh @@ -5,8 +5,9 @@ # 2.0; you may not use this file except in compliance with the Elastic License # 2.0. # +set -e -curl -X POST http://localhost:5601/${BASE_PATH}/api/alerts/alert \ +curl -X POST ${KIBANA_URL}${SPACE_URL}/api/alerts/alert \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ -H 'kbn-xsrf: true' \ -H 'Content-Type: application/json' \ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/__snapshots__/get_signals_template.test.ts.snap b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/__snapshots__/get_signals_template.test.ts.snap index 1abe55b782c32..30a417b1f0c9f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/__snapshots__/get_signals_template.test.ts.snap +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/__snapshots__/get_signals_template.test.ts.snap @@ -2832,6 +2832,9 @@ Object { "original_time": Object { "type": "date", }, + "owner": Object { + "type": "keyword", + }, "parent": Object { "properties": Object { "depth": Object { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/signals_mapping.json b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/signals_mapping.json index d6a06848592cc..81c5c3b296ba8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/signals_mapping.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/signals_mapping.json @@ -77,6 +77,9 @@ } } }, + "owner": { + "type": "keyword" + }, "rule": { "properties": { "id": { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts index 0fed141ca4dbc..a949934039fb7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts @@ -34,6 +34,7 @@ export const sampleRuleSO = (params: T): SavedObject { _meta: { version: SIGNALS_TEMPLATE_VERSION, }, + owner: 'siem', parent: { id: sampleIdGuid, type: 'event', @@ -111,6 +112,7 @@ describe('buildBulkBody', () => { _meta: { version: SIGNALS_TEMPLATE_VERSION, }, + owner: 'siem', parent: { id: sampleIdGuid, type: 'event', @@ -195,6 +197,7 @@ describe('buildBulkBody', () => { kind: 'event', module: 'system', }, + owner: 'siem', parent: { id: sampleIdGuid, type: 'event', @@ -255,6 +258,7 @@ describe('buildBulkBody', () => { dataset: 'socket', module: 'system', }, + owner: 'siem', parent: { id: sampleIdGuid, type: 'event', @@ -308,6 +312,7 @@ describe('buildBulkBody', () => { original_event: { kind: 'event', }, + owner: 'siem', parent: { id: sampleIdGuid, type: 'event', @@ -361,6 +366,7 @@ describe('buildBulkBody', () => { version: SIGNALS_TEMPLATE_VERSION, }, original_signal: 123, + owner: 'siem', parent: { id: sampleIdGuid, type: 'event', @@ -414,6 +420,7 @@ describe('buildBulkBody', () => { version: SIGNALS_TEMPLATE_VERSION, }, original_signal: { child_1: { child_2: 'nested data' } }, + owner: 'siem', parent: { id: sampleIdGuid, type: 'event', @@ -467,6 +474,7 @@ describe('buildSignalFromSequence', () => { _meta: { version: SIGNALS_TEMPLATE_VERSION, }, + owner: 'siem', parents: [ { id: sampleIdGuid, @@ -551,6 +559,7 @@ describe('buildSignalFromSequence', () => { _meta: { version: SIGNALS_TEMPLATE_VERSION, }, + owner: 'siem', parents: [ { id: sampleIdGuid, @@ -638,6 +647,7 @@ describe('buildSignalFromEvent', () => { version: SIGNALS_TEMPLATE_VERSION, }, original_time: '2020-04-20T21:27:45.000Z', + owner: 'siem', parent: { id: sampleIdGuid, rule: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.ts index 10cc168700447..7db6fe298eda1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.ts @@ -28,7 +28,7 @@ export const buildBulkBody = ( ): SignalHit => { const rule = buildRuleWithOverrides(ruleSO, doc._source!); const signal: Signal = { - ...buildSignal([doc], rule), + ...buildSignal([doc], rule, ruleSO.attributes.consumer), ...additionalSignalFields(doc), }; const event = buildEventTypeSignal(doc); @@ -98,7 +98,7 @@ export const buildSignalFromSequence = ( ruleSO: SavedObject ): SignalHit => { const rule = buildRuleWithoutOverrides(ruleSO); - const signal: Signal = buildSignal(events, rule); + const signal: Signal = buildSignal(events, rule, ruleSO.attributes.consumer); const mergedEvents = objectArrayIntersection(events.map((event) => event._source)); return { ...mergedEvents, @@ -127,7 +127,7 @@ export const buildSignalFromEvent = ( buildRuleWithOverrides(ruleSO, event._source) : buildRuleWithoutOverrides(ruleSO); const signal: Signal = { - ...buildSignal([event], rule), + ...buildSignal([event], rule, ruleSO.attributes.consumer), ...additionalSignalFields(event), }; const eventFields = buildEventTypeSignal(event); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.test.ts index 3a30da170d3f2..5530277d7ad77 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.test.ts @@ -31,7 +31,7 @@ describe('buildSignal', () => { delete doc._source.event; const rule = getRulesSchemaMock(); const signal = { - ...buildSignal([doc], rule), + ...buildSignal([doc], rule, 'siem'), ...additionalSignalFields(doc), }; const expected: Signal = { @@ -61,6 +61,7 @@ describe('buildSignal', () => { }, ], original_time: '2020-04-20T21:27:45.000Z', + owner: 'siem', status: 'open', rule: { author: [], @@ -112,7 +113,7 @@ describe('buildSignal', () => { }; const rule = getRulesSchemaMock(); const signal = { - ...buildSignal([doc], rule), + ...buildSignal([doc], rule, 'siem'), ...additionalSignalFields(doc), }; const expected: Signal = { @@ -142,6 +143,7 @@ describe('buildSignal', () => { }, ], original_time: '2020-04-20T21:27:45.000Z', + owner: 'siem', original_event: { action: 'socket_opened', dataset: 'socket', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.ts index a415c83e857c2..fd87bc7256d94 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.ts @@ -77,7 +77,7 @@ export const removeClashes = (doc: BaseSignalHit): BaseSignalHit => { * @param docs The parent signals/events of the new signal to be built. * @param rule The rule that is generating the new signal. */ -export const buildSignal = (docs: BaseSignalHit[], rule: RulesSchema): Signal => { +export const buildSignal = (docs: BaseSignalHit[], rule: RulesSchema, owner: string): Signal => { const _meta = { version: SIGNALS_TEMPLATE_VERSION, }; @@ -93,6 +93,7 @@ export const buildSignal = (docs: BaseSignalHit[], rule: RulesSchema): Signal => parents, ancestors, status: 'open', + owner, rule, depth, }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/eql.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/eql.test.ts index f8f77bd2bf6e6..56a131c5ac6c8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/eql.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/eql.test.ts @@ -34,6 +34,7 @@ describe('eql_executor', () => { actions: [], enabled: true, name: 'rule-name', + consumer: 'siem', tags: ['some fake tag 1', 'some fake tag 2'], createdBy: 'sample user', createdAt: '2020-03-27T22:55:59.577Z', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threshold.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threshold.test.ts index 5d62b28b73ae8..9e937587b69b2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threshold.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threshold.test.ts @@ -28,6 +28,7 @@ describe('threshold_executor', () => { actions: [], enabled: true, name: 'rule-name', + consumer: 'siem', tags: ['some fake tag 1', 'some fake tag 2'], createdBy: 'sample user', createdAt: '2020-03-27T22:55:59.577Z', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts index 8f34e58ebc85b..ef92ffbb77bfe 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts @@ -211,6 +211,7 @@ export interface Signal { }; original_time?: string; original_event?: SearchTypes; + owner?: string; status: Status; threshold_result?: ThresholdResult; original_signal?: SearchTypes; @@ -235,6 +236,7 @@ export interface AlertAttributes { schedule: { interval: string; }; + consumer: string; throttle: string; params: T; } diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 5aa298d6789be..d1787bbca8809 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -9,7 +9,7 @@ import { once } from 'lodash'; import { Observable } from 'rxjs'; import { i18n } from '@kbn/i18n'; import LRU from 'lru-cache'; - +import { mappingFromFieldMap } from '../../rule_registry/common/mapping_from_field_map'; import { CoreSetup, CoreStart, @@ -237,7 +237,11 @@ export class Plugin implements IPlugin = { - buildDsl: (options: TimelineEventsAllRequestOptions) => { + buildDsl: ({ authFilter, ...options }: TimelineEventsAllRequestOptions) => { if (options.pagination && options.pagination.querySize >= DEFAULT_MAX_TABLE_QUERY_SIZE) { throw new Error(`No query size above ${DEFAULT_MAX_TABLE_QUERY_SIZE}`); } const { fieldRequested, ...queryOptions } = cloneDeep(options); queryOptions.fields = buildFieldsRequest(fieldRequested); - return buildTimelineEventsAllQuery(queryOptions); + return buildTimelineEventsAllQuery({ ...queryOptions, authFilter }); }, parse: async ( options: TimelineEventsAllRequestOptions, diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/query.events_all.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/query.events_all.dsl.ts index 8aa69b2d87dc9..8168d6327c705 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/query.events_all.dsl.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/query.events_all.dsl.ts @@ -23,6 +23,7 @@ export const buildTimelineEventsAllQuery = ({ pagination: { activePage, querySize }, sort, timerange, + authFilter, }: Omit) => { const filterClause = [...createQueryFilterClauses(filterQuery)]; @@ -46,7 +47,7 @@ export const buildTimelineEventsAllQuery = ({ return []; }; - const filter = [...filterClause, ...getTimerangeFilter(timerange), { match_all: {} }]; + const filter = [...filterClause, ...getTimerangeFilter(timerange), { match_all: {} }, authFilter]; const getSortField = (sortFields: TimelineRequestSortField[]) => sortFields.map((item) => { diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/index.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/index.ts index 4dfa9831f9e6e..32db9f6ca5fc8 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/index.ts @@ -5,13 +5,18 @@ * 2.0. */ -import { map, mergeMap } from 'rxjs/operators'; +import { map, flatMap, mergeMap } from 'rxjs/operators'; +import { from } from 'rxjs'; import { ISearchStrategy, PluginStart, shimHitsTotal, } from '../../../../../../src/plugins/data/server'; -import { ENHANCED_ES_SEARCH_STRATEGY } from '../../../../../../src/plugins/data/common'; +import { + PluginStartContract as AlertPluginStartContract, + AlertingAuthorizationEntity, + AlertingAuthorizationFilterType, +} from '../../../../alerting/server'; import { TimelineFactoryQueryTypes, TimelineStrategyResponseType, @@ -21,18 +26,39 @@ import { securitySolutionTimelineFactory } from './factory'; import { SecuritySolutionTimelineFactory } from './factory/types'; export const securitySolutionTimelineSearchStrategyProvider = ( - data: PluginStart + data: PluginStart, + alerting: AlertPluginStartContract ): ISearchStrategy, TimelineStrategyResponseType> => { - const es = data.search.getSearchStrategy(ENHANCED_ES_SEARCH_STRATEGY); + const es = data.search.searchAsInternalUser; return { search: (request, options, deps) => { - if (request.factoryQueryType == null) { + const factoryQueryType = request.factoryQueryType; + + if (factoryQueryType == null) { throw new Error('factoryQueryType is required'); } + const alertingAuthorizationClient = alerting.getAlertingAuthorizationWithRequest( + deps.request + ); + const queryFactory: SecuritySolutionTimelineFactory = - securitySolutionTimelineFactory[request.factoryQueryType]; - const dsl = queryFactory.buildDsl(request); - return es.search({ ...request, params: dsl }, options, deps).pipe( + securitySolutionTimelineFactory[factoryQueryType]; + + const getAuthFilter = async () => { + return alertingAuthorizationClient.getFindAuthorizationFilter( + AlertingAuthorizationEntity.Alert, + { + type: AlertingAuthorizationFilterType.ESDSL, + fieldNames: { consumer: 'kibana.rac.alert.owner', ruleTypeId: 'rule.id' }, + } + ); + }; + + return from(getAuthFilter()).pipe( + flatMap(({ filter }) => { + const dsl = queryFactory.buildDsl({ ...request, authFilter: filter }); + return es.search({ ...request, params: dsl }, options, deps); + }), map((response) => { return { ...response, diff --git a/x-pack/plugins/security_solution/server/types.ts b/x-pack/plugins/security_solution/server/types.ts index ea63e188ab26d..4feb4ed18e55d 100644 --- a/x-pack/plugins/security_solution/server/types.ts +++ b/x-pack/plugins/security_solution/server/types.ts @@ -9,6 +9,7 @@ import type { IRouter, RequestHandlerContext } from 'src/core/server'; import type { ListsApiRequestHandlerContext } from '../../lists/server'; import type { LicensingApiRequestHandlerContext } from '../../licensing/server'; import type { AlertingApiRequestHandlerContext } from '../../alerting/server'; +import type { RacApiRequestHandlerContext } from '../../rule_registry/server'; import { AppClient } from './client'; @@ -23,6 +24,7 @@ export type SecuritySolutionRequestHandlerContext = RequestHandlerContext & { licensing: LicensingApiRequestHandlerContext; alerting: AlertingApiRequestHandlerContext; lists?: ListsApiRequestHandlerContext; + ruleRegistry?: RacApiRequestHandlerContext; }; export type SecuritySolutionPluginRouter = IRouter; diff --git a/x-pack/test/api_integration/apis/security/privileges.ts b/x-pack/test/api_integration/apis/security/privileges.ts index 5830fc2d1017f..3814101c0af49 100644 --- a/x-pack/test/api_integration/apis/security/privileges.ts +++ b/x-pack/test/api_integration/apis/security/privileges.ts @@ -61,7 +61,7 @@ export default function ({ getService }: FtrProviderContext) { infrastructure: ['all', 'read'], logs: ['all', 'read'], uptime: ['all', 'read'], - apm: ['all', 'read'], + apm: ['all', 'read', 'minimal_all', 'minimal_read', 'alert_manage'], ml: ['all', 'read'], siem: ['all', 'read'], fleet: ['all', 'read'], diff --git a/x-pack/test/apm_api_integration/tests/alerts/rule_registry.ts b/x-pack/test/apm_api_integration/tests/alerts/rule_registry.ts index e9392a611b5b7..f31e17ad2c48b 100644 --- a/x-pack/test/apm_api_integration/tests/alerts/rule_registry.ts +++ b/x-pack/test/apm_api_integration/tests/alerts/rule_registry.ts @@ -360,6 +360,9 @@ export default function ApiTest({ getService }: FtrProviderContext) { "kibana.rac.alert.id": Array [ "apm.transaction_error_rate_opbeans-go_request", ], + "kibana.rac.alert.owner": Array [ + "apm", + ], "kibana.rac.alert.producer": Array [ "apm", ], @@ -430,6 +433,9 @@ export default function ApiTest({ getService }: FtrProviderContext) { "kibana.rac.alert.id": Array [ "apm.transaction_error_rate_opbeans-go_request", ], + "kibana.rac.alert.owner": Array [ + "apm", + ], "kibana.rac.alert.producer": Array [ "apm", ], @@ -532,6 +538,9 @@ export default function ApiTest({ getService }: FtrProviderContext) { "kibana.rac.alert.id": Array [ "apm.transaction_error_rate_opbeans-go_request", ], + "kibana.rac.alert.owner": Array [ + "apm", + ], "kibana.rac.alert.producer": Array [ "apm", ], diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_ml.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_ml.ts index 9e7fb0ea7c84b..480925d25119c 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_ml.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_ml.ts @@ -189,6 +189,7 @@ export default ({ getService }: FtrProviderContext) => { depth: 0, }, original_time: '2020-11-16T22:58:08.000Z', + owner: 'siem', }, }); }); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts index 43576b80b3738..8e9812c05ca55 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts @@ -220,6 +220,7 @@ export default ({ getService }: FtrProviderContext) => { origin: '/var/log/wtmp', }, original_time: fullSignal.signal.original_time, + owner: 'siem', parent: { depth: 0, id: 'UBXOBmkBR346wHgnLP8T', diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts index 8638f6c1bd7ed..d7f942be29f58 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts @@ -128,6 +128,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], status: 'open', + owner: 'siem', depth: 1, parent: { id: 'BhbXBmkBR346wHgn4PeZ', @@ -186,6 +187,7 @@ export default ({ getService }: FtrProviderContext) => { index: 'auditbeat-8.0.0-2019.02.19-000001', depth: 0, }, + owner: 'siem', original_time: '2019-02-19T17:40:03.790Z', original_event: { action: 'socket_closed', @@ -249,6 +251,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], status: 'open', + owner: 'siem', depth: 2, parent: { rule: signalNoRule.parent?.rule, // parent.rule is always changing so skip testing it @@ -371,6 +374,7 @@ export default ({ getService }: FtrProviderContext) => { category: 'configuration', module: 'auditd', }, + owner: 'siem', parent: { depth: 0, id: '9xbRBmkBR346wHgngz2D', @@ -511,6 +515,7 @@ export default ({ getService }: FtrProviderContext) => { category: 'configuration', module: 'auditd', }, + owner: 'siem', parent: { depth: 0, id: '9xbRBmkBR346wHgngz2D', @@ -676,6 +681,7 @@ export default ({ getService }: FtrProviderContext) => { category: 'anomoly', module: 'auditd', }, + owner: 'siem', parent: { depth: 0, id: 'VhXOBmkBR346wHgnLP8T', @@ -777,6 +783,7 @@ export default ({ getService }: FtrProviderContext) => { type: 'signal', }, ], + owner: 'siem', parents: [ { depth: 1, @@ -873,6 +880,7 @@ export default ({ getService }: FtrProviderContext) => { status: 'open', rule: fullSignal.signal.rule, original_time: fullSignal.signal.original_time, + owner: 'siem', depth: 1, parent: { id: eventIds[0], @@ -1031,6 +1039,7 @@ export default ({ getService }: FtrProviderContext) => { status: 'open', rule: fullSignal.signal.rule, original_time: fullSignal.signal.original_time, + owner: 'siem', depth: 1, parent: { id: eventIds[0], @@ -1122,6 +1131,7 @@ export default ({ getService }: FtrProviderContext) => { rule: fullSignal.signal.rule, original_time: fullSignal.signal.original_time, depth: 1, + owner: 'siem', parent: { id: eventIds[0], type: 'event', @@ -1222,6 +1232,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], status: 'open', + owner: 'siem', depth: 1, parent: { id: '1', @@ -1287,6 +1298,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], status: 'open', + owner: 'siem', depth: 2, parent: { rule: signalNoRule.parent?.rule, // parent.rule is always changing so skip testing it @@ -1374,6 +1386,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], status: 'open', + owner: 'siem', depth: 1, parent: { id: '1', @@ -1445,6 +1458,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], status: 'open', + owner: 'siem', depth: 2, parent: { rule: signalNoRule.parent?.rule, // parent.rule is always changing so skip testing it @@ -1707,6 +1721,7 @@ export default ({ getService }: FtrProviderContext) => { }, original_time: fullSignal.signal.original_time, depth: 1, + owner: 'siem', parent: { id: 'UBXOBmkBR346wHgnLP8T', type: 'event', diff --git a/x-pack/test/functional/es_archives/rule_registry/alerts/data.json b/x-pack/test/functional/es_archives/rule_registry/alerts/data.json new file mode 100644 index 0000000000000..82256f6779c95 --- /dev/null +++ b/x-pack/test/functional/es_archives/rule_registry/alerts/data.json @@ -0,0 +1,13 @@ +{ + "type": "doc", + "value": { + "index": ".alerts-observability-apm", + "id": "NoxgpHkBqbdrfX07MqXV", + "source": { + "rule.id": "apm.error_rate", + "message": "hello world 1", + "kibana.rac.alert.owner": "apm", + "kibana.rac.alert.status": "open" + } + } +} diff --git a/x-pack/test/functional/es_archives/rule_registry/alerts/mappings.json b/x-pack/test/functional/es_archives/rule_registry/alerts/mappings.json new file mode 100644 index 0000000000000..773a98f4e4e23 --- /dev/null +++ b/x-pack/test/functional/es_archives/rule_registry/alerts/mappings.json @@ -0,0 +1,23 @@ +{ + "type": "index", + "value": { + "index": ".alerts-observability-apm", + "mappings": { + "properties": { + "message": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "kibana.rac.alert.owner": { + "type": "keyword", + "ignore_above": 256 + } + } + } + } +} diff --git a/x-pack/test/rule_registry/common/config.ts b/x-pack/test/rule_registry/common/config.ts new file mode 100644 index 0000000000000..21597864c4c0e --- /dev/null +++ b/x-pack/test/rule_registry/common/config.ts @@ -0,0 +1,149 @@ +/* + * 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 { CA_CERT_PATH } from '@kbn/dev-utils'; +import { FtrConfigProviderContext } from '@kbn/test'; + +import { services } from './services'; +import { getAllExternalServiceSimulatorPaths } from '../../alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin'; + +interface CreateTestConfigOptions { + license: string; + disabledPlugins?: string[]; + ssl?: boolean; + testFiles?: string[]; +} + +// test.not-enabled is specifically not enabled +const enabledActionTypes = [ + '.email', + '.index', + '.jira', + '.pagerduty', + '.resilient', + '.server-log', + '.servicenow', + '.servicenow-sir', + '.slack', + '.webhook', + '.case', + 'test.authorization', + 'test.failing', + 'test.index-record', + 'test.noop', + 'test.rate-limit', +]; + +export function createTestConfig(name: string, options: CreateTestConfigOptions) { + const { license = 'trial', disabledPlugins = [], ssl = false, testFiles = [] } = options; + + return async ({ readConfigFile }: FtrConfigProviderContext) => { + const xPackApiIntegrationTestsConfig = await readConfigFile( + require.resolve('../../api_integration/config.ts') + ); + + const servers = { + ...xPackApiIntegrationTestsConfig.get('servers'), + elasticsearch: { + ...xPackApiIntegrationTestsConfig.get('servers.elasticsearch'), + protocol: ssl ? 'https' : 'http', + }, + }; + + // Find all folders in ./fixtures/plugins + // const allFiles = fs.readdirSync(path.resolve(__dirname, 'fixtures', 'plugins')); + // const plugins = allFiles.filter((file) => + // fs.statSync(path.resolve(__dirname, 'fixtures', 'plugins', file)).isDirectory() + // ); + + // This is needed so that we can correctly use the alerting test frameworks mock implementation for the connectors. + // const alertingAllFiles = fs.readdirSync( + // path.resolve( + // __dirname, + // '..', + // '..', + // 'alerting_api_integration', + // 'common', + // 'fixtures', + // 'plugins' + // ) + // ); + + // const alertingPlugins = alertingAllFiles.filter((file) => + // fs + // .statSync( + // path.resolve( + // __dirname, + // '..', + // '..', + // 'alerting_api_integration', + // 'common', + // 'fixtures', + // 'plugins', + // file + // ) + // ) + // .isDirectory() + // ); + + return { + testFiles: testFiles ? testFiles : [require.resolve('../tests/common')], + servers, + services, + junit: { + reportName: 'X-Pack Case API Integration Tests', + }, + esArchiver: xPackApiIntegrationTestsConfig.get('esArchiver'), + esTestCluster: { + ...xPackApiIntegrationTestsConfig.get('esTestCluster'), + license, + ssl, + serverArgs: [ + `xpack.license.self_generated.type=${license}`, + `xpack.security.enabled=${ + !disabledPlugins.includes('security') && ['trial', 'basic'].includes(license) + }`, + ], + }, + kbnTestServer: { + ...xPackApiIntegrationTestsConfig.get('kbnTestServer'), + serverArgs: [ + ...xPackApiIntegrationTestsConfig.get('kbnTestServer.serverArgs'), + `--xpack.actions.allowedHosts=${JSON.stringify(['localhost', 'some.non.existent.com'])}`, + `--xpack.actions.enabledActionTypes=${JSON.stringify(enabledActionTypes)}`, + '--xpack.eventLog.logEntries=true', + ...disabledPlugins.map((key) => `--xpack.${key}.enabled=false`), + // Actions simulators plugin. Needed for testing push to external services. + // ...alertingPlugins.map( + // (pluginDir) => + // `--plugin-path=${path.resolve( + // __dirname, + // '..', + // '..', + // 'alerting_api_integration', + // 'common', + // 'fixtures', + // 'plugins', + // pluginDir + // )}` + // ), + // ...plugins.map( + // (pluginDir) => + // `--plugin-path=${path.resolve(__dirname, 'fixtures', 'plugins', pluginDir)}` + // ), + `--server.xsrf.whitelist=${JSON.stringify(getAllExternalServiceSimulatorPaths())}`, + ...(ssl + ? [ + `--elasticsearch.hosts=${servers.elasticsearch.protocol}://${servers.elasticsearch.hostname}:${servers.elasticsearch.port}`, + `--elasticsearch.ssl.certificateAuthorities=${CA_CERT_PATH}`, + ] + : []), + ], + }, + }; + }; +} diff --git a/x-pack/test/rule_registry/common/ftr_provider_context.d.ts b/x-pack/test/rule_registry/common/ftr_provider_context.d.ts new file mode 100644 index 0000000000000..aa56557c09df8 --- /dev/null +++ b/x-pack/test/rule_registry/common/ftr_provider_context.d.ts @@ -0,0 +1,12 @@ +/* + * 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 { GenericFtrProviderContext } from '@kbn/test'; + +import { services } from './services'; + +export type FtrProviderContext = GenericFtrProviderContext; diff --git a/x-pack/test/rule_registry/common/lib/authentication/index.ts b/x-pack/test/rule_registry/common/lib/authentication/index.ts new file mode 100644 index 0000000000000..9ffcd21eb13c1 --- /dev/null +++ b/x-pack/test/rule_registry/common/lib/authentication/index.ts @@ -0,0 +1,105 @@ +/* + * 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 { FtrProviderContext as CommonFtrProviderContext } from '../../../common/ftr_provider_context'; +import { Role, User, UserInfo } from './types'; +import { users } from './users'; +import { roles } from './roles'; +import { spaces } from './spaces'; + +export const getUserInfo = (user: User): UserInfo => ({ + username: user.username, + full_name: user.username.replace('_', ' '), + email: `${user.username}@elastic.co`, +}); + +export const createSpaces = async (getService: CommonFtrProviderContext['getService']) => { + const spacesService = getService('spaces'); + for (const space of spaces) { + await spacesService.create(space); + } +}; + +/** + * Creates the users and roles for use in the tests. Defaults to specific users and roles used by the security_and_spaces + * scenarios but can be passed specific ones as well. + */ +export const createUsersAndRoles = async ( + getService: CommonFtrProviderContext['getService'], + usersToCreate: User[] = users, + rolesToCreate: Role[] = roles +) => { + const security = getService('security'); + + const createRole = async ({ name, privileges }: Role) => { + return security.role.create(name, privileges); + }; + + const createUser = async (user: User) => { + const userInfo = getUserInfo(user); + + return security.user.create(user.username, { + password: user.password, + roles: user.roles, + full_name: userInfo.full_name, + email: userInfo.email, + }); + }; + + for (const role of rolesToCreate) { + await createRole(role); + } + + for (const user of usersToCreate) { + await createUser(user); + } +}; + +export const deleteSpaces = async (getService: CommonFtrProviderContext['getService']) => { + const spacesService = getService('spaces'); + for (const space of spaces) { + try { + await spacesService.delete(space.id); + } catch (error) { + // ignore errors because if a migration is run it will delete the .kibana index which remove the spaces and users + } + } +}; + +export const deleteUsersAndRoles = async ( + getService: CommonFtrProviderContext['getService'], + usersToDelete: User[] = users, + rolesToDelete: Role[] = roles +) => { + const security = getService('security'); + + for (const user of usersToDelete) { + try { + await security.user.delete(user.username); + } catch (error) { + // ignore errors because if a migration is run it will delete the .kibana index which remove the spaces and users + } + } + + for (const role of rolesToDelete) { + try { + await security.role.delete(role.name); + } catch (error) { + // ignore errors because if a migration is run it will delete the .kibana index which remove the spaces and users + } + } +}; + +export const createSpacesAndUsers = async (getService: CommonFtrProviderContext['getService']) => { + await createSpaces(getService); + await createUsersAndRoles(getService); +}; + +export const deleteSpacesAndUsers = async (getService: CommonFtrProviderContext['getService']) => { + await deleteSpaces(getService); + await deleteUsersAndRoles(getService); +}; diff --git a/x-pack/test/rule_registry/common/lib/authentication/roles.ts b/x-pack/test/rule_registry/common/lib/authentication/roles.ts new file mode 100644 index 0000000000000..37f407e7a0600 --- /dev/null +++ b/x-pack/test/rule_registry/common/lib/authentication/roles.ts @@ -0,0 +1,266 @@ +/* + * 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 './types'; + +export const noKibanaPrivileges: Role = { + name: 'no_kibana_privileges', + privileges: { + elasticsearch: { + indices: [ + { + names: ['*'], + privileges: ['all'], + }, + ], + }, + }, +}; + +export const globalRead: Role = { + name: 'global_read', + privileges: { + elasticsearch: { + indices: [ + { + names: ['*'], + privileges: ['all'], + }, + ], + }, + kibana: [ + { + feature: { + siem: ['read'], + apm: ['read'], + actions: ['read'], + actionsSimulators: ['read'], + }, + spaces: ['*'], + }, + ], + }, +}; + +export const securitySolutionOnlyAll: Role = { + name: 'sec_only_all', + privileges: { + elasticsearch: { + indices: [ + { + names: ['*'], + privileges: ['all'], + }, + ], + }, + kibana: [ + { + feature: { + siem: ['all'], + actions: ['all'], + actionsSimulators: ['all'], + }, + spaces: ['space1'], + }, + ], + }, +}; + +export const securitySolutionOnlyRead: Role = { + name: 'sec_only_read', + privileges: { + elasticsearch: { + indices: [ + { + names: ['*'], + privileges: ['all'], + }, + ], + }, + kibana: [ + { + feature: { + siem: ['read'], + actions: ['read'], + actionsSimulators: ['read'], + }, + spaces: ['space1'], + }, + ], + }, +}; + +export const observabilityOnlyAll: Role = { + name: 'obs_only_all', + privileges: { + elasticsearch: { + indices: [ + { + names: ['*'], + privileges: ['all'], + }, + ], + }, + kibana: [ + { + feature: { + apm: ['all'], + actions: ['all'], + actionsSimulators: ['all'], + }, + spaces: ['space1'], + }, + ], + }, +}; + +export const observabilityOnlyRead: Role = { + name: 'obs_only_read', + privileges: { + elasticsearch: { + indices: [ + { + names: ['*'], + privileges: ['all'], + }, + ], + }, + kibana: [ + { + feature: { + apm: ['read'], + actions: ['read'], + actionsSimulators: ['read'], + }, + spaces: ['space1'], + }, + ], + }, +}; + +export const roles = [ + noKibanaPrivileges, + globalRead, + securitySolutionOnlyAll, + securitySolutionOnlyRead, + observabilityOnlyAll, + observabilityOnlyRead, +]; + +/** + * These roles have access to all spaces. + */ + +export const securitySolutionOnlyAllSpacesAll: Role = { + name: 'sec_only_all', + privileges: { + elasticsearch: { + indices: [ + { + names: ['*'], + privileges: ['all'], + }, + ], + }, + kibana: [ + { + feature: { + siem: ['all'], + actions: ['all'], + actionsSimulators: ['all'], + }, + spaces: ['*'], + }, + ], + }, +}; + +export const securitySolutionOnlyReadSpacesAll: Role = { + name: 'sec_only_read', + privileges: { + elasticsearch: { + indices: [ + { + names: ['*'], + privileges: ['all'], + }, + ], + }, + kibana: [ + { + feature: { + siem: ['read'], + actions: ['read'], + actionsSimulators: ['read'], + }, + spaces: ['*'], + }, + ], + }, +}; + +export const observabilityOnlyAllSpacesAll: Role = { + name: 'obs_only_all', + privileges: { + elasticsearch: { + indices: [ + { + names: ['*'], + privileges: ['all'], + }, + ], + }, + kibana: [ + { + feature: { + apm: ['all'], + actions: ['all'], + actionsSimulators: ['all'], + }, + spaces: ['*'], + }, + ], + }, +}; + +export const observabilityOnlyReadSpacesAll: Role = { + name: 'obs_only_read', + privileges: { + elasticsearch: { + indices: [ + { + names: ['*'], + privileges: ['all'], + }, + ], + }, + kibana: [ + { + feature: { + apm: ['read'], + actions: ['read'], + actionsSimulators: ['read'], + }, + spaces: ['*'], + }, + ], + }, +}; + +/** + * These roles are specifically for the security_only tests where the spaces plugin is disabled. Most of the roles (except + * for noKibanaPrivileges) have spaces: ['*'] effectively giving it access to the space1 space since no other spaces + * will exist when the spaces plugin is disabled. + */ +export const rolesDefaultSpace = [ + noKibanaPrivileges, + globalRead, + securitySolutionOnlyAllSpacesAll, + securitySolutionOnlyReadSpacesAll, + observabilityOnlyAllSpacesAll, + observabilityOnlyReadSpacesAll, +]; diff --git a/x-pack/test/rule_registry/common/lib/authentication/spaces.ts b/x-pack/test/rule_registry/common/lib/authentication/spaces.ts new file mode 100644 index 0000000000000..b3925ea0fe898 --- /dev/null +++ b/x-pack/test/rule_registry/common/lib/authentication/spaces.ts @@ -0,0 +1,26 @@ +/* + * 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 { Space } from './types'; + +const space1: Space = { + id: 'space1', + name: 'Space 1', + disabledFeatures: [], +}; + +const space2: Space = { + id: 'space2', + name: 'Space 2', + disabledFeatures: [], +}; + +export const spaces: Space[] = [space1, space2]; + +export const getSpaceUrlPrefix = (spaceId: string) => { + return spaceId && spaceId !== 'default' ? `/s/${spaceId}` : ``; +}; diff --git a/x-pack/test/rule_registry/common/lib/authentication/types.ts b/x-pack/test/rule_registry/common/lib/authentication/types.ts new file mode 100644 index 0000000000000..3bf3629441f93 --- /dev/null +++ b/x-pack/test/rule_registry/common/lib/authentication/types.ts @@ -0,0 +1,54 @@ +/* + * 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 interface Space { + id: string; + namespace?: string; + name: string; + disabledFeatures: string[]; +} + +export interface User { + username: string; + password: string; + description?: string; + roles: string[]; +} + +export interface UserInfo { + username: string; + full_name: string; + email: string; +} + +interface FeaturesPrivileges { + [featureId: string]: string[]; +} + +interface ElasticsearchIndices { + names: string[]; + privileges: string[]; +} + +export interface ElasticSearchPrivilege { + cluster?: string[]; + indices?: ElasticsearchIndices[]; +} + +export interface KibanaPrivilege { + spaces: string[]; + base?: string[]; + feature?: FeaturesPrivileges; +} + +export interface Role { + name: string; + privileges: { + elasticsearch?: ElasticSearchPrivilege; + kibana?: KibanaPrivilege[]; + }; +} diff --git a/x-pack/test/rule_registry/common/lib/authentication/users.ts b/x-pack/test/rule_registry/common/lib/authentication/users.ts new file mode 100644 index 0000000000000..1fa6e3c9f4990 --- /dev/null +++ b/x-pack/test/rule_registry/common/lib/authentication/users.ts @@ -0,0 +1,141 @@ +/* + * 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 { + securitySolutionOnlyAll, + observabilityOnlyAll, + securitySolutionOnlyRead, + observabilityOnlyRead, + globalRead as globalReadRole, + noKibanaPrivileges as noKibanaPrivilegesRole, + securitySolutionOnlyAllSpacesAll, + securitySolutionOnlyReadSpacesAll, + observabilityOnlyAllSpacesAll, + observabilityOnlyReadSpacesAll, +} from './roles'; +import { User } from './types'; + +export const superUser: User = { + username: 'superuser', + password: 'superuser', + roles: ['superuser'], +}; + +export const secOnly: User = { + username: 'sec_only', + password: 'sec_only', + roles: [securitySolutionOnlyAll.name], +}; + +export const secOnlyRead: User = { + username: 'sec_only_read', + password: 'sec_only_read', + roles: [securitySolutionOnlyRead.name], +}; + +export const obsOnly: User = { + username: 'obs_only', + password: 'obs_only', + roles: [observabilityOnlyAll.name], +}; + +export const obsOnlyRead: User = { + username: 'obs_only_read', + password: 'obs_only_read', + roles: [observabilityOnlyRead.name], +}; + +export const obsSec: User = { + username: 'obs_sec', + password: 'obs_sec', + roles: [securitySolutionOnlyAll.name, observabilityOnlyAll.name], +}; + +export const obsSecRead: User = { + username: 'obs_sec_read', + password: 'obs_sec_read', + roles: [securitySolutionOnlyRead.name, observabilityOnlyRead.name], +}; + +export const globalRead: User = { + username: 'global_read', + password: 'global_read', + roles: [globalReadRole.name], +}; + +export const noKibanaPrivileges: User = { + username: 'no_kibana_privileges', + password: 'no_kibana_privileges', + roles: [noKibanaPrivilegesRole.name], +}; + +export const users = [ + superUser, + secOnly, + secOnlyRead, + obsOnly, + obsOnlyRead, + obsSec, + obsSecRead, + globalRead, + noKibanaPrivileges, +]; + +/** + * These users will have access to all spaces. + */ + +export const secOnlySpacesAll: User = { + username: 'sec_only', + password: 'sec_only', + roles: [securitySolutionOnlyAllSpacesAll.name], +}; + +export const secOnlyReadSpacesAll: User = { + username: 'sec_only_read', + password: 'sec_only_read', + roles: [securitySolutionOnlyReadSpacesAll.name], +}; + +export const obsOnlySpacesAll: User = { + username: 'obs_only', + password: 'obs_only', + roles: [observabilityOnlyAllSpacesAll.name], +}; + +export const obsOnlyReadSpacesAll: User = { + username: 'obs_only_read', + password: 'obs_only_read', + roles: [observabilityOnlyReadSpacesAll.name], +}; + +export const obsSecSpacesAll: User = { + username: 'obs_sec', + password: 'obs_sec', + roles: [securitySolutionOnlyAllSpacesAll.name, observabilityOnlyAllSpacesAll.name], +}; + +export const obsSecReadSpacesAll: User = { + username: 'obs_sec_read', + password: 'obs_sec_read', + roles: [securitySolutionOnlyReadSpacesAll.name, observabilityOnlyReadSpacesAll.name], +}; + +/** + * These users are for the security_only tests because most of them have access to the default space instead of 'space1' + */ +export const usersDefaultSpace = [ + superUser, + secOnlySpacesAll, + secOnlyReadSpacesAll, + obsOnlySpacesAll, + obsOnlyReadSpacesAll, + obsSecSpacesAll, + obsSecReadSpacesAll, + globalRead, + noKibanaPrivileges, +]; diff --git a/x-pack/test/rule_registry/common/services.ts b/x-pack/test/rule_registry/common/services.ts new file mode 100644 index 0000000000000..7e415338c405f --- /dev/null +++ b/x-pack/test/rule_registry/common/services.ts @@ -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 { services } from '../../api_integration/services'; diff --git a/x-pack/test/rule_registry/security_and_spaces/config_basic.ts b/x-pack/test/rule_registry/security_and_spaces/config_basic.ts new file mode 100644 index 0000000000000..98b7b1abe98e7 --- /dev/null +++ b/x-pack/test/rule_registry/security_and_spaces/config_basic.ts @@ -0,0 +1,15 @@ +/* + * 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 { createTestConfig } from '../common/config'; + +// eslint-disable-next-line import/no-default-export +export default createTestConfig('security_and_spaces', { + license: 'basic', + ssl: true, + testFiles: [require.resolve('./tests/basic')], +}); diff --git a/x-pack/test/rule_registry/security_and_spaces/config_trial.ts b/x-pack/test/rule_registry/security_and_spaces/config_trial.ts new file mode 100644 index 0000000000000..b5328fd83c2cb --- /dev/null +++ b/x-pack/test/rule_registry/security_and_spaces/config_trial.ts @@ -0,0 +1,15 @@ +/* + * 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 { createTestConfig } from '../common/config'; + +// eslint-disable-next-line import/no-default-export +export default createTestConfig('security_and_spaces', { + license: 'trial', + ssl: true, + testFiles: [require.resolve('./tests/trial')], +}); diff --git a/x-pack/test/rule_registry/security_and_spaces/roles_users_utils/index.ts b/x-pack/test/rule_registry/security_and_spaces/roles_users_utils/index.ts new file mode 100644 index 0000000000000..b320446cbe05f --- /dev/null +++ b/x-pack/test/rule_registry/security_and_spaces/roles_users_utils/index.ts @@ -0,0 +1,124 @@ +/* + * 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 { assertUnreachable } from '../../../../plugins/security_solution/common/utility_types'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { + t1AnalystUser, + t2AnalystUser, + hunterUser, + ruleAuthorUser, + socManagerUser, + platformEngineerUser, + detectionsAdminUser, + readerUser, + t1AnalystRole, + t2AnalystRole, + hunterRole, + ruleAuthorRole, + socManagerRole, + platformEngineerRole, + detectionsAdminRole, + readerRole, +} from '../../../../plugins/security_solution/server/lib/detection_engine/scripts/roles_users'; + +import { ROLES } from '../../../../plugins/security_solution/common/test'; + +export const createUserAndRole = async ( + getService: FtrProviderContext['getService'], + role: ROLES +): Promise => { + switch (role) { + case ROLES.detections_admin: + return postRoleAndUser( + ROLES.detections_admin, + detectionsAdminRole, + detectionsAdminUser, + getService + ); + case ROLES.t1_analyst: + return postRoleAndUser(ROLES.t1_analyst, t1AnalystRole, t1AnalystUser, getService); + case ROLES.t2_analyst: + return postRoleAndUser(ROLES.t2_analyst, t2AnalystRole, t2AnalystUser, getService); + case ROLES.hunter: + return postRoleAndUser(ROLES.hunter, hunterRole, hunterUser, getService); + case ROLES.rule_author: + return postRoleAndUser(ROLES.rule_author, ruleAuthorRole, ruleAuthorUser, getService); + case ROLES.soc_manager: + return postRoleAndUser(ROLES.soc_manager, socManagerRole, socManagerUser, getService); + case ROLES.platform_engineer: + return postRoleAndUser( + ROLES.platform_engineer, + platformEngineerRole, + platformEngineerUser, + getService + ); + case ROLES.reader: + return postRoleAndUser(ROLES.reader, readerRole, readerUser, getService); + default: + return assertUnreachable(role); + } +}; + +/** + * Given a roleName and security service this will delete the roleName + * and user + * @param roleName The user and role to delete with the same name + * @param securityService The security service + */ +export const deleteUserAndRole = async ( + getService: FtrProviderContext['getService'], + roleName: ROLES +): Promise => { + const securityService = getService('security'); + await securityService.user.delete(roleName); + await securityService.role.delete(roleName); +}; + +interface UserInterface { + password: string; + roles: string[]; + full_name: string; + email: string; +} + +interface RoleInterface { + elasticsearch: { + cluster: string[]; + indices: Array<{ + names: string[]; + privileges: string[]; + }>; + }; + kibana: Array<{ + feature: { + ml: string[]; + siem: string[]; + actions: string[]; + builtInAlerts: string[]; + }; + spaces: string[]; + }>; +} + +export const postRoleAndUser = async ( + roleName: string, + role: RoleInterface, + user: UserInterface, + getService: FtrProviderContext['getService'] +): Promise => { + const securityService = getService('security'); + await securityService.role.create(roleName, { + kibana: role.kibana, + elasticsearch: role.elasticsearch, + }); + await securityService.user.create(roleName, { + password: 'changeme', + full_name: user.full_name, + roles: user.roles, + }); +}; diff --git a/x-pack/test/rule_registry/security_and_spaces/tests/basic/get_alerts.ts b/x-pack/test/rule_registry/security_and_spaces/tests/basic/get_alerts.ts new file mode 100644 index 0000000000000..49812a82adf2c --- /dev/null +++ b/x-pack/test/rule_registry/security_and_spaces/tests/basic/get_alerts.ts @@ -0,0 +1,174 @@ +/* + * 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 { + secOnly, + secOnlyRead, + globalRead, + obsOnly, + obsOnlyRead, + obsOnlySpacesAll, + obsOnlyReadSpacesAll, + obsSec, + obsSecRead, + superUser, + noKibanaPrivileges, +} from '../../../common/lib/authentication/users'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { getSpaceUrlPrefix } from '../../../common/lib/authentication/spaces'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + const TEST_URL = '/api/rac/alerts'; + const SPACE1 = 'space1'; + const SPACE2 = 'space2'; + + describe('rbac', () => { + before(async () => { + await esArchiver.load('rule_registry/alerts'); + }); + describe('Users:', () => { + it(`${superUser.username} should be able to access the APM alert in ${SPACE1}`, async () => { + await supertestWithoutAuth + .get( + `${getSpaceUrlPrefix( + SPACE1 + )}${TEST_URL}?id=NoxgpHkBqbdrfX07MqXV&assetName=observability-apm` + ) + .auth(superUser.username, superUser.password) + .set('kbn-xsrf', 'true') + .expect(200); + }); + it(`${globalRead.username} should be able to access the APM alert in ${SPACE1}`, async () => { + await supertestWithoutAuth + .get( + `${getSpaceUrlPrefix( + SPACE1 + )}${TEST_URL}?id=NoxgpHkBqbdrfX07MqXV&assetName=observability-apm` + ) + .auth(globalRead.username, globalRead.password) + .set('kbn-xsrf', 'true') + .expect(200); + }); + it(`${obsOnlySpacesAll.username} should be able to access the APM alert in ${SPACE1}`, async () => { + await supertestWithoutAuth + .get( + `${getSpaceUrlPrefix( + SPACE1 + )}${TEST_URL}?id=NoxgpHkBqbdrfX07MqXV&assetName=observability-apm` + ) + .auth(obsOnlySpacesAll.username, obsOnlySpacesAll.password) + .set('kbn-xsrf', 'true') + .expect(200); + }); + it(`${obsOnlyReadSpacesAll.username} should be able to access the APM alert in ${SPACE1}`, async () => { + await supertestWithoutAuth + .get( + `${getSpaceUrlPrefix( + SPACE1 + )}${TEST_URL}?id=NoxgpHkBqbdrfX07MqXV&assetName=observability-apm` + ) + .auth(obsOnlyReadSpacesAll.username, obsOnlyReadSpacesAll.password) + .set('kbn-xsrf', 'true') + .expect(200); + }); + + for (const scenario of [ + { + user: noKibanaPrivileges, + }, + { + user: secOnly, + }, + { + user: secOnlyRead, + }, + ]) { + it(`${scenario.user.username} should not be able to access the APM alert in ${SPACE1}`, async () => { + await supertestWithoutAuth + .get( + `${getSpaceUrlPrefix( + SPACE1 + )}${TEST_URL}?id=NoxgpHkBqbdrfX07MqXV&assetName=observability-apm` + ) + .auth(scenario.user.username, scenario.user.password) + .set('kbn-xsrf', 'true') + .expect(403); + }); + } + }); + + describe('Space:', () => { + for (const scenario of [ + { user: superUser, space: SPACE1 }, + { user: globalRead, space: SPACE1 }, + ]) { + it(`${scenario.user.username} should be able to access the APM alert in ${SPACE2}`, async () => { + await supertestWithoutAuth + .get( + `${getSpaceUrlPrefix( + SPACE2 + )}${TEST_URL}?id=NoxgpHkBqbdrfX07MqXV&assetName=observability-apm` + ) + .auth(scenario.user.username, scenario.user.password) + .set('kbn-xsrf', 'true') + .expect(200); + }); + } + + for (const scenario of [ + { user: secOnly }, + { user: secOnlyRead }, + { user: obsSec }, + { user: obsSecRead }, + { + user: noKibanaPrivileges, + }, + { + user: obsOnly, + }, + { + user: obsOnlyRead, + }, + ]) { + it(`${scenario.user.username} with right to access space1 only, should not be able to access the APM alert in ${SPACE2}`, async () => { + await supertestWithoutAuth + .get( + `${getSpaceUrlPrefix( + SPACE2 + )}${TEST_URL}?id=NoxgpHkBqbdrfX07MqXV&assetName=observability-apm` + ) + .auth(scenario.user.username, scenario.user.password) + .set('kbn-xsrf', 'true') + .expect(403); + }); + } + }); + + describe('extra params', () => { + it('should NOT allow to pass a filter query parameter', async () => { + await supertest + .get(`${getSpaceUrlPrefix(SPACE1)}${TEST_URL}?sortOrder=asc&namespaces[0]=*`) + .set('kbn-xsrf', 'true') + .send() + .expect(400); + }); + + it('should NOT allow to pass a non supported query parameter', async () => { + await supertest + .get(`${getSpaceUrlPrefix(SPACE1)}${TEST_URL}?notExists=something`) + .set('kbn-xsrf', 'true') + .send() + .expect(400); + }); + }); + }); +}; diff --git a/x-pack/test/rule_registry/security_and_spaces/tests/basic/index.ts b/x-pack/test/rule_registry/security_and_spaces/tests/basic/index.ts new file mode 100644 index 0000000000000..24e3fd9d4e5e5 --- /dev/null +++ b/x-pack/test/rule_registry/security_and_spaces/tests/basic/index.ts @@ -0,0 +1,29 @@ +/* + * 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 { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { createSpacesAndUsers, deleteSpacesAndUsers } from '../../../common/lib/authentication'; + +// eslint-disable-next-line import/no-default-export +export default ({ loadTestFile, getService }: FtrProviderContext): void => { + describe('rules security and spaces enabled: basic', function () { + // Fastest ciGroup for the moment. + this.tags('ciGroup5'); + + before(async () => { + await createSpacesAndUsers(getService); + }); + + after(async () => { + await deleteSpacesAndUsers(getService); + }); + + // Basic + loadTestFile(require.resolve('./get_alerts')); + loadTestFile(require.resolve('./update_alert')); + }); +}; diff --git a/x-pack/test/rule_registry/security_and_spaces/tests/basic/update_alert.ts b/x-pack/test/rule_registry/security_and_spaces/tests/basic/update_alert.ts new file mode 100644 index 0000000000000..a65b7b002d68e --- /dev/null +++ b/x-pack/test/rule_registry/security_and_spaces/tests/basic/update_alert.ts @@ -0,0 +1,151 @@ +/* + * 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 { + secOnly, + secOnlyRead, + obsOnlySpacesAll, + obsOnlyReadSpacesAll, + superUser, + noKibanaPrivileges, +} from '../../../common/lib/authentication/users'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { getSpaceUrlPrefix } from '../../../common/lib/authentication/spaces'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const esArchiver = getService('esArchiver'); + + const TEST_URL = '/api/rac/alerts'; + const SPACE1 = 'space1'; + + describe('rbac', () => { + describe('Users update:', () => { + beforeEach(async () => { + await esArchiver.load('rule_registry/alerts'); + }); + afterEach(async () => { + await esArchiver.unload('rule_registry/alerts'); + }); + it(`${superUser.username} should be able to update the APM alert in ${SPACE1}`, async () => { + await supertestWithoutAuth + .post(`${getSpaceUrlPrefix(SPACE1)}${TEST_URL}`) + .auth(superUser.username, superUser.password) + .set('kbn-xsrf', 'true') + .send({ ids: ['NoxgpHkBqbdrfX07MqXV'], status: 'closed', assetName: 'observability-apm' }) + .expect(200); + }); + // it(`${globalRead.username} should be able to access the APM alert in ${SPACE1}`, async () => { + // const res = await supertestWithoutAuth + // .get(`${getSpaceUrlPrefix(SPACE1)}${TEST_URL}?id=NoxgpHkBqbdrfX07MqXV`) + // .auth(globalRead.username, globalRead.password) + // .set('kbn-xsrf', 'true') + // .expect(200); + // // console.error('RES', res); + // }); + it(`${obsOnlySpacesAll.username} should be able to update the APM alert in ${SPACE1}`, async () => { + await supertestWithoutAuth + .post(`${getSpaceUrlPrefix(SPACE1)}${TEST_URL}`) + .auth(obsOnlySpacesAll.username, obsOnlySpacesAll.password) + .set('kbn-xsrf', 'true') + .send({ ids: ['NoxgpHkBqbdrfX07MqXV'], status: 'closed', assetName: 'observability-apm' }) + .expect(200); + }); + it(`${obsOnlyReadSpacesAll.username} should NOT be able to update the APM alert in ${SPACE1}`, async () => { + await supertestWithoutAuth + .post(`${getSpaceUrlPrefix(SPACE1)}${TEST_URL}`) + .auth(obsOnlyReadSpacesAll.username, obsOnlyReadSpacesAll.password) + .set('kbn-xsrf', 'true') + .send({ ids: ['NoxgpHkBqbdrfX07MqXV'], status: 'closed', assetName: 'observability-apm' }) + .expect(403); + }); + + for (const scenario of [ + { + user: noKibanaPrivileges, + }, + { + user: secOnly, + }, + { + user: secOnlyRead, + }, + ]) { + it(`${scenario.user.username} should NOT be able to update the APM alert in ${SPACE1}`, async () => { + await supertestWithoutAuth + .post(`${getSpaceUrlPrefix(SPACE1)}${TEST_URL}`) + .auth(scenario.user.username, scenario.user.password) + .set('kbn-xsrf', 'true') + .send({ + ids: ['NoxgpHkBqbdrfX07MqXV'], + status: 'closed', + assetName: 'observability-apm', + }) + .expect(403); + }); + } + }); + + // describe('Space:', () => { + // for (const scenario of [ + // { user: superUser, space: SPACE1 }, + // { user: globalRead, space: SPACE1 }, + // ]) { + // it(`${scenario.user.username} should be able to access the APM alert in ${SPACE2}`, async () => { + // await supertestWithoutAuth + // .get(`${getSpaceUrlPrefix(SPACE2)}${TEST_URL}?id=NoxgpHkBqbdrfX07MqXV`) + // .auth(scenario.user.username, scenario.user.password) + // .set('kbn-xsrf', 'true') + // .expect(200); + // }); + // } + + // for (const scenario of [ + // { user: secOnly }, + // { user: secOnlyRead }, + // { user: obsSec }, + // { user: obsSecRead }, + // { + // user: noKibanaPrivileges, + // }, + // { + // user: obsOnly, + // }, + // { + // user: obsOnlyRead, + // }, + // ]) { + // it(`${scenario.user.username} with right to access space1 only, should not be able to access the APM alert in ${SPACE2}`, async () => { + // await supertestWithoutAuth + // .get(`${getSpaceUrlPrefix(SPACE2)}${TEST_URL}?id=NoxgpHkBqbdrfX07MqXV`) + // .auth(scenario.user.username, scenario.user.password) + // .set('kbn-xsrf', 'true') + // .expect(403); + // }); + // } + // }); + + // describe('extra params', () => { + // it('should NOT allow to pass a filter query parameter', async () => { + // await supertest + // .get(`${getSpaceUrlPrefix(SPACE1)}${TEST_URL}?sortOrder=asc&namespaces[0]=*`) + // .set('kbn-xsrf', 'true') + // .send() + // .expect(400); + // }); + + // it('should NOT allow to pass a non supported query parameter', async () => { + // await supertest + // .get(`${getSpaceUrlPrefix(SPACE1)}${TEST_URL}?notExists=something`) + // .set('kbn-xsrf', 'true') + // .send() + // .expect(400); + // }); + // }); + }); +};