[Entity Analytics][Privilege Monitoring] Engine initialization API (#215663)

## Summary 

This PR introduces the first building blocks for the [Entity Analytics
Privileged
Monitoring](https://github.com/elastic/security-team/issues/9971).
We follow the approach used in the Entity Store and add a new "Engine",
which consists of the following components:
* Public API
  * INIT and HEALTH routes
* Kibana task 
* Privilege Monitoring Data Client
* Engine Saved Object
* API key manager
* Related storage indices
* Feature Flag: `privilegeMonitoringEnabled` set to `false` by default.
* API integration test configuration
  * only tests that the health endpoint is available
* Auditing and Telemetry 


## Testing steps

1. Make sure to add `privilegeMonitoringEnabled` to your
`kibana.dev.yaml`
2. In devtools, ensure the API is working with `GET
kbn:/api/entity_analytics/monitoring/privileges/health`
3. Start the engine with: `POST
kbn:/api/entity_analytics/monitoring/engine/init`
4. Look for `DEBUG` logs mentioning the
`entity_analytics:monitoring:privileges:engine` task

---------

Co-authored-by: CAWilson94 <charlotte.wilson@elastic.co>
Co-authored-by: Charlotte Alexandra Wilson <CAWilson94@users.noreply.github.com>
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Tiago Vila Verde 2025-04-11 13:25:24 +02:00 committed by GitHub
parent b4d3a2a8f2
commit 1bf39845da
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
52 changed files with 1930 additions and 23 deletions

View file

@ -24,7 +24,7 @@ disabled:
# MKI only configs files
- x-pack/test_serverless/functional/test_suites/security/config.mki_only.ts
defaultQueue: 'n2-4-spot'
defaultQueue: "n2-4-spot"
enabled:
- x-pack/test_serverless/api_integration/test_suites/security/config.ts
- x-pack/test_serverless/api_integration/test_suites/security/config.feature_flags.ts
@ -106,6 +106,7 @@ enabled:
- x-pack/test/security_solution_api_integration/test_suites/entity_analytics/risk_engine/trial_license_complete_tier/configs/serverless.config.ts
- x-pack/test/security_solution_api_integration/test_suites/entity_analytics/risk_engine/basic_license_essentials_tier/configs/serverless.config.ts
- x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/configs/serverless.config.ts
- x-pack/test/security_solution_api_integration/test_suites/entity_analytics/monitoring/trial_license_complete_tier/configs/serverless.config.ts
- x-pack/test/security_solution_api_integration/test_suites/lists_and_exception_lists/exception_lists_items/trial_license_complete_tier/configs/serverless.config.ts
- x-pack/test/security_solution_api_integration/test_suites/lists_and_exception_lists/authorization/exceptions/lists/essentials_tier/configs/serverless.config.ts
- x-pack/test/security_solution_api_integration/test_suites/lists_and_exception_lists/authorization/exceptions/common/essentials_tier/configs/serverless.config.ts

View file

@ -81,6 +81,7 @@ enabled:
- x-pack/test/security_solution_api_integration/test_suites/entity_analytics/risk_engine/trial_license_complete_tier/configs/ess.config.ts
- x-pack/test/security_solution_api_integration/test_suites/entity_analytics/risk_engine/basic_license_essentials_tier/configs/ess.config.ts
- x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/configs/ess.config.ts
- x-pack/test/security_solution_api_integration/test_suites/entity_analytics/monitoring/trial_license_complete_tier/configs/ess.config.ts
- x-pack/test/security_solution_api_integration/test_suites/lists_and_exception_lists/exception_lists_items/trial_license_complete_tier/configs/ess.config.ts
- x-pack/test/security_solution_api_integration/test_suites/lists_and_exception_lists/lists_items/trial_license_complete_tier/configs/ess.config.ts
- x-pack/test/security_solution_api_integration/test_suites/explore/hosts/trial_license_complete_tier/configs/ess.config.ts

View file

@ -13243,6 +13243,35 @@ paths:
summary: Create or update a protection updates note
tags:
- Security Endpoint Management API
/api/entity_analytics/monitoring/engine/init:
post:
operationId: InitMonitoringEngine
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/Security_Entity_Analytics_API_MonitoringEngineDescriptor'
description: Successful response
summary: Initialize the Privilege Monitoring Engine
tags:
- Security Entity Analytics API
/api/entity_analytics/monitoring/privileges/health:
get:
operationId: PrivMonHealth
responses:
'200':
content:
application/json:
schema:
type: object
properties:
ok:
type: boolean
description: Successful response
summary: Health check on Privilege Monitoring
tags:
- Security Entity Analytics API
/api/entity_store/enable:
post:
operationId: InitEntityStore
@ -62564,6 +62593,14 @@ components:
type: string
Security_Entity_Analytics_API_Metadata:
$ref: '#/components/schemas/Security_Entity_Analytics_API_TransformStatsMetadata'
Security_Entity_Analytics_API_MonitoringEngineDescriptor:
type: object
properties:
status:
$ref: '#/components/schemas/Security_Entity_Analytics_API_EngineStatus'
required:
- type
- status
Security_Entity_Analytics_API_RiskEngineScheduleNowErrorResponse:
type: object
properties:

View file

@ -15402,6 +15402,35 @@ paths:
summary: Create or update a protection updates note
tags:
- Security Endpoint Management API
/api/entity_analytics/monitoring/engine/init:
post:
operationId: InitMonitoringEngine
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/Security_Entity_Analytics_API_MonitoringEngineDescriptor'
description: Successful response
summary: Initialize the Privilege Monitoring Engine
tags:
- Security Entity Analytics API
/api/entity_analytics/monitoring/privileges/health:
get:
operationId: PrivMonHealth
responses:
'200':
content:
application/json:
schema:
type: object
properties:
ok:
type: boolean
description: Successful response
summary: Health check on Privilege Monitoring
tags:
- Security Entity Analytics API
/api/entity_store/enable:
post:
operationId: InitEntityStore
@ -72049,6 +72078,14 @@ components:
type: string
Security_Entity_Analytics_API_Metadata:
$ref: '#/components/schemas/Security_Entity_Analytics_API_TransformStatsMetadata'
Security_Entity_Analytics_API_MonitoringEngineDescriptor:
type: object
properties:
status:
$ref: '#/components/schemas/Security_Entity_Analytics_API_EngineStatus'
required:
- type
- status
Security_Entity_Analytics_API_RiskEngineScheduleNowErrorResponse:
type: object
properties:

View file

@ -873,6 +873,9 @@
"policy-settings-protection-updates-note": [
"note"
],
"privilege-monitoring-status": [
"status"
],
"product-doc-install-status": [
"index_name",
"installation_status",

View file

@ -2913,6 +2913,14 @@
}
}
},
"privilege-monitoring-status": {
"dynamic": false,
"properties": {
"status": {
"type": "keyword"
}
}
},
"product-doc-install-status": {
"dynamic": false,
"properties": {

View file

@ -11,4 +11,4 @@ export { registerCoreObjectTypes } from './registration';
// set minimum number of registered saved objects to ensure no object types are removed after 8.8
// declared in internal implementation exclicilty to prevent unintended changes.
export const SAVED_OBJECT_TYPES_COUNT = 128 as const;
export const SAVED_OBJECT_TYPES_COUNT = 129 as const;

View file

@ -149,6 +149,7 @@ describe('checking migration metadata changes on all registered SO types', () =>
"osquery-pack-asset": "cd140bc2e4b092e93692b587bf6e38051ef94c75",
"osquery-saved-query": "6095e288750aa3164dfe186c74bc5195c2bf2bd4",
"policy-settings-protection-updates-note": "33924bb246f9e5bcb876109cc83e3c7a28308352",
"privilege-monitoring-status": "9b11c4a49e679e2827b0468ba27269a19345c049",
"product-doc-install-status": "ca6e96840228e4cc2f11bae24a0797f4f7238c8c",
"query": "501bece68f26fe561286a488eabb1a8ab12f1137",
"risk-engine-configuration": "bab237d09c2e7189dddddcb1b28f19af69755efb",

View file

@ -117,6 +117,7 @@ const previouslyRegisteredTypes = [
'osquery-usage-metric',
'osquery-manager-usage-metric',
'policy-settings-protection-updates-note',
'privilege-monitoring-status',
'product-doc-install-status',
'query',
'rules-settings',

View file

@ -58,26 +58,35 @@ export const canDisableEntityDiscovery = async (client: ElasticsearchClient) =>
);
};
export const entityDefinitionRuntimePrivileges = (sourceIndices: string[]) => ({
cluster: ['manage_transform', 'manage_ingest_pipelines', 'manage_index_templates'],
index: [
{
names: [ENTITY_INTERNAL_INDICES_PATTERN],
privileges: ['create_index', 'delete_index', 'index', 'create_doc', 'auto_configure', 'read'],
},
{
names: [...sourceIndices, ENTITY_INTERNAL_INDICES_PATTERN],
privileges: ['read', 'view_index_metadata'],
},
],
application: [
{
application: 'kibana-.kibana',
privileges: [`saved_object:${SO_ENTITY_DEFINITION_TYPE}/*`],
resources: ['*'],
},
],
});
export const entityDefinitionRuntimePrivileges = (sourceIndices: string[]) => {
return {
cluster: ['manage_transform', 'manage_ingest_pipelines', 'manage_index_templates'],
index: [
{
names: [ENTITY_INTERNAL_INDICES_PATTERN],
privileges: [
'create_index',
'delete_index',
'index',
'create_doc',
'auto_configure',
'read',
],
},
{
names: [...sourceIndices, ENTITY_INTERNAL_INDICES_PATTERN],
privileges: ['read', 'view_index_metadata'],
},
],
application: [
{
application: 'kibana-.kibana',
privileges: [`saved_object:${SO_ENTITY_DEFINITION_TYPE}/*`],
resources: ['*'],
},
],
};
};
export const entityDefinitionDeletionPrivileges = {
cluster: ['manage_transform', 'manage_ingest_pipelines', 'manage_index_templates'],

View file

@ -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<typeof EngineStatus>;
export const EngineStatus = z.enum(['installing', 'started', 'stopped', 'updating', 'error']);
export type EngineStatusEnum = typeof EngineStatus.enum;
export const EngineStatusEnum = EngineStatus.enum;
export type MonitoringEngineDescriptor = z.infer<typeof MonitoringEngineDescriptor>;
export const MonitoringEngineDescriptor = z.object({
status: EngineStatus,
});
export type EngineComponentResource = z.infer<typeof EngineComponentResource>;
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<typeof EngineComponentStatus>;
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(),
});

View file

@ -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:
MonitoringEngineDescriptor:
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

View file

@ -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 { MonitoringEngineDescriptor } from '../common.gen';
export type InitMonitoringEngineResponse = z.infer<typeof InitMonitoringEngineResponse>;
export const InitMonitoringEngineResponse = MonitoringEngineDescriptor;

View file

@ -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/MonitoringEngineDescriptor"

View file

@ -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: Health check on Privilege Monitoring
* version: 2023-10-31
*/
import { z } from '@kbn/zod';
export type PrivMonHealthResponse = z.infer<typeof PrivMonHealthResponse>;
export const PrivMonHealthResponse = z.object({
ok: z.boolean().optional(),
});

View file

@ -0,0 +1,23 @@
openapi: 3.0.0
info:
title: Health check on Privilege Monitoring
version: "2023-10-31"
paths:
/api/entity_analytics/monitoring/privileges/health:
get:
x-labels: [ess, serverless]
x-codegen-enabled: true
operationId: PrivMonHealth
summary: Health check on Privilege Monitoring
responses:
"200":
description: Successful response
content:
application/json:
schema:
type: object
properties:
ok:
type: boolean

View file

@ -258,6 +258,8 @@ import type {
GetEntityStoreStatusRequestQueryInput,
GetEntityStoreStatusResponse,
} from './entity_analytics/entity_store/status.gen';
import type { InitMonitoringEngineResponse } from './entity_analytics/privilege_monitoring/engine/init.gen';
import type { PrivMonHealthResponse } from './entity_analytics/privilege_monitoring/health.gen';
import type { CleanUpRiskEngineResponse } from './entity_analytics/risk_engine/engine_cleanup_route.gen';
import type {
ConfigureRiskEngineSavedObjectRequestBodyInput,
@ -1664,6 +1666,18 @@ finalize it.
})
.catch(catchAxiosErrorFormatAndThrow);
}
async initMonitoringEngine() {
this.log.info(`${new Date().toISOString()} Calling API InitMonitoringEngine`);
return this.kbnClient
.request<InitMonitoringEngineResponse>({
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
*/
@ -1907,6 +1921,18 @@ The edit action is idempotent, meaning that if you add a tag to a rule that alre
})
.catch(catchAxiosErrorFormatAndThrow);
}
async privMonHealth() {
this.log.info(`${new Date().toISOString()} Calling API PrivMonHealth`);
return this.kbnClient
.request<PrivMonHealthResponse>({
path: '/api/entity_analytics/monitoring/privileges/health',
headers: {
[ELASTIC_HTTP_VERSION_HEADER]: '2023-10-31',
},
method: 'GET',
})
.catch(catchAxiosErrorFormatAndThrow);
}
async readAlertsIndex() {
this.log.info(`${new Date().toISOString()} Calling API ReadAlertsIndex`);
return this.kbnClient

View file

@ -220,6 +220,11 @@ export const allowedExperimentalValues = Object.freeze({
*/
serviceEntityStoreEnabled: true,
/**
* Enables Privilege Monitoring
*/
privilegeMonitoringEnabled: false,
/**
* Disables the siem migrations feature
*/

View file

@ -306,6 +306,35 @@ paths:
summary: List asset criticality records
tags:
- Security Entity Analytics API
/api/entity_analytics/monitoring/engine/init:
post:
operationId: InitMonitoringEngine
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/MonitoringEngineDescriptor'
description: Successful response
summary: Initialize the Privilege Monitoring Engine
tags:
- Security Entity Analytics API
/api/entity_analytics/monitoring/privileges/health:
get:
operationId: PrivMonHealth
responses:
'200':
content:
application/json:
schema:
type: object
properties:
ok:
type: boolean
description: Successful response
summary: Health check on Privilege Monitoring
tags:
- Security Entity Analytics API
/api/entity_store/enable:
post:
operationId: InitEntityStore
@ -1342,6 +1371,14 @@ components:
type: string
Metadata:
$ref: '#/components/schemas/TransformStatsMetadata'
MonitoringEngineDescriptor:
type: object
properties:
status:
$ref: '#/components/schemas/EngineStatus'
required:
- type
- status
RiskEngineScheduleNowErrorResponse:
type: object
properties:

View file

@ -306,6 +306,35 @@ paths:
summary: List asset criticality records
tags:
- Security Entity Analytics API
/api/entity_analytics/monitoring/engine/init:
post:
operationId: InitMonitoringEngine
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/MonitoringEngineDescriptor'
description: Successful response
summary: Initialize the Privilege Monitoring Engine
tags:
- Security Entity Analytics API
/api/entity_analytics/monitoring/privileges/health:
get:
operationId: PrivMonHealth
responses:
'200':
content:
application/json:
schema:
type: object
properties:
ok:
type: boolean
description: Successful response
summary: Health check on Privilege Monitoring
tags:
- Security Entity Analytics API
/api/entity_store/enable:
post:
operationId: InitEntityStore
@ -1342,6 +1371,14 @@ components:
type: string
Metadata:
$ref: '#/components/schemas/TransformStatsMetadata'
MonitoringEngineDescriptor:
type: object
properties:
status:
$ref: '#/components/schemas/EngineStatus'
required:
- type
- status
RiskEngineScheduleNowErrorResponse:
type: object
properties:

View file

@ -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(),
@ -170,6 +172,7 @@ const createSecuritySolutionRequestContextMock = (
getDataViewsService: jest.fn(),
getEntityStoreApiKeyManager: 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),

View file

@ -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];

View file

@ -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<void>;
}
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<PrivilegeMonitoringAPIKey>(
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<PrivilegeMonitoringAPIKey | undefined> => {
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;
}

View file

@ -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: ['*'],
},
],
});

