From 2f794f81f0b533ba88a1e0739e6dcef4658034e4 Mon Sep 17 00:00:00 2001 From: CAWilson94 Date: Mon, 24 Feb 2025 13:57:29 +0000 Subject: [PATCH 01/19] init route and data client setup for privMon api --- .../privilege_monitoring/engine/init.gen.ts | 22 ++++ .../engine/init.schema.yaml | 24 ++++ .../common/api/quickstart_client.gen.ts | 13 ++ .../PrivilegeMonitoringDataClient.ts | 44 +++++++ .../privilege_monitoring/auth/api_key.ts | 112 ++++++++++++++++++ .../privilege_monitoring/routes/init.ts | 58 +++++++++ .../routes/register_entity_store_routes.ts | 19 +++ .../register_entity_analytics_routes.ts | 2 + .../server/request_context_factory.ts | 14 +++ .../plugins/security_solution/server/types.ts | 2 + .../services/security_solution_api.gen.ts | 7 ++ 11 files changed, 317 insertions(+) create mode 100644 x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/privilege_monitoring/engine/init.gen.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/privilege_monitoring/engine/init.schema.yaml create mode 100644 x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/PrivilegeMonitoringDataClient.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/auth/api_key.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/routes/init.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/routes/register_entity_store_routes.ts diff --git a/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/privilege_monitoring/engine/init.gen.ts b/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/privilege_monitoring/engine/init.gen.ts new file mode 100644 index 0000000000000..bdf603a1b79eb --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/privilege_monitoring/engine/init.gen.ts @@ -0,0 +1,22 @@ +/* + * 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. + */ + +/* + * NOTICE: Do not edit this file manually. + * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator. + * + * info: + * title: Init Privilege Monitoring Engine + * version: 2023-10-31 + */ + +import { z } from '@kbn/zod'; + +export type InitMonitoringEngineResponse = z.infer; +export const InitMonitoringEngineResponse = z.object({ + acknowledged: z.boolean().optional(), +}); diff --git a/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/privilege_monitoring/engine/init.schema.yaml b/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/privilege_monitoring/engine/init.schema.yaml new file mode 100644 index 0000000000000..c06491ae02427 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/privilege_monitoring/engine/init.schema.yaml @@ -0,0 +1,24 @@ +openapi: 3.0.0 + +info: + title: Init Privilege Monitoring Engine + version: 2023-10-31 +paths: + /api/entity_analytics/monitoring/engine/init: + post: + x-labels: [ess, serverless] + x-codegen-enabled: true + operationId: InitMonitoringEngine + summary: Initialize the Privilege Monitoring Engine + + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + properties: + acknowledged: + type: boolean + diff --git a/x-pack/solutions/security/plugins/security_solution/common/api/quickstart_client.gen.ts b/x-pack/solutions/security/plugins/security_solution/common/api/quickstart_client.gen.ts index 67e4ca160e32d..7aa1ad0efcd52 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/api/quickstart_client.gen.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/api/quickstart_client.gen.ts @@ -279,6 +279,7 @@ import type { GetEntityStoreStatusRequestQueryInput, GetEntityStoreStatusResponse, } from './entity_analytics/entity_store/status.gen'; +import type { InitMonitoringEngineResponse } from './entity_analytics/privilege_monitoring/engine/init.gen'; import type { CleanUpRiskEngineResponse } from './entity_analytics/risk_engine/engine_cleanup_route.gen'; import type { ConfigureRiskEngineSavedObjectRequestBodyInput, @@ -1689,6 +1690,18 @@ finalize it. }) .catch(catchAxiosErrorFormatAndThrow); } + async initMonitoringEngine() { + this.log.info(`${new Date().toISOString()} Calling API InitMonitoringEngine`); + return this.kbnClient + .request({ + path: '/api/entity_analytics/monitoring/engine/init', + headers: { + [ELASTIC_HTTP_VERSION_HEADER]: '2023-10-31', + }, + method: 'POST', + }) + .catch(catchAxiosErrorFormatAndThrow); + } /** * Initializes the Risk Engine by creating the necessary indices and mappings, removing old transforms, and starting the new risk engine */ diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/PrivilegeMonitoringDataClient.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/PrivilegeMonitoringDataClient.ts new file mode 100644 index 0000000000000..e44144414b25b --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/PrivilegeMonitoringDataClient.ts @@ -0,0 +1,44 @@ +/* + * 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 { + Logger, + ElasticsearchClient, + SavedObjectsClientContract, + AuditLogger, + IScopedClusterClient, + AnalyticsServiceSetup, +} from '@kbn/core/server'; + +import type { TaskManagerStartContract } from '@kbn/task-manager-plugin/server'; +import type { ApiKeyManager } from './auth/api_key'; + +interface PrivilegeMonitoringClientOpts { + logger: Logger; + clusterClient: IScopedClusterClient; + namespace: string; + soClient: SavedObjectsClientContract; + taskManager?: TaskManagerStartContract; + auditLogger?: AuditLogger; + kibanaVersion: string; + telemetry?: AnalyticsServiceSetup; + apiKeyManager?: ApiKeyManager; +} + +export class PrivilegeMonitoringDataClient { + private apiKeyGenerator?: ApiKeyManager; + private esClient: ElasticsearchClient; + + constructor(private readonly opts: PrivilegeMonitoringClientOpts) { + this.esClient = opts.clusterClient.asInternalUser; + this.apiKeyGenerator = opts.apiKeyManager; + } + + init() { + return Promise.resolve(); + } +} diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/auth/api_key.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/auth/api_key.ts new file mode 100644 index 0000000000000..812df752f341d --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/auth/api_key.ts @@ -0,0 +1,112 @@ +/* + * 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 { KibanaRequest } from '@kbn/core-http-server'; +import { generateEntityDiscoveryAPIKey } from '@kbn/entityManager-plugin/server/lib/auth'; +import { EntityDiscoveryApiKeyType } from '@kbn/entityManager-plugin/server/saved_objects'; +import type { CoreStart } from '@kbn/core-lifecycle-server'; +import type { Logger } from '@kbn/logging'; +import type { SecurityPluginStart } from '@kbn/security-plugin-types-server'; +import type { EncryptedSavedObjectsPluginStart } from '@kbn/encrypted-saved-objects-plugin/server'; +import { getFakeKibanaRequest } from '@kbn/security-plugin/server/authentication/api_keys/fake_kibana_request'; +import type { EntityDiscoveryAPIKey } from '@kbn/entityManager-plugin/server/lib/auth/api_key/api_key'; +import { getSpaceAwareEntityDiscoverySavedObjectId } from '@kbn/entityManager-plugin/server/lib/auth/api_key/saved_object'; +import { SavedObjectsErrorHelpers } from '@kbn/core-saved-objects-server'; + +export interface ApiKeyManager { + generate: () => Promise; +} + +export const getApiKeyManager = ({ + core, + logger, + security, + encryptedSavedObjects, + request, + namespace, +}: { + core: CoreStart; + logger: Logger; + security: SecurityPluginStart; + encryptedSavedObjects?: EncryptedSavedObjectsPluginStart; + request?: KibanaRequest; + namespace: string; +}) => ({ + generate: async () => { + if (!encryptedSavedObjects) { + throw new Error( + 'Unable to create API key. Ensure encrypted Saved Object client is enabled in this environment.' + ); + } else if (!request) { + throw new Error('Unable to create API key due to invalid request'); + } else { + const apiKey = await generateEntityDiscoveryAPIKey( + { + core, + config: {}, + logger, + security, + encryptedSavedObjects, + }, + request + ); + + const soClient = core.savedObjects.getScopedClient(request, { + includedHiddenTypes: [EntityDiscoveryApiKeyType.name], + }); + + await soClient.create(EntityDiscoveryApiKeyType.name, apiKey, { + id: getSpaceAwareEntityDiscoverySavedObjectId(namespace), + overwrite: true, + managed: true, + }); + } + }, + getApiKey: async () => { + if (!encryptedSavedObjects) { + throw Error( + 'Unable to retrieve API key. Ensure encrypted Saved Object client is enabled in this environment.' + ); + } + try { + const encryptedSavedObjectsClient = encryptedSavedObjects.getClient({ + includedHiddenTypes: [EntityDiscoveryApiKeyType.name], + }); + return ( + await encryptedSavedObjectsClient.getDecryptedAsInternalUser( + EntityDiscoveryApiKeyType.name, + getSpaceAwareEntityDiscoverySavedObjectId(namespace) + ) + ).attributes; + } catch (err) { + if (SavedObjectsErrorHelpers.isNotFoundError(err)) { + return undefined; + } + throw err; + } + }, + getRequestFromApiKey: async (apiKey: EntityDiscoveryAPIKey) => { + return getFakeKibanaRequest({ + id: apiKey.id, + api_key: apiKey.apiKey, + }); + }, + getClientFromApiKey: async (apiKey: EntityDiscoveryAPIKey) => { + const fakeRequest = getFakeKibanaRequest({ + id: apiKey.id, + api_key: apiKey.apiKey, + }); + const clusterClient = core.elasticsearch.client.asScoped(fakeRequest); + const soClient = core.savedObjects.getScopedClient(fakeRequest, { + includedHiddenTypes: [EntityDiscoveryApiKeyType.name], + }); + return { + clusterClient, + soClient, + }; + }, +}); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/routes/init.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/routes/init.ts new file mode 100644 index 0000000000000..e72b9302b6014 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/routes/init.ts @@ -0,0 +1,58 @@ +/* + * 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 { IKibanaResponse, Logger } from '@kbn/core/server'; +import { buildSiemResponse } from '@kbn/lists-plugin/server/routes/utils'; +import { transformError } from '@kbn/securitysolution-es-utils'; + +import type { InitMonitoringEngineResponse } from '../../../../../common/api/entity_analytics/privilege_monitoring/engine/init.gen'; +import { API_VERSIONS, APP_ID } from '../../../../../common/constants'; +import type { EntityAnalyticsRoutesDeps } from '../../types'; + +export const initPrivilegeMonitoringEngineRoute = ( + router: EntityAnalyticsRoutesDeps['router'], + logger: Logger, + config: EntityAnalyticsRoutesDeps['config'] +) => { + router.versioned + .post({ + access: 'public', + path: '/api/entity_analytics/monitoring/engine/init', + security: { + authz: { + requiredPrivileges: ['securitySolution', `${APP_ID}-entity-analytics`], + }, + }, + }) + .addVersion( + { + version: API_VERSIONS.public.v1, + validate: {}, + }, + + async ( + context, + request, + response + ): Promise> => { + const siemResponse = buildSiemResponse(response); + const secSol = await context.securitySolution; + + try { + const body = await secSol.getPrivilegeMonitoringDataClient().init(); + return response.ok({ body: { acknowledged: true } }); + } catch (e) { + const error = transformError(e); + logger.error(`Error initializing privilege monitoring engine: ${error.message}`); + return siemResponse.error({ + statusCode: error.statusCode, + body: error.message, + }); + } + } + ); +}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/routes/register_entity_store_routes.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/routes/register_entity_store_routes.ts new file mode 100644 index 0000000000000..8d911939f20b8 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/routes/register_entity_store_routes.ts @@ -0,0 +1,19 @@ +/* + * 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 { EntityAnalyticsRoutesDeps } from '../../types'; + +import { initPrivilegeMonitoringEngineRoute } from './init'; + +export const registerPrivilegeMonitoringRoutes = ({ + router, + logger, + getStartServices, + config, +}: EntityAnalyticsRoutesDeps) => { + initPrivilegeMonitoringEngineRoute(router, logger, config); +}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/register_entity_analytics_routes.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/register_entity_analytics_routes.ts index bd097e8641637..864ad5bc440c2 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/register_entity_analytics_routes.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/register_entity_analytics_routes.ts @@ -10,6 +10,7 @@ import { registerRiskScoreRoutes } from './risk_score/routes'; import { registerRiskEngineRoutes } from './risk_engine/routes'; import type { EntityAnalyticsRoutesDeps } from './types'; import { registerEntityStoreRoutes } from './entity_store/routes'; +import { registerPrivilegeMonitoringRoutes } from './privilege_monitoring/routes/register_entity_store_routes'; export const registerEntityAnalyticsRoutes = (routeDeps: EntityAnalyticsRoutesDeps) => { registerAssetCriticalityRoutes(routeDeps); @@ -18,4 +19,5 @@ export const registerEntityAnalyticsRoutes = (routeDeps: EntityAnalyticsRoutesDe if (!routeDeps.config.experimentalFeatures.entityStoreDisabled) { registerEntityStoreRoutes(routeDeps); } + registerPrivilegeMonitoringRoutes(routeDeps); }; diff --git a/x-pack/solutions/security/plugins/security_solution/server/request_context_factory.ts b/x-pack/solutions/security/plugins/security_solution/server/request_context_factory.ts index 34c3b9759f732..93491fe5b380f 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/request_context_factory.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/request_context_factory.ts @@ -37,6 +37,7 @@ import type { SecuritySolutionApiRequestHandlerContext, SecuritySolutionRequestHandlerContext, } from './types'; +import { PrivilegeMonitoringDataClient } from './lib/entity_analytics/privilege_monitoring/PrivilegeMonitoringDataClient'; export interface IRequestContextFactory { create( @@ -238,6 +239,19 @@ export class RequestContextFactory implements IRequestContextFactory { auditLogger: getAuditLogger(), }) ), + getPrivilegeMonitoringDataClient: memoize( + () => + new PrivilegeMonitoringDataClient({ + logger: options.logger, + clusterClient: coreContext.elasticsearch.client, + namespace: getSpaceId(), + soClient: coreContext.savedObjects.client, + taskManager: startPlugins.taskManager, + auditLogger: getAuditLogger(), + kibanaVersion: options.kibanaVersion, + telemetry: core.analytics, + }) + ), getEntityStoreDataClient: memoize(() => { const clusterClient = coreContext.elasticsearch.client; const logger = options.logger; diff --git a/x-pack/solutions/security/plugins/security_solution/server/types.ts b/x-pack/solutions/security/plugins/security_solution/server/types.ts index cf6dd27591501..207aa40885a95 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/types.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/types.ts @@ -39,6 +39,7 @@ import type { IDetectionRulesClient } from './lib/detection_engine/rule_manageme import type { EntityStoreDataClient } from './lib/entity_analytics/entity_store/entity_store_data_client'; import type { SiemRuleMigrationsClient } from './lib/siem_migrations/rules/siem_rule_migrations_service'; import type { AssetInventoryDataClient } from './lib/asset_inventory/asset_inventory_data_client'; +import { PrivilegeMonitoringDataClient } from './lib/entity_analytics/privilege_monitoring/PrivilegeMonitoringDataClient'; export { AppClient }; export interface SecuritySolutionApiRequestHandlerContext { @@ -62,6 +63,7 @@ export interface SecuritySolutionApiRequestHandlerContext { getRiskScoreDataClient: () => RiskScoreDataClient; getAssetCriticalityDataClient: () => AssetCriticalityDataClient; getEntityStoreDataClient: () => EntityStoreDataClient; + getPrivilegeMonitoringDataClient: () => PrivilegeMonitoringDataClient; getSiemRuleMigrationsClient: () => SiemRuleMigrationsClient; getInferenceClient: () => InferenceClient; getAssetInventoryClient: () => AssetInventoryDataClient; diff --git a/x-pack/test/api_integration/services/security_solution_api.gen.ts b/x-pack/test/api_integration/services/security_solution_api.gen.ts index 6cdf64afab48f..1e25ba9e1e649 100644 --- a/x-pack/test/api_integration/services/security_solution_api.gen.ts +++ b/x-pack/test/api_integration/services/security_solution_api.gen.ts @@ -1170,6 +1170,13 @@ finalize it. .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .send(props.body as object); }, + initMonitoringEngine(kibanaSpace: string = 'default') { + return supertest + .post(routeWithNamespace('/api/entity_analytics/monitoring/engine/init', kibanaSpace)) + .set('kbn-xsrf', 'true') + .set(ELASTIC_HTTP_VERSION_HEADER, '2023-10-31') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana'); + }, /** * Initializes the Risk Engine by creating the necessary indices and mappings, removing old transforms, and starting the new risk engine */ From eafe365de6bbdcbe23a9f0d9e0a07a19660e9347 Mon Sep 17 00:00:00 2001 From: CAWilson94 Date: Mon, 24 Feb 2025 15:16:50 +0000 Subject: [PATCH 02/19] kibana task boilerplate for monitoring WiP --- .../privilege_monitoring/tasks/constants.ts | 10 ++ .../tasks/privilege_monitoring_task.ts | 107 ++++++++++++++++++ .../security_solution/server/plugin.ts | 9 ++ 3 files changed, 126 insertions(+) create mode 100644 x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/tasks/constants.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/tasks/privilege_monitoring_task.ts diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/tasks/constants.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/tasks/constants.ts new file mode 100644 index 0000000000000..d400145baa140 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/tasks/constants.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. + */ + +export const TYPE = 'entity_analytics:monitoring_engine:great_success'; +export const VERSION = '1.0.0'; +export const TIMEOUT = '10m'; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/tasks/privilege_monitoring_task.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/tasks/privilege_monitoring_task.ts new file mode 100644 index 0000000000000..82092212341e3 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/tasks/privilege_monitoring_task.ts @@ -0,0 +1,107 @@ +/* + * 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 { stateSchemaByVersion } from '@kbn/alerting-state-types'; +import type { Logger, AnalyticsServiceSetup } from '@kbn/core/server'; +import type { + TaskManagerSetupContract, + TaskRunCreatorFunction, +} from '@kbn/task-manager-plugin/server'; + +import type { ExperimentalFeatures } from '../../../../../common'; +import type { EntityAnalyticsRoutesDeps } from '../../types'; + +import { TYPE, VERSION, TIMEOUT } from './constants'; + +interface RegisterParams { + getStartServices: EntityAnalyticsRoutesDeps['getStartServices']; + logger: Logger; + telemetry: AnalyticsServiceSetup; + taskManager: TaskManagerSetupContract | undefined; + experimentalFeatures: ExperimentalFeatures; +} + +const getTaskName = (): string => TYPE; + +const getTaskId = (namespace: string): string => `${TYPE}:${namespace}:${VERSION}`; + +export const registerPrivilegeMonitoringTask = ({ + getStartServices, + logger, + telemetry, + taskManager, + experimentalFeatures, +}: RegisterParams): void => { + if (!taskManager) { + logger.info( + '[Privilege Monitoring] Task Manager is unavailable; skipping privilege monitoring task registration.' + ); + return; + } + + taskManager.registerTaskDefinitions({ + [getTaskName()]: { + title: 'Entity Analytics Privilege Monitoring - Great Success', + timeout: TIMEOUT, + stateSchemaByVersion, + createTaskRunner: createPrivilegeMonitoringTaskRunnerFactory({ + logger, + telemetry, + experimentalFeatures, + }), + }, + }); +}; + +const createPrivilegeMonitoringTaskRunnerFactory = + (deps: { + logger: Logger; + telemetry: AnalyticsServiceSetup; + experimentalFeatures: ExperimentalFeatures; + }): TaskRunCreatorFunction => + ({ taskInstance }) => { + let cancelled = false; + const isCancelled = () => cancelled; + return { + run: runPrivilegeMonitoringTask({ + isCancelled, + logger: deps.logger, + telemetry: deps.telemetry, + experimentalFeatures: deps.experimentalFeatures, + }), + cancel: async () => { + cancelled = true; + }, + }; + }; + +interface RunParams { + isCancelled: () => boolean; + logger: Logger; + telemetry: AnalyticsServiceSetup; + experimentalFeatures: ExperimentalFeatures; +} + +const runPrivilegeMonitoringTask = async ({ + isCancelled, + logger, + telemetry, + experimentalFeatures, +}: RunParams): Promise => { + logger.info('[Privilege Monitoring] Running privilege monitoring task'); + if (isCancelled()) { + logger.info('[Privilege Monitoring] Task was cancelled.'); + return; + } + + try { + // eslint-disable-next-line no-console + console.log('GREAT SUCCESS'); + } catch (e) { + logger.error('[Privilege Monitoring] Error running privilege monitoring task', e); + } +}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/plugin.ts b/x-pack/solutions/security/plugins/security_solution/server/plugin.ts index a655b777760e1..7726bd962d005 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/plugin.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/plugin.ts @@ -268,6 +268,15 @@ export class Plugin implements ISecuritySolutionPlugin { }); } + + registerPrivilegeMonitoringTask({ + getStartServices: core.getStartServices, + taskManager: plugins.taskManager, + logger: this.logger, + kibanaVersion: pluginContext.env.packageInfo.version, + experimentalFeatures, + }) + const requestContextFactory = new RequestContextFactory({ config, logger, From ad00e20f72f9b922a9bee7e7a07b630080eef782 Mon Sep 17 00:00:00 2001 From: CAWilson94 Date: Tue, 25 Feb 2025 13:43:24 +0000 Subject: [PATCH 03/19] Kibana task working Wip --- .../PrivilegeMonitoringDataClient.ts | 13 +++- .../privilege_monitoring/tasks/constants.ts | 2 + .../tasks/privilege_monitoring_task.ts | 75 +++++++++++++++---- .../security_solution/server/plugin.ts | 5 +- 4 files changed, 75 insertions(+), 20 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/PrivilegeMonitoringDataClient.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/PrivilegeMonitoringDataClient.ts index e44144414b25b..0beb0f9012134 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/PrivilegeMonitoringDataClient.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/PrivilegeMonitoringDataClient.ts @@ -16,6 +16,7 @@ import type { import type { TaskManagerStartContract } from '@kbn/task-manager-plugin/server'; import type { ApiKeyManager } from './auth/api_key'; +import { startPrivilegeMonitoringTask } from './tasks/privilege_monitoring_task'; interface PrivilegeMonitoringClientOpts { logger: Logger; @@ -38,7 +39,15 @@ export class PrivilegeMonitoringDataClient { this.apiKeyGenerator = opts.apiKeyManager; } - init() { - return Promise.resolve(); + async init() { + if (!this.opts.taskManager) { + throw new Error('Task Manager is not available'); + } + + await startPrivilegeMonitoringTask({ + logger: this.opts.logger, + namespace: this.opts.namespace, + taskManager: this.opts.taskManager + }) } } diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/tasks/constants.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/tasks/constants.ts index d400145baa140..ccaf017c0ca1b 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/tasks/constants.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/tasks/constants.ts @@ -5,6 +5,8 @@ * 2.0. */ +export const SCOPE = ['securitySolution']; export const TYPE = 'entity_analytics:monitoring_engine:great_success'; export const VERSION = '1.0.0'; export const TIMEOUT = '10m'; +export const INTERVAL = '1m'; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/tasks/privilege_monitoring_task.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/tasks/privilege_monitoring_task.ts index 82092212341e3..f3af7acb80876 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/tasks/privilege_monitoring_task.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/tasks/privilege_monitoring_task.ts @@ -5,17 +5,20 @@ * 2.0. */ -import { stateSchemaByVersion } from '@kbn/alerting-state-types'; + import type { Logger, AnalyticsServiceSetup } from '@kbn/core/server'; import type { + ConcreteTaskInstance, TaskManagerSetupContract, + TaskManagerStartContract, TaskRunCreatorFunction, } from '@kbn/task-manager-plugin/server'; import type { ExperimentalFeatures } from '../../../../../common'; import type { EntityAnalyticsRoutesDeps } from '../../types'; -import { TYPE, VERSION, TIMEOUT } from './constants'; +import { TYPE, VERSION, TIMEOUT, SCOPE, INTERVAL } from './constants'; +import { defaultState, stateSchemaByVersion } from './state'; interface RegisterParams { getStartServices: EntityAnalyticsRoutesDeps['getStartServices']; @@ -23,6 +26,21 @@ interface RegisterParams { telemetry: AnalyticsServiceSetup; taskManager: TaskManagerSetupContract | undefined; experimentalFeatures: ExperimentalFeatures; + kibanaVersion: string; +} + +interface RunParams { + isCancelled: () => boolean; + logger: Logger; + telemetry: AnalyticsServiceSetup; + experimentalFeatures: ExperimentalFeatures; + taskInstance: ConcreteTaskInstance; +} + +interface StartParams { + logger: Logger; + namespace: string; + taskManager: TaskManagerStartContract; } const getTaskName = (): string => TYPE; @@ -34,8 +52,10 @@ export const registerPrivilegeMonitoringTask = ({ logger, telemetry, taskManager, + kibanaVersion, experimentalFeatures, }: RegisterParams): void => { + console.log(`registering GREAT SUCCESS`); if (!taskManager) { logger.info( '[Privilege Monitoring] Task Manager is unavailable; skipping privilege monitoring task registration.' @@ -65,31 +85,28 @@ const createPrivilegeMonitoringTaskRunnerFactory = }): TaskRunCreatorFunction => ({ taskInstance }) => { let cancelled = false; + console.log(`Creating GREAT SUCCESS factory`); const isCancelled = () => cancelled; return { - run: runPrivilegeMonitoringTask({ - isCancelled, - logger: deps.logger, - telemetry: deps.telemetry, - experimentalFeatures: deps.experimentalFeatures, - }), + run: async () => + runPrivilegeMonitoringTask({ + isCancelled, + logger: deps.logger, + telemetry: deps.telemetry, + taskInstance, + experimentalFeatures: deps.experimentalFeatures, + }), cancel: async () => { cancelled = true; }, }; }; -interface RunParams { - isCancelled: () => boolean; - logger: Logger; - telemetry: AnalyticsServiceSetup; - experimentalFeatures: ExperimentalFeatures; -} - const runPrivilegeMonitoringTask = async ({ isCancelled, logger, telemetry, + taskInstance, experimentalFeatures, }: RunParams): Promise => { logger.info('[Privilege Monitoring] Running privilege monitoring task'); @@ -99,9 +116,35 @@ const runPrivilegeMonitoringTask = async ({ } try { - // eslint-disable-next-line no-console console.log('GREAT SUCCESS'); } catch (e) { logger.error('[Privilege Monitoring] Error running privilege monitoring task', e); } }; + +export const startPrivilegeMonitoringTask = async ({ + logger, + namespace, + taskManager, +}: StartParams) => { + const taskId = getTaskId(namespace); + + try { + console.log(`Starting the start GREAT SUCCESS`); + await taskManager.ensureScheduled({ + id: taskId, + taskType: getTaskName(), + scope: SCOPE, + schedule: { + interval: INTERVAL, + }, + state: { ...defaultState, namespace }, + params: { version: VERSION }, + }); + } catch (e) { + logger.warn( + `[Privilege Monitoring] [task ${taskId}]: error scheduling task, received ${e.message}` + ); + throw e; + } +}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/plugin.ts b/x-pack/solutions/security/plugins/security_solution/server/plugin.ts index 7726bd962d005..8397f308a25d2 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/plugin.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/plugin.ts @@ -133,6 +133,7 @@ import { scheduleEntityAnalyticsMigration } from './lib/entity_analytics/migrati import { SiemMigrationsService } from './lib/siem_migrations/siem_migrations_service'; import { TelemetryConfigProvider } from '../common/telemetry_config/telemetry_config_provider'; import { TelemetryConfigWatcher } from './endpoint/lib/policy/telemetry_watch'; +import { registerPrivilegeMonitoringTask } from './lib/entity_analytics/privilege_monitoring/tasks/privilege_monitoring_task'; export type { SetupPlugins, StartPlugins, PluginSetup, PluginStart } from './plugin_contract'; @@ -268,14 +269,14 @@ export class Plugin implements ISecuritySolutionPlugin { }); } - registerPrivilegeMonitoringTask({ getStartServices: core.getStartServices, taskManager: plugins.taskManager, logger: this.logger, + telemetry: core.analytics, kibanaVersion: pluginContext.env.packageInfo.version, experimentalFeatures, - }) + }); const requestContextFactory = new RequestContextFactory({ config, From bb1949141ca3f83f8b096a96f6566d0a0f68d665 Mon Sep 17 00:00:00 2001 From: CAWilson94 Date: Tue, 25 Feb 2025 15:37:09 +0000 Subject: [PATCH 04/19] state schema added for privmon --- .../privilege_monitoring/tasks/state.ts | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/tasks/state.ts diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/tasks/state.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/tasks/state.ts new file mode 100644 index 0000000000000..387c2c9f88455 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/tasks/state.ts @@ -0,0 +1,39 @@ +/* + * 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 { schema, type TypeOf } from '@kbn/config-schema'; + +/** + * WARNING: Do not modify the existing versioned schema(s) below, instead define a new version (ex: 2, 3, 4). + * This is required to support zero-downtime upgrades and rollbacks. See https://github.com/elastic/kibana/issues/155764. + * + * As you add a new schema version, don't forget to change latestTaskStateSchema variable to reference the latest schema. + * For example, changing stateSchemaByVersion[1].schema to stateSchemaByVersion[2].schema. + */ +export const stateSchemaByVersion = { + 1: { + up: (state: Record) => ({ + lastExecutionTimestamp: state.lastExecutionTimestamp || undefined, + runs: state.runs || 0, + namespace: typeof state.namespace === 'string' ? state.namespace : 'default', + }), + schema: schema.object({ + lastExecutionTimestamp: schema.maybe(schema.string()), + namespace: schema.string(), + runs: schema.number(), + }), + }, +}; + +const latestTaskStateSchema = stateSchemaByVersion[1].schema; +export type LatestTaskStateSchema = TypeOf; + +export const defaultState: LatestTaskStateSchema = { + lastExecutionTimestamp: undefined, + namespace: 'default', + runs: 0, +}; From 0c608ae7b6dbb10d97a708e3f7c74d5d2603974a Mon Sep 17 00:00:00 2001 From: CAWilson94 Date: Wed, 26 Feb 2025 12:07:08 +0000 Subject: [PATCH 05/19] indicies creation, console log cleanup, config and hook in indices start WiP save point --- .../PrivilegeMonitoringDataClient.ts | 16 ++++++++++++-- .../privilege_monitoring/configurations.ts | 12 +++++++++++ .../{tasks => }/constants.ts | 3 +++ .../privilege_monitoring/indicies.ts | 21 +++++++++++++++++++ .../tasks/privilege_monitoring_task.ts | 9 ++------ 5 files changed, 52 insertions(+), 9 deletions(-) create mode 100644 x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/configurations.ts rename x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/{tasks => }/constants.ts (77%) create mode 100644 x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/indicies.ts diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/PrivilegeMonitoringDataClient.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/PrivilegeMonitoringDataClient.ts index 0beb0f9012134..2bff5675ea513 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/PrivilegeMonitoringDataClient.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/PrivilegeMonitoringDataClient.ts @@ -17,6 +17,7 @@ import type { import type { TaskManagerStartContract } from '@kbn/task-manager-plugin/server'; import type { ApiKeyManager } from './auth/api_key'; import { startPrivilegeMonitoringTask } from './tasks/privilege_monitoring_task'; +import { createOrUpdateIndex } from '../utils/create_or_update_index'; interface PrivilegeMonitoringClientOpts { logger: Logger; @@ -40,6 +41,7 @@ export class PrivilegeMonitoringDataClient { } async init() { + await this.createOrUpdateIndex(); if (!this.opts.taskManager) { throw new Error('Task Manager is not available'); } @@ -47,7 +49,17 @@ export class PrivilegeMonitoringDataClient { await startPrivilegeMonitoringTask({ logger: this.opts.logger, namespace: this.opts.namespace, - taskManager: this.opts.taskManager - }) + taskManager: this.opts.taskManager, + }); + } + + public async createOrUpdateIndex() { + await createOrUpdateIndex({ + esClient: this.esClient, + logger: this.opts.logger, + options: { + index: '', // TODO: update + }, + }); } } diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/configurations.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/configurations.ts new file mode 100644 index 0000000000000..9f264497f94ec --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/configurations.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 type { FieldMap } from '@kbn/alerts-as-data-utils'; + +export const privilegedMonitorUserFieldMap: FieldMap = { + // TODO: fill this in. +}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/tasks/constants.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/constants.ts similarity index 77% rename from x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/tasks/constants.ts rename to x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/constants.ts index ccaf017c0ca1b..97d1a2871267f 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/tasks/constants.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/constants.ts @@ -10,3 +10,6 @@ export const TYPE = 'entity_analytics:monitoring_engine:great_success'; export const VERSION = '1.0.0'; export const TIMEOUT = '10m'; export const INTERVAL = '1m'; + +// Upgrade this value to force a mappings update on the next Kibana startup +export const PRIVILEGE_MONITORING_MAPPINGS_VERSIONS = 1; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/indicies.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/indicies.ts new file mode 100644 index 0000000000000..89e1e253d6c45 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/indicies.ts @@ -0,0 +1,21 @@ +/* + * 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. + */ + +type PrincipalType = 'user' | 'group'; + +// Static index names: may be more obvious and easier to manage. +export const privilegedMonitorBaseIndexName = '.entity_analytics.monitoring'; +// Used in Phase 0. +export const getPrivilegedMonitorUsersIndex = (namespace: string) => + `${privilegedMonitorBaseIndexName}.users-${namespace}`; +// Not required in phase 0. +export const getPrivilegedMonitorGroupsIndex = (namespace: string) => + `${privilegedMonitorBaseIndexName}.groups-${namespace}`; + +// Dynamic Index names: based on user or group usage. Not sure if this is good practice within Kibana, TODO: ask around. +export const getPrivilegedMonitorIndex = (namespace: string, principleType: PrincipalType) => + `${privilegedMonitorBaseIndexName}.${principleType}-${namespace}`; \ No newline at end of file diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/tasks/privilege_monitoring_task.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/tasks/privilege_monitoring_task.ts index f3af7acb80876..e8e567b57cf95 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/tasks/privilege_monitoring_task.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/tasks/privilege_monitoring_task.ts @@ -5,7 +5,6 @@ * 2.0. */ - import type { Logger, AnalyticsServiceSetup } from '@kbn/core/server'; import type { ConcreteTaskInstance, @@ -17,7 +16,7 @@ import type { import type { ExperimentalFeatures } from '../../../../../common'; import type { EntityAnalyticsRoutesDeps } from '../../types'; -import { TYPE, VERSION, TIMEOUT, SCOPE, INTERVAL } from './constants'; +import { TYPE, VERSION, TIMEOUT, SCOPE, INTERVAL } from '../constants'; import { defaultState, stateSchemaByVersion } from './state'; interface RegisterParams { @@ -55,7 +54,6 @@ export const registerPrivilegeMonitoringTask = ({ kibanaVersion, experimentalFeatures, }: RegisterParams): void => { - console.log(`registering GREAT SUCCESS`); if (!taskManager) { logger.info( '[Privilege Monitoring] Task Manager is unavailable; skipping privilege monitoring task registration.' @@ -85,7 +83,6 @@ const createPrivilegeMonitoringTaskRunnerFactory = }): TaskRunCreatorFunction => ({ taskInstance }) => { let cancelled = false; - console.log(`Creating GREAT SUCCESS factory`); const isCancelled = () => cancelled; return { run: async () => @@ -109,14 +106,13 @@ const runPrivilegeMonitoringTask = async ({ taskInstance, experimentalFeatures, }: RunParams): Promise => { - logger.info('[Privilege Monitoring] Running privilege monitoring task'); if (isCancelled()) { logger.info('[Privilege Monitoring] Task was cancelled.'); return; } try { - console.log('GREAT SUCCESS'); + logger.info('[Privilege Monitoring] Running privilege monitoring task'); } catch (e) { logger.error('[Privilege Monitoring] Error running privilege monitoring task', e); } @@ -130,7 +126,6 @@ export const startPrivilegeMonitoringTask = async ({ const taskId = getTaskId(namespace); try { - console.log(`Starting the start GREAT SUCCESS`); await taskManager.ensureScheduled({ id: taskId, taskType: getTaskName(), From fa40058c1ca3a2963c46e06f0f7d38f7ea4a6ee8 Mon Sep 17 00:00:00 2001 From: CAWilson94 Date: Thu, 27 Feb 2025 11:25:20 +0000 Subject: [PATCH 06/19] user indices, basic schema added and test started with mocks --- .../routes/__mocks__/request_context.ts | 3 +++ .../privilege_monitoring/indicies.ts | 25 ++++++++++++++++++- .../privilege_monitoring_data_client.mock.ts | 9 +++++++ .../privilege_monitoring_data_client.test.ts | 15 +++++++++++ ...ts => privilege_monitoring_data_client.ts} | 8 +++++- .../server/request_context_factory.ts | 2 +- .../plugins/security_solution/server/types.ts | 2 +- 7 files changed, 60 insertions(+), 4 deletions(-) create mode 100644 x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/privilege_monitoring_data_client.mock.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/privilege_monitoring_data_client.test.ts rename x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/{PrivilegeMonitoringDataClient.ts => privilege_monitoring_data_client.ts} (87%) diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts index e31782c13cd1d..023954fefc827 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts @@ -44,6 +44,7 @@ import { packageServiceMock } from '@kbn/fleet-plugin/server/services/epm/packag import type { EndpointInternalFleetServicesInterface } from '../../../../endpoint/services/fleet'; import { siemMigrationsServiceMock } from '../../../siem_migrations/__mocks__/mocks'; import { AssetInventoryDataClientMock } from '../../../asset_inventory/asset_inventory_data_client.mock'; +import { privilegeMonitorDataClientMock } from '../../../entity_analytics/privilege_monitoring/privilege_monitoring_data_client.mock'; export const createMockClients = () => { const core = coreMock.createRequestHandlerContext(); @@ -76,6 +77,7 @@ export const createMockClients = () => { riskScoreDataClient: riskScoreDataClientMock.create(), assetCriticalityDataClient: assetCriticalityDataClientMock.create(), entityStoreDataClient: entityStoreDataClientMock.create(), + privilegeMonitorDataClient: privilegeMonitorDataClientMock.create(), internalFleetServices: { packages: packageServiceMock.createClient(), @@ -169,6 +171,7 @@ const createSecuritySolutionRequestContextMock = ( getAuditLogger: jest.fn(() => mockAuditLogger), getDataViewsService: jest.fn(), getEntityStoreDataClient: jest.fn(() => clients.entityStoreDataClient), + getPrivilegeMonitoringDataClient: jest.fn(() => clients.privilegeMonitorDataClient), getSiemRuleMigrationsClient: jest.fn(() => clients.siemRuleMigrationsClient), getInferenceClient: jest.fn(() => clients.getInferenceClient()), getAssetInventoryClient: jest.fn(() => clients.assetInventoryDataClient), diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/indicies.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/indicies.ts index 89e1e253d6c45..be0bac3354cdf 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/indicies.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/indicies.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { MappingTypeMapping } from "@elastic/elasticsearch/lib/api/types"; + type PrincipalType = 'user' | 'group'; // Static index names: may be more obvious and easier to manage. @@ -18,4 +20,25 @@ export const getPrivilegedMonitorGroupsIndex = (namespace: string) => // Dynamic Index names: based on user or group usage. Not sure if this is good practice within Kibana, TODO: ask around. export const getPrivilegedMonitorIndex = (namespace: string, principleType: PrincipalType) => - `${privilegedMonitorBaseIndexName}.${principleType}-${namespace}`; \ No newline at end of file + `${privilegedMonitorBaseIndexName}.${principleType}-${namespace}`; + +export type MappingProperties = NonNullable; + +export const PRIVILEGED_MONITOR_USERS_INDEX_MAPPING: MappingProperties = { + 'event.ingested': { + type: 'date', + }, + '@timestamp': { + type: 'date', + }, + 'user.name': { + type: 'keyword', + }, + 'labels.is_privileged': { + type: 'boolean', + }, +}; + +export const generateUserIndexMappings = (): MappingTypeMapping => ({ + properties: PRIVILEGED_MONITOR_USERS_INDEX_MAPPING, +}); \ No newline at end of file diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/privilege_monitoring_data_client.mock.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/privilege_monitoring_data_client.mock.ts new file mode 100644 index 0000000000000..1cc3138590f79 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/privilege_monitoring_data_client.mock.ts @@ -0,0 +1,9 @@ + +import type { PrivilegeMonitoringDataClient } from './privilege_monitoring_data_client'; + +const createPrivilegeMonitorDataClientMock = () => + ({ + init: jest.fn(), + } as unknown as jest.Mocked); + +export const privilegeMonitorDataClientMock = { create: createPrivilegeMonitorDataClientMock }; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/privilege_monitoring_data_client.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/privilege_monitoring_data_client.test.ts new file mode 100644 index 0000000000000..968391519d835 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/privilege_monitoring_data_client.test.ts @@ -0,0 +1,15 @@ +import { elasticsearchServiceMock, savedObjectsClientMock, loggingSystemMock } from "@kbn/core/server/mocks"; +import { PrivilegeMonitoringDataClient } from "./privilege_monitoring_data_client"; + +describe('Privilege Monitoring Data Client', () => { + const mockSavedObjectClient = savedObjectsClientMock.create(); + const clusterClientMock = elasticsearchServiceMock.createScopedClusterClient(); + const loggerMock = loggingSystemMock.createLogger(); + const dataClient = new PrivilegeMonitoringDataClient({ + logger: loggerMock, + clusterClient: clusterClientMock, + namespace: 'default', + soClient: mockSavedObjectClient, + kibanaVersion: '8.0.0' + }); +}); \ No newline at end of file diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/PrivilegeMonitoringDataClient.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/privilege_monitoring_data_client.ts similarity index 87% rename from x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/PrivilegeMonitoringDataClient.ts rename to x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/privilege_monitoring_data_client.ts index 2bff5675ea513..0f3ca063fa2ba 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/PrivilegeMonitoringDataClient.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/privilege_monitoring_data_client.ts @@ -18,6 +18,7 @@ import type { TaskManagerStartContract } from '@kbn/task-manager-plugin/server'; import type { ApiKeyManager } from './auth/api_key'; import { startPrivilegeMonitoringTask } from './tasks/privilege_monitoring_task'; import { createOrUpdateIndex } from '../utils/create_or_update_index'; +import { generateUserIndexMappings, getPrivilegedMonitorUsersIndex } from './indicies'; interface PrivilegeMonitoringClientOpts { logger: Logger; @@ -58,8 +59,13 @@ export class PrivilegeMonitoringDataClient { esClient: this.esClient, logger: this.opts.logger, options: { - index: '', // TODO: update + index: this.getIndex(), + mappings: generateUserIndexMappings(), }, }); } + + public getIndex(){ + return getPrivilegedMonitorUsersIndex(this.opts.namespace); + } } diff --git a/x-pack/solutions/security/plugins/security_solution/server/request_context_factory.ts b/x-pack/solutions/security/plugins/security_solution/server/request_context_factory.ts index 93491fe5b380f..b94e1b52e9717 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/request_context_factory.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/request_context_factory.ts @@ -37,7 +37,7 @@ import type { SecuritySolutionApiRequestHandlerContext, SecuritySolutionRequestHandlerContext, } from './types'; -import { PrivilegeMonitoringDataClient } from './lib/entity_analytics/privilege_monitoring/PrivilegeMonitoringDataClient'; +import { PrivilegeMonitoringDataClient } from './lib/entity_analytics/privilege_monitoring/privilege_monitoring_data_client'; export interface IRequestContextFactory { create( diff --git a/x-pack/solutions/security/plugins/security_solution/server/types.ts b/x-pack/solutions/security/plugins/security_solution/server/types.ts index 207aa40885a95..68d23f6f66226 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/types.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/types.ts @@ -39,7 +39,7 @@ import type { IDetectionRulesClient } from './lib/detection_engine/rule_manageme import type { EntityStoreDataClient } from './lib/entity_analytics/entity_store/entity_store_data_client'; import type { SiemRuleMigrationsClient } from './lib/siem_migrations/rules/siem_rule_migrations_service'; import type { AssetInventoryDataClient } from './lib/asset_inventory/asset_inventory_data_client'; -import { PrivilegeMonitoringDataClient } from './lib/entity_analytics/privilege_monitoring/PrivilegeMonitoringDataClient'; +import { PrivilegeMonitoringDataClient } from './lib/entity_analytics/privilege_monitoring/privilege_monitoring_data_client'; export { AppClient }; export interface SecuritySolutionApiRequestHandlerContext { From 3d7cb7f1e9cc4eea3ee2f1afbad696b8ab1f957b Mon Sep 17 00:00:00 2001 From: Charlotte Alexandra Wilson Date: Thu, 27 Feb 2025 11:32:46 +0000 Subject: [PATCH 07/19] remove unnecessary fieldMap config --- .../entity_analytics/privilege_monitoring/configurations.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/configurations.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/configurations.ts index 9f264497f94ec..1fec1c76430eb 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/configurations.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/configurations.ts @@ -4,9 +4,3 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - -import type { FieldMap } from '@kbn/alerts-as-data-utils'; - -export const privilegedMonitorUserFieldMap: FieldMap = { - // TODO: fill this in. -}; From fefde7ec2dee8385692ab9cd4323018fe6fb51c2 Mon Sep 17 00:00:00 2001 From: CAWilson94 Date: Thu, 27 Feb 2025 11:46:56 +0000 Subject: [PATCH 08/19] base mapping for groups index --- .../privilege_monitoring/indicies.ts | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/indicies.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/indicies.ts index be0bac3354cdf..857e25a7adb1a 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/indicies.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/indicies.ts @@ -39,6 +39,27 @@ export const PRIVILEGED_MONITOR_USERS_INDEX_MAPPING: MappingProperties = { }, }; +export const PRIVILEGED_MONITOR_GROUPS_INDEX_MAPPING: MappingProperties = { + 'event.ingested': { + type: 'date', + }, + '@timestamp': { + type: 'date', + }, + 'group.name': { + type: 'keyword', + }, + 'indexPattern': { + type: 'keyword', + }, + 'nameMatcher': { + type: 'keyword', + }, + 'labels.is_privileged': { + type: 'boolean', + }, +}; + export const generateUserIndexMappings = (): MappingTypeMapping => ({ properties: PRIVILEGED_MONITOR_USERS_INDEX_MAPPING, -}); \ No newline at end of file +}); From 3cde8900f1cb1efbd853a981639b6917df4eccaf Mon Sep 17 00:00:00 2001 From: CAWilson94 Date: Fri, 28 Feb 2025 11:44:39 +0000 Subject: [PATCH 09/19] Wip: Saved objects Co-authored-by: Tiago Vila Verde --- .../saved_object/engine_descriptor_type.ts | 2 +- .../privilege_monitoring/auth/api_key.ts | 221 +++++++++++------- .../privilege_monitoring/constants.ts | 8 + .../privilege_monitoring/indicies.ts | 6 - .../saved_object/privilege_monitoring.ts | 74 ++++++ .../saved_object/privilege_monitoring_type.ts | 35 +++ .../security_solution/server/saved_objects.ts | 2 + 7 files changed, 261 insertions(+), 87 deletions(-) create mode 100644 x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/saved_object/privilege_monitoring.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/saved_object/privilege_monitoring_type.ts diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/saved_object/engine_descriptor_type.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/saved_object/engine_descriptor_type.ts index 07146ec145236..7ceed4ec39f78 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/saved_object/engine_descriptor_type.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/saved_object/engine_descriptor_type.ts @@ -35,7 +35,7 @@ export const entityEngineDescriptorTypeMappings: SavedObjectsType['mappings'] = type: 'keyword', // timestampFieldName : @timestamp | event.ingested }, }, -}; +}; const version1: SavedObjectsModelVersion = { changes: [ diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/auth/api_key.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/auth/api_key.ts index 812df752f341d..1c1c6924a24d5 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/auth/api_key.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/auth/api_key.ts @@ -6,107 +6,168 @@ */ import type { KibanaRequest } from '@kbn/core-http-server'; -import { generateEntityDiscoveryAPIKey } from '@kbn/entityManager-plugin/server/lib/auth'; -import { EntityDiscoveryApiKeyType } from '@kbn/entityManager-plugin/server/saved_objects'; import type { CoreStart } from '@kbn/core-lifecycle-server'; import type { Logger } from '@kbn/logging'; import type { SecurityPluginStart } from '@kbn/security-plugin-types-server'; import type { EncryptedSavedObjectsPluginStart } from '@kbn/encrypted-saved-objects-plugin/server'; import { getFakeKibanaRequest } from '@kbn/security-plugin/server/authentication/api_keys/fake_kibana_request'; -import type { EntityDiscoveryAPIKey } from '@kbn/entityManager-plugin/server/lib/auth/api_key/api_key'; import { getSpaceAwareEntityDiscoverySavedObjectId } from '@kbn/entityManager-plugin/server/lib/auth/api_key/saved_object'; import { SavedObjectsErrorHelpers } from '@kbn/core-saved-objects-server'; +import { SavedObjectsType } from '@kbn/core/server'; +import { EntityManagerServerSetup } from '@kbn/entityManager-plugin/server/types'; export interface ApiKeyManager { generate: () => Promise; } -export const getApiKeyManager = ({ - core, - logger, - security, - encryptedSavedObjects, - request, - namespace, -}: { +export interface ApiKeyManagerDependencies { core: CoreStart; logger: Logger; security: SecurityPluginStart; encryptedSavedObjects?: EncryptedSavedObjectsPluginStart; request?: KibanaRequest; namespace: string; -}) => ({ - generate: async () => { - if (!encryptedSavedObjects) { - throw new Error( - 'Unable to create API key. Ensure encrypted Saved Object client is enabled in this environment.' - ); - } else if (!request) { - throw new Error('Unable to create API key due to invalid request'); - } else { - const apiKey = await generateEntityDiscoveryAPIKey( - { - core, - config: {}, - logger, - security, - encryptedSavedObjects, - }, - request - ); - - const soClient = core.savedObjects.getScopedClient(request, { - includedHiddenTypes: [EntityDiscoveryApiKeyType.name], - }); +} + + +export const getApiKeyManager = (deps: ApiKeyManagerDependencies) => { + + return { + generate: generate(deps), + getApiKey: getApiKey(deps), + getRequestFromApiKey: getRequestFromApiKey(deps), + getClientFromApiKey: getClientFromApiKey(deps), + } +} + + + +const generate = async (deps: ApiKeyManagerDependencies) => { + const { core, logger, security, encryptedSavedObjects, request, namespace } = deps + if (!encryptedSavedObjects) { + throw new Error( + 'Unable to create API key. Ensure encrypted Saved Object client is enabled in this environment.' + ); + } else if (!request) { + throw new Error('Unable to create API key due to invalid request'); + } else { + const apiKey = await generateAPIKey( + { + core, + config: {}, + logger, + security, + encryptedSavedObjects, + }, + request + ); + + const soClient = core.savedObjects.getScopedClient(request, { + includedHiddenTypes: [PrivilegeMonitoringApiKeyType.name], + }); - await soClient.create(EntityDiscoveryApiKeyType.name, apiKey, { - id: getSpaceAwareEntityDiscoverySavedObjectId(namespace), - overwrite: true, - managed: true, + await soClient.create(PrivilegeMonitoringApiKeyType.name, apiKey, { + id: getSpaceAwareEntityDiscoverySavedObjectId(namespace), + overwrite: true, + managed: true, + }); + } +} +const getApiKey: async () => { + if (!encryptedSavedObjects) { + throw Error( + 'Unable to retrieve API key. Ensure encrypted Saved Object client is enabled in this environment.' + ); + } + try { + const encryptedSavedObjectsClient = encryptedSavedObjects.getClient({ + includedHiddenTypes: [PrivilegeMonitoringApiKeyType.name], + }); + return ( + await encryptedSavedObjectsClient.getDecryptedAsInternalUser( + PrivilegeMonitoringApiKeyType.name, + getSpaceAwareEntityDiscoverySavedObjectId(namespace) + ) + ).attributes; + } catch (err) { + if (SavedObjectsErrorHelpers.isNotFoundError(err)) { + return undefined; + } + throw err; + } + }, + getRequestFromApiKey: async (apiKey: PrivilegeMonitoringAPIKey) => { + return getFakeKibanaRequest({ + id: apiKey.id, + api_key: apiKey.apiKey, }); - } - }, - getApiKey: async () => { - if (!encryptedSavedObjects) { - throw Error( - 'Unable to retrieve API key. Ensure encrypted Saved Object client is enabled in this environment.' - ); - } - try { - const encryptedSavedObjectsClient = encryptedSavedObjects.getClient({ - includedHiddenTypes: [EntityDiscoveryApiKeyType.name], + }, + getClientFromApiKey: async (apiKey: PrivilegeMonitoringAPIKey) => { + const fakeRequest = getFakeKibanaRequest({ + id: apiKey.id, + api_key: apiKey.apiKey, }); - return ( - await encryptedSavedObjectsClient.getDecryptedAsInternalUser( - EntityDiscoveryApiKeyType.name, - getSpaceAwareEntityDiscoverySavedObjectId(namespace) - ) - ).attributes; - } catch (err) { - if (SavedObjectsErrorHelpers.isNotFoundError(err)) { - return undefined; - } - throw err; - } - }, - getRequestFromApiKey: async (apiKey: EntityDiscoveryAPIKey) => { - return getFakeKibanaRequest({ - id: apiKey.id, - api_key: apiKey.apiKey, - }); - }, - getClientFromApiKey: async (apiKey: EntityDiscoveryAPIKey) => { - const fakeRequest = getFakeKibanaRequest({ - id: apiKey.id, - api_key: apiKey.apiKey, - }); - const clusterClient = core.elasticsearch.client.asScoped(fakeRequest); - const soClient = core.savedObjects.getScopedClient(fakeRequest, { - includedHiddenTypes: [EntityDiscoveryApiKeyType.name], - }); + const clusterClient = core.elasticsearch.client.asScoped(fakeRequest); + const soClient = core.savedObjects.getScopedClient(fakeRequest, { + includedHiddenTypes: [PrivilegeMonitoringApiKeyType.name], + }); + return { + clusterClient, + soClient, + }; + }, + } + +}; + + +export const generateAPIKey = async ( + req: KibanaRequest, + server: EntityManagerServerSetup, +): Promise => { + const apiKey = await server.security.authc.apiKeys.grantAsInternalUser(req, { + name: 'Entity discovery API key', + role_descriptors: { + entity_discovery_admin: entityDefinitionRuntimePrivileges, + }, + metadata: { + description: + 'API key used to manage the transforms and ingest pipelines created by the entity discovery framework', + }, + }); + + if (apiKey !== null) { return { - clusterClient, - soClient, + id: apiKey.id, + name: apiKey.name, + apiKey: apiKey.api_key, }; + } +}; + + + +export const SO_PRIVILEGE_MONITORING_API_KEY_TYPE = 'entity-discovery-api-key'; + +export const PrivilegeMonitoringApiKeyType: SavedObjectsType = { + name: SO_PRIVILEGE_MONITORING_API_KEY_TYPE, + hidden: true, + namespaceType: 'multiple-isolated', + mappings: { + dynamic: false, + properties: { + apiKey: { type: 'binary' }, + }, }, -}); + management: { + importableAndExportable: false, + displayName: 'Privilege Monitoring API key', + }, +}; + + +export interface PrivilegeMonitoringAPIKey { + id: string; + name: string; + apiKey: string; +} diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/constants.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/constants.ts index 97d1a2871267f..d1b512057316f 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/constants.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/constants.ts @@ -5,6 +5,7 @@ * 2.0. */ + export const SCOPE = ['securitySolution']; export const TYPE = 'entity_analytics:monitoring_engine:great_success'; export const VERSION = '1.0.0'; @@ -13,3 +14,10 @@ export const INTERVAL = '1m'; // Upgrade this value to force a mappings update on the next Kibana startup export const PRIVILEGE_MONITORING_MAPPINGS_VERSIONS = 1; + +export const PRIVILEGE_MONITORING_ENGINE_STATUS = { + INSTALLING: 'installing', + STARTED: 'started', + STOPPED: 'stopped', + ERROR: 'error', + }; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/indicies.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/indicies.ts index 857e25a7adb1a..d7264150017b8 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/indicies.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/indicies.ts @@ -7,8 +7,6 @@ import { MappingTypeMapping } from "@elastic/elasticsearch/lib/api/types"; -type PrincipalType = 'user' | 'group'; - // Static index names: may be more obvious and easier to manage. export const privilegedMonitorBaseIndexName = '.entity_analytics.monitoring'; // Used in Phase 0. @@ -18,10 +16,6 @@ export const getPrivilegedMonitorUsersIndex = (namespace: string) => export const getPrivilegedMonitorGroupsIndex = (namespace: string) => `${privilegedMonitorBaseIndexName}.groups-${namespace}`; -// Dynamic Index names: based on user or group usage. Not sure if this is good practice within Kibana, TODO: ask around. -export const getPrivilegedMonitorIndex = (namespace: string, principleType: PrincipalType) => - `${privilegedMonitorBaseIndexName}.${principleType}-${namespace}`; - export type MappingProperties = NonNullable; export const PRIVILEGED_MONITOR_USERS_INDEX_MAPPING: MappingProperties = { diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/saved_object/privilege_monitoring.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/saved_object/privilege_monitoring.ts new file mode 100644 index 0000000000000..a63f1d4ee9b31 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/saved_object/privilege_monitoring.ts @@ -0,0 +1,74 @@ +import { SavedObjectsClientContract } from "@kbn/core/server"; +import { privilegeMonitoringTypeName } from "./privilege_monitoring_type"; +import { PRIVILEGE_MONITORING_ENGINE_STATUS } from "../constants"; + +interface privilegeMonitoringEngineDescriptorDependencies { + soClient: SavedObjectsClientContract; + namespace: string; +} + +interface PrivilegedMonitoringEngineDescriptor { + status: string; + apiKey: string; +} + +export type PrivilegeMonitoringEngineStatus = 'installing' | 'started' | 'stopped' | 'error'; + +export class PrivilegeMonitoringEngineDescriptorClient { + + constructor(private readonly deps: privilegeMonitoringEngineDescriptorDependencies) { } + + getSavedObjectId() { + return `privilege-monitoring-${this.deps.namespace}`; + } + + async init() { + const { attributes } = await this.deps.soClient.create( + privilegeMonitoringTypeName, + { + status: PRIVILEGE_MONITORING_ENGINE_STATUS.INSTALLING, + apiKey: '', + }, + { id: this.getSavedObjectId() } + ); + return attributes; + } + + async update(engine: Partial) { + const id = this.getSavedObjectId(); + const { attributes } = await this.deps.soClient.update( + privilegeMonitoringTypeName, + id, + engine, + { refresh: 'wait_for' } + ); + return attributes; + } + + async updateStatus(status: PrivilegeMonitoringEngineStatus) { + return this.update({ status }) + } + + async find() { + return this.deps.soClient.find({ + type: privilegeMonitoringTypeName, + namespaces: [this.deps.namespace], + }); + } + + async get() { + const id = this.getSavedObjectId(); + const { attributes } = await this.deps.soClient.get( + privilegeMonitoringTypeName, + id + ); + return attributes; + } + + async delete() { + const id = this.getSavedObjectId(); + return this.deps.soClient.delete(privilegeMonitoringTypeName, id); + } + + +} \ No newline at end of file diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/saved_object/privilege_monitoring_type.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/saved_object/privilege_monitoring_type.ts new file mode 100644 index 0000000000000..9e3237740f796 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/saved_object/privilege_monitoring_type.ts @@ -0,0 +1,35 @@ + +import { SECURITY_SOLUTION_SAVED_OBJECT_INDEX, SavedObjectsModelVersion } from '@kbn/core/packages/saved-objects/server'; +import type { SavedObjectsType } from '@kbn/core/server'; + + +export const privilegeMonitoringTypeName = 'privilege-monitoring-status'; + +export const privilegeMonitoringTypeNameMappings: SavedObjectsType['mappings'] = { + dynamic: false, + properties: { + status: { + type: 'keyword', + }, + }, + }; + + const version1: SavedObjectsModelVersion = { + changes: [ + { + type: 'mappings_addition', + addedMappings: { + status: { type: 'keyword' }, + }, + }, + ], + }; + + export const privilegeMonitoringType: SavedObjectsType = { + name: privilegeMonitoringTypeName, + indexPattern: SECURITY_SOLUTION_SAVED_OBJECT_INDEX, + hidden: false, + namespaceType: 'multiple-isolated', + mappings: privilegeMonitoringTypeNameMappings, + modelVersions: { 1: version1 }, + }; \ No newline at end of file diff --git a/x-pack/solutions/security/plugins/security_solution/server/saved_objects.ts b/x-pack/solutions/security/plugins/security_solution/server/saved_objects.ts index 8df5eeead9daa..2a45e630b1b63 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/saved_objects.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/saved_objects.ts @@ -17,6 +17,7 @@ import { type as signalsMigrationType } from './lib/detection_engine/migrations/ import { manifestType, unifiedManifestType } from './endpoint/lib/artifacts/saved_object_mappings'; import { riskEngineConfigurationType } from './lib/entity_analytics/risk_engine/saved_object'; import { entityEngineDescriptorType } from './lib/entity_analytics/entity_store/saved_object'; +import { privilegeMonitoringType } from './lib/entity_analytics/privilege_monitoring/saved_object/privilege_monitoring_type'; const types = [ noteType, @@ -29,6 +30,7 @@ const types = [ signalsMigrationType, riskEngineConfigurationType, entityEngineDescriptorType, + privilegeMonitoringType, protectionUpdatesNoteType, promptType, ]; From 76ad02a037da33028a410bc0b5a169f15c097832 Mon Sep 17 00:00:00 2001 From: CAWilson94 Date: Fri, 28 Feb 2025 13:42:40 +0000 Subject: [PATCH 10/19] priv data client using es asCurrentUser --- .../privilege_monitoring/privilege_monitoring_data_client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/privilege_monitoring_data_client.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/privilege_monitoring_data_client.ts index 0f3ca063fa2ba..94a6e416f19ee 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/privilege_monitoring_data_client.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/privilege_monitoring_data_client.ts @@ -37,7 +37,7 @@ export class PrivilegeMonitoringDataClient { private esClient: ElasticsearchClient; constructor(private readonly opts: PrivilegeMonitoringClientOpts) { - this.esClient = opts.clusterClient.asInternalUser; + this.esClient = opts.clusterClient.asCurrentUser; this.apiKeyGenerator = opts.apiKeyManager; } From fb18be38fb21db47fef393a376fb25ac32d383ce Mon Sep 17 00:00:00 2001 From: CAWilson94 Date: Fri, 28 Feb 2025 16:04:01 +0000 Subject: [PATCH 11/19] init flow on data client with descriptor and update SO Status when installing --- .../privilege_monitoring_data_client.ts | 61 ++++++++++++++++++- 1 file changed, 58 insertions(+), 3 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/privilege_monitoring_data_client.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/privilege_monitoring_data_client.ts index 94a6e416f19ee..6babeafa512de 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/privilege_monitoring_data_client.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/privilege_monitoring_data_client.ts @@ -19,6 +19,7 @@ import type { ApiKeyManager } from './auth/api_key'; import { startPrivilegeMonitoringTask } from './tasks/privilege_monitoring_task'; import { createOrUpdateIndex } from '../utils/create_or_update_index'; import { generateUserIndexMappings, getPrivilegedMonitorUsersIndex } from './indicies'; +import { PrivilegeMonitoringEngineDescriptorClient } from './saved_object/privilege_monitoring'; interface PrivilegeMonitoringClientOpts { logger: Logger; @@ -32,26 +33,71 @@ interface PrivilegeMonitoringClientOpts { apiKeyManager?: ApiKeyManager; } + // TODO: update this - need some openAPI generated types here + interface InitPrivilegedMonitoringEntityEngineResponse { + status: string; + apiKey: string; +} + export class PrivilegeMonitoringDataClient { private apiKeyGenerator?: ApiKeyManager; private esClient: ElasticsearchClient; + private engineClient: PrivilegeMonitoringEngineDescriptorClient; + constructor(private readonly opts: PrivilegeMonitoringClientOpts) { this.esClient = opts.clusterClient.asCurrentUser; this.apiKeyGenerator = opts.apiKeyManager; + this.engineClient = new PrivilegeMonitoringEngineDescriptorClient({ + soClient: opts.soClient, + namespace: opts.namespace, + }) + } + + + private async enable(){ + /** + * TODO: fill this in + */ } +/** + * + * init the engine, + * kibana task created, + * create save object with engine status and api key of user who enabled engine -- + * ticket does not say enable, but is this implied? Or just ability of saved object when we want to enable engine? + * indices created + * + * definition and descriptor -- different things. ** + */ + async init():Promise { - async init() { - await this.createOrUpdateIndex(); if (!this.opts.taskManager) { throw new Error('Task Manager is not available'); } + await this.createOrUpdateIndex().catch((e) => { + if (e.meta.body.error.type === 'resource_already_exists_exception') { + this.opts.logger.info('Privilege monitoring index already exists'); + } + }); + + const descriptor = await this.engineClient.init(); + this.log('debug', `Initialized privileged monitoring engine saved object`); + + if (this.apiKeyGenerator) { + await this.apiKeyGenerator.generate(); // TODO: need this in a saved object? + } + await startPrivilegeMonitoringTask({ logger: this.opts.logger, namespace: this.opts.namespace, taskManager: this.opts.taskManager, }); + + await this.engineClient.update({ status: 'installing' }) + + return descriptor; } public async createOrUpdateIndex() { @@ -65,7 +111,16 @@ export class PrivilegeMonitoringDataClient { }); } - public getIndex(){ + public getIndex() { return getPrivilegedMonitorUsersIndex(this.opts.namespace); } + + private log( + level: Exclude, + msg: string + ) { + this.opts.logger[level]( + `[Privileged Monitoring Engine][namespace: ${this.opts.namespace}] ${msg}` + ); + } } From 682c188df18499c120b7bb6a61f82f884e2f4054 Mon Sep 17 00:00:00 2001 From: CAWilson94 Date: Mon, 3 Mar 2025 10:39:49 +0000 Subject: [PATCH 12/19] formatting and logging --- .../privilege_monitoring_data_client.ts | 44 +++---- .../saved_object/privilege_monitoring.ts | 120 +++++++++--------- .../tasks/privilege_monitoring_task.ts | 2 + .../server/request_context_factory.ts | 28 ++-- 4 files changed, 98 insertions(+), 96 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/privilege_monitoring_data_client.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/privilege_monitoring_data_client.ts index 6babeafa512de..1e955f8d85e33 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/privilege_monitoring_data_client.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/privilege_monitoring_data_client.ts @@ -33,8 +33,8 @@ interface PrivilegeMonitoringClientOpts { apiKeyManager?: ApiKeyManager; } - // TODO: update this - need some openAPI generated types here - interface InitPrivilegedMonitoringEntityEngineResponse { +// TODO: update this - need some openAPI generated types here +interface InitPrivilegedMonitoringEntityEngineResponse { status: string; apiKey: string; } @@ -44,34 +44,31 @@ export class PrivilegeMonitoringDataClient { private esClient: ElasticsearchClient; private engineClient: PrivilegeMonitoringEngineDescriptorClient; - constructor(private readonly opts: PrivilegeMonitoringClientOpts) { this.esClient = opts.clusterClient.asCurrentUser; this.apiKeyGenerator = opts.apiKeyManager; - this.engineClient = new PrivilegeMonitoringEngineDescriptorClient({ + this.engineClient = new PrivilegeMonitoringEngineDescriptorClient({ soClient: opts.soClient, namespace: opts.namespace, - }) + }); } - - private async enable(){ - /** + private async enable() { + /** * TODO: fill this in */ } -/** - * - * init the engine, - * kibana task created, - * create save object with engine status and api key of user who enabled engine -- - * ticket does not say enable, but is this implied? Or just ability of saved object when we want to enable engine? - * indices created - * - * definition and descriptor -- different things. ** - */ - async init():Promise { - + /** + * + * init the engine, + * kibana task created, + * create save object with engine status and api key of user who enabled engine -- + * ticket does not say enable, but is this implied? Or just ability of saved object when we want to enable engine? + * indices created + * + * definition and descriptor -- different things. ** + */ + async init(): Promise { if (!this.opts.taskManager) { throw new Error('Task Manager is not available'); } @@ -95,7 +92,7 @@ export class PrivilegeMonitoringDataClient { taskManager: this.opts.taskManager, }); - await this.engineClient.update({ status: 'installing' }) + await this.engineClient.update({ status: 'installing' }); return descriptor; } @@ -115,10 +112,7 @@ export class PrivilegeMonitoringDataClient { return getPrivilegedMonitorUsersIndex(this.opts.namespace); } - private log( - level: Exclude, - msg: string - ) { + private log(level: Exclude, msg: string) { this.opts.logger[level]( `[Privileged Monitoring Engine][namespace: ${this.opts.namespace}] ${msg}` ); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/saved_object/privilege_monitoring.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/saved_object/privilege_monitoring.ts index a63f1d4ee9b31..7227d4ba97e1b 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/saved_object/privilege_monitoring.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/saved_object/privilege_monitoring.ts @@ -1,74 +1,78 @@ -import { SavedObjectsClientContract } from "@kbn/core/server"; -import { privilegeMonitoringTypeName } from "./privilege_monitoring_type"; -import { PRIVILEGE_MONITORING_ENGINE_STATUS } from "../constants"; +/* + * 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 privilegeMonitoringEngineDescriptorDependencies { - soClient: SavedObjectsClientContract; - namespace: string; +import type { SavedObjectsClientContract } from '@kbn/core/server'; +import { privilegeMonitoringTypeName } from './privilege_monitoring_type'; +import { PRIVILEGE_MONITORING_ENGINE_STATUS } from '../constants'; + +interface PrivilegeMonitoringEngineDescriptorDependencies { + soClient: SavedObjectsClientContract; + namespace: string; } interface PrivilegedMonitoringEngineDescriptor { - status: string; - apiKey: string; + status: string; + apiKey: string; } export type PrivilegeMonitoringEngineStatus = 'installing' | 'started' | 'stopped' | 'error'; export class PrivilegeMonitoringEngineDescriptorClient { + constructor(private readonly deps: PrivilegeMonitoringEngineDescriptorDependencies) {} - constructor(private readonly deps: privilegeMonitoringEngineDescriptorDependencies) { } - - getSavedObjectId() { - return `privilege-monitoring-${this.deps.namespace}`; - } + getSavedObjectId() { + return `privilege-monitoring-${this.deps.namespace}`; + } - async init() { - const { attributes } = await this.deps.soClient.create( - privilegeMonitoringTypeName, - { - status: PRIVILEGE_MONITORING_ENGINE_STATUS.INSTALLING, - apiKey: '', - }, - { id: this.getSavedObjectId() } - ); - return attributes; - } + async init() { + const { attributes } = await this.deps.soClient.create( + privilegeMonitoringTypeName, + { + status: PRIVILEGE_MONITORING_ENGINE_STATUS.INSTALLING, + apiKey: '', + }, + { id: this.getSavedObjectId() } + ); + return attributes; + } - async update(engine: Partial) { - const id = this.getSavedObjectId(); - const { attributes } = await this.deps.soClient.update( - privilegeMonitoringTypeName, - id, - engine, - { refresh: 'wait_for' } - ); - return attributes; - } - - async updateStatus(status: PrivilegeMonitoringEngineStatus) { - return this.update({ status }) - } + async update(engine: Partial) { + const id = this.getSavedObjectId(); + const { attributes } = await this.deps.soClient.update( + privilegeMonitoringTypeName, + id, + engine, + { refresh: 'wait_for' } + ); + return attributes; + } - async find() { - return this.deps.soClient.find({ - type: privilegeMonitoringTypeName, - namespaces: [this.deps.namespace], - }); - } + async updateStatus(status: PrivilegeMonitoringEngineStatus) { + return this.update({ status }); + } - async get() { - const id = this.getSavedObjectId(); - const { attributes } = await this.deps.soClient.get( - privilegeMonitoringTypeName, - id - ); - return attributes; - } + async find() { + return this.deps.soClient.find({ + type: privilegeMonitoringTypeName, + namespaces: [this.deps.namespace], + }); + } - async delete() { - const id = this.getSavedObjectId(); - return this.deps.soClient.delete(privilegeMonitoringTypeName, id); - } + async get() { + const id = this.getSavedObjectId(); + const { attributes } = await this.deps.soClient.get( + privilegeMonitoringTypeName, + id + ); + return attributes; + } - -} \ No newline at end of file + async delete() { + const id = this.getSavedObjectId(); + return this.deps.soClient.delete(privilegeMonitoringTypeName, id); + } +} diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/tasks/privilege_monitoring_task.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/tasks/privilege_monitoring_task.ts index e8e567b57cf95..116b853802b4b 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/tasks/privilege_monitoring_task.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/tasks/privilege_monitoring_task.ts @@ -136,6 +136,8 @@ export const startPrivilegeMonitoringTask = async ({ state: { ...defaultState, namespace }, params: { version: VERSION }, }); + // eslint-disable-next-line no-console + console.log(`Starting the start GREAT SUCCESS`); } catch (e) { logger.warn( `[Privilege Monitoring] [task ${taskId}]: error scheduling task, received ${e.message}` diff --git a/x-pack/solutions/security/plugins/security_solution/server/request_context_factory.ts b/x-pack/solutions/security/plugins/security_solution/server/request_context_factory.ts index b94e1b52e9717..ff17949bebc27 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/request_context_factory.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/request_context_factory.ts @@ -239,20 +239,22 @@ export class RequestContextFactory implements IRequestContextFactory { auditLogger: getAuditLogger(), }) ), - getPrivilegeMonitoringDataClient: memoize( - () => - new PrivilegeMonitoringDataClient({ - logger: options.logger, - clusterClient: coreContext.elasticsearch.client, - namespace: getSpaceId(), - soClient: coreContext.savedObjects.client, - taskManager: startPlugins.taskManager, - auditLogger: getAuditLogger(), - kibanaVersion: options.kibanaVersion, - telemetry: core.analytics, - }) - ), + getPrivilegeMonitoringDataClient: memoize(() => { + // TODO:add soClient with ApiKeyType as with getEntityStoreDataClient + return new PrivilegeMonitoringDataClient({ + logger: options.logger, + clusterClient: coreContext.elasticsearch.client, + namespace: getSpaceId(), + soClient: coreContext.savedObjects.client, + taskManager: startPlugins.taskManager, + auditLogger: getAuditLogger(), + kibanaVersion: options.kibanaVersion, + telemetry: core.analytics, + // TODO: add apiKeyManager + }); + }), getEntityStoreDataClient: memoize(() => { + // why are we defining this here, but other places we do it inline? const clusterClient = coreContext.elasticsearch.client; const logger = options.logger; From f58a685bedb1ae112447fc7148f3347e69a8de7e Mon Sep 17 00:00:00 2001 From: CAWilson94 Date: Mon, 3 Mar 2025 12:13:40 +0000 Subject: [PATCH 13/19] Saved object, check if already exists, update. Formatting and prettier with logs --- .../privilege_monitoring_data_client.ts | 2 +- .../saved_object/privilege_monitoring.ts | 25 ++++++++- .../saved_object/privilege_monitoring_type.ts | 56 ++++++++++--------- 3 files changed, 56 insertions(+), 27 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/privilege_monitoring_data_client.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/privilege_monitoring_data_client.ts index 1e955f8d85e33..f3f8ce3ad9cfc 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/privilege_monitoring_data_client.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/privilege_monitoring_data_client.ts @@ -93,7 +93,7 @@ export class PrivilegeMonitoringDataClient { }); await this.engineClient.update({ status: 'installing' }); - + this.log('debug', `Updated privileged monitoring engine saved object status to installing`); return descriptor; } diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/saved_object/privilege_monitoring.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/saved_object/privilege_monitoring.ts index 7227d4ba97e1b..305117c78de11 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/saved_object/privilege_monitoring.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/saved_object/privilege_monitoring.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { SavedObjectsClientContract } from '@kbn/core/server'; +import type { SavedObjectsClientContract, SavedObjectsFindResponse } from '@kbn/core/server'; import { privilegeMonitoringTypeName } from './privilege_monitoring_type'; import { PRIVILEGE_MONITORING_ENGINE_STATUS } from '../constants'; @@ -29,6 +29,10 @@ export class PrivilegeMonitoringEngineDescriptorClient { } async init() { + const engineDescriptor = await this.find(); + if (engineDescriptor.total === 1) { + return this.updateExistingDescriptor(engineDescriptor); + } const { attributes } = await this.deps.soClient.create( privilegeMonitoringTypeName, { @@ -40,6 +44,25 @@ export class PrivilegeMonitoringEngineDescriptorClient { return attributes; } + private async updateExistingDescriptor( + engineDescriptor: SavedObjectsFindResponse + ) { + const old = engineDescriptor.saved_objects[0].attributes; + const update = { + ...old, + error: undefined, + status: PRIVILEGE_MONITORING_ENGINE_STATUS.INSTALLING, + apiKey: '', + }; + await this.deps.soClient.update( + privilegeMonitoringTypeName, + this.getSavedObjectId(), + update, + { refresh: 'wait_for' } + ); + return update; + } + async update(engine: Partial) { const id = this.getSavedObjectId(); const { attributes } = await this.deps.soClient.update( diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/saved_object/privilege_monitoring_type.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/saved_object/privilege_monitoring_type.ts index 9e3237740f796..b0bfbc7c74d76 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/saved_object/privilege_monitoring_type.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/saved_object/privilege_monitoring_type.ts @@ -1,35 +1,41 @@ +/* + * 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 { SECURITY_SOLUTION_SAVED_OBJECT_INDEX, SavedObjectsModelVersion } from '@kbn/core/packages/saved-objects/server'; +import type { SavedObjectsModelVersion } from '@kbn/core-saved-objects-server'; +import { SECURITY_SOLUTION_SAVED_OBJECT_INDEX } from '@kbn/core-saved-objects-server'; import type { SavedObjectsType } from '@kbn/core/server'; - export const privilegeMonitoringTypeName = 'privilege-monitoring-status'; export const privilegeMonitoringTypeNameMappings: SavedObjectsType['mappings'] = { - dynamic: false, - properties: { - status: { - type: 'keyword', - }, + dynamic: false, + properties: { + status: { + type: 'keyword', }, - }; + }, +}; - const version1: SavedObjectsModelVersion = { - changes: [ - { - type: 'mappings_addition', - addedMappings: { - status: { type: 'keyword' }, - }, +const version1: SavedObjectsModelVersion = { + changes: [ + { + type: 'mappings_addition', + addedMappings: { + status: { type: 'keyword' }, }, - ], - }; + }, + ], +}; - export const privilegeMonitoringType: SavedObjectsType = { - name: privilegeMonitoringTypeName, - indexPattern: SECURITY_SOLUTION_SAVED_OBJECT_INDEX, - hidden: false, - namespaceType: 'multiple-isolated', - mappings: privilegeMonitoringTypeNameMappings, - modelVersions: { 1: version1 }, - }; \ No newline at end of file +export const privilegeMonitoringType: SavedObjectsType = { + name: privilegeMonitoringTypeName, + indexPattern: SECURITY_SOLUTION_SAVED_OBJECT_INDEX, + hidden: false, + namespaceType: 'multiple-isolated', + mappings: privilegeMonitoringTypeNameMappings, + modelVersions: { 1: version1 }, +}; From 9fdfd586d46650f0cfa1844923852cf56a366df0 Mon Sep 17 00:00:00 2001 From: CAWilson94 Date: Mon, 3 Mar 2025 13:42:54 +0000 Subject: [PATCH 14/19] KibanaTask Catch - Error status check added to Saved Object and formatting. --- .../privilege_monitoring/constants.ts | 11 +++--- .../privilege_monitoring_data_client.ts | 38 ++++++++++--------- .../saved_object/privilege_monitoring.ts | 2 + 3 files changed, 28 insertions(+), 23 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/constants.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/constants.ts index d1b512057316f..038039072a72b 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/constants.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/constants.ts @@ -5,7 +5,6 @@ * 2.0. */ - export const SCOPE = ['securitySolution']; export const TYPE = 'entity_analytics:monitoring_engine:great_success'; export const VERSION = '1.0.0'; @@ -16,8 +15,8 @@ export const INTERVAL = '1m'; export const PRIVILEGE_MONITORING_MAPPINGS_VERSIONS = 1; export const PRIVILEGE_MONITORING_ENGINE_STATUS = { - INSTALLING: 'installing', - STARTED: 'started', - STOPPED: 'stopped', - ERROR: 'error', - }; + INSTALLING: 'installing', + STARTED: 'started', + STOPPED: 'stopped', + ERROR: 'error', +}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/privilege_monitoring_data_client.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/privilege_monitoring_data_client.ts index f3f8ce3ad9cfc..db718c3de4d43 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/privilege_monitoring_data_client.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/privilege_monitoring_data_client.ts @@ -20,6 +20,7 @@ import { startPrivilegeMonitoringTask } from './tasks/privilege_monitoring_task' import { createOrUpdateIndex } from '../utils/create_or_update_index'; import { generateUserIndexMappings, getPrivilegedMonitorUsersIndex } from './indicies'; import { PrivilegeMonitoringEngineDescriptorClient } from './saved_object/privilege_monitoring'; +import { PRIVILEGE_MONITORING_ENGINE_STATUS } from './constants'; interface PrivilegeMonitoringClientOpts { logger: Logger; @@ -58,16 +59,7 @@ export class PrivilegeMonitoringDataClient { * TODO: fill this in */ } - /** - * - * init the engine, - * kibana task created, - * create save object with engine status and api key of user who enabled engine -- - * ticket does not say enable, but is this implied? Or just ability of saved object when we want to enable engine? - * indices created - * - * definition and descriptor -- different things. ** - */ + async init(): Promise { if (!this.opts.taskManager) { throw new Error('Task Manager is not available'); @@ -86,14 +78,26 @@ export class PrivilegeMonitoringDataClient { await this.apiKeyGenerator.generate(); // TODO: need this in a saved object? } - await startPrivilegeMonitoringTask({ - logger: this.opts.logger, - namespace: this.opts.namespace, - taskManager: this.opts.taskManager, - }); + try { + await startPrivilegeMonitoringTask({ + logger: this.opts.logger, + namespace: this.opts.namespace, + taskManager: this.opts.taskManager, + }); + } catch (e) { + this.log('error', `Error starting privilege monitoring task: ${e}`); + // TODO: audit failed initialization here + // TODO: telemetry, report event. Reference entity_store_data_client.ts line 476 + await this.engineClient.update({ + status: PRIVILEGE_MONITORING_ENGINE_STATUS.ERROR, + error: {// TODO: double check this, taken from entity_store_data_client.ts line 480 example. + message: e.message, + stack: e.stack, + action: 'init', + }, + }); + } - await this.engineClient.update({ status: 'installing' }); - this.log('debug', `Updated privileged monitoring engine saved object status to installing`); return descriptor; } diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/saved_object/privilege_monitoring.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/saved_object/privilege_monitoring.ts index 305117c78de11..4d273c1820317 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/saved_object/privilege_monitoring.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/saved_object/privilege_monitoring.ts @@ -14,9 +14,11 @@ interface PrivilegeMonitoringEngineDescriptorDependencies { namespace: string; } +// TODO: will be updated with openAPI generated types interface PrivilegedMonitoringEngineDescriptor { status: string; apiKey: string; + error?: Record; } export type PrivilegeMonitoringEngineStatus = 'installing' | 'started' | 'stopped' | 'error'; From 8e178d18594f5be6ec0f4c583c965cac04c0ce8e Mon Sep 17 00:00:00 2001 From: CAWilson94 Date: Tue, 4 Mar 2025 13:48:09 +0000 Subject: [PATCH 15/19] jest testing for privmon savedObject client wrapper --- .../{indicies.ts => indices.ts} | 10 +- .../privilege_monitoring_data_client.ts | 5 +- .../saved_object/privilege_monitoring.test.ts | 167 ++++++++++++++++++ 3 files changed, 175 insertions(+), 7 deletions(-) rename x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/{indicies.ts => indices.ts} (90%) create mode 100644 x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/saved_object/privilege_monitoring.test.ts diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/indicies.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/indices.ts similarity index 90% rename from x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/indicies.ts rename to x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/indices.ts index d7264150017b8..fdaa7628455e0 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/indicies.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/indices.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { MappingTypeMapping } from "@elastic/elasticsearch/lib/api/types"; +import type { MappingTypeMapping } from '@elastic/elasticsearch/lib/api/types'; // Static index names: may be more obvious and easier to manage. export const privilegedMonitorBaseIndexName = '.entity_analytics.monitoring'; @@ -19,7 +19,7 @@ export const getPrivilegedMonitorGroupsIndex = (namespace: string) => export type MappingProperties = NonNullable; export const PRIVILEGED_MONITOR_USERS_INDEX_MAPPING: MappingProperties = { - 'event.ingested': { + 'event.ingested': { type: 'date', }, '@timestamp': { @@ -34,7 +34,7 @@ export const PRIVILEGED_MONITOR_USERS_INDEX_MAPPING: MappingProperties = { }; export const PRIVILEGED_MONITOR_GROUPS_INDEX_MAPPING: MappingProperties = { - 'event.ingested': { + 'event.ingested': { type: 'date', }, '@timestamp': { @@ -43,10 +43,10 @@ export const PRIVILEGED_MONITOR_GROUPS_INDEX_MAPPING: MappingProperties = { 'group.name': { type: 'keyword', }, - 'indexPattern': { + indexPattern: { type: 'keyword', }, - 'nameMatcher': { + nameMatcher: { type: 'keyword', }, 'labels.is_privileged': { diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/privilege_monitoring_data_client.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/privilege_monitoring_data_client.ts index db718c3de4d43..982578d656e6d 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/privilege_monitoring_data_client.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/privilege_monitoring_data_client.ts @@ -18,7 +18,7 @@ import type { TaskManagerStartContract } from '@kbn/task-manager-plugin/server'; import type { ApiKeyManager } from './auth/api_key'; import { startPrivilegeMonitoringTask } from './tasks/privilege_monitoring_task'; import { createOrUpdateIndex } from '../utils/create_or_update_index'; -import { generateUserIndexMappings, getPrivilegedMonitorUsersIndex } from './indicies'; +import { generateUserIndexMappings, getPrivilegedMonitorUsersIndex } from './indices'; import { PrivilegeMonitoringEngineDescriptorClient } from './saved_object/privilege_monitoring'; import { PRIVILEGE_MONITORING_ENGINE_STATUS } from './constants'; @@ -90,7 +90,8 @@ export class PrivilegeMonitoringDataClient { // TODO: telemetry, report event. Reference entity_store_data_client.ts line 476 await this.engineClient.update({ status: PRIVILEGE_MONITORING_ENGINE_STATUS.ERROR, - error: {// TODO: double check this, taken from entity_store_data_client.ts line 480 example. + error: { + // TODO: double check this, taken from entity_store_data_client.ts line 480 example. message: e.message, stack: e.stack, action: 'init', diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/saved_object/privilege_monitoring.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/saved_object/privilege_monitoring.test.ts new file mode 100644 index 0000000000000..94f12d3a3c6d9 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/saved_object/privilege_monitoring.test.ts @@ -0,0 +1,167 @@ +/* + * 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 { + SavedObject, + SavedObjectsClientContract, + SavedObjectsFindResponse, +} from '@kbn/core/server'; +import { PrivilegeMonitoringEngineDescriptorClient } from './privilege_monitoring'; +import { privilegeMonitoringTypeName } from './privilege_monitoring_type'; +import { PRIVILEGE_MONITORING_ENGINE_STATUS } from '../constants'; + +describe('PrivilegeMonitoringEngineDescriptorClient', () => { + let soClient: jest.Mocked; + let client: PrivilegeMonitoringEngineDescriptorClient; + const namespace = 'test-namespace'; + + beforeEach(() => { + soClient = { + create: jest.fn(), + update: jest.fn(), + find: jest.fn(), + get: jest.fn(), + delete: jest.fn(), + } as unknown as jest.Mocked; + + client = new PrivilegeMonitoringEngineDescriptorClient({ soClient, namespace }); + }); + + it('should return the correct saved object ID', () => { + expect(client.getSavedObjectId()).toBe(`privilege-monitoring-${namespace}`); + }); + + it('should initialize a new descriptor if none exists', async () => { + soClient.find.mockResolvedValue({ + total: 0, + saved_objects: [], + } as unknown as SavedObjectsFindResponse); + soClient.create.mockResolvedValue({ + id: `privilege-monitoring-${namespace}`, + type: privilegeMonitoringTypeName, + attributes: { status: 'installing' as unknown, apiKey: '' as unknown }, + references: [], + }); + + const result = await client.init(); + + expect(soClient.create).toHaveBeenCalledWith( + privilegeMonitoringTypeName, + { status: PRIVILEGE_MONITORING_ENGINE_STATUS.INSTALLING, apiKey: '' }, + { id: `privilege-monitoring-${namespace}` } + ); + expect(result).toEqual({ status: 'installing', apiKey: '' }); + }); + + it('should update an existing descriptor if one exists', async () => { + const existingDescriptor = { + total: 1, + saved_objects: [{ attributes: { status: 'started', apiKey: 'old-key' } }], + } as SavedObjectsFindResponse; + + soClient.find.mockResolvedValue( + existingDescriptor as unknown as SavedObjectsFindResponse + ); + soClient.update.mockResolvedValue({ + id: `privilege-monitoring-${namespace}`, + type: privilegeMonitoringTypeName, + attributes: { status: 'installing' as unknown, apiKey: '' as unknown }, + references: [], + }); + + const result = await client.init(); + + expect(soClient.update).toHaveBeenCalledWith( + privilegeMonitoringTypeName, + `privilege-monitoring-${namespace}`, + { status: PRIVILEGE_MONITORING_ENGINE_STATUS.INSTALLING, apiKey: '', error: undefined }, + { refresh: 'wait_for' } + ); + expect(result).toEqual({ status: 'installing', apiKey: '' }); + }); + + it('should update the descriptor', async () => { + soClient.update.mockResolvedValue({ + id: `privilege-monitoring-${namespace}`, + type: privilegeMonitoringTypeName, + attributes: { status: 'started' as unknown }, + references: [], + }); + + const result = await client.update({ status: 'started' }); + + expect(soClient.update).toHaveBeenCalledWith( + privilegeMonitoringTypeName, + `privilege-monitoring-${namespace}`, + { status: 'started' }, + { refresh: 'wait_for' } + ); + expect(result).toEqual({ status: 'started' }); + }); + + it('should update the status', async () => { + soClient.update.mockResolvedValue({ + id: `privilege-monitoring-${namespace}`, + type: privilegeMonitoringTypeName, + attributes: { status: 'started' as unknown }, + references: [], + }); + + const result = await client.updateStatus('started'); + + expect(soClient.update).toHaveBeenCalledWith( + privilegeMonitoringTypeName, + `privilege-monitoring-${namespace}`, + { status: 'started' }, + { refresh: 'wait_for' } + ); + expect(result).toEqual({ status: 'started' }); + }); + + it('should find descriptors', async () => { + const findResponse = { + total: 1, + saved_objects: [{ attributes: { status: 'started', apiKey: 'key' } }], + }; + soClient.find.mockResolvedValue(findResponse as SavedObjectsFindResponse); + + const result = await client.find(); + + expect(soClient.find).toHaveBeenCalledWith({ + type: privilegeMonitoringTypeName, + namespaces: [namespace], + }); + expect(result).toEqual(findResponse); + }); + + it('should get a descriptor', async () => { + const getResponse = { + id: `privilege-monitoring-${namespace}`, + type: privilegeMonitoringTypeName, + attributes: { status: 'started' as unknown, apiKey: 'key' as unknown }, + references: [], + }; + soClient.get.mockResolvedValue(getResponse as unknown as SavedObject); + + const result = await client.get(); + + expect(soClient.get).toHaveBeenCalledWith( + privilegeMonitoringTypeName, + `privilege-monitoring-${namespace}` + ); + expect(result).toEqual(getResponse.attributes); + }); + + it('should delete a descriptor', async () => { + await client.delete(); + + expect(soClient.delete).toHaveBeenCalledWith( + privilegeMonitoringTypeName, + `privilege-monitoring-${namespace}` + ); + }); +}); From 4a9c1c7ac0f11e8c68c2d2084eb419120e4233eb Mon Sep 17 00:00:00 2001 From: Tiago Vila Verde Date: Wed, 5 Mar 2025 11:57:59 +0100 Subject: [PATCH 16/19] auditing --- .../privilege_monitoring/common.gen.ts | 48 ++++++++++++++ .../privilege_monitoring/common.schema.yaml | 62 +++++++++++++++++++ .../privilege_monitoring/auditing/actions.ts | 18 ++++++ .../privilege_monitoring_data_client.ts | 46 ++++++++++++++ 4 files changed, 174 insertions(+) create mode 100644 x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/privilege_monitoring/common.gen.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/privilege_monitoring/common.schema.yaml create mode 100644 x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/auditing/actions.ts diff --git a/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/privilege_monitoring/common.gen.ts b/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/privilege_monitoring/common.gen.ts new file mode 100644 index 0000000000000..333330bf26f12 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/privilege_monitoring/common.gen.ts @@ -0,0 +1,48 @@ +/* + * 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. + */ + +/* + * NOTICE: Do not edit this file manually. + * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator. + * + * info: + * title: Privilege Monitoring Common Schema + * version: 1 + */ + +import { z } from '@kbn/zod'; + +export type EngineStatus = z.infer; +export const EngineStatus = z.enum(['installing', 'started', 'stopped', 'updating', 'error']); +export type EngineStatusEnum = typeof EngineStatus.enum; +export const EngineStatusEnum = EngineStatus.enum; + +export type EngineDescriptor = z.infer; +export const EngineDescriptor = z.object({ + status: EngineStatus, +}); + +export type EngineComponentResource = z.infer; +export const EngineComponentResource = z.enum(['privmon_engine', 'index', 'task']); +export type EngineComponentResourceEnum = typeof EngineComponentResource.enum; +export const EngineComponentResourceEnum = EngineComponentResource.enum; + +export type EngineComponentStatus = z.infer; +export const EngineComponentStatus = z.object({ + id: z.string(), + installed: z.boolean(), + resource: EngineComponentResource, + health: z.enum(['green', 'yellow', 'red', 'unknown']).optional(), + errors: z + .array( + z.object({ + title: z.string().optional(), + message: z.string().optional(), + }) + ) + .optional(), +}); diff --git a/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/privilege_monitoring/common.schema.yaml b/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/privilege_monitoring/common.schema.yaml new file mode 100644 index 0000000000000..1d6c6e65d4a35 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/privilege_monitoring/common.schema.yaml @@ -0,0 +1,62 @@ +openapi: 3.0.0 +info: + title: Privilege Monitoring Common Schema + description: Common schema for Privilege Monitoring + version: "1" +paths: {} +components: + schemas: + EngineDescriptor: + type: object + required: + - type + - status + properties: + status: + $ref: "#/components/schemas/EngineStatus" + + EngineStatus: + type: string + enum: + - installing + - started + - stopped + - updating + - error + + EngineComponentStatus: + type: object + required: + - id + - installed + - resource + properties: + id: + type: string + installed: + type: boolean + resource: + $ref: "#/components/schemas/EngineComponentResource" + health: + type: string + enum: + - green + - yellow + - red + - unknown + errors: + type: array + items: + type: object + properties: + title: + type: string + message: + type: string + + EngineComponentResource: + type: string + enum: + - privmon_engine + - index + - task diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/auditing/actions.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/auditing/actions.ts new file mode 100644 index 0000000000000..61feb7063f591 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/auditing/actions.ts @@ -0,0 +1,18 @@ +/* + * 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 PrivilegeMonitoringEngineActions = { + INIT: 'init', + START: 'start', + STOP: 'stop', + CREATE: 'create', + DELETE: 'delete', + EXECUTE: 'execute', +} as const; + +export type PrivilegeMonitoringEngineActions = + (typeof PrivilegeMonitoringEngineActions)[keyof typeof PrivilegeMonitoringEngineActions]; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/privilege_monitoring_data_client.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/privilege_monitoring_data_client.ts index 982578d656e6d..5f6d154a7de05 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/privilege_monitoring_data_client.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/privilege_monitoring_data_client.ts @@ -12,15 +12,22 @@ import type { AuditLogger, IScopedClusterClient, AnalyticsServiceSetup, + AuditEvent, } from '@kbn/core/server'; import type { TaskManagerStartContract } from '@kbn/task-manager-plugin/server'; +import { + EngineComponentResourceEnum, + type EngineComponentResource, +} from '../../../../common/api/entity_analytics/privilege_monitoring/common.gen'; import type { ApiKeyManager } from './auth/api_key'; import { startPrivilegeMonitoringTask } from './tasks/privilege_monitoring_task'; import { createOrUpdateIndex } from '../utils/create_or_update_index'; import { generateUserIndexMappings, getPrivilegedMonitorUsersIndex } from './indices'; import { PrivilegeMonitoringEngineDescriptorClient } from './saved_object/privilege_monitoring'; import { PRIVILEGE_MONITORING_ENGINE_STATUS } from './constants'; +import { AUDIT_CATEGORY, AUDIT_OUTCOME, AUDIT_TYPE } from '../audit'; +import { PrivilegeMonitoringEngineActions } from './auditing/actions'; interface PrivilegeMonitoringClientOpts { logger: Logger; @@ -65,6 +72,12 @@ export class PrivilegeMonitoringDataClient { throw new Error('Task Manager is not available'); } + this.audit( + PrivilegeMonitoringEngineActions.INIT, + EngineComponentResourceEnum.privmon_engine, + 'Initializing privilege monitoring engine' + ); + await this.createOrUpdateIndex().catch((e) => { if (e.meta.body.error.type === 'resource_already_exists_exception') { this.opts.logger.info('Privilege monitoring index already exists'); @@ -122,4 +135,37 @@ export class PrivilegeMonitoringDataClient { `[Privileged Monitoring Engine][namespace: ${this.opts.namespace}] ${msg}` ); } + + private audit( + action: PrivilegeMonitoringEngineActions, + resource: EngineComponentResource, + msg: string, + error?: Error + ) { + // NOTE: Excluding errors, all auditing events are currently WRITE events, meaning the outcome is always UNKNOWN. + // This may change in the future, depending on the audit action. + const outcome = error ? AUDIT_OUTCOME.FAILURE : AUDIT_OUTCOME.UNKNOWN; + + const type = + action === PrivilegeMonitoringEngineActions.CREATE + ? AUDIT_TYPE.CREATION + : PrivilegeMonitoringEngineActions.DELETE + ? AUDIT_TYPE.DELETION + : AUDIT_TYPE.CHANGE; + + const category = AUDIT_CATEGORY.DATABASE; + + const message = error ? `${msg}: ${error.message}` : msg; + const event: AuditEvent = { + message: `[Privilege Monitoring] ${message}`, + event: { + action: `${action}_${resource}`, + category, + outcome, + type, + }, + }; + + return this.opts.auditLogger?.log(event); + } } From 5cf08144644e8fcb5317a8d11e7b34abb6d1c66c Mon Sep 17 00:00:00 2001 From: Tiago Vila Verde Date: Wed, 5 Mar 2025 12:44:09 +0100 Subject: [PATCH 17/19] encrypted saved object --- .../privilege_monitoring/auth/api_key.ts | 167 ++++++++---------- .../privilege_monitoring/auth/privileges.ts | 30 ++++ .../privilege_monitoring/auth/saved_object.ts | 13 ++ .../privilege_monitoring/constants.ts | 4 + 4 files changed, 122 insertions(+), 92 deletions(-) create mode 100644 x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/auth/privileges.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/auth/saved_object.ts diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/auth/api_key.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/auth/api_key.ts index 1c1c6924a24d5..98b529b78d4b0 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/auth/api_key.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/auth/api_key.ts @@ -11,10 +11,11 @@ import type { Logger } from '@kbn/logging'; import type { SecurityPluginStart } from '@kbn/security-plugin-types-server'; import type { EncryptedSavedObjectsPluginStart } from '@kbn/encrypted-saved-objects-plugin/server'; import { getFakeKibanaRequest } from '@kbn/security-plugin/server/authentication/api_keys/fake_kibana_request'; -import { getSpaceAwareEntityDiscoverySavedObjectId } from '@kbn/entityManager-plugin/server/lib/auth/api_key/saved_object'; + import { SavedObjectsErrorHelpers } from '@kbn/core-saved-objects-server'; -import { SavedObjectsType } from '@kbn/core/server'; -import { EntityManagerServerSetup } from '@kbn/entityManager-plugin/server/types'; +import type { SavedObjectsType } from '@kbn/core/server'; +import { getPrivmonEncryptedSavedObjectId } from './saved_object'; +import { privilegeMonitoringRuntimePrivileges } from './privileges'; export interface ApiKeyManager { generate: () => Promise; @@ -29,110 +30,95 @@ export interface ApiKeyManagerDependencies { namespace: string; } - export const getApiKeyManager = (deps: ApiKeyManagerDependencies) => { - return { generate: generate(deps), getApiKey: getApiKey(deps), - getRequestFromApiKey: getRequestFromApiKey(deps), getClientFromApiKey: getClientFromApiKey(deps), - } -} - - + getRequestFromApiKey, + }; +}; const generate = async (deps: ApiKeyManagerDependencies) => { - const { core, logger, security, encryptedSavedObjects, request, namespace } = deps - if (!encryptedSavedObjects) { - throw new Error( - 'Unable to create API key. Ensure encrypted Saved Object client is enabled in this environment.' - ); - } else if (!request) { - throw new Error('Unable to create API key due to invalid request'); - } else { - const apiKey = await generateAPIKey( - { - core, - config: {}, - logger, - security, - encryptedSavedObjects, - }, - request - ); - - const soClient = core.savedObjects.getScopedClient(request, { - includedHiddenTypes: [PrivilegeMonitoringApiKeyType.name], - }); - - await soClient.create(PrivilegeMonitoringApiKeyType.name, apiKey, { - id: getSpaceAwareEntityDiscoverySavedObjectId(namespace), - overwrite: true, - managed: true, - }); - } -} -const getApiKey: async () => { - if (!encryptedSavedObjects) { - throw Error( - 'Unable to retrieve API key. Ensure encrypted Saved Object client is enabled in this environment.' - ); - } - try { - const encryptedSavedObjectsClient = encryptedSavedObjects.getClient({ - includedHiddenTypes: [PrivilegeMonitoringApiKeyType.name], - }); - return ( - await encryptedSavedObjectsClient.getDecryptedAsInternalUser( - PrivilegeMonitoringApiKeyType.name, - getSpaceAwareEntityDiscoverySavedObjectId(namespace) - ) - ).attributes; - } catch (err) { - if (SavedObjectsErrorHelpers.isNotFoundError(err)) { - return undefined; - } - throw err; - } - }, - getRequestFromApiKey: async (apiKey: PrivilegeMonitoringAPIKey) => { - return getFakeKibanaRequest({ - id: apiKey.id, - api_key: apiKey.apiKey, - }); - }, - getClientFromApiKey: async (apiKey: PrivilegeMonitoringAPIKey) => { - const fakeRequest = getFakeKibanaRequest({ - id: apiKey.id, - api_key: apiKey.apiKey, - }); - const clusterClient = core.elasticsearch.client.asScoped(fakeRequest); - const soClient = core.savedObjects.getScopedClient(fakeRequest, { - includedHiddenTypes: [PrivilegeMonitoringApiKeyType.name], - }); - return { - clusterClient, - soClient, - }; - }, + const { core, encryptedSavedObjects, request, namespace } = deps; + if (!encryptedSavedObjects) { + throw new Error( + 'Unable to create API key. Ensure encrypted Saved Object client is enabled in this environment.' + ); + } else if (!request) { + throw new Error('Unable to create API key due to invalid request'); + } else { + const apiKey = await generateAPIKey(request, deps); + + const soClient = core.savedObjects.getScopedClient(request, { + includedHiddenTypes: [PrivilegeMonitoringApiKeyType.name], + }); + + await soClient.create(PrivilegeMonitoringApiKeyType.name, apiKey, { + id: getPrivmonEncryptedSavedObjectId(namespace), + overwrite: true, + managed: true, + }); } +}; +const getApiKey = async (deps: ApiKeyManagerDependencies) => { + if (!deps.encryptedSavedObjects) { + throw Error( + 'Unable to retrieve API key. Ensure encrypted Saved Object client is enabled in this environment.' + ); + } + try { + const encryptedSavedObjectsClient = deps.encryptedSavedObjects.getClient({ + includedHiddenTypes: [PrivilegeMonitoringApiKeyType.name], + }); + return ( + await encryptedSavedObjectsClient.getDecryptedAsInternalUser( + PrivilegeMonitoringApiKeyType.name, + getPrivmonEncryptedSavedObjectId(deps.namespace) + ) + ).attributes; + } catch (err) { + if (SavedObjectsErrorHelpers.isNotFoundError(err)) { + return undefined; + } + throw err; + } }; +const getRequestFromApiKey = async (apiKey: PrivilegeMonitoringAPIKey) => { + return getFakeKibanaRequest({ + id: apiKey.id, + api_key: apiKey.apiKey, + }); +}; +const getClientFromApiKey = + (deps: ApiKeyManagerDependencies) => async (apiKey: PrivilegeMonitoringAPIKey) => { + const fakeRequest = getFakeKibanaRequest({ + id: apiKey.id, + api_key: apiKey.apiKey, + }); + const clusterClient = deps.core.elasticsearch.client.asScoped(fakeRequest); + const soClient = deps.core.savedObjects.getScopedClient(fakeRequest, { + includedHiddenTypes: [PrivilegeMonitoringApiKeyType.name], + }); + return { + clusterClient, + soClient, + }; + }; export const generateAPIKey = async ( req: KibanaRequest, - server: EntityManagerServerSetup, + deps: ApiKeyManagerDependencies ): Promise => { - const apiKey = await server.security.authc.apiKeys.grantAsInternalUser(req, { - name: 'Entity discovery API key', + const apiKey = await deps.security.authc.apiKeys.grantAsInternalUser(req, { + name: 'Privilege Monitoring API key', role_descriptors: { - entity_discovery_admin: entityDefinitionRuntimePrivileges, + privmon_admin: privilegeMonitoringRuntimePrivileges, }, metadata: { - description: - 'API key used to manage the transforms and ingest pipelines created by the entity discovery framework', + description: 'API key used to manage the resources in the privilege monitoring engine', }, }); @@ -145,8 +131,6 @@ export const generateAPIKey = async ( } }; - - export const SO_PRIVILEGE_MONITORING_API_KEY_TYPE = 'entity-discovery-api-key'; export const PrivilegeMonitoringApiKeyType: SavedObjectsType = { @@ -165,7 +149,6 @@ export const PrivilegeMonitoringApiKeyType: SavedObjectsType = { }, }; - export interface PrivilegeMonitoringAPIKey { id: string; name: string; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/auth/privileges.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/auth/privileges.ts new file mode 100644 index 0000000000000..9e1a2b84a6688 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/auth/privileges.ts @@ -0,0 +1,30 @@ +/* + * 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 { PRIVILEGE_MONITORING_INTERNAL_INDICES_PATTERN } from '../constants'; +import { privilegeMonitoringTypeName } from '../saved_object/privilege_monitoring_type'; + +export const privilegeMonitoringRuntimePrivileges = (sourceIndices: string[]) => ({ + cluster: ['manage_ingest_pipelines', 'manage_index_templates'], + index: [ + { + names: [PRIVILEGE_MONITORING_INTERNAL_INDICES_PATTERN], + privileges: ['create_index', 'delete_index', 'index', 'create_doc', 'auto_configure', 'read'], + }, + { + names: [...sourceIndices, PRIVILEGE_MONITORING_INTERNAL_INDICES_PATTERN], + privileges: ['read', 'view_index_metadata'], + }, + ], + application: [ + { + application: 'kibana-.kibana', + privileges: [`saved_object:${privilegeMonitoringTypeName}/*`], + resources: ['*'], + }, + ], +}); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/auth/saved_object.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/auth/saved_object.ts new file mode 100644 index 0000000000000..126abec897639 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/auth/saved_object.ts @@ -0,0 +1,13 @@ +/* + * 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 { v5 as uuidv5 } from 'uuid'; + +const PRIVMON_API_KEY_SO_ID = '19540C97-E35C-485B-8566-FB86EC8455E4'; + +export const getPrivmonEncryptedSavedObjectId = (space: string) => { + return uuidv5(space, PRIVMON_API_KEY_SO_ID); +}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/constants.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/constants.ts index 038039072a72b..fbfb9713212ae 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/constants.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/constants.ts @@ -20,3 +20,7 @@ export const PRIVILEGE_MONITORING_ENGINE_STATUS = { STOPPED: 'stopped', ERROR: 'error', }; + +// Base constants +export const PRIVMON_BASE_PREFIX = 'privmon' as const; +export const PRIVILEGE_MONITORING_INTERNAL_INDICES_PATTERN = `.${PRIVMON_BASE_PREFIX}*` as const; From ac50ca4e2d08e88285827d680f06fa1996598b8e Mon Sep 17 00:00:00 2001 From: Tiago Vila Verde Date: Wed, 5 Mar 2025 14:12:10 +0100 Subject: [PATCH 18/19] improving auth --- .../privilege_monitoring/engine/init.gen.ts | 8 ++-- .../engine/init.schema.yaml | 12 ++---- .../privilege_monitoring/auth/api_key.ts | 2 +- .../privilege_monitoring/constants.ts | 2 +- .../privilege_monitoring_data_client.ts | 41 +++++++++---------- .../saved_object/privilege_monitoring.ts | 10 ++--- 6 files changed, 33 insertions(+), 42 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/privilege_monitoring/engine/init.gen.ts b/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/privilege_monitoring/engine/init.gen.ts index bdf603a1b79eb..2ad8476b56fc4 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/privilege_monitoring/engine/init.gen.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/privilege_monitoring/engine/init.gen.ts @@ -14,9 +14,9 @@ * version: 2023-10-31 */ -import { z } from '@kbn/zod'; +import type { z } from '@kbn/zod'; + +import { EngineDescriptor } from '../common.gen'; export type InitMonitoringEngineResponse = z.infer; -export const InitMonitoringEngineResponse = z.object({ - acknowledged: z.boolean().optional(), -}); +export const InitMonitoringEngineResponse = EngineDescriptor; diff --git a/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/privilege_monitoring/engine/init.schema.yaml b/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/privilege_monitoring/engine/init.schema.yaml index c06491ae02427..2403ae441c678 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/privilege_monitoring/engine/init.schema.yaml +++ b/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/privilege_monitoring/engine/init.schema.yaml @@ -4,21 +4,17 @@ info: title: Init Privilege Monitoring Engine version: 2023-10-31 paths: - /api/entity_analytics/monitoring/engine/init: + /api/entity_analytics/monitoring/engine/init: post: x-labels: [ess, serverless] x-codegen-enabled: true operationId: InitMonitoringEngine summary: Initialize the Privilege Monitoring Engine - + responses: - '200': + "200": description: Successful response content: application/json: schema: - type: object - properties: - acknowledged: - type: boolean - + $ref: "../common.schema.yaml#/components/schemas/EngineDescriptor" diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/auth/api_key.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/auth/api_key.ts index 98b529b78d4b0..28633c3512c14 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/auth/api_key.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/auth/api_key.ts @@ -131,7 +131,7 @@ export const generateAPIKey = async ( } }; -export const SO_PRIVILEGE_MONITORING_API_KEY_TYPE = 'entity-discovery-api-key'; +export const SO_PRIVILEGE_MONITORING_API_KEY_TYPE = 'privmon-api-key'; export const PrivilegeMonitoringApiKeyType: SavedObjectsType = { name: SO_PRIVILEGE_MONITORING_API_KEY_TYPE, diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/constants.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/constants.ts index fbfb9713212ae..dc47d90289915 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/constants.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/constants.ts @@ -19,7 +19,7 @@ export const PRIVILEGE_MONITORING_ENGINE_STATUS = { STARTED: 'started', STOPPED: 'stopped', ERROR: 'error', -}; +} as const; // Base constants export const PRIVMON_BASE_PREFIX = 'privmon' as const; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/privilege_monitoring_data_client.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/privilege_monitoring_data_client.ts index 5f6d154a7de05..4786f4b5f4102 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/privilege_monitoring_data_client.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/privilege_monitoring_data_client.ts @@ -16,6 +16,7 @@ import type { } from '@kbn/core/server'; import type { TaskManagerStartContract } from '@kbn/task-manager-plugin/server'; +import type { InitMonitoringEngineResponse } from '../../../../common/api/entity_analytics/privilege_monitoring/engine/init.gen'; import { EngineComponentResourceEnum, type EngineComponentResource, @@ -41,12 +42,6 @@ interface PrivilegeMonitoringClientOpts { apiKeyManager?: ApiKeyManager; } -// TODO: update this - need some openAPI generated types here -interface InitPrivilegedMonitoringEntityEngineResponse { - status: string; - apiKey: string; -} - export class PrivilegeMonitoringDataClient { private apiKeyGenerator?: ApiKeyManager; private esClient: ElasticsearchClient; @@ -67,39 +62,44 @@ export class PrivilegeMonitoringDataClient { */ } - async init(): Promise { + async init(): Promise { if (!this.opts.taskManager) { throw new Error('Task Manager is not available'); } - this.audit( PrivilegeMonitoringEngineActions.INIT, EngineComponentResourceEnum.privmon_engine, 'Initializing privilege monitoring engine' ); - await this.createOrUpdateIndex().catch((e) => { - if (e.meta.body.error.type === 'resource_already_exists_exception') { - this.opts.logger.info('Privilege monitoring index already exists'); - } - }); - const descriptor = await this.engineClient.init(); this.log('debug', `Initialized privileged monitoring engine saved object`); - if (this.apiKeyGenerator) { - await this.apiKeyGenerator.generate(); // TODO: need this in a saved object? - } - try { + await this.createOrUpdateIndex().catch((e) => { + if (e.meta.body.error.type === 'resource_already_exists_exception') { + this.opts.logger.info('Privilege monitoring index already exists'); + } + }); + + if (this.apiKeyGenerator) { + await this.apiKeyGenerator.generate(); // TODO: need this in a saved object? + } + await startPrivilegeMonitoringTask({ logger: this.opts.logger, namespace: this.opts.namespace, taskManager: this.opts.taskManager, }); } catch (e) { - this.log('error', `Error starting privilege monitoring task: ${e}`); - // TODO: audit failed initialization here + this.log('error', `Error initializing privilege monitoring engine: ${e}`); + this.audit( + PrivilegeMonitoringEngineActions.INIT, + EngineComponentResourceEnum.privmon_engine, + 'Failed to initialize privilege monitoring engine', + e + ); + // TODO: telemetry, report event. Reference entity_store_data_client.ts line 476 await this.engineClient.update({ status: PRIVILEGE_MONITORING_ENGINE_STATUS.ERROR, @@ -111,7 +111,6 @@ export class PrivilegeMonitoringDataClient { }, }); } - return descriptor; } diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/saved_object/privilege_monitoring.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/saved_object/privilege_monitoring.ts index 4d273c1820317..f67430a89ae71 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/saved_object/privilege_monitoring.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/saved_object/privilege_monitoring.ts @@ -6,6 +6,7 @@ */ import type { SavedObjectsClientContract, SavedObjectsFindResponse } from '@kbn/core/server'; +import type { EngineDescriptor } from '../../../../../common/api/entity_analytics/privilege_monitoring/common.gen'; import { privilegeMonitoringTypeName } from './privilege_monitoring_type'; import { PRIVILEGE_MONITORING_ENGINE_STATUS } from '../constants'; @@ -14,15 +15,11 @@ interface PrivilegeMonitoringEngineDescriptorDependencies { namespace: string; } -// TODO: will be updated with openAPI generated types interface PrivilegedMonitoringEngineDescriptor { - status: string; - apiKey: string; + status: EngineDescriptor['status']; error?: Record; } -export type PrivilegeMonitoringEngineStatus = 'installing' | 'started' | 'stopped' | 'error'; - export class PrivilegeMonitoringEngineDescriptorClient { constructor(private readonly deps: PrivilegeMonitoringEngineDescriptorDependencies) {} @@ -39,7 +36,6 @@ export class PrivilegeMonitoringEngineDescriptorClient { privilegeMonitoringTypeName, { status: PRIVILEGE_MONITORING_ENGINE_STATUS.INSTALLING, - apiKey: '', }, { id: this.getSavedObjectId() } ); @@ -76,7 +72,7 @@ export class PrivilegeMonitoringEngineDescriptorClient { return attributes; } - async updateStatus(status: PrivilegeMonitoringEngineStatus) { + async updateStatus(status: EngineDescriptor['status']) { return this.update({ status }); } From f1bb716e6f32693fa81b0aa4e737a5a0f89dc076 Mon Sep 17 00:00:00 2001 From: Tiago Vila Verde Date: Wed, 5 Mar 2025 15:17:50 +0100 Subject: [PATCH 19/19] telemetry --- .../privilege_monitoring_data_client.ts | 18 ++++++++++- .../lib/telemetry/event_based/events.ts | 30 +++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/privilege_monitoring_data_client.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/privilege_monitoring_data_client.ts index 4786f4b5f4102..51c6c3779de26 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/privilege_monitoring_data_client.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/privilege_monitoring_data_client.ts @@ -16,6 +16,7 @@ import type { } from '@kbn/core/server'; import type { TaskManagerStartContract } from '@kbn/task-manager-plugin/server'; +import moment from 'moment'; import type { InitMonitoringEngineResponse } from '../../../../common/api/entity_analytics/privilege_monitoring/engine/init.gen'; import { EngineComponentResourceEnum, @@ -29,6 +30,10 @@ import { PrivilegeMonitoringEngineDescriptorClient } from './saved_object/privil import { PRIVILEGE_MONITORING_ENGINE_STATUS } from './constants'; import { AUDIT_CATEGORY, AUDIT_OUTCOME, AUDIT_TYPE } from '../audit'; import { PrivilegeMonitoringEngineActions } from './auditing/actions'; +import { + PRIVMON_ENGINE_INITIALIZATION_EVENT, + PRIVMON_ENGINE_RESOURCE_INIT_FAILURE_EVENT, +} from '../../telemetry/event_based/events'; interface PrivilegeMonitoringClientOpts { logger: Logger; @@ -66,6 +71,8 @@ export class PrivilegeMonitoringDataClient { if (!this.opts.taskManager) { throw new Error('Task Manager is not available'); } + const setupStartTime = moment().utc().toISOString(); + this.audit( PrivilegeMonitoringEngineActions.INIT, EngineComponentResourceEnum.privmon_engine, @@ -91,6 +98,12 @@ export class PrivilegeMonitoringDataClient { namespace: this.opts.namespace, taskManager: this.opts.taskManager, }); + + const setupEndTime = moment().utc().toISOString(); + const duration = moment(setupEndTime).diff(moment(setupStartTime), 'seconds'); + this.opts.telemetry?.reportEvent(PRIVMON_ENGINE_INITIALIZATION_EVENT.eventType, { + duration, + }); } catch (e) { this.log('error', `Error initializing privilege monitoring engine: ${e}`); this.audit( @@ -100,7 +113,10 @@ export class PrivilegeMonitoringDataClient { e ); - // TODO: telemetry, report event. Reference entity_store_data_client.ts line 476 + this.opts.telemetry?.reportEvent(PRIVMON_ENGINE_RESOURCE_INIT_FAILURE_EVENT.eventType, { + error: e.message, + }); + await this.engineClient.update({ status: PRIVILEGE_MONITORING_ENGINE_STATUS.ERROR, error: { diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/telemetry/event_based/events.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/telemetry/event_based/events.ts index 1370716e7c9fe..2c52a663bf7ee 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/telemetry/event_based/events.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/telemetry/event_based/events.ts @@ -213,6 +213,34 @@ export const ENTITY_STORE_USAGE_EVENT: EventTypeOpts<{ }, }; +export const PRIVMON_ENGINE_INITIALIZATION_EVENT: EventTypeOpts<{ + duration: number; +}> = { + eventType: 'privmon_engine_initialization', + schema: { + duration: { + type: 'long', + _meta: { + description: 'Duration (in seconds) of the privilege monitoring engine initialization', + }, + }, + }, +}; + +export const PRIVMON_ENGINE_RESOURCE_INIT_FAILURE_EVENT: EventTypeOpts<{ + error: string; +}> = { + eventType: 'privmon_engine_resource_init_failure', + schema: { + error: { + type: 'keyword', + _meta: { + description: 'Error message for a resource initialization failure', + }, + }, + }, +}; + export const ALERT_SUPPRESSION_EVENT: EventTypeOpts<{ suppressionAlertsCreated: number; suppressionAlertsSuppressed: number; @@ -994,6 +1022,8 @@ export const events = [ ENTITY_ENGINE_RESOURCE_INIT_FAILURE_EVENT, ENTITY_ENGINE_INITIALIZATION_EVENT, ENTITY_STORE_USAGE_EVENT, + PRIVMON_ENGINE_INITIALIZATION_EVENT, + PRIVMON_ENGINE_RESOURCE_INIT_FAILURE_EVENT, TELEMETRY_DATA_STREAM_EVENT, TELEMETRY_ILM_POLICY_EVENT, TELEMETRY_ILM_STATS_EVENT,