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/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..2ad8476b56fc4 --- /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 type { z } from '@kbn/zod'; + +import { EngineDescriptor } from '../common.gen'; + +export type InitMonitoringEngineResponse = z.infer; +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 new file mode 100644 index 0000000000000..2403ae441c678 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/privilege_monitoring/engine/init.schema.yaml @@ -0,0 +1,20 @@ +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: + $ref: "../common.schema.yaml#/components/schemas/EngineDescriptor" 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/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/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/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/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..28633c3512c14 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/auth/api_key.ts @@ -0,0 +1,156 @@ +/* + * 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 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 { SavedObjectsErrorHelpers } from '@kbn/core-saved-objects-server'; +import type { SavedObjectsType } from '@kbn/core/server'; +import { getPrivmonEncryptedSavedObjectId } from './saved_object'; +import { privilegeMonitoringRuntimePrivileges } from './privileges'; + +export interface ApiKeyManager { + generate: () => Promise; +} + +export interface ApiKeyManagerDependencies { + core: CoreStart; + logger: Logger; + security: SecurityPluginStart; + encryptedSavedObjects?: EncryptedSavedObjectsPluginStart; + request?: KibanaRequest; + namespace: string; +} + +export const getApiKeyManager = (deps: ApiKeyManagerDependencies) => { + return { + generate: generate(deps), + getApiKey: getApiKey(deps), + getClientFromApiKey: getClientFromApiKey(deps), + getRequestFromApiKey, + }; +}; + +const generate = async (deps: ApiKeyManagerDependencies) => { + 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, + deps: ApiKeyManagerDependencies +): Promise => { + const apiKey = await deps.security.authc.apiKeys.grantAsInternalUser(req, { + name: 'Privilege Monitoring API key', + role_descriptors: { + privmon_admin: privilegeMonitoringRuntimePrivileges, + }, + metadata: { + description: 'API key used to manage the resources in the privilege monitoring engine', + }, + }); + + if (apiKey !== null) { + return { + id: apiKey.id, + name: apiKey.name, + apiKey: apiKey.api_key, + }; + } +}; + +export const SO_PRIVILEGE_MONITORING_API_KEY_TYPE = 'privmon-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/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/configurations.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/configurations.ts new file mode 100644 index 0000000000000..1fec1c76430eb --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/configurations.ts @@ -0,0 +1,6 @@ +/* + * 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. + */ 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 new file mode 100644 index 0000000000000..dc47d90289915 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/constants.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +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'; + +// 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', +} as const; + +// Base constants +export const PRIVMON_BASE_PREFIX = 'privmon' as const; +export const PRIVILEGE_MONITORING_INTERNAL_INDICES_PATTERN = `.${PRIVMON_BASE_PREFIX}*` as const; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/indices.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/indices.ts new file mode 100644 index 0000000000000..fdaa7628455e0 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/indices.ts @@ -0,0 +1,59 @@ +/* + * 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 { MappingTypeMapping } from '@elastic/elasticsearch/lib/api/types'; + +// 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}`; + +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 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, +}); 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/privilege_monitoring_data_client.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/privilege_monitoring_data_client.ts new file mode 100644 index 0000000000000..51c6c3779de26 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/privilege_monitoring_data_client.ts @@ -0,0 +1,186 @@ +/* + * 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, + AuditEvent, +} 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, + 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'; +import { + PRIVMON_ENGINE_INITIALIZATION_EVENT, + PRIVMON_ENGINE_RESOURCE_INIT_FAILURE_EVENT, +} from '../../telemetry/event_based/events'; + +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; + 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 + */ + } + + async init(): Promise { + if (!this.opts.taskManager) { + throw new Error('Task Manager is not available'); + } + const setupStartTime = moment().utc().toISOString(); + + this.audit( + PrivilegeMonitoringEngineActions.INIT, + EngineComponentResourceEnum.privmon_engine, + 'Initializing privilege monitoring engine' + ); + + const descriptor = await this.engineClient.init(); + this.log('debug', `Initialized privileged monitoring engine 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, + }); + + 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( + PrivilegeMonitoringEngineActions.INIT, + EngineComponentResourceEnum.privmon_engine, + 'Failed to initialize privilege monitoring engine', + e + ); + + 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: { + // TODO: double check this, taken from entity_store_data_client.ts line 480 example. + message: e.message, + stack: e.stack, + action: 'init', + }, + }); + } + return descriptor; + } + + public async createOrUpdateIndex() { + await createOrUpdateIndex({ + esClient: this.esClient, + logger: this.opts.logger, + options: { + index: this.getIndex(), + mappings: generateUserIndexMappings(), + }, + }); + } + + 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}` + ); + } + + 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); + } +} 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/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}` + ); + }); +}); 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..f67430a89ae71 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/saved_object/privilege_monitoring.ts @@ -0,0 +1,99 @@ +/* + * 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 { 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'; + +interface PrivilegeMonitoringEngineDescriptorDependencies { + soClient: SavedObjectsClientContract; + namespace: string; +} + +interface PrivilegedMonitoringEngineDescriptor { + status: EngineDescriptor['status']; + error?: Record; +} + +export class PrivilegeMonitoringEngineDescriptorClient { + constructor(private readonly deps: PrivilegeMonitoringEngineDescriptorDependencies) {} + + getSavedObjectId() { + return `privilege-monitoring-${this.deps.namespace}`; + } + + async init() { + const engineDescriptor = await this.find(); + if (engineDescriptor.total === 1) { + return this.updateExistingDescriptor(engineDescriptor); + } + const { attributes } = await this.deps.soClient.create( + privilegeMonitoringTypeName, + { + status: PRIVILEGE_MONITORING_ENGINE_STATUS.INSTALLING, + }, + { id: this.getSavedObjectId() } + ); + 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( + privilegeMonitoringTypeName, + id, + engine, + { refresh: 'wait_for' } + ); + return attributes; + } + + async updateStatus(status: EngineDescriptor['status']) { + 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); + } +} 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..b0bfbc7c74d76 --- /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,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 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', + }, + }, +}; + +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 }, +}; 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..116b853802b4b --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/tasks/privilege_monitoring_task.ts @@ -0,0 +1,147 @@ +/* + * 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, 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, SCOPE, INTERVAL } from '../constants'; +import { defaultState, stateSchemaByVersion } from './state'; + +interface RegisterParams { + getStartServices: EntityAnalyticsRoutesDeps['getStartServices']; + logger: Logger; + 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; + +const getTaskId = (namespace: string): string => `${TYPE}:${namespace}:${VERSION}`; + +export const registerPrivilegeMonitoringTask = ({ + getStartServices, + logger, + telemetry, + taskManager, + kibanaVersion, + 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: async () => + runPrivilegeMonitoringTask({ + isCancelled, + logger: deps.logger, + telemetry: deps.telemetry, + taskInstance, + experimentalFeatures: deps.experimentalFeatures, + }), + cancel: async () => { + cancelled = true; + }, + }; + }; + +const runPrivilegeMonitoringTask = async ({ + isCancelled, + logger, + telemetry, + taskInstance, + experimentalFeatures, +}: RunParams): Promise => { + if (isCancelled()) { + logger.info('[Privilege Monitoring] Task was cancelled.'); + return; + } + + try { + logger.info('[Privilege Monitoring] Running privilege monitoring task'); + } 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 { + await taskManager.ensureScheduled({ + id: taskId, + taskType: getTaskName(), + scope: SCOPE, + schedule: { + interval: INTERVAL, + }, + 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}` + ); + throw e; + } +}; 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, +}; 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/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, 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..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,6 +269,15 @@ 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, logger, 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..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 @@ -37,6 +37,7 @@ import type { SecuritySolutionApiRequestHandlerContext, SecuritySolutionRequestHandlerContext, } from './types'; +import { PrivilegeMonitoringDataClient } from './lib/entity_analytics/privilege_monitoring/privilege_monitoring_data_client'; export interface IRequestContextFactory { create( @@ -238,7 +239,22 @@ export class RequestContextFactory implements IRequestContextFactory { auditLogger: getAuditLogger(), }) ), + 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; 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, ]; 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..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,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/privilege_monitoring_data_client'; 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 */