View file

@ -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);
};

View file

@ -0,0 +1,23 @@
/*
* 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:privileges:engine';
export const VERSION = '1.0.0';
export const TIMEOUT = '10m';
export const INTERVAL = '10m';
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;

View file

@ -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<MappingTypeMapping['properties']>;
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,
});

View file

@ -0,0 +1,15 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { PrivilegeMonitoringDataClient } from './privilege_monitoring_data_client';
const createPrivilegeMonitorDataClientMock = () =>
({
init: jest.fn(),
} as unknown as jest.Mocked<PrivilegeMonitoringDataClient>);
export const privilegeMonitorDataClientMock = { create: createPrivilegeMonitorDataClientMock };

View file

@ -0,0 +1,140 @@
/*
* 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 {
elasticsearchServiceMock,
savedObjectsClientMock,
loggingSystemMock,
} from '@kbn/core/server/mocks';
import { PrivilegeMonitoringDataClient } from './privilege_monitoring_data_client';
import type { TaskManagerStartContract } from '@kbn/task-manager-plugin/server/plugin';
import { PrivilegeMonitoringEngineActions } from './auditing/actions';
import { EngineComponentResourceEnum } from '../../../../common/api/entity_analytics/privilege_monitoring/common.gen';
import { startPrivilegeMonitoringTask as mockStartPrivilegeMonitoringTask } from './tasks/privilege_monitoring_task';
import type { AuditLogger } from '@kbn/core/server';
jest.mock('./tasks/privilege_monitoring_task', () => {
return {
startPrivilegeMonitoringTask: jest.fn().mockResolvedValue(undefined),
};
});
jest.mock('./saved_object/privilege_monitoring', () => {
return {
PrivilegeMonitoringEngineDescriptorClient: jest.fn().mockImplementation(() => ({
init: jest.fn().mockResolvedValue({ status: 'success' }),
update: jest.fn(),
})),
};
});
describe('Privilege Monitoring Data Client', () => {
const mockSavedObjectClient = savedObjectsClientMock.create();
const clusterClientMock = elasticsearchServiceMock.createScopedClusterClient();
const loggerMock = loggingSystemMock.createLogger();
const auditMock = { log: jest.fn().mockReturnValue(undefined) };
loggerMock.debug = jest.fn();
const defaultOpts = {
logger: loggerMock,
clusterClient: clusterClientMock,
namespace: 'default',
soClient: mockSavedObjectClient,
kibanaVersion: '8.0.0',
taskManager: {} as TaskManagerStartContract,
auditLogger: auditMock as unknown as AuditLogger,
};
let dataClient: PrivilegeMonitoringDataClient;
const mockDescriptor = { status: 'success' };
const mockCreateOrUpdateIndex = jest.fn().mockResolvedValue(undefined);
beforeEach(() => {
jest.clearAllMocks();
dataClient = new PrivilegeMonitoringDataClient(defaultOpts);
});
describe('init', () => {
it('should initialize the privilege monitoring engine successfully', async () => {
dataClient.createOrUpdateIndex = mockCreateOrUpdateIndex;
const result = await dataClient.init();
expect(mockCreateOrUpdateIndex).toHaveBeenCalled();
expect(mockStartPrivilegeMonitoringTask).toHaveBeenCalled();
expect(loggerMock.debug).toHaveBeenCalledTimes(1);
expect(auditMock.log).toHaveBeenCalled();
expect(result).toEqual(mockDescriptor);
});
it('should throw if taskManager is not available', async () => {
const { taskManager, ...optsWithoutTaskManager } = defaultOpts;
dataClient = new PrivilegeMonitoringDataClient(optsWithoutTaskManager);
await expect(dataClient.init()).rejects.toThrow('Task Manager is not available');
});
it('should log a message if index already exists', async () => {
const error = {
meta: {
body: {
error: {
type: 'resource_already_exists_exception',
},
},
},
};
dataClient.createOrUpdateIndex = jest.fn().mockRejectedValue(error);
Object.defineProperty(dataClient, 'engineClient', {
value: {
init: jest.fn().mockResolvedValue({ status: 'success' }),
update: jest.fn(),
},
});
await dataClient.init();
expect(loggerMock.info).toHaveBeenCalledWith('Privilege monitoring index already exists');
});
it('should handle unexpected errors and update engine status', async () => {
const fakeError = new Error('Something went wrong');
dataClient.createOrUpdateIndex = jest.fn().mockRejectedValue(fakeError);
const mockAudit = jest.fn();
const mockLog = jest.fn();
Object.defineProperty(dataClient, 'audit', { value: mockAudit });
Object.defineProperty(dataClient, 'log', { value: mockLog });
await dataClient.init();
expect(mockLog).toHaveBeenCalledWith(
'error',
expect.stringContaining('Error initializing privilege monitoring engine')
);
expect(mockAudit).toHaveBeenCalledWith(
PrivilegeMonitoringEngineActions.INIT,
EngineComponentResourceEnum.privmon_engine,
'Failed to initialize privilege monitoring engine',
expect.any(Error)
);
});
});
describe('audit', () => {
it('should log audit events successfully', async () => {
// TODO: implement once we have more auditing
});
it('should handle errors during audit logging', async () => {
// TODO: implement once we have more auditing
});
});
});

View file

@ -0,0 +1,180 @@
/*
* 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,
});
}
async init(): Promise<InitMonitoringEngineResponse> {
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();
}
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: {
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<keyof Logger, 'get' | 'log' | 'isLevelEnabled'>, 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);
}
}

View file

@ -0,0 +1,52 @@
/*
* 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 { PrivMonHealthResponse } from '../../../../../common/api/entity_analytics/privilege_monitoring/health.gen';
import { API_VERSIONS, APP_ID } from '../../../../../common/constants';
import type { EntityAnalyticsRoutesDeps } from '../../types';
export const healthCheckPrivilegeMonitoringRoute = (
router: EntityAnalyticsRoutesDeps['router'],
logger: Logger,
config: EntityAnalyticsRoutesDeps['config']
) => {
router.versioned
.get({
access: 'public',
path: '/api/entity_analytics/monitoring/privileges/health',
security: {
authz: {
requiredPrivileges: ['securitySolution', `${APP_ID}-entity-analytics`],
},
},
})
.addVersion(
{
version: API_VERSIONS.public.v1,
validate: {},
},
async (context, request, response): Promise<IKibanaResponse<PrivMonHealthResponse>> => {
const siemResponse = buildSiemResponse(response);
try {
return response.ok({ body: { ok: true } });
} catch (e) {
const error = transformError(e);
logger.error(`Error checking privilege monitoring health: ${error.message}`);
return siemResponse.error({
statusCode: error.statusCode,
body: error.message,
});
}
}
);
};

View file

@ -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<IKibanaResponse<InitMonitoringEngineResponse>> => {
const siemResponse = buildSiemResponse(response);
const secSol = await context.securitySolution;
try {
const body = await secSol.getPrivilegeMonitoringDataClient().init();
return response.ok({ body });
} catch (e) {
const error = transformError(e);
logger.error(`Error initializing privilege monitoring engine: ${error.message}`);
return siemResponse.error({
statusCode: error.statusCode,
body: error.message,
});
}
}
);
};

View file

@ -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.
*/
import type { EntityAnalyticsRoutesDeps } from '../../types';
import { healthCheckPrivilegeMonitoringRoute } from './health';
import { initPrivilegeMonitoringEngineRoute } from './init';
export const registerPrivilegeMonitoringRoutes = ({
router,
logger,
getStartServices,
config,
}: EntityAnalyticsRoutesDeps) => {
initPrivilegeMonitoringEngineRoute(router, logger, config);
healthCheckPrivilegeMonitoringRoute(router, logger, config);
};

