From 86619d048b299367bfab47c10b9c71dac8c88681 Mon Sep 17 00:00:00 2001 From: Devin Hurley Date: Tue, 1 Jun 2021 19:46:20 -0400 Subject: [PATCH 01/16] squashed commit WIP - trying to fix integration tests, broken authz for observer user / role updates authz feature builder to what ying had before we messed it up in our branch fixes integration tests add rac api access to apm adds getIndex functionality which requires the asset name to be passed in, same style as in the rule registry data client, adds update integration tests fix small merge conflict and update shell script fix merge conflict in alerting test file fix most type errors fix the rest of the type failures fix integration tests fix integration tests fix type error with feature registration in apm fix integration tests in apm and security solution fix type checker fix jest tests for apm remove console.error statements for eslint fix type check --- .../src/technical_field_names.ts | 3 + .../alerting_authorization.mock.ts | 1 + .../authorization/alerting_authorization.ts | 14 +- x-pack/plugins/alerting/server/index.ts | 7 + x-pack/plugins/apm/common/alert_types.ts | 10 +- .../alerting/alerting_flyout/index.tsx | 7 +- x-pack/plugins/apm/server/feature.ts | 55 ++- x-pack/plugins/apm/server/index.ts | 1 + .../alerts/register_error_count_alert_type.ts | 8 +- ...egister_transaction_duration_alert_type.ts | 8 +- ...ister_transaction_error_rate_alert_type.ts | 8 +- .../apm/server/lib/alerts/test_utils/index.ts | 3 + x-pack/plugins/monitoring/server/types.ts | 2 + .../field_maps/technical_rule_field_map.ts | 2 + .../plugins/rule_registry/common/constants.ts | 8 + .../server/alert_data_client/alert_client.ts | 400 ++++++++++++++++++ .../alert_data_client/alert_client_factory.ts | 68 +++ .../server/alert_data_client/audit_logger.ts | 89 ++++ .../server/alert_data_client/utils.ts | 72 ++++ x-pack/plugins/rule_registry/server/index.ts | 1 + x-pack/plugins/rule_registry/server/plugin.ts | 117 ++++- .../server/routes/get_alert_by_id.ts | 69 +++ .../rule_registry/server/routes/index.ts | 16 + .../server/routes/update_alert_by_id.ts | 66 +++ .../server/routes/utils/route_validation.ts | 56 +++ .../rule_registry/server/scripts/README.md | 24 ++ .../server/scripts/get_alert_by_id.sh | 15 + .../server/scripts/get_observability_alert.sh | 21 + .../scripts/get_security_solution_alert.sh | 22 + .../server/scripts/hunter/README.md | 5 + .../scripts/hunter/delete_detections_user.sh | 11 + .../scripts/hunter/detections_role.json | 38 ++ .../scripts/hunter/detections_user.json | 6 + .../scripts/hunter/get_detections_role.sh | 11 + .../server/scripts/hunter/index.ts | 10 + .../scripts/hunter/post_detections_role.sh | 14 + .../scripts/hunter/post_detections_user.sh | 14 + .../server/scripts/observer/README.md | 5 + .../observer/delete_detections_user.sh | 11 + .../scripts/observer/detections_role.json | 41 ++ .../scripts/observer/detections_user.json | 6 + .../scripts/observer/get_detections_role.sh | 11 + .../observer/get_observability_alert.sh | 21 + .../observer/get_security_solution_alert.sh | 22 + .../server/scripts/observer/index.ts | 10 + .../scripts/observer/post_detections_role.sh | 14 + .../scripts/observer/post_detections_user.sh | 14 + .../scripts/update_observability_alert.sh | 19 + x-pack/plugins/rule_registry/server/types.ts | 16 + .../create_lifecycle_rule_type_factory.ts | 12 + x-pack/plugins/rule_registry/tsconfig.json | 9 +- .../routes/index/signals_mapping.json | 3 + .../signals/__mocks__/es_results.ts | 1 + .../signals/build_bulk_body.ts | 6 +- .../signals/build_signal.test.ts | 4 +- .../detection_engine/signals/build_signal.ts | 3 +- .../signals/executors/eql.test.ts | 1 + .../signals/executors/threshold.test.ts | 1 + .../lib/detection_engine/signals/types.ts | 2 + .../security_solution/server/plugin.ts | 4 +- .../plugins/security_solution/server/types.ts | 2 + .../apis/security/privileges.ts | 2 +- .../tests/alerts/rule_registry.ts | 9 + .../common/config.ts | 12 + .../security_and_spaces/tests/create_ml.ts | 1 + .../tests/generating_signals.ts | 15 + .../rule_registry/alerts/data.json | 13 + .../rule_registry/alerts/mappings.json | 23 + x-pack/test/rule_registry/common/config.ts | 149 +++++++ .../common/ftr_provider_context.d.ts | 12 + .../common/lib/authentication/index.ts | 105 +++++ .../common/lib/authentication/roles.ts | 266 ++++++++++++ .../common/lib/authentication/spaces.ts | 26 ++ .../common/lib/authentication/types.ts | 54 +++ .../common/lib/authentication/users.ts | 141 ++++++ x-pack/test/rule_registry/common/services.ts | 8 + .../security_and_spaces/config_basic.ts | 15 + .../security_and_spaces/config_trial.ts | 15 + .../roles_users_utils/index.ts | 124 ++++++ .../tests/basic/get_alerts.ts | 174 ++++++++ .../security_and_spaces/tests/basic/index.ts | 29 ++ .../tests/basic/update_alert.ts | 158 +++++++ 82 files changed, 2823 insertions(+), 48 deletions(-) create mode 100644 x-pack/plugins/rule_registry/common/constants.ts create mode 100644 x-pack/plugins/rule_registry/server/alert_data_client/alert_client.ts create mode 100644 x-pack/plugins/rule_registry/server/alert_data_client/alert_client_factory.ts create mode 100644 x-pack/plugins/rule_registry/server/alert_data_client/audit_logger.ts create mode 100644 x-pack/plugins/rule_registry/server/alert_data_client/utils.ts create mode 100644 x-pack/plugins/rule_registry/server/routes/get_alert_by_id.ts create mode 100644 x-pack/plugins/rule_registry/server/routes/index.ts create mode 100644 x-pack/plugins/rule_registry/server/routes/update_alert_by_id.ts create mode 100644 x-pack/plugins/rule_registry/server/routes/utils/route_validation.ts create mode 100644 x-pack/plugins/rule_registry/server/scripts/README.md create mode 100755 x-pack/plugins/rule_registry/server/scripts/get_alert_by_id.sh create mode 100755 x-pack/plugins/rule_registry/server/scripts/get_observability_alert.sh create mode 100755 x-pack/plugins/rule_registry/server/scripts/get_security_solution_alert.sh create mode 100644 x-pack/plugins/rule_registry/server/scripts/hunter/README.md create mode 100755 x-pack/plugins/rule_registry/server/scripts/hunter/delete_detections_user.sh create mode 100644 x-pack/plugins/rule_registry/server/scripts/hunter/detections_role.json create mode 100644 x-pack/plugins/rule_registry/server/scripts/hunter/detections_user.json create mode 100755 x-pack/plugins/rule_registry/server/scripts/hunter/get_detections_role.sh create mode 100644 x-pack/plugins/rule_registry/server/scripts/hunter/index.ts create mode 100755 x-pack/plugins/rule_registry/server/scripts/hunter/post_detections_role.sh create mode 100755 x-pack/plugins/rule_registry/server/scripts/hunter/post_detections_user.sh create mode 100644 x-pack/plugins/rule_registry/server/scripts/observer/README.md create mode 100755 x-pack/plugins/rule_registry/server/scripts/observer/delete_detections_user.sh create mode 100644 x-pack/plugins/rule_registry/server/scripts/observer/detections_role.json create mode 100644 x-pack/plugins/rule_registry/server/scripts/observer/detections_user.json create mode 100755 x-pack/plugins/rule_registry/server/scripts/observer/get_detections_role.sh create mode 100755 x-pack/plugins/rule_registry/server/scripts/observer/get_observability_alert.sh create mode 100755 x-pack/plugins/rule_registry/server/scripts/observer/get_security_solution_alert.sh create mode 100644 x-pack/plugins/rule_registry/server/scripts/observer/index.ts create mode 100755 x-pack/plugins/rule_registry/server/scripts/observer/post_detections_role.sh create mode 100755 x-pack/plugins/rule_registry/server/scripts/observer/post_detections_user.sh create mode 100755 x-pack/plugins/rule_registry/server/scripts/update_observability_alert.sh create mode 100644 x-pack/test/functional/es_archives/rule_registry/alerts/data.json create mode 100644 x-pack/test/functional/es_archives/rule_registry/alerts/mappings.json create mode 100644 x-pack/test/rule_registry/common/config.ts create mode 100644 x-pack/test/rule_registry/common/ftr_provider_context.d.ts create mode 100644 x-pack/test/rule_registry/common/lib/authentication/index.ts create mode 100644 x-pack/test/rule_registry/common/lib/authentication/roles.ts create mode 100644 x-pack/test/rule_registry/common/lib/authentication/spaces.ts create mode 100644 x-pack/test/rule_registry/common/lib/authentication/types.ts create mode 100644 x-pack/test/rule_registry/common/lib/authentication/users.ts create mode 100644 x-pack/test/rule_registry/common/services.ts create mode 100644 x-pack/test/rule_registry/security_and_spaces/config_basic.ts create mode 100644 x-pack/test/rule_registry/security_and_spaces/config_trial.ts create mode 100644 x-pack/test/rule_registry/security_and_spaces/roles_users_utils/index.ts create mode 100644 x-pack/test/rule_registry/security_and_spaces/tests/basic/get_alerts.ts create mode 100644 x-pack/test/rule_registry/security_and_spaces/tests/basic/index.ts create mode 100644 x-pack/test/rule_registry/security_and_spaces/tests/basic/update_alert.ts 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/x-pack/plugins/alerting/server/authorization/alerting_authorization.mock.ts b/x-pack/plugins/alerting/server/authorization/alerting_authorization.mock.ts index 4e4cd4419a5a2..fd29274422b13 100644 --- a/x-pack/plugins/alerting/server/authorization/alerting_authorization.mock.ts +++ b/x-pack/plugins/alerting/server/authorization/alerting_authorization.mock.ts @@ -16,6 +16,7 @@ const createAlertingAuthorizationMock = () => { ensureAuthorized: jest.fn(), filterByRuleTypeAuthorization: jest.fn(), getFindAuthorizationFilter: jest.fn(), + getAuthorizedAlertsIndices: jest.fn(), }; return mocked; }; diff --git a/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts b/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts index 7506accd8b88e..46cb9444d4732 100644 --- a/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts +++ b/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts @@ -138,6 +138,14 @@ export class AlertingAuthorization { return this.authorization?.mode?.useRbacForRequest(this.request) ?? false; } + public getAuthorizedAlertsIndices(owner: string): string | undefined { + return owner === 'apm' + ? '.alerts-observability-apm' + : owner === 'securitySolution' + ? '.siem-signals*' + : undefined; + } + public async ensureAuthorized({ ruleTypeId, consumer, operation, entity }: EnsureAuthorizedOpts) { const { authorization } = this; @@ -158,7 +166,7 @@ export class AlertingAuthorization { const shouldAuthorizeConsumer = !this.exemptConsumerIds.includes(consumer); const checkPrivileges = authorization.checkPrivilegesDynamicallyWithRequest(this.request); - const { hasAllRequested, username, privileges } = await checkPrivileges({ + const checkPrivParams = { kibana: shouldAuthorizeConsumer && consumer !== ruleType.producer ? [ @@ -172,7 +180,8 @@ export class AlertingAuthorization { // be created for exempt consumers if user has producer level privileges requiredPrivilegesByScope.producer, ], - }); + }; + const { hasAllRequested, username, privileges } = await checkPrivileges(checkPrivParams); if (!isAvailableConsumer) { /** @@ -182,6 +191,7 @@ export class AlertingAuthorization { * as Privileged. * This check will ensure we don't accidentally let these through */ + // This should also log the type they're trying to access rule/alert throw Boom.forbidden( this.auditLogger.logAuthorizationFailure( username, 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..f9d7e293363d8 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: [], @@ -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..5af7f1130beff 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, 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/server/alert_data_client/alert_client.ts b/x-pack/plugins/rule_registry/server/alert_data_client/alert_client.ts new file mode 100644 index 0000000000000..30bed3026e58c --- /dev/null +++ b/x-pack/plugins/rule_registry/server/alert_data_client/alert_client.ts @@ -0,0 +1,400 @@ +/* + * 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 Boom from '@hapi/boom'; +import { estypes } from '@elastic/elasticsearch'; +import { PublicMethodsOf } from '@kbn/utility-types'; + +import { SanitizedAlert } from '../../../alerting/common'; +import { + AlertTypeParams, + // PartialAlert +} from '../../../alerting/server'; +import { + ReadOperations, + // AlertingAuthorizationFilterType, + AlertingAuthorization, + WriteOperations, + AlertingAuthorizationEntity, +} from '../../../alerting/server'; +import { Logger, ElasticsearchClient, HttpResponsePayload } from '../../../../../src/core/server'; +import { ParsedTechnicalFields } from '../../common/parse_technical_fields'; +import { RacAuthorizationAuditLogger } from './audit_logger'; +import { RuleDataPluginService } from '../rule_data_plugin_service'; + +export interface ConstructorOptions { + logger: Logger; + authorization: PublicMethodsOf; + spaceId?: string; + auditLogger: RacAuthorizationAuditLogger; + esClient: ElasticsearchClient; + index: string; + ruleDataService: RuleDataPluginService; +} + +interface IndexType { + [key: string]: unknown; +} + +export interface FindOptions extends IndexType { + perPage?: number; + page?: number; + search?: string; + defaultSearchOperator?: 'AND' | 'OR'; + searchFields?: string[]; + sortField?: string; + sortOrder?: estypes.SortOrder; + hasReference?: { + type: string; + id: string; + }; + fields?: string[]; + filter?: string; +} + +export interface CreateAlertParams { + esClient: ElasticsearchClient; + owner: 'observability' | 'securitySolution'; +} + +export interface FindResult { + page: number; + perPage: number; + total: number; + data: Array>; +} + +export interface UpdateOptions { + id: string; + owner: string; + data: { + status: string; + }; + assetName: string; // observability-apm see here: x-pack/plugins/apm/server/plugin.ts:191 +} + +export interface BulkUpdateOptions { + ids: string[]; + owner: string; + data: { + status: string; + }; + query: unknown; + assetName: string; +} + +interface GetAlertParams { + id: string; + assetName: string; // observability-apm see here: x-pack/plugins/apm/server/plugin.ts:191 +} + +export interface GetAlertInstanceSummaryParams { + id: string; + dateStart?: string; +} + +// const alertingAuthorizationFilterOpts: AlertingAuthorizationFilterOpts = { +// type: AlertingAuthorizationFilterType.ESDSL, +// fieldNames: { ruleTypeId: 'alert.alertTypeId', consumer: 'alert.owner' }, +// }; + +export class AlertsClient { + private readonly logger: Logger; + private readonly auditLogger: RacAuthorizationAuditLogger; + // private readonly spaceId?: string; + // private readonly alertsIndex: string; + private readonly authorization: PublicMethodsOf; + private readonly esClient: ElasticsearchClient; + private readonly ruleDataService: RuleDataPluginService; + + constructor({ + auditLogger, + authorization, + logger, + spaceId, + esClient, + index, + ruleDataService, + }: ConstructorOptions) { + this.logger = logger; + // this.spaceId = spaceId; + this.authorization = authorization; + this.esClient = esClient; + this.auditLogger = auditLogger; + // this.alertsIndex = index; + 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) { + // possibly append spaceId here? + return this.ruleDataService.getFullAssetName(assetName); // await this.authorization.getAuthorizedAlertsIndices(); + } + + // TODO: Type out alerts (rule registry fields + alerting alerts type) + public async get({ + id, + assetName, + }: GetAlertParams): Promise { + // first search for the alert specified, then check if user has access to it + // and return search results + // const query = buildAlertsSearchQuery({ + // index: this.getAlertsIndex(assetName), // '.alerts-observability-apm', + // alertId: id, + // }); + // TODO: Type out alerts (rule registry fields + alerting alerts type) + try { + const { body: result } = await this.esClient.get({ + index: this.getAlertsIndex(assetName), // '.alerts-observability-apm', + id, + }); + if ( + result == null || + result._source == null || + result._source['rule.id'] == null || + result._source['kibana.rac.alert.owner'] == null + ) { + return undefined; + } + + try { + // use security plugin routes to check what URIs user is authorized to + await this.authorization.ensureAuthorized({ + ruleTypeId: result._source['rule.id'], + consumer: result._source['kibana.rac.alert.owner'], + operation: ReadOperations.Get, + entity: AlertingAuthorizationEntity.Alert, + }); + } catch (error) { + throw Boom.forbidden( + this.auditLogger.racAuthorizationFailure({ + owner: result._source['kibana.rac.alert.owner'], + operation: ReadOperations.Get, + type: 'access', + }) + ); + } + + return result; + } catch (exc) { + throw exc; + } + } + + // public async find({ owner }: { owner: string }): Promise { + // let authorizationTuple; + // try { + // authorizationTuple = await this.authorization.getFindAuthorizationFilter( + // AlertingAuthorizationEntity.Alert, + // alertingAuthorizationFilterOpts + // ); + // } catch (error) { + // this.auditLogger.racAuthorizationFailure({ + // owner, + // operation: ReadOperations.Find, + // type: 'access', + // }); + // throw error; + // } + + // const { + // filter: authorizationFilter, + // ensureRuleTypeIsAuthorized, + // logSuccessfulAuthorization, + // } = authorizationTuple; + + // try { + // ensureRuleTypeIsAuthorized('siem.signals', owner, AlertingAuthorizationEntity.Alert); + // } catch (error) { + // this.logger.error(`Unable to bulk find alerts for ${owner}. Error follows: ${error}`); + // throw error; + // } + // } + + public async update({ + id, + owner, + data, + assetName, + }: UpdateOptions): Promise { + // TODO: Type out alerts (rule registry fields + alerting alerts type) + // TODO: use MGET + const { body: result } = await this.esClient.get({ + index: this.getAlertsIndex(assetName), // '.alerts-observability-apm', // '.siem-signals-devin-hurley-default', + id, + }); + const hits = result._source; + if (hits == null || hits['rule.id'] == null || hits['kibana.rac.alert.owner'] == null) { + return undefined; + } + + try { + // ASSUMPTION: user bulk updating alerts from single owner/space + // may need to iterate to support rules shared across spaces + await this.authorization.ensureAuthorized({ + ruleTypeId: hits['rule.id'], + consumer: hits['kibana.rac.alert.owner'], + operation: WriteOperations.Update, + entity: AlertingAuthorizationEntity.Alert, + }); + + try { + const index = this.getAlertsIndex(assetName); // this.authorization.getAuthorizedAlertsIndices(hits['kibana.rac.alert.owner']); + + const updateParameters = { + id, + index, + body: { + doc: { + 'kibana.rac.alert.status': data.status, + }, + }, + }; + + const res = await this.esClient.update( + updateParameters + ); + return res.body.get?._source; + } catch (error) { + // TODO: Update error message + this.logger.error(''); + throw error; + } + } catch (error) { + throw Boom.forbidden( + this.auditLogger.racAuthorizationFailure({ + owner: hits['kibana.rac.alert.owner'], + operation: ReadOperations.Get, + type: 'access', + }) + ); + } + } + + // public async bulkUpdate({ + // ids, + // query, + // assetName, + // data, + // }: BulkUpdateOptions): Promise> { + // const { status } = data; + // let queryObject; + // if (ids) { + // // maybe use an aggs query to make this fast + // queryObject = { + // ids: { values: ids }, + // // USE AGGS and then get returned fields against ensureAuthorizedForAllRuleTypes + // aggs: { + // ...(await this.authorization.getFindAuthorizationFilter( + // AlertingAuthorizationEntity.Alert, + // { + // type: AlertingAuthorizationFilterType.ESDSL, + // fieldNames: { consumer: 'kibana.rac.alert.owner', ruleTypeId: 'rule.id' }, + // }, + // WriteOperations.Update + // )), + // }, + // }; + // } + // console.error('QUERY OBJECT', JSON.stringify(queryObject, null, 2)); + // if (query) { + // queryObject = { + // bool: { + // ...query, + // }, + // }; + // } + // try { + // const result = await this.esClient.updateByQuery({ + // index: this.getAlertsIndex(assetName), + // conflicts: 'abort', // conflicts ?? 'abort', + // // @ts-expect-error refresh should allow for 'wait_for' + // refresh: 'wait_for', + // body: { + // script: { + // source: `ctx._source.signal.status = '${status}'`, + // lang: 'painless', + // }, + // query: queryObject, + // }, + // ignoreUnavailable: true, + // }); + // return result; + // } catch (err) { + // // TODO: Update error message + // this.logger.error(''); + // console.error('UPDATE ERROR', JSON.stringify(err, null, 2)); + // throw err; + // } + // // Looking like we may need to first fetch the alerts to ensure we are + // // pulling the correct ruleTypeId and owner + // // await this.esClient.mget() + + // // try { + // // // ASSUMPTION: user bulk updating alerts from single owner/space + // // // may need to iterate to support rules shared across spaces + + // // const ruleTypes = await this.authorization.ensureAuthorizedForAllRuleTypes({ + // // owner, + // // operation: WriteOperations.Update, + // // entity: AlertingAuthorizationEntity.Alert, + // // }); + + // // const totalRuleTypes = this.authorization.getRuleTypesByProducer(owner); + + // // console.error('RULE TYPES', ruleTypes); + + // // // await this.authorization.ensureAuthorized({ + // // // ruleTypeId: 'siem.signals', // can they update multiple at once or will a single one just be passed in? + // // // consumer: owner, + // // // operation: WriteOperations.Update, + // // // entity: AlertingAuthorizationEntity.Alert, + // // // }); + + // // try { + // // const index = this.authorization.getAuthorizedAlertsIndices(owner); + // // if (index == null) { + // // throw Error(`cannot find authorized index for owner: ${owner}`); + // // } + + // // const body = ids.flatMap((id) => [ + // // { + // // update: { + // // _id: id, + // // _index: this.authorization.getAuthorizedAlertsIndices(ruleTypes[0].producer), + // // }, + // // }, + // // { + // // doc: { 'kibana.rac.alert.status': data.status }, + // // }, + // // ]); + + // // const result = await this.esClient.bulk({ + // // index, + // // body, + // // }); + // // return result; + // // } catch (updateError) { + // // this.logger.error( + // // `Unable to bulk update alerts for ${owner}. Error follows: ${updateError}` + // // ); + // // throw updateError; + // // } + // // } catch (error) { + // // console.error("HERE'S THE ERROR", error); + // // throw Boom.forbidden( + // // this.auditLogger.racAuthorizationFailure({ + // // owner, + // // operation: ReadOperations.Get, + // // type: 'access', + // // }) + // // ); + // // } + // } +} diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/alert_client_factory.ts b/x-pack/plugins/rule_registry/server/alert_data_client/alert_client_factory.ts new file mode 100644 index 0000000000000..5d110222ca788 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/alert_data_client/alert_client_factory.ts @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { 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 './alert_client'; +import { RacAuthorizationAuditLogger } from './audit_logger'; +import { RuleDataPluginService } from '../rule_data_plugin_service'; + +export interface RacClientFactoryOpts { + logger: Logger; + getSpaceId: (request: KibanaRequest) => string | undefined; + esClient: ElasticsearchClient; + getAlertingAuthorization: (request: KibanaRequest) => PublicMethodsOf; + securityPluginSetup: SecurityPluginSetup | undefined; + ruleDataService: RuleDataPluginService | undefined; +} + +export class AlertsClientFactory { + private isInitialized = false; + private logger!: Logger; + private getSpaceId!: (request: KibanaRequest) => string | undefined; + private esClient!: ElasticsearchClient; + private getAlertingAuthorization!: ( + request: KibanaRequest + ) => PublicMethodsOf; + private securityPluginSetup!: SecurityPluginSetup | undefined; + private ruleDataService!: RuleDataPluginService | undefined; + + public initialize(options: RacClientFactoryOpts) { + /** + * This should be called by the plugin's start() method. + */ + if (this.isInitialized) { + throw new Error('AlertsClientFactory (RAC) already initialized'); + } + + this.getAlertingAuthorization = options.getAlertingAuthorization; + this.isInitialized = true; + this.logger = options.logger; + this.getSpaceId = options.getSpaceId; + this.esClient = options.esClient; + this.securityPluginSetup = options.securityPluginSetup; + this.ruleDataService = options.ruleDataService; + } + + public async create(request: KibanaRequest, index: string): Promise { + const { securityPluginSetup, getAlertingAuthorization, logger } = this; + const spaceId = this.getSpaceId(request); + + return new AlertsClient({ + spaceId, + logger, + index, + authorization: getAlertingAuthorization(request), + auditLogger: new RacAuthorizationAuditLogger(securityPluginSetup?.audit.asScoped(request)), + esClient: this.esClient, + ruleDataService: this.ruleDataService!, + }); + } +} diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/audit_logger.ts b/x-pack/plugins/rule_registry/server/alert_data_client/audit_logger.ts new file mode 100644 index 0000000000000..cdbf9a3b48d42 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/alert_data_client/audit_logger.ts @@ -0,0 +1,89 @@ +/* + * 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 { EcsEventType } from '@kbn/logging'; +import { AuditLogger } from '../../../security/server'; + +export enum AuthorizationResult { + Unauthorized = 'Unauthorized', + Authorized = 'Authorized', +} + +export enum Result { + Success = 'Success', + Failure = 'Failure', +} + +export class RacAuthorizationAuditLogger { + private readonly auditLogger: AuditLogger; + + constructor(auditLogger: AuditLogger = { log() {} }) { + this.auditLogger = auditLogger; + } + + public getAuthorizationMessage( + authorizationResult: AuthorizationResult, + owner: string, + operation: string + ): string { + return `${authorizationResult} to ${operation} "${owner}" alert"`; + } + + public racAuthorizationFailure({ + owner, + operation, + type, + error, + }: { + owner: string; + operation: string; + type: EcsEventType; + error?: Error; + }): string { + const message = this.getAuthorizationMessage( + AuthorizationResult.Unauthorized, + owner, + operation + ); + this.auditLogger.log({ + message, + event: { + action: 'rac_authorization_failure', + category: ['authentication'], + type: [type], + outcome: 'failure', + }, + error: error && { + code: error.name, + message: error.message, + }, + }); + return message; + } + + public racAuthorizationSuccess({ + owner, + operation, + type, + }: { + owner: string; + operation: string; + type: EcsEventType; + }): string { + const message = this.getAuthorizationMessage(AuthorizationResult.Authorized, owner, operation); + this.auditLogger.log({ + message, + event: { + action: 'rac_authorization_success', + category: ['authentication'], + type: [type], + outcome: 'success', + }, + }); + return message; + } +} diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/utils.ts b/x-pack/plugins/rule_registry/server/alert_data_client/utils.ts new file mode 100644 index 0000000000000..0cadbafd63916 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/alert_data_client/utils.ts @@ -0,0 +1,72 @@ +/* + * 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. + */ + +interface BuildAlertsSearchQuery { + alertId: string; + index: string; + from?: string; + to?: string; + size?: number; +} + +export const buildAlertsSearchQuery = ({ + alertId, + index, + from, + to, + size, +}: BuildAlertsSearchQuery) => ({ + index, + body: { + size, + query: { + bool: { + filter: [ + { + bool: { + should: { + match: { + _id: alertId, + }, + }, + minimum_should_match: 1, + }, + }, + // { + // range: { + // '@timestamp': { + // gt: from, + // lte: to, + // format: 'epoch_millis', + // }, + // }, + // }, + ], + }, + }, + }, +}); + +interface BuildAlertsUpdateParams { + ids: string[]; + index: string; + status: string; +} + +export const buildAlertsUpdateParameters = ({ ids, index, status }: BuildAlertsUpdateParams) => ({ + index, + body: ids.flatMap((id) => [ + { + update: { + _id: id, + }, + }, + { + doc: { 'kibana.rac.alert.status': status }, + }, + ]), +}); 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..4a213ecfbc8c6 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/alert_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 | undefined; + 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,51 @@ 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, + getSpaceId(request: KibanaRequest) { + return plugins.spaces?.spacesService.getSpaceId(request); + }, + 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, this.config.index); + }; + + return { + getRacClientWithRequest, + alerting: plugins.alerting, + }; + } + + private createRouteHandlerContext = (): IContextProvider => { + const { alertsClientFactory, config } = this; + return async function alertsRouteHandlerContext( + context, + request + ): Promise { + return { + getAlertsClient: async () => { + const createdClient = alertsClientFactory.create(request, config.index); + 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..ec8d83b933705 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/routes/get_alert_by_id.ts @@ -0,0 +1,69 @@ +/* + * 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 }); + if (alert == null) { + throw new Error('could not get alert'); + } + 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..52f0200c47a39 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/routes/update_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 { 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], + owner: 'apm', + 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/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..4ec4ca1afff02 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/alert_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..28a929aa49e33 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'; @@ -72,6 +73,12 @@ export const createLifecycleRuleTypeFactory: CreateLifecycleRuleTypeFactory = ({ } = options; const ruleExecutorData = getRuleExecutorData(type, options); + const so = await options.services.savedObjectsClient.get( + 'alert', + ruleExecutorData[RULE_UUID] + ); + + logger.debug(`LOGGER RULE REGISTRY CONSUMER ${so.attributes.consumer}`); const decodedState = wrappedStateRt.decode(previousState); @@ -180,6 +187,7 @@ export const createLifecycleRuleTypeFactory: CreateLifecycleRuleTypeFactory = ({ ...ruleExecutorData, [TIMESTAMP]: timestamp, [EVENT_KIND]: 'state', + [OWNER]: so.attributes.consumer, [ALERT_ID]: alertId, }; @@ -220,7 +228,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/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/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 { 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..67678bdbc8f04 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 = { @@ -112,7 +112,7 @@ describe('buildSignal', () => { }; const rule = getRulesSchemaMock(); const signal = { - ...buildSignal([doc], rule), + ...buildSignal([doc], rule, 'siem'), ...additionalSignalFields(doc), }; const expected: Signal = { 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..aeb03d8729fe3 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -319,7 +319,7 @@ export class Plugin implements IPlugin; 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/common/config.ts b/x-pack/test/detection_engine_api_integration/common/config.ts index 659c836eb9207..88dc51efd0549 100644 --- a/x-pack/test/detection_engine_api_integration/common/config.ts +++ b/x-pack/test/detection_engine_api_integration/common/config.ts @@ -70,6 +70,18 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) `--xpack.actions.allowedHosts=${JSON.stringify(['localhost', 'some.non.existent.com'])}`, `--xpack.actions.enabledActionTypes=${JSON.stringify(enabledActionTypes)}`, '--xpack.eventLog.logEntries=true', + `--logging.verbose=true`, + `--logging.events.log=${JSON.stringify([ + 'alerts', + 'ruleRegistry', + 'info', + 'warning', + 'error', + 'fatal', + ])}`, + `--logging.events.request=${JSON.stringify(['info', 'warning', 'error', 'fatal'])}`, + `--logging.events.error='*'`, + `--logging.events.ops=__no-ops__`, ...disabledPlugins.map((key) => `--xpack.${key}.enabled=false`), ...(ssl ? [ 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/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..71676283b5cb2 --- /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) { + const res = 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..961cd8bdfb5f0 --- /dev/null +++ b/x-pack/test/rule_registry/security_and_spaces/tests/basic/update_alert.ts @@ -0,0 +1,158 @@ +/* + * 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', () => { + 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); + // }); + // }); + }); +}; From 8f10a4d4c52a6ca6e7abb24039fa77df37ade284 Mon Sep 17 00:00:00 2001 From: Devin Hurley Date: Tue, 1 Jun 2021 20:47:47 -0400 Subject: [PATCH 02/16] update security solution jest tests --- .../detection_engine/signals/build_bulk_body.test.ts | 10 ++++++++++ .../lib/detection_engine/signals/build_signal.test.ts | 2 ++ 2 files changed, 12 insertions(+) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.test.ts index 4d3ca26f5a71e..5300c4d9e2092 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.test.ts @@ -50,6 +50,7 @@ describe('buildBulkBody', () => { _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_signal.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.test.ts index 67678bdbc8f04..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 @@ -61,6 +61,7 @@ describe('buildSignal', () => { }, ], original_time: '2020-04-20T21:27:45.000Z', + owner: 'siem', status: 'open', rule: { author: [], @@ -142,6 +143,7 @@ describe('buildSignal', () => { }, ], original_time: '2020-04-20T21:27:45.000Z', + owner: 'siem', original_event: { action: 'socket_opened', dataset: 'socket', From d1a252acba97179eb1215d5077071d14ece866b5 Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Tue, 1 Jun 2021 14:34:48 -0700 Subject: [PATCH 03/16] cleaning up PR and adding basic unit tests --- .../server/alert_data_client/alert_client.ts | 400 ------------------ .../alert_data_client/alerts_client.mock.ts | 27 ++ .../server/alert_data_client/alerts_client.ts | 162 +++++++ .../alerts_client_factory.test.ts | 94 ++++ ...nt_factory.ts => alerts_client_factory.ts} | 25 +- .../alert_data_client/audit_events.test.ts | 87 ++++ .../server/alert_data_client/audit_events.ts | 61 +++ .../server/alert_data_client/audit_logger.ts | 89 ---- .../alert_data_client/tests/get.test.ts | 164 +++++++ .../server/alert_data_client/utils.ts | 72 ---- x-pack/plugins/rule_registry/server/plugin.ts | 13 +- .../server/rule_data_plugin_service/index.ts | 2 +- .../rule_data_plugin_service.mock.ts | 31 ++ 13 files changed, 643 insertions(+), 584 deletions(-) delete mode 100644 x-pack/plugins/rule_registry/server/alert_data_client/alert_client.ts create mode 100644 x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.mock.ts create mode 100644 x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts create mode 100644 x-pack/plugins/rule_registry/server/alert_data_client/alerts_client_factory.test.ts rename x-pack/plugins/rule_registry/server/alert_data_client/{alert_client_factory.ts => alerts_client_factory.ts} (71%) create mode 100644 x-pack/plugins/rule_registry/server/alert_data_client/audit_events.test.ts create mode 100644 x-pack/plugins/rule_registry/server/alert_data_client/audit_events.ts delete mode 100644 x-pack/plugins/rule_registry/server/alert_data_client/audit_logger.ts create mode 100644 x-pack/plugins/rule_registry/server/alert_data_client/tests/get.test.ts delete mode 100644 x-pack/plugins/rule_registry/server/alert_data_client/utils.ts create mode 100644 x-pack/plugins/rule_registry/server/rule_data_plugin_service/rule_data_plugin_service.mock.ts diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/alert_client.ts b/x-pack/plugins/rule_registry/server/alert_data_client/alert_client.ts deleted file mode 100644 index 30bed3026e58c..0000000000000 --- a/x-pack/plugins/rule_registry/server/alert_data_client/alert_client.ts +++ /dev/null @@ -1,400 +0,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 Boom from '@hapi/boom'; -import { estypes } from '@elastic/elasticsearch'; -import { PublicMethodsOf } from '@kbn/utility-types'; - -import { SanitizedAlert } from '../../../alerting/common'; -import { - AlertTypeParams, - // PartialAlert -} from '../../../alerting/server'; -import { - ReadOperations, - // AlertingAuthorizationFilterType, - AlertingAuthorization, - WriteOperations, - AlertingAuthorizationEntity, -} from '../../../alerting/server'; -import { Logger, ElasticsearchClient, HttpResponsePayload } from '../../../../../src/core/server'; -import { ParsedTechnicalFields } from '../../common/parse_technical_fields'; -import { RacAuthorizationAuditLogger } from './audit_logger'; -import { RuleDataPluginService } from '../rule_data_plugin_service'; - -export interface ConstructorOptions { - logger: Logger; - authorization: PublicMethodsOf; - spaceId?: string; - auditLogger: RacAuthorizationAuditLogger; - esClient: ElasticsearchClient; - index: string; - ruleDataService: RuleDataPluginService; -} - -interface IndexType { - [key: string]: unknown; -} - -export interface FindOptions extends IndexType { - perPage?: number; - page?: number; - search?: string; - defaultSearchOperator?: 'AND' | 'OR'; - searchFields?: string[]; - sortField?: string; - sortOrder?: estypes.SortOrder; - hasReference?: { - type: string; - id: string; - }; - fields?: string[]; - filter?: string; -} - -export interface CreateAlertParams { - esClient: ElasticsearchClient; - owner: 'observability' | 'securitySolution'; -} - -export interface FindResult { - page: number; - perPage: number; - total: number; - data: Array>; -} - -export interface UpdateOptions { - id: string; - owner: string; - data: { - status: string; - }; - assetName: string; // observability-apm see here: x-pack/plugins/apm/server/plugin.ts:191 -} - -export interface BulkUpdateOptions { - ids: string[]; - owner: string; - data: { - status: string; - }; - query: unknown; - assetName: string; -} - -interface GetAlertParams { - id: string; - assetName: string; // observability-apm see here: x-pack/plugins/apm/server/plugin.ts:191 -} - -export interface GetAlertInstanceSummaryParams { - id: string; - dateStart?: string; -} - -// const alertingAuthorizationFilterOpts: AlertingAuthorizationFilterOpts = { -// type: AlertingAuthorizationFilterType.ESDSL, -// fieldNames: { ruleTypeId: 'alert.alertTypeId', consumer: 'alert.owner' }, -// }; - -export class AlertsClient { - private readonly logger: Logger; - private readonly auditLogger: RacAuthorizationAuditLogger; - // private readonly spaceId?: string; - // private readonly alertsIndex: string; - private readonly authorization: PublicMethodsOf; - private readonly esClient: ElasticsearchClient; - private readonly ruleDataService: RuleDataPluginService; - - constructor({ - auditLogger, - authorization, - logger, - spaceId, - esClient, - index, - ruleDataService, - }: ConstructorOptions) { - this.logger = logger; - // this.spaceId = spaceId; - this.authorization = authorization; - this.esClient = esClient; - this.auditLogger = auditLogger; - // this.alertsIndex = index; - 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) { - // possibly append spaceId here? - return this.ruleDataService.getFullAssetName(assetName); // await this.authorization.getAuthorizedAlertsIndices(); - } - - // TODO: Type out alerts (rule registry fields + alerting alerts type) - public async get({ - id, - assetName, - }: GetAlertParams): Promise { - // first search for the alert specified, then check if user has access to it - // and return search results - // const query = buildAlertsSearchQuery({ - // index: this.getAlertsIndex(assetName), // '.alerts-observability-apm', - // alertId: id, - // }); - // TODO: Type out alerts (rule registry fields + alerting alerts type) - try { - const { body: result } = await this.esClient.get({ - index: this.getAlertsIndex(assetName), // '.alerts-observability-apm', - id, - }); - if ( - result == null || - result._source == null || - result._source['rule.id'] == null || - result._source['kibana.rac.alert.owner'] == null - ) { - return undefined; - } - - try { - // use security plugin routes to check what URIs user is authorized to - await this.authorization.ensureAuthorized({ - ruleTypeId: result._source['rule.id'], - consumer: result._source['kibana.rac.alert.owner'], - operation: ReadOperations.Get, - entity: AlertingAuthorizationEntity.Alert, - }); - } catch (error) { - throw Boom.forbidden( - this.auditLogger.racAuthorizationFailure({ - owner: result._source['kibana.rac.alert.owner'], - operation: ReadOperations.Get, - type: 'access', - }) - ); - } - - return result; - } catch (exc) { - throw exc; - } - } - - // public async find({ owner }: { owner: string }): Promise { - // let authorizationTuple; - // try { - // authorizationTuple = await this.authorization.getFindAuthorizationFilter( - // AlertingAuthorizationEntity.Alert, - // alertingAuthorizationFilterOpts - // ); - // } catch (error) { - // this.auditLogger.racAuthorizationFailure({ - // owner, - // operation: ReadOperations.Find, - // type: 'access', - // }); - // throw error; - // } - - // const { - // filter: authorizationFilter, - // ensureRuleTypeIsAuthorized, - // logSuccessfulAuthorization, - // } = authorizationTuple; - - // try { - // ensureRuleTypeIsAuthorized('siem.signals', owner, AlertingAuthorizationEntity.Alert); - // } catch (error) { - // this.logger.error(`Unable to bulk find alerts for ${owner}. Error follows: ${error}`); - // throw error; - // } - // } - - public async update({ - id, - owner, - data, - assetName, - }: UpdateOptions): Promise { - // TODO: Type out alerts (rule registry fields + alerting alerts type) - // TODO: use MGET - const { body: result } = await this.esClient.get({ - index: this.getAlertsIndex(assetName), // '.alerts-observability-apm', // '.siem-signals-devin-hurley-default', - id, - }); - const hits = result._source; - if (hits == null || hits['rule.id'] == null || hits['kibana.rac.alert.owner'] == null) { - return undefined; - } - - try { - // ASSUMPTION: user bulk updating alerts from single owner/space - // may need to iterate to support rules shared across spaces - await this.authorization.ensureAuthorized({ - ruleTypeId: hits['rule.id'], - consumer: hits['kibana.rac.alert.owner'], - operation: WriteOperations.Update, - entity: AlertingAuthorizationEntity.Alert, - }); - - try { - const index = this.getAlertsIndex(assetName); // this.authorization.getAuthorizedAlertsIndices(hits['kibana.rac.alert.owner']); - - const updateParameters = { - id, - index, - body: { - doc: { - 'kibana.rac.alert.status': data.status, - }, - }, - }; - - const res = await this.esClient.update( - updateParameters - ); - return res.body.get?._source; - } catch (error) { - // TODO: Update error message - this.logger.error(''); - throw error; - } - } catch (error) { - throw Boom.forbidden( - this.auditLogger.racAuthorizationFailure({ - owner: hits['kibana.rac.alert.owner'], - operation: ReadOperations.Get, - type: 'access', - }) - ); - } - } - - // public async bulkUpdate({ - // ids, - // query, - // assetName, - // data, - // }: BulkUpdateOptions): Promise> { - // const { status } = data; - // let queryObject; - // if (ids) { - // // maybe use an aggs query to make this fast - // queryObject = { - // ids: { values: ids }, - // // USE AGGS and then get returned fields against ensureAuthorizedForAllRuleTypes - // aggs: { - // ...(await this.authorization.getFindAuthorizationFilter( - // AlertingAuthorizationEntity.Alert, - // { - // type: AlertingAuthorizationFilterType.ESDSL, - // fieldNames: { consumer: 'kibana.rac.alert.owner', ruleTypeId: 'rule.id' }, - // }, - // WriteOperations.Update - // )), - // }, - // }; - // } - // console.error('QUERY OBJECT', JSON.stringify(queryObject, null, 2)); - // if (query) { - // queryObject = { - // bool: { - // ...query, - // }, - // }; - // } - // try { - // const result = await this.esClient.updateByQuery({ - // index: this.getAlertsIndex(assetName), - // conflicts: 'abort', // conflicts ?? 'abort', - // // @ts-expect-error refresh should allow for 'wait_for' - // refresh: 'wait_for', - // body: { - // script: { - // source: `ctx._source.signal.status = '${status}'`, - // lang: 'painless', - // }, - // query: queryObject, - // }, - // ignoreUnavailable: true, - // }); - // return result; - // } catch (err) { - // // TODO: Update error message - // this.logger.error(''); - // console.error('UPDATE ERROR', JSON.stringify(err, null, 2)); - // throw err; - // } - // // Looking like we may need to first fetch the alerts to ensure we are - // // pulling the correct ruleTypeId and owner - // // await this.esClient.mget() - - // // try { - // // // ASSUMPTION: user bulk updating alerts from single owner/space - // // // may need to iterate to support rules shared across spaces - - // // const ruleTypes = await this.authorization.ensureAuthorizedForAllRuleTypes({ - // // owner, - // // operation: WriteOperations.Update, - // // entity: AlertingAuthorizationEntity.Alert, - // // }); - - // // const totalRuleTypes = this.authorization.getRuleTypesByProducer(owner); - - // // console.error('RULE TYPES', ruleTypes); - - // // // await this.authorization.ensureAuthorized({ - // // // ruleTypeId: 'siem.signals', // can they update multiple at once or will a single one just be passed in? - // // // consumer: owner, - // // // operation: WriteOperations.Update, - // // // entity: AlertingAuthorizationEntity.Alert, - // // // }); - - // // try { - // // const index = this.authorization.getAuthorizedAlertsIndices(owner); - // // if (index == null) { - // // throw Error(`cannot find authorized index for owner: ${owner}`); - // // } - - // // const body = ids.flatMap((id) => [ - // // { - // // update: { - // // _id: id, - // // _index: this.authorization.getAuthorizedAlertsIndices(ruleTypes[0].producer), - // // }, - // // }, - // // { - // // doc: { 'kibana.rac.alert.status': data.status }, - // // }, - // // ]); - - // // const result = await this.esClient.bulk({ - // // index, - // // body, - // // }); - // // return result; - // // } catch (updateError) { - // // this.logger.error( - // // `Unable to bulk update alerts for ${owner}. Error follows: ${updateError}` - // // ); - // // throw updateError; - // // } - // // } catch (error) { - // // console.error("HERE'S THE ERROR", error); - // // throw Boom.forbidden( - // // this.auditLogger.racAuthorizationFailure({ - // // owner, - // // operation: ReadOperations.Get, - // // type: 'access', - // // }) - // // ); - // // } - // } -} 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..56227d7664217 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts @@ -0,0 +1,162 @@ +/* + * 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'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { RawAlert } from '../../../alerting/server/types'; +import { AlertTypeParams, PartialAlert } from '../../../alerting/server'; +import { + ReadOperations, + AlertingAuthorization, + WriteOperations, + AlertingAuthorizationEntity, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../alerting/server/authorization'; +import { Logger, ElasticsearchClient, HttpResponsePayload } from '../../../../../src/core/server'; +import { alertAuditEvent, AlertAuditAction } from './audit_events'; +import { RuleDataPluginService } from '../rule_data_plugin_service'; +import { AuditLogger } from '../../../security/server'; + +export interface ConstructorOptions { + logger: Logger; + authorization: PublicMethodsOf; + auditLogger?: AuditLogger; + esClient: ElasticsearchClient; + ruleDataService: RuleDataPluginService; +} + +export interface UpdateOptions { + id: string; + owner: 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: RuleDataPluginService; + + 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); + } + + // TODO: Type out alerts (rule registry fields + alerting alerts type) + 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 { body: result } = await this.esClient.get({ + index: this.getAlertsIndex(assetName), + id, + }); + + // this.authorization leverages the alerting plugin's authorization + // client exposed to us for reuse + await this.authorization.ensureAuthorized({ + ruleTypeId: result._source['rule.id'], + consumer: result._source['kibana.rac.alert.owner'], + operation: ReadOperations.Get, + entity: AlertingAuthorizationEntity.Alert, + }); + + this.auditLogger?.log( + alertAuditEvent({ + action: AlertAuditAction.GET, + id, + }) + ); + + return result; + } 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, + owner, + data, + assetName, + }: UpdateOptions): Promise> { + try { + // TODO: Type out alerts (rule registry fields + alerting alerts type) + const result = await this.esClient.get({ + index: this.getAlertsIndex(assetName), + id, + }); + const hits = result.body._source; + + // ASSUMPTION: user bulk updating alerts from single owner/space + // may need to iterate to support rules shared across spaces + await this.authorization.ensureAuthorized({ + ruleTypeId: hits['rule.id'], + consumer: hits['kibana.rac.alert.owner'], + operation: WriteOperations.Update, + entity: AlertingAuthorizationEntity.Alert, + }); + + const index = this.getAlertsIndex(assetName); + + const updateParameters = { + id, + index, + body: { + doc: { + 'kibana.rac.alert.status': data.status, + }, + }, + }; + + return this.esClient.update(updateParameters); + } catch (error) { + this.auditLogger?.log( + alertAuditEvent({ + action: AlertAuditAction.GET, + 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..8afc33c836293 --- /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: jest.Mocked = { + 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/alert_client_factory.ts b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client_factory.ts similarity index 71% rename from x-pack/plugins/rule_registry/server/alert_data_client/alert_client_factory.ts rename to x-pack/plugins/rule_registry/server/alert_data_client/alerts_client_factory.ts index 5d110222ca788..f494e2e6e829a 100644 --- a/x-pack/plugins/rule_registry/server/alert_data_client/alert_client_factory.ts +++ b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client_factory.ts @@ -10,31 +10,28 @@ 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 './alert_client'; -import { RacAuthorizationAuditLogger } from './audit_logger'; +import { AlertsClient } from './alerts_client'; import { RuleDataPluginService } from '../rule_data_plugin_service'; -export interface RacClientFactoryOpts { +export interface AlertsClientFactoryProps { logger: Logger; - getSpaceId: (request: KibanaRequest) => string | undefined; esClient: ElasticsearchClient; getAlertingAuthorization: (request: KibanaRequest) => PublicMethodsOf; securityPluginSetup: SecurityPluginSetup | undefined; - ruleDataService: RuleDataPluginService | undefined; + ruleDataService: RuleDataPluginService | null; } export class AlertsClientFactory { private isInitialized = false; private logger!: Logger; - private getSpaceId!: (request: KibanaRequest) => string | undefined; private esClient!: ElasticsearchClient; private getAlertingAuthorization!: ( request: KibanaRequest ) => PublicMethodsOf; private securityPluginSetup!: SecurityPluginSetup | undefined; - private ruleDataService!: RuleDataPluginService | undefined; + private ruleDataService!: RuleDataPluginService; - public initialize(options: RacClientFactoryOpts) { + public initialize(options: AlertsClientFactoryProps) { /** * This should be called by the plugin's start() method. */ @@ -42,25 +39,25 @@ export class AlertsClientFactory { 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.getSpaceId = options.getSpaceId; this.esClient = options.esClient; this.securityPluginSetup = options.securityPluginSetup; this.ruleDataService = options.ruleDataService; } - public async create(request: KibanaRequest, index: string): Promise { + public async create(request: KibanaRequest): Promise { const { securityPluginSetup, getAlertingAuthorization, logger } = this; - const spaceId = this.getSpaceId(request); return new AlertsClient({ - spaceId, logger, - index, authorization: getAlertingAuthorization(request), - auditLogger: new RacAuthorizationAuditLogger(securityPluginSetup?.audit.asScoped(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/audit_logger.ts b/x-pack/plugins/rule_registry/server/alert_data_client/audit_logger.ts deleted file mode 100644 index cdbf9a3b48d42..0000000000000 --- a/x-pack/plugins/rule_registry/server/alert_data_client/audit_logger.ts +++ /dev/null @@ -1,89 +0,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 { EcsEventType } from '@kbn/logging'; -import { AuditLogger } from '../../../security/server'; - -export enum AuthorizationResult { - Unauthorized = 'Unauthorized', - Authorized = 'Authorized', -} - -export enum Result { - Success = 'Success', - Failure = 'Failure', -} - -export class RacAuthorizationAuditLogger { - private readonly auditLogger: AuditLogger; - - constructor(auditLogger: AuditLogger = { log() {} }) { - this.auditLogger = auditLogger; - } - - public getAuthorizationMessage( - authorizationResult: AuthorizationResult, - owner: string, - operation: string - ): string { - return `${authorizationResult} to ${operation} "${owner}" alert"`; - } - - public racAuthorizationFailure({ - owner, - operation, - type, - error, - }: { - owner: string; - operation: string; - type: EcsEventType; - error?: Error; - }): string { - const message = this.getAuthorizationMessage( - AuthorizationResult.Unauthorized, - owner, - operation - ); - this.auditLogger.log({ - message, - event: { - action: 'rac_authorization_failure', - category: ['authentication'], - type: [type], - outcome: 'failure', - }, - error: error && { - code: error.name, - message: error.message, - }, - }); - return message; - } - - public racAuthorizationSuccess({ - owner, - operation, - type, - }: { - owner: string; - operation: string; - type: EcsEventType; - }): string { - const message = this.getAuthorizationMessage(AuthorizationResult.Authorized, owner, operation); - this.auditLogger.log({ - message, - event: { - action: 'rac_authorization_success', - category: ['authentication'], - type: [type], - outcome: 'success', - }, - }); - return 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..ced46551eea39 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/alert_data_client/tests/get.test.ts @@ -0,0 +1,164 @@ +/* + * 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: { + _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 { + "_id": "NoxgpHkBqbdrfX07MqXV", + "_index": ".alerts-observability-apm", + "_source": 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: { + _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 { + "_id": "NoxgpHkBqbdrfX07MqXV", + "_index": ".alerts-observability-apm", + "_source": 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/utils.ts b/x-pack/plugins/rule_registry/server/alert_data_client/utils.ts deleted file mode 100644 index 0cadbafd63916..0000000000000 --- a/x-pack/plugins/rule_registry/server/alert_data_client/utils.ts +++ /dev/null @@ -1,72 +0,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. - */ - -interface BuildAlertsSearchQuery { - alertId: string; - index: string; - from?: string; - to?: string; - size?: number; -} - -export const buildAlertsSearchQuery = ({ - alertId, - index, - from, - to, - size, -}: BuildAlertsSearchQuery) => ({ - index, - body: { - size, - query: { - bool: { - filter: [ - { - bool: { - should: { - match: { - _id: alertId, - }, - }, - minimum_should_match: 1, - }, - }, - // { - // range: { - // '@timestamp': { - // gt: from, - // lte: to, - // format: 'epoch_millis', - // }, - // }, - // }, - ], - }, - }, - }, -}); - -interface BuildAlertsUpdateParams { - ids: string[]; - index: string; - status: string; -} - -export const buildAlertsUpdateParameters = ({ ids, index, status }: BuildAlertsUpdateParams) => ({ - index, - body: ids.flatMap((id) => [ - { - update: { - _id: id, - }, - }, - { - doc: { 'kibana.rac.alert.status': status }, - }, - ]), -}); diff --git a/x-pack/plugins/rule_registry/server/plugin.ts b/x-pack/plugins/rule_registry/server/plugin.ts index 4a213ecfbc8c6..9e63b0a39230b 100644 --- a/x-pack/plugins/rule_registry/server/plugin.ts +++ b/x-pack/plugins/rule_registry/server/plugin.ts @@ -14,7 +14,7 @@ import { IContextProvider, } from 'src/core/server'; import { SecurityPluginSetup } from '../../security/server'; -import { AlertsClientFactory } from './alert_data_client/alert_client_factory'; +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'; @@ -61,7 +61,7 @@ export class RuleRegistryPlugin private readonly logger: Logger; private eventLogService: EventLogService | null; private readonly alertsClientFactory: AlertsClientFactory; - private ruleDataService: RuleDataPluginService | null | undefined; + private ruleDataService: RuleDataPluginService | null; private security: SecurityPluginSetup | undefined; constructor(initContext: PluginInitializerContext) { @@ -143,9 +143,6 @@ export class RuleRegistryPlugin alertsClientFactory.initialize({ logger, - getSpaceId(request: KibanaRequest) { - return plugins.spaces?.spacesService.getSpaceId(request); - }, esClient: core.elasticsearch.client.asInternalUser, // NOTE: Alerts share the authorization client with the alerting plugin getAlertingAuthorization(request: KibanaRequest) { @@ -156,7 +153,7 @@ export class RuleRegistryPlugin }); const getRacClientWithRequest = (request: KibanaRequest) => { - return alertsClientFactory.create(request, this.config.index); + return alertsClientFactory.create(request); }; return { @@ -166,14 +163,14 @@ export class RuleRegistryPlugin } private createRouteHandlerContext = (): IContextProvider => { - const { alertsClientFactory, config } = this; + const { alertsClientFactory } = this; return async function alertsRouteHandlerContext( context, request ): Promise { return { getAlertsClient: async () => { - const createdClient = alertsClientFactory.create(request, config.index); + const createdClient = alertsClientFactory.create(request); return createdClient; }, }; 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..949a434ecdf6f --- /dev/null +++ b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/rule_data_plugin_service.mock.ts @@ -0,0 +1,31 @@ +/* + * 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 as unknown) as jest.Mocked; +}; + +export const ruleDataPluginServiceMock: { + create: (_: RuleDataPluginServiceConstructorOptions) => jest.Mocked; +} = { + create: createRuleDataPluginServiceMock, +}; From 49f83b5c4325ce22522df4d6dbfa3e63c4630699 Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Wed, 2 Jun 2021 00:54:29 -0700 Subject: [PATCH 04/16] still need to clean up types in tests and update one test file --- .../server/alert_data_client/alerts_client.ts | 79 ++++-- .../alert_data_client/tests/get.test.ts | 22 +- .../alert_data_client/tests/update.test.ts | 257 ++++++++++++++++++ .../server/routes/get_alert_by_id.ts | 3 - 4 files changed, 319 insertions(+), 42 deletions(-) create mode 100644 x-pack/plugins/rule_registry/server/alert_data_client/tests/update.test.ts 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 index 56227d7664217..21ecd997b114c 100644 --- 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 @@ -5,9 +5,7 @@ * 2.0. */ import { PublicMethodsOf } from '@kbn/utility-types'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { RawAlert } from '../../../alerting/server/types'; -import { AlertTypeParams, PartialAlert } from '../../../alerting/server'; +import { AlertTypeParams } from '../../../alerting/server'; import { ReadOperations, AlertingAuthorization, @@ -15,10 +13,11 @@ import { AlertingAuthorizationEntity, // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../../alerting/server/authorization'; -import { Logger, ElasticsearchClient, HttpResponsePayload } from '../../../../../src/core/server'; +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; @@ -30,7 +29,6 @@ export interface ConstructorOptions { export interface UpdateOptions { id: string; - owner: string; data: { status: string; }; @@ -73,20 +71,46 @@ export class AlertsClient { return this.ruleDataService?.getFullAssetName(assetName); } - // TODO: Type out alerts (rule registry fields + alerting alerts type) - public async get({ id, assetName }: GetAlertParams): Promise { + private async fetchAlert({ 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 { body: result } = await this.esClient.get({ + 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: result._source['rule.id'], - consumer: result._source['kibana.rac.alert.owner'], + ruleTypeId: alert['rule.id'], + consumer: alert['kibana.rac.alert.owner'], operation: ReadOperations.Get, entity: AlertingAuthorizationEntity.Alert, }); @@ -98,7 +122,7 @@ export class AlertsClient { }) ); - return result; + return alert; } catch (error) { this.logger.debug(`[rac] - Error fetching alert with id of "${id}"`); this.auditLogger?.log( @@ -114,23 +138,19 @@ export class AlertsClient { public async update({ id, - owner, data, assetName, - }: UpdateOptions): Promise> { + }: UpdateOptions): Promise { try { - // TODO: Type out alerts (rule registry fields + alerting alerts type) - const result = await this.esClient.get({ - index: this.getAlertsIndex(assetName), + // TODO: use MGET + const alert = await this.fetchAlert({ id, + assetName, }); - const hits = result.body._source; - // ASSUMPTION: user bulk updating alerts from single owner/space - // may need to iterate to support rules shared across spaces await this.authorization.ensureAuthorized({ - ruleTypeId: hits['rule.id'], - consumer: hits['kibana.rac.alert.owner'], + ruleTypeId: alert['rule.id'], + consumer: alert['kibana.rac.alert.owner'], operation: WriteOperations.Update, entity: AlertingAuthorizationEntity.Alert, }); @@ -147,11 +167,22 @@ export class AlertsClient { }, }; - return this.esClient.update(updateParameters); + 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.GET, + action: AlertAuditAction.UPDATE, id, error, }) 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 index ced46551eea39..f57631ecb1d7a 100644 --- 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 @@ -58,14 +58,10 @@ describe('get()', () => { const result = await alertsClient.get({ id: '1', assetName: 'observability-apm' }); expect(result).toMatchInlineSnapshot(` Object { - "_id": "NoxgpHkBqbdrfX07MqXV", - "_index": ".alerts-observability-apm", - "_source": Object { - "kibana.rac.alert.owner": "apm", - "kibana.rac.alert.status": "open", - "message": "hello world 1", - "rule.id": "apm.error_rate", - }, + "kibana.rac.alert.owner": "apm", + "kibana.rac.alert.status": "open", + "message": "hello world 1", + "rule.id": "apm.error_rate", } `); expect(esClientMock.get).toHaveBeenCalledTimes(1); @@ -128,17 +124,13 @@ describe('get()', () => { ruleTypeId: 'apm.error_rate', }); expect(result).toMatchInlineSnapshot(` - Object { - "_id": "NoxgpHkBqbdrfX07MqXV", - "_index": ".alerts-observability-apm", - "_source": Object { + 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 () => { 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..439c6b68a6bf4 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/alert_data_client/tests/update.test.ts @@ -0,0 +1,257 @@ +/* + * 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: { + _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: { + get: { + _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': '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: { + _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: { + _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: { + get: { + _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': '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/routes/get_alert_by_id.ts b/x-pack/plugins/rule_registry/server/routes/get_alert_by_id.ts index ec8d83b933705..039c10d4c37a1 100644 --- 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 @@ -37,9 +37,6 @@ export const getAlertByIdRoute = (router: IRouter) => const alertsClient = await context.rac.getAlertsClient(); const { id, assetName } = request.query; const alert = await alertsClient.get({ id, assetName }); - if (alert == null) { - throw new Error('could not get alert'); - } return response.ok({ body: alert, }); From 35b4a72474a316dbc25cd5af4859d3f181674568 Mon Sep 17 00:00:00 2001 From: Devin Hurley Date: Wed, 2 Jun 2021 09:12:24 -0400 Subject: [PATCH 05/16] fixes snapshot for signals template --- .../index/__snapshots__/get_signals_template.test.ts.snap | 3 +++ 1 file changed, 3 insertions(+) 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 { From 098b74463ef50d9aa2cc0d9bed6aeb2f83bc94e5 Mon Sep 17 00:00:00 2001 From: Devin Hurley Date: Wed, 2 Jun 2021 11:34:53 -0400 Subject: [PATCH 06/16] fix tests --- .../security_and_spaces/tests/create_threat_matching.ts | 1 + .../test/rule_registry/common/lib/authentication/index.ts | 2 +- .../security_and_spaces/tests/basic/update_alert.ts | 7 ------- 3 files changed, 2 insertions(+), 8 deletions(-) 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/rule_registry/common/lib/authentication/index.ts b/x-pack/test/rule_registry/common/lib/authentication/index.ts index 71676283b5cb2..9ffcd21eb13c1 100644 --- a/x-pack/test/rule_registry/common/lib/authentication/index.ts +++ b/x-pack/test/rule_registry/common/lib/authentication/index.ts @@ -51,7 +51,7 @@ export const createUsersAndRoles = async ( }; for (const role of rolesToCreate) { - const res = await createRole(role); + await createRole(role); } for (const user of usersToCreate) { 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 index 961cd8bdfb5f0..a65b7b002d68e 100644 --- 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 @@ -8,13 +8,8 @@ import { secOnly, secOnlyRead, - globalRead, - obsOnly, - obsOnlyRead, obsOnlySpacesAll, obsOnlyReadSpacesAll, - obsSec, - obsSecRead, superUser, noKibanaPrivileges, } from '../../../common/lib/authentication/users'; @@ -24,12 +19,10 @@ 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', () => { describe('Users update:', () => { From ee0f2cd2840ac3da2e6b7c896c124946968cad72 Mon Sep 17 00:00:00 2001 From: Devin Hurley Date: Wed, 2 Jun 2021 18:12:38 -0400 Subject: [PATCH 07/16] fix type check failures --- .../alerting_authorization.mock.ts | 2 +- .../server/alert_data_client/alerts_client.ts | 12 +++--- .../alerts_client_factory.test.ts | 2 +- .../alerts_client_factory.ts | 4 +- .../alert_data_client/tests/get.test.ts | 4 ++ .../alert_data_client/tests/update.test.ts | 39 +++++++++++++++++-- .../server/routes/update_alert_by_id.ts | 1 - .../rule_data_plugin_service.mock.ts | 6 ++- x-pack/plugins/rule_registry/server/types.ts | 2 +- 9 files changed, 54 insertions(+), 18 deletions(-) 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 fd29274422b13..09e7787d19fa9 100644 --- a/x-pack/plugins/alerting/server/authorization/alerting_authorization.mock.ts +++ b/x-pack/plugins/alerting/server/authorization/alerting_authorization.mock.ts @@ -22,7 +22,7 @@ const createAlertingAuthorizationMock = () => { }; export const alertingAuthorizationMock: { - create: () => AlertingAuthorizationMock; + create: () => jest.Mocked>; // AlertingAuthorizationMock; } = { create: createAlertingAuthorizationMock, }; 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 index 21ecd997b114c..617c369da1f9c 100644 --- 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 @@ -24,7 +24,7 @@ export interface ConstructorOptions { authorization: PublicMethodsOf; auditLogger?: AuditLogger; esClient: ElasticsearchClient; - ruleDataService: RuleDataPluginService; + ruleDataService: PublicMethodsOf; } export interface UpdateOptions { @@ -47,7 +47,7 @@ export class AlertsClient { private readonly auditLogger?: AuditLogger; private readonly authorization: PublicMethodsOf; private readonly esClient: ElasticsearchClient; - private readonly ruleDataService: RuleDataPluginService; + private readonly ruleDataService: PublicMethodsOf; constructor({ auditLogger, @@ -109,8 +109,8 @@ export class AlertsClient { // this.authorization leverages the alerting plugin's authorization // client exposed to us for reuse await this.authorization.ensureAuthorized({ - ruleTypeId: alert['rule.id'], - consumer: alert['kibana.rac.alert.owner'], + 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, }); @@ -149,8 +149,8 @@ export class AlertsClient { }); await this.authorization.ensureAuthorized({ - ruleTypeId: alert['rule.id'], - consumer: alert['kibana.rac.alert.owner'], + 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, }); 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 index 8afc33c836293..1b5fc6322fa94 100644 --- 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 @@ -24,7 +24,7 @@ const ruleDataServiceMock = ruleDataPluginServiceMock.create( ); const alertingAuthMock = alertingAuthorizationMock.create(); -const alertsClientFactoryParams: jest.Mocked = { +const alertsClientFactoryParams: AlertsClientFactoryProps = { logger: loggingSystemMock.create().get(), getAlertingAuthorization: (_: KibanaRequest) => alertingAuthMock, securityPluginSetup, 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 index f494e2e6e829a..8909fc71f38c5 100644 --- 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 @@ -18,7 +18,7 @@ export interface AlertsClientFactoryProps { esClient: ElasticsearchClient; getAlertingAuthorization: (request: KibanaRequest) => PublicMethodsOf; securityPluginSetup: SecurityPluginSetup | undefined; - ruleDataService: RuleDataPluginService | null; + ruleDataService: PublicMethodsOf | null; } export class AlertsClientFactory { @@ -29,7 +29,7 @@ export class AlertsClientFactory { request: KibanaRequest ) => PublicMethodsOf; private securityPluginSetup!: SecurityPluginSetup | undefined; - private ruleDataService!: RuleDataPluginService; + private ruleDataService!: PublicMethodsOf; public initialize(options: AlertsClientFactoryProps) { /** 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 index f57631ecb1d7a..03796a57facab 100644 --- 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 @@ -44,6 +44,8 @@ describe('get()', () => { esClientMock.get.mockResolvedValueOnce( elasticsearchClientMock.createApiResponse({ body: { + found: true, + _type: 'alert', _index: '.alerts-observability-apm', _id: 'NoxgpHkBqbdrfX07MqXV', _source: { @@ -100,6 +102,8 @@ describe('get()', () => { esClientMock.get.mockResolvedValueOnce( elasticsearchClientMock.createApiResponse({ body: { + found: true, + _type: 'alert', _index: '.alerts-observability-apm', _id: 'NoxgpHkBqbdrfX07MqXV', _source: { 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 index 439c6b68a6bf4..e1edba023406d 100644 --- 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 @@ -44,6 +44,8 @@ describe('update()', () => { esClientMock.get.mockResolvedValueOnce( elasticsearchClientMock.createApiResponse({ body: { + found: true, + _type: 'alert', _index: '.alerts-observability-apm', _id: 'NoxgpHkBqbdrfX07MqXV', _source: { @@ -58,9 +60,21 @@ describe('update()', () => { 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: { - _index: '.alerts-observability-apm', - _id: 'NoxgpHkBqbdrfX07MqXV', + found: true, + _seq_no: 1, + _primary_term: 2, _source: { 'rule.id': 'apm.error_rate', message: 'hello world 1', @@ -140,6 +154,8 @@ describe('update()', () => { esClientMock.get.mockResolvedValueOnce( elasticsearchClientMock.createApiResponse({ body: { + found: true, + _type: 'alert', _index: '.alerts-observability-apm', _id: 'NoxgpHkBqbdrfX07MqXV', _source: { @@ -177,6 +193,8 @@ describe('update()', () => { esClientMock.get.mockResolvedValueOnce( elasticsearchClientMock.createApiResponse({ body: { + found: true, + _type: 'alert', _index: '.alerts-observability-apm', _id: 'NoxgpHkBqbdrfX07MqXV', _source: { @@ -188,12 +206,25 @@ describe('update()', () => { }, }) ); + 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: { - _index: '.alerts-observability-apm', - _id: 'NoxgpHkBqbdrfX07MqXV', + found: true, + _seq_no: 1, + _primary_term: 2, _source: { 'rule.id': 'apm.error_rate', message: 'hello world 1', 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 index 52f0200c47a39..66f89d02d5a2e 100644 --- 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 @@ -35,7 +35,6 @@ export const updateAlertByIdRoute = (router: IRouter) const thing = await racClient?.update({ id: ids[0], - owner: 'apm', data: { status }, assetName, }); 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 index 949a434ecdf6f..d5f89ad8b7889 100644 --- 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 @@ -21,11 +21,13 @@ const createRuleDataPluginServiceMock = (_: RuleDataPluginServiceConstructorOpti createOrUpdateIndexTemplate: jest.fn(), createOrUpdateLifecyclePolicy: jest.fn(), }; - return (mocked as unknown) as jest.Mocked; + return mocked; }; export const ruleDataPluginServiceMock: { - create: (_: RuleDataPluginServiceConstructorOptions) => jest.Mocked; + create: ( + _: RuleDataPluginServiceConstructorOptions + ) => jest.Mocked>; } = { create: createRuleDataPluginServiceMock, }; diff --git a/x-pack/plugins/rule_registry/server/types.ts b/x-pack/plugins/rule_registry/server/types.ts index 4ec4ca1afff02..f8bd1940b10a8 100644 --- a/x-pack/plugins/rule_registry/server/types.ts +++ b/x-pack/plugins/rule_registry/server/types.ts @@ -13,7 +13,7 @@ import { AlertTypeState, } from '../../alerting/common'; import { AlertType } from '../../alerting/server'; -import { AlertsClient } from './alert_data_client/alert_client'; +import { AlertsClient } from './alert_data_client/alerts_client'; type SimpleAlertType< TParams extends AlertTypeParams = {}, From 404e59203dcfe6f7fa365fcc2fad59b42d366971 Mon Sep 17 00:00:00 2001 From: Devin Hurley Date: Thu, 3 Jun 2021 08:33:05 -0400 Subject: [PATCH 08/16] update cypress test --- .../cypress/integration/detection_alerts/alerts_details.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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', }; From 9529d6d78a577da2b9467b746b17c360a5d1ddb1 Mon Sep 17 00:00:00 2001 From: Devin Hurley Date: Thu, 3 Jun 2021 08:54:04 -0400 Subject: [PATCH 09/16] undo changes in alert authz class, updates alert privilege in apm feature to 'read', utilizes the 'rule' object available in executor params over querying for the rule SO directly --- .../server/authorization/alerting_authorization.ts | 14 ++------------ x-pack/plugins/apm/server/feature.ts | 2 +- .../utils/create_lifecycle_rule_type_factory.ts | 9 +++------ 3 files changed, 6 insertions(+), 19 deletions(-) diff --git a/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts b/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts index 46cb9444d4732..7506accd8b88e 100644 --- a/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts +++ b/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts @@ -138,14 +138,6 @@ export class AlertingAuthorization { return this.authorization?.mode?.useRbacForRequest(this.request) ?? false; } - public getAuthorizedAlertsIndices(owner: string): string | undefined { - return owner === 'apm' - ? '.alerts-observability-apm' - : owner === 'securitySolution' - ? '.siem-signals*' - : undefined; - } - public async ensureAuthorized({ ruleTypeId, consumer, operation, entity }: EnsureAuthorizedOpts) { const { authorization } = this; @@ -166,7 +158,7 @@ export class AlertingAuthorization { const shouldAuthorizeConsumer = !this.exemptConsumerIds.includes(consumer); const checkPrivileges = authorization.checkPrivilegesDynamicallyWithRequest(this.request); - const checkPrivParams = { + const { hasAllRequested, username, privileges } = await checkPrivileges({ kibana: shouldAuthorizeConsumer && consumer !== ruleType.producer ? [ @@ -180,8 +172,7 @@ export class AlertingAuthorization { // be created for exempt consumers if user has producer level privileges requiredPrivilegesByScope.producer, ], - }; - const { hasAllRequested, username, privileges } = await checkPrivileges(checkPrivParams); + }); if (!isAvailableConsumer) { /** @@ -191,7 +182,6 @@ export class AlertingAuthorization { * as Privileged. * This check will ensure we don't accidentally let these through */ - // This should also log the type they're trying to access rule/alert throw Boom.forbidden( this.auditLogger.logAuthorizationFailure( username, diff --git a/x-pack/plugins/apm/server/feature.ts b/x-pack/plugins/apm/server/feature.ts index f9d7e293363d8..7d8b0dcff36cf 100644 --- a/x-pack/plugins/apm/server/feature.ts +++ b/x-pack/plugins/apm/server/feature.ts @@ -43,7 +43,7 @@ export const APM_FEATURE = { all: Object.values(AlertType), }, alert: { - all: Object.values(AlertType), + read: Object.values(AlertType), }, }, management: { 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 28a929aa49e33..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 @@ -70,15 +70,12 @@ export const createLifecycleRuleTypeFactory: CreateLifecycleRuleTypeFactory = ({ const { services: { alertInstanceFactory }, state: previousState, + rule, } = options; const ruleExecutorData = getRuleExecutorData(type, options); - const so = await options.services.savedObjectsClient.get( - 'alert', - ruleExecutorData[RULE_UUID] - ); - logger.debug(`LOGGER RULE REGISTRY CONSUMER ${so.attributes.consumer}`); + logger.debug(`LOGGER RULE REGISTRY CONSUMER ${rule.consumer}`); const decodedState = wrappedStateRt.decode(previousState); @@ -187,7 +184,7 @@ export const createLifecycleRuleTypeFactory: CreateLifecycleRuleTypeFactory = ({ ...ruleExecutorData, [TIMESTAMP]: timestamp, [EVENT_KIND]: 'state', - [OWNER]: so.attributes.consumer, + [OWNER]: rule.consumer, [ALERT_ID]: alertId, }; From 78ec73c877dd0517074395dd7a7de4a10d54e9ee Mon Sep 17 00:00:00 2001 From: Devin Hurley Date: Thu, 3 Jun 2021 09:09:33 -0400 Subject: [PATCH 10/16] remove verbose logging from detection api integration tests --- .../common/config.ts | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/x-pack/test/detection_engine_api_integration/common/config.ts b/x-pack/test/detection_engine_api_integration/common/config.ts index 88dc51efd0549..659c836eb9207 100644 --- a/x-pack/test/detection_engine_api_integration/common/config.ts +++ b/x-pack/test/detection_engine_api_integration/common/config.ts @@ -70,18 +70,6 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) `--xpack.actions.allowedHosts=${JSON.stringify(['localhost', 'some.non.existent.com'])}`, `--xpack.actions.enabledActionTypes=${JSON.stringify(enabledActionTypes)}`, '--xpack.eventLog.logEntries=true', - `--logging.verbose=true`, - `--logging.events.log=${JSON.stringify([ - 'alerts', - 'ruleRegistry', - 'info', - 'warning', - 'error', - 'fatal', - ])}`, - `--logging.events.request=${JSON.stringify(['info', 'warning', 'error', 'fatal'])}`, - `--logging.events.error='*'`, - `--logging.events.ops=__no-ops__`, ...disabledPlugins.map((key) => `--xpack.${key}.enabled=false`), ...(ssl ? [ From 0896f00b986c83cce64dcff2c366c8c0cc1067d6 Mon Sep 17 00:00:00 2001 From: Devin Hurley Date: Thu, 3 Jun 2021 09:23:22 -0400 Subject: [PATCH 11/16] fix type --- .../alerting/server/authorization/alerting_authorization.mock.ts | 1 - 1 file changed, 1 deletion(-) 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 09e7787d19fa9..283f1f1b46ff9 100644 --- a/x-pack/plugins/alerting/server/authorization/alerting_authorization.mock.ts +++ b/x-pack/plugins/alerting/server/authorization/alerting_authorization.mock.ts @@ -16,7 +16,6 @@ const createAlertingAuthorizationMock = () => { ensureAuthorized: jest.fn(), filterByRuleTypeAuthorization: jest.fn(), getFindAuthorizationFilter: jest.fn(), - getAuthorizedAlertsIndices: jest.fn(), }; return mocked; }; From 4d4920edc29e0aac1212099498f2440701fdc111 Mon Sep 17 00:00:00 2001 From: Devin Hurley Date: Thu, 3 Jun 2021 11:41:51 -0400 Subject: [PATCH 12/16] fix jest tests, adds missing mocked rule object to alert executor params --- x-pack/plugins/apm/server/lib/alerts/test_utils/index.ts | 1 + 1 file changed, 1 insertion(+) 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 5af7f1130beff..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 @@ -69,6 +69,7 @@ export const createRuleTypeMocks = () => { executor: async ({ params }: { params: Record }) => { return alertExecutor({ services, + rule: { consumer: 'apm' }, params, startedAt: new Date(), }); From c0872b577da6986b7e4ef33cb5c59ca45bc3df39 Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Wed, 9 Jun 2021 01:04:54 -0700 Subject: [PATCH 13/16] wip - addition of rbac to tgrid, based off of existing tgrid prior to move to new rac tgrid work --- .../ese_search/ese_search_strategy.ts | 2 +- .../authorization/alerting_authorization.ts | 4 + .../alerting_authorization_kuery.ts | 1 + .../common/mapping_from_field_map.ts | 2 +- .../log/utils/mapping_from_field_map.ts | 2 +- .../server/rule_data_client/index.ts | 2 + .../create_persistence_rule_type_factory.ts | 24 +- .../scripts/create_reference_rule_query.sh | 3 +- .../create_reference_rule_threshold.sh | 6 +- .../security_solution/server/plugin.ts | 237 +++++++++++++----- .../timeline/factory/events/all/index.ts | 4 +- .../events/all/query.events_all.dsl.ts | 3 +- .../server/search_strategy/timeline/index.ts | 52 +++- 13 files changed, 254 insertions(+), 88 deletions(-) diff --git a/src/plugins/data/server/search/strategies/ese_search/ese_search_strategy.ts b/src/plugins/data/server/search/strategies/ese_search/ese_search_strategy.ts index ab6162f756ea8..5d16dca3d0da7 100644 --- a/src/plugins/data/server/search/strategies/ese_search/ese_search_strategy.ts +++ b/src/plugins/data/server/search/strategies/ese_search/ese_search_strategy.ts @@ -39,7 +39,7 @@ export const enhancedEsSearchStrategyProvider = ( legacyConfig$: Observable, logger: Logger, usage?: SearchUsage, - useInternalUser: boolean = false + useInternalUser: boolean = true ): ISearchStrategy => { async function cancelAsyncSearch(id: string, esClient: IScopedClusterClient) { try { diff --git a/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts b/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts index 7506accd8b88e..8fdcb54d95657 100644 --- a/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts +++ b/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts @@ -258,6 +258,8 @@ export class AlertingAuthorization { authorizationEntity ); + console.log('---------------YO-----------', authorizedRuleTypes); + if (!authorizedRuleTypes.size) { throw Boom.forbidden( this.auditLogger.logUnscopedAuthorizationFailure(username!, 'find', authorizationEntity) @@ -382,6 +384,8 @@ export class AlertingAuthorization { kibana: [...privilegeToRuleType.keys()], }); + console.log('-----------WTF-------------', JSON.stringify(privileges)); + return { username, hasAllRequested, diff --git a/x-pack/plugins/alerting/server/authorization/alerting_authorization_kuery.ts b/x-pack/plugins/alerting/server/authorization/alerting_authorization_kuery.ts index eb6f1605f2ba5..c673090382111 100644 --- a/x-pack/plugins/alerting/server/authorization/alerting_authorization_kuery.ts +++ b/x-pack/plugins/alerting/server/authorization/alerting_authorization_kuery.ts @@ -20,6 +20,7 @@ export enum AlertingAuthorizationFilterType { export interface AlertingAuthorizationFilterOpts { type: AlertingAuthorizationFilterType; fieldNames: AlertingAuthorizationFilterFieldNames; + owner?: string; } interface AlertingAuthorizationFilterFieldNames { 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/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/rule_data_client/index.ts b/x-pack/plugins/rule_registry/server/rule_data_client/index.ts index 43122ba49519a..c422d07b20d8b 100644 --- a/x-pack/plugins/rule_registry/server/rule_data_client/index.ts +++ b/x-pack/plugins/rule_registry/server/rule_data_client/index.ts @@ -81,6 +81,8 @@ export class RuleDataClient implements IRuleDataClient { }); } const error = new ResponseError(response); + console.log('-----GET WRITER------', JSON.stringify(error)); + throw error; } return response; 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/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..2d6a9906b6e5a 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 http://localhost:5601/s/awesome-possum/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/reference_rules/scripts/create_reference_rule_threshold.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/scripts/create_reference_rule_threshold.sh index 8b486b165c34b..017f86465d16e 100755 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/scripts/create_reference_rule_threshold.sh +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/scripts/create_reference_rule_threshold.sh @@ -6,11 +6,11 @@ # 2.0. # -curl -X POST http://localhost:5601/${BASE_PATH}/api/alerts/alert \ +curl -s -k \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ - -H 'kbn-xsrf: true' \ -H 'Content-Type: application/json' \ - --verbose \ + -H 'kbn-xsrf: 123' \ + -X POST ${KIBANA_URL}${SPACE_URL}/api/alerts/alert \ -d ' { "params":{ diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index aeb03d8729fe3..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: (options: TimelineEventsAllRequestOptions, authFilter) => { 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..ae5cee64952b9 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,19 @@ * 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 +27,52 @@ 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); 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 () => { + // const { + // filter: authorizationFilter, + // ensureRuleTypeIsAuthorized, + // logSuccessfulAuthorization, + // } = await alertingAuthorizationClient.getFindAuthorizationFilter( + // AlertingAuthorizationEntity.Alert, + // { + // type: AlertingAuthorizationFilterType.ESDSL, + // fieldNames: { consumer: 'kibana.rac.alert.owner', ruleTypeId: 'rule.id' }, + // } + // ); + + return alertingAuthorizationClient.getFindAuthorizationFilter( + AlertingAuthorizationEntity.Alert, + { + type: AlertingAuthorizationFilterType.ESDSL, + fieldNames: { consumer: 'kibana.rac.alert.owner', ruleTypeId: 'rule.id' }, + } + ); + }; + + return from(getAuthFilter()).pipe( + flatMap(({ filter }) => { + console.log('---------------------ARG----------------', JSON.stringify(filter)); + const dsl = queryFactory.buildDsl(request, filter); + return es.search({ ...request, params: dsl }, options, deps); + }), map((response) => { return { ...response, From ecd8016e23ee1fd48a3b583558f4e2ad2fa27b76 Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Wed, 9 Jun 2021 01:21:17 -0700 Subject: [PATCH 14/16] did some cleanup --- .../authorization/alerting_authorization.ts | 4 ---- .../server/rule_data_client/index.ts | 2 -- .../search_strategy/timeline/events/all/index.ts | 2 ++ .../scripts/create_reference_rule_query.sh | 2 +- .../scripts/create_reference_rule_threshold.sh | 6 +++--- .../timeline/factory/events/all/index.ts | 2 +- .../server/search_strategy/timeline/index.ts | 15 +-------------- 7 files changed, 8 insertions(+), 25 deletions(-) diff --git a/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts b/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts index 8fdcb54d95657..7506accd8b88e 100644 --- a/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts +++ b/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts @@ -258,8 +258,6 @@ export class AlertingAuthorization { authorizationEntity ); - console.log('---------------YO-----------', authorizedRuleTypes); - if (!authorizedRuleTypes.size) { throw Boom.forbidden( this.auditLogger.logUnscopedAuthorizationFailure(username!, 'find', authorizationEntity) @@ -384,8 +382,6 @@ export class AlertingAuthorization { kibana: [...privilegeToRuleType.keys()], }); - console.log('-----------WTF-------------', JSON.stringify(privileges)); - return { username, hasAllRequested, diff --git a/x-pack/plugins/rule_registry/server/rule_data_client/index.ts b/x-pack/plugins/rule_registry/server/rule_data_client/index.ts index c422d07b20d8b..43122ba49519a 100644 --- a/x-pack/plugins/rule_registry/server/rule_data_client/index.ts +++ b/x-pack/plugins/rule_registry/server/rule_data_client/index.ts @@ -81,8 +81,6 @@ export class RuleDataClient implements IRuleDataClient { }); } const error = new ResponseError(response); - console.log('-----GET WRITER------', JSON.stringify(error)); - throw error; } return response; 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/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 2d6a9906b6e5a..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 @@ -7,7 +7,7 @@ # set -e -curl -X POST http://localhost:5601/s/awesome-possum/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/reference_rules/scripts/create_reference_rule_threshold.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/scripts/create_reference_rule_threshold.sh index 017f86465d16e..8b486b165c34b 100755 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/scripts/create_reference_rule_threshold.sh +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/scripts/create_reference_rule_threshold.sh @@ -6,11 +6,11 @@ # 2.0. # -curl -s -k \ +curl -X POST http://localhost:5601/${BASE_PATH}/api/alerts/alert \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -H 'kbn-xsrf: true' \ -H 'Content-Type: application/json' \ - -H 'kbn-xsrf: 123' \ - -X POST ${KIBANA_URL}${SPACE_URL}/api/alerts/alert \ + --verbose \ -d ' { "params":{ diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/index.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/index.ts index 713838d99052a..48c9f841722b5 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/index.ts @@ -23,7 +23,7 @@ import { TIMELINE_EVENTS_FIELDS } from './constants'; import { buildFieldsRequest, formatTimelineData } from './helpers'; export const timelineEventsAll: SecuritySolutionTimelineFactory = { - buildDsl: (options: TimelineEventsAllRequestOptions, authFilter) => { + 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}`); } 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 ae5cee64952b9..c3503abef7589 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 @@ -46,18 +46,6 @@ export const securitySolutionTimelineSearchStrategyProvider = { - // const { - // filter: authorizationFilter, - // ensureRuleTypeIsAuthorized, - // logSuccessfulAuthorization, - // } = await alertingAuthorizationClient.getFindAuthorizationFilter( - // AlertingAuthorizationEntity.Alert, - // { - // type: AlertingAuthorizationFilterType.ESDSL, - // fieldNames: { consumer: 'kibana.rac.alert.owner', ruleTypeId: 'rule.id' }, - // } - // ); - return alertingAuthorizationClient.getFindAuthorizationFilter( AlertingAuthorizationEntity.Alert, { @@ -69,8 +57,7 @@ export const securitySolutionTimelineSearchStrategyProvider = { - console.log('---------------------ARG----------------', JSON.stringify(filter)); - const dsl = queryFactory.buildDsl(request, filter); + const dsl = queryFactory.buildDsl({ ...request, authFilter: filter }); return es.search({ ...request, params: dsl }, options, deps); }), map((response) => { From eb9d0e6a732be00beb3ba13bb9dd549ba0e28714 Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Wed, 9 Jun 2021 09:20:06 -0700 Subject: [PATCH 15/16] added code sponsored by Lukas to expose internal search strategy --- src/plugins/data/server/search/search_service.ts | 13 +++++++++++++ .../strategies/ese_search/ese_search_strategy.ts | 2 +- src/plugins/data/server/search/types.ts | 5 +++++ .../server/search_strategy/timeline/index.ts | 3 +-- 4 files changed, 20 insertions(+), 3 deletions(-) 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/strategies/ese_search/ese_search_strategy.ts b/src/plugins/data/server/search/strategies/ese_search/ese_search_strategy.ts index 5d16dca3d0da7..ab6162f756ea8 100644 --- a/src/plugins/data/server/search/strategies/ese_search/ese_search_strategy.ts +++ b/src/plugins/data/server/search/strategies/ese_search/ese_search_strategy.ts @@ -39,7 +39,7 @@ export const enhancedEsSearchStrategyProvider = ( legacyConfig$: Observable, logger: Logger, usage?: SearchUsage, - useInternalUser: boolean = true + useInternalUser: boolean = false ): ISearchStrategy => { async function cancelAsyncSearch(id: string, esClient: IScopedClusterClient) { try { 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/security_solution/server/search_strategy/timeline/index.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/index.ts index c3503abef7589..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 @@ -12,7 +12,6 @@ import { PluginStart, shimHitsTotal, } from '../../../../../../src/plugins/data/server'; -import { ENHANCED_ES_SEARCH_STRATEGY } from '../../../../../../src/plugins/data/common'; import { PluginStartContract as AlertPluginStartContract, AlertingAuthorizationEntity, @@ -30,7 +29,7 @@ export const securitySolutionTimelineSearchStrategyProvider = , TimelineStrategyResponseType> => { - const es = data.search.getSearchStrategy(ENHANCED_ES_SEARCH_STRATEGY); + const es = data.search.searchAsInternalUser; return { search: (request, options, deps) => { const factoryQueryType = request.factoryQueryType; From b16e7c5c6b738de324400490e0daec701b0f6ace Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Wed, 9 Jun 2021 09:21:57 -0700 Subject: [PATCH 16/16] removed unnecessary change to type --- .../server/authorization/alerting_authorization_kuery.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/plugins/alerting/server/authorization/alerting_authorization_kuery.ts b/x-pack/plugins/alerting/server/authorization/alerting_authorization_kuery.ts index c673090382111..eb6f1605f2ba5 100644 --- a/x-pack/plugins/alerting/server/authorization/alerting_authorization_kuery.ts +++ b/x-pack/plugins/alerting/server/authorization/alerting_authorization_kuery.ts @@ -20,7 +20,6 @@ export enum AlertingAuthorizationFilterType { export interface AlertingAuthorizationFilterOpts { type: AlertingAuthorizationFilterType; fieldNames: AlertingAuthorizationFilterFieldNames; - owner?: string; } interface AlertingAuthorizationFilterFieldNames {