View file

@ -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<SavedObjectsClientContract>;
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<SavedObjectsClientContract>;
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<unknown, unknown>);
soClient.create.mockResolvedValue({
id: `privilege-monitoring-${namespace}`,
type: privilegeMonitoringTypeName,
attributes: { status: 'installing' as unknown },
references: [],
});
const result = await client.init();
expect(soClient.create).toHaveBeenCalledWith(
privilegeMonitoringTypeName,
{ status: PRIVILEGE_MONITORING_ENGINE_STATUS.INSTALLING },
{ id: `privilege-monitoring-${namespace}` }
);
expect(result).toEqual({ status: 'installing' });
});
it('should update an existing descriptor if one exists', async () => {
const existingDescriptor = {
total: 1,
saved_objects: [{ attributes: { status: 'started', apiKey: 'old-key' } }],
} as SavedObjectsFindResponse<unknown, unknown>;
soClient.find.mockResolvedValue(
existingDescriptor as unknown as SavedObjectsFindResponse<unknown, unknown>
);
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<unknown, unknown>);
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<unknown>);
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}`
);
});
});

View file

@ -0,0 +1,100 @@
/*
* 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 { MonitoringEngineDescriptor } 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: MonitoringEngineDescriptor['status'];
error?: Record<string, unknown>;
}
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<PrivilegedMonitoringEngineDescriptor>(
privilegeMonitoringTypeName,
{
status: PRIVILEGE_MONITORING_ENGINE_STATUS.INSTALLING,
},
{ id: this.getSavedObjectId() }
);
return attributes;
}
private async updateExistingDescriptor(
engineDescriptor: SavedObjectsFindResponse<PrivilegedMonitoringEngineDescriptor, unknown>
) {
const old = engineDescriptor.saved_objects[0].attributes;
const update = {
...old,
error: undefined,
status: PRIVILEGE_MONITORING_ENGINE_STATUS.INSTALLING,
apiKey: '',
};
await this.deps.soClient.update<PrivilegedMonitoringEngineDescriptor>(
privilegeMonitoringTypeName,
this.getSavedObjectId(),
update,
{ refresh: 'wait_for' }
);
return update;
}
async update(engine: Partial<PrivilegedMonitoringEngineDescriptor>) {
const id = this.getSavedObjectId();
const { attributes } = await this.deps.soClient.update<PrivilegedMonitoringEngineDescriptor>(
privilegeMonitoringTypeName,
id,
engine,
{ refresh: 'wait_for' }
);
return attributes;
}
async updateStatus(status: MonitoringEngineDescriptor['status']) {
return this.update({ status });
}
async find() {
return this.deps.soClient.find<PrivilegedMonitoringEngineDescriptor>({
type: privilegeMonitoringTypeName,
namespaces: [this.deps.namespace],
});
}
async get() {
const id = this.getSavedObjectId();
const { attributes } = await this.deps.soClient.get<PrivilegedMonitoringEngineDescriptor>(
privilegeMonitoringTypeName,
id
);
return attributes;
}
async delete() {
const id = this.getSavedObjectId();
return this.deps.soClient.delete(privilegeMonitoringTypeName, id);
}
}

View file

@ -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 },
};

View file

@ -0,0 +1,162 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { Logger, AnalyticsServiceSetup } from '@kbn/core/server';
import type {
ConcreteTaskInstance,
TaskManagerSetupContract,
TaskManagerStartContract,
TaskRunCreatorFunction,
} from '@kbn/task-manager-plugin/server';
import moment from 'moment';
import type { ExperimentalFeatures } from '../../../../../common';
import type { EntityAnalyticsRoutesDeps } from '../../types';
import { TYPE, VERSION, TIMEOUT, SCOPE, INTERVAL } from '../constants';
import {
defaultState,
stateSchemaByVersion,
type LatestTaskStateSchema as PrivilegeMonitoringTaskState,
} 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',
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<{
state: PrivilegeMonitoringTaskState;
}> => {
const state = taskInstance.state as PrivilegeMonitoringTaskState;
const taskStartTime = moment().utc().toISOString();
const updatedState = {
lastExecutionTimestamp: taskStartTime,
namespace: state.namespace,
runs: state.runs + 1,
};
if (isCancelled()) {
logger.info('[Privilege Monitoring] Task was cancelled.');
return { state: updatedState };
}
try {
logger.info('[Privilege Monitoring] Running privilege monitoring task');
} catch (e) {
logger.error('[Privilege Monitoring] Error running privilege monitoring task', e);
}
return { state: updatedState };
};
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 },
});
logger.info(`Scheduling privilege monitoring task with id ${taskId}`);
} catch (e) {
logger.warn(
`[Privilege Monitoring] [task ${taskId}]: error scheduling task, received ${e.message}`
);
throw e;
}
};

View file

@ -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<string, unknown>) => ({
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<typeof latestTaskStateSchema>;
export const defaultState: LatestTaskStateSchema = {
lastExecutionTimestamp: undefined,
namespace: 'default',
runs: 0,
};

View file

@ -10,7 +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_privilege_monitoring_routes';
export const registerEntityAnalyticsRoutes = (routeDeps: EntityAnalyticsRoutesDeps) => {
registerAssetCriticalityRoutes(routeDeps);
registerRiskScoreRoutes(routeDeps);
@ -18,4 +18,7 @@ export const registerEntityAnalyticsRoutes = (routeDeps: EntityAnalyticsRoutesDe
if (!routeDeps.config.experimentalFeatures.entityStoreDisabled) {
registerEntityStoreRoutes(routeDeps);
}
if (routeDeps.config.experimentalFeatures.privilegeMonitoringEnabled) {
registerPrivilegeMonitoringRoutes(routeDeps);
}
};

View file

@ -221,6 +221,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;
@ -1271,6 +1299,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,

View file

@ -132,6 +132,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';
@ -267,6 +268,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,

View file

@ -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(
@ -249,7 +250,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;

View file

@ -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,
];

View file

@ -39,7 +39,9 @@ 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 type { PrivilegeMonitoringDataClient } from './lib/entity_analytics/privilege_monitoring/privilege_monitoring_data_client';
import type { ApiKeyManager } from './lib/entity_analytics/entity_store/auth/api_key';
export { AppClient };
export interface SecuritySolutionApiRequestHandlerContext {
@ -64,6 +66,7 @@ export interface SecuritySolutionApiRequestHandlerContext {
getRiskScoreDataClient: () => RiskScoreDataClient;
getAssetCriticalityDataClient: () => AssetCriticalityDataClient;
getEntityStoreDataClient: () => EntityStoreDataClient;
getPrivilegeMonitoringDataClient: () => PrivilegeMonitoringDataClient;
getSiemRuleMigrationsClient: () => SiemRuleMigrationsClient;
getInferenceClient: () => InferenceClient;
getAssetInventoryClient: () => AssetInventoryDataClient;

View file

@ -1184,6 +1184,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
*/
@ -1363,6 +1370,13 @@ The edit action is idempotent, meaning that if you add a tag to a rule that alre
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.send(props.body as object);
},
privMonHealth(kibanaSpace: string = 'default') {
return supertest
.get(routeWithNamespace('/api/entity_analytics/monitoring/privileges/health', kibanaSpace))
.set('kbn-xsrf', 'true')
.set(ELASTIC_HTTP_VERSION_HEADER, '2023-10-31')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana');
},
readAlertsIndex(kibanaSpace: string = 'default') {
return supertest
.get(routeWithNamespace('/api/detection_engine/index', kibanaSpace))

View file

@ -142,6 +142,7 @@ export default function ({ getService }: FtrProviderContext) {
'endpoint:complete-external-response-actions',
'endpoint:metadata-check-transforms-task',
'endpoint:user-artifact-packager',
'entity_analytics:monitoring:privileges:engine',
'entity_store:data_view:refresh',
'entity_store:field_retention:enrichment',
'fleet:automatic-agent-upgrade-task',

View file

@ -105,6 +105,13 @@
"entity_analytics:entity_store:server:ess": "npm run initialize-server:ea:trial_complete entity_store ess",
"entity_analytics:entity_store:runner:ess": "npm run run-tests:ea:trial_complete entity_store ess essEnv --",
"entity_analytics:monitoring:server:serverless": "npm run initialize-server:ea:trial_complete monitoring serverless",
"entity_analytics:monitoring:runner:serverless": "npm run run-tests:ea:trial_complete monitoring serverless serverlessEnv",
"entity_analytics:monitoring:qa:serverless": "npm run run-tests:ea:trial_complete monitoring serverless qaPeriodicEnv",
"entity_analytics:monitoring:qa:serverless:release": "npm run run-tests:ea:trial_complete monitoring serverless qaEnv",
"entity_analytics:monitoring:server:ess": "npm run initialize-server:ea:trial_complete monitoring ess",
"entity_analytics:monitoring:runner:ess": "npm run run-tests:ea:trial_complete monitoring ess essEnv --",
"edr_workflows:artifacts:server:serverless": "npm run initialize-server:edr-workflows artifacts serverless",
"edr_workflows:artifacts:runner:serverless": "npm run run-tests:edr-workflows artifacts serverless serverlessEnv",
"edr_workflows:artifacts:qa:serverless": "npm run run-tests:edr-workflows artifacts serverless qaPeriodicEnv",

View file

@ -0,0 +1,31 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { FtrConfigProviderContext } from '@kbn/test';
export default async function ({ readConfigFile }: FtrConfigProviderContext) {
const functionalConfig = await readConfigFile(
require.resolve('../../../../../config/ess/config.base.trial')
);
return {
...functionalConfig.getAll(),
kbnTestServer: {
...functionalConfig.get('kbnTestServer'),
serverArgs: [
...functionalConfig.get('kbnTestServer.serverArgs'),
`--xpack.securitySolution.enableExperimental=${JSON.stringify([
'privilegeMonitoringEnabled',
])}`,
],
},
testFiles: [require.resolve('..')],
junit: {
reportName:
'Entity Analytics - Privilege Monitoring Integration Tests - ESS Env - Trial License',
},
};
}

View file

@ -0,0 +1,24 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { createTestConfig } from '../../../../../config/serverless/config.base';
export default createTestConfig({
kbnTestServerArgs: [
`--xpack.securitySolution.enableExperimental=${JSON.stringify(['privilegeMonitoringEnabled'])}`,
`--xpack.securitySolutionServerless.productTypes=${JSON.stringify([
{ product_line: 'security', product_tier: 'complete' },
{ product_line: 'endpoint', product_tier: 'complete' },
{ product_line: 'cloud', product_tier: 'complete' },
])}`,
],
testFiles: [require.resolve('..')],
junit: {
reportName:
'Entity Analytics - Privilege Monitoring Integration Tests - Serverless Env - Complete Tier',
},
});

View file

@ -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 expect from '@kbn/expect';
import { FtrProviderContext } from '../../../../ftr_provider_context';
import { dataViewRouteHelpersFactory } from '../../utils/data_view';
export default ({ getService }: FtrProviderContext) => {
const api = getService('securitySolutionApi');
const supertest = getService('supertest');
const log = getService('log');
describe('@ess @serverless @skipInServerlessMKI Entity Privilege Monitoring APIs', () => {
const dataView = dataViewRouteHelpersFactory(supertest);
before(async () => {
await dataView.create('security-solution');
});
after(async () => {
await dataView.delete('security-solution');
});
describe('health', () => {
it('should be healthy', async () => {
log.info(`Checking health of privilege monitoring`);
const res = await api.privMonHealth();
if (res.status !== 200) {
log.error(`Health check failed`);
log.error(JSON.stringify(res.body));
}
expect(res.status).eql(200);
});
});
});
};

View file

@ -0,0 +1,14 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { FtrProviderContext } from '../../../../ftr_provider_context';
export default function ({ loadTestFile }: FtrProviderContext) {
describe('Entity Analytics - Privilege Monitoring', function () {
loadTestFile(require.resolve('./engine'));
});
}

View file

@ -374,6 +374,18 @@ export default function ({ getService }: FtrProviderContext) {
"saved_object:entity-engine-status/delete",
"saved_object:entity-engine-status/bulk_delete",
"saved_object:entity-engine-status/share_to_space",
"saved_object:privilege-monitoring-status/bulk_get",
"saved_object:privilege-monitoring-status/get",
"saved_object:privilege-monitoring-status/find",
"saved_object:privilege-monitoring-status/open_point_in_time",
"saved_object:privilege-monitoring-status/close_point_in_time",
"saved_object:privilege-monitoring-status/create",
"saved_object:privilege-monitoring-status/bulk_create",
"saved_object:privilege-monitoring-status/update",
"saved_object:privilege-monitoring-status/bulk_update",
"saved_object:privilege-monitoring-status/delete",
"saved_object:privilege-monitoring-status/bulk_delete",
"saved_object:privilege-monitoring-status/share_to_space",
"saved_object:policy-settings-protection-updates-note/bulk_get",
"saved_object:policy-settings-protection-updates-note/get",
"saved_object:policy-settings-protection-updates-note/find",
@ -1218,6 +1230,18 @@ export default function ({ getService }: FtrProviderContext) {
"saved_object:entity-engine-status/delete",
"saved_object:entity-engine-status/bulk_delete",
"saved_object:entity-engine-status/share_to_space",
"saved_object:privilege-monitoring-status/bulk_get",
"saved_object:privilege-monitoring-status/get",
"saved_object:privilege-monitoring-status/find",
"saved_object:privilege-monitoring-status/open_point_in_time",
"saved_object:privilege-monitoring-status/close_point_in_time",
"saved_object:privilege-monitoring-status/create",
"saved_object:privilege-monitoring-status/bulk_create",
"saved_object:privilege-monitoring-status/update",
"saved_object:privilege-monitoring-status/bulk_update",
"saved_object:privilege-monitoring-status/delete",
"saved_object:privilege-monitoring-status/bulk_delete",
"saved_object:privilege-monitoring-status/share_to_space",
"saved_object:policy-settings-protection-updates-note/bulk_get",
"saved_object:policy-settings-protection-updates-note/get",
"saved_object:policy-settings-protection-updates-note/find",
@ -1840,6 +1864,11 @@ export default function ({ getService }: FtrProviderContext) {
"saved_object:entity-engine-status/find",
"saved_object:entity-engine-status/open_point_in_time",
"saved_object:entity-engine-status/close_point_in_time",
"saved_object:privilege-monitoring-status/bulk_get",
"saved_object:privilege-monitoring-status/get",
"saved_object:privilege-monitoring-status/find",
"saved_object:privilege-monitoring-status/open_point_in_time",
"saved_object:privilege-monitoring-status/close_point_in_time",
"saved_object:policy-settings-protection-updates-note/bulk_get",
"saved_object:policy-settings-protection-updates-note/get",
"saved_object:policy-settings-protection-updates-note/find",
@ -2209,6 +2238,11 @@ export default function ({ getService }: FtrProviderContext) {
"saved_object:entity-engine-status/find",
"saved_object:entity-engine-status/open_point_in_time",
"saved_object:entity-engine-status/close_point_in_time",
"saved_object:privilege-monitoring-status/bulk_get",
"saved_object:privilege-monitoring-status/get",
"saved_object:privilege-monitoring-status/find",
"saved_object:privilege-monitoring-status/open_point_in_time",
"saved_object:privilege-monitoring-status/close_point_in_time",
"saved_object:policy-settings-protection-updates-note/bulk_get",
"saved_object:policy-settings-protection-updates-note/get",
"saved_object:policy-settings-protection-updates-note/find",