diff --git a/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/privilege_monitoring/monitoring_entity_source/monitoring_entity_source.gen.ts b/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/privilege_monitoring/monitoring_entity_source/monitoring_entity_source.gen.ts index 6025c2d4930fe..080f10e74da97 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/privilege_monitoring/monitoring_entity_source/monitoring_entity_source.gen.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/privilege_monitoring/monitoring_entity_source/monitoring_entity_source.gen.ts @@ -65,11 +65,11 @@ export const UpdatedMonitoringEntitySource = z.object({ .optional(), }); -export type MonitoringEntitySource = z.infer; -export const MonitoringEntitySource = z.object({ - id: z.string(), - name: z.string(), - type: z.string(), +export type MonitoringEntitySourceProperties = z.infer; +export const MonitoringEntitySourceProperties = z.object({ + name: z.string().optional(), + type: z.string().optional(), + managed: z.boolean().optional(), indexPattern: z.string().optional(), integrationName: z.string().optional(), enabled: z.boolean().optional(), @@ -88,6 +88,16 @@ export const MonitoringEntitySource = z.object({ .optional(), }); +export type MonitoringEntitySourceNoId = z.infer; +export const MonitoringEntitySourceNoId = MonitoringEntitySourceProperties.merge(z.object({})); + +export type MonitoringEntitySource = z.infer; +export const MonitoringEntitySource = MonitoringEntitySourceProperties.merge( + z.object({ + id: z.string(), + }) +); + export type CreateEntitySourceRequestBody = z.infer; export const CreateEntitySourceRequestBody = CreateMonitoringEntitySource; export type CreateEntitySourceRequestBodyInput = z.input; @@ -127,7 +137,7 @@ export const UpdateEntitySourceRequestParams = z.object({ export type UpdateEntitySourceRequestParamsInput = z.input; export type UpdateEntitySourceRequestBody = z.infer; -export const UpdateEntitySourceRequestBody = MonitoringEntitySource; +export const UpdateEntitySourceRequestBody = MonitoringEntitySourceNoId; export type UpdateEntitySourceRequestBodyInput = z.input; export type UpdateEntitySourceResponse = z.infer; diff --git a/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/privilege_monitoring/monitoring_entity_source/monitoring_entity_source.schema.yaml b/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/privilege_monitoring/monitoring_entity_source/monitoring_entity_source.schema.yaml index 9f0ba11da1dbe..24f74e05b4369 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/privilege_monitoring/monitoring_entity_source/monitoring_entity_source.schema.yaml +++ b/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/privilege_monitoring/monitoring_entity_source/monitoring_entity_source.schema.yaml @@ -58,7 +58,7 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/MonitoringEntitySource" + $ref: "#/components/schemas/MonitoringEntitySourceNoId" responses: "200": description: Entity source updated successfully @@ -194,16 +194,15 @@ components: - type: string - type: object - MonitoringEntitySource: + MonitoringEntitySourceProperties: type: object - required: [type, name, id, managed] properties: - id: - type: string name: type: string type: type: string + managed: + type: boolean indexPattern: type: string integrationName: @@ -231,5 +230,20 @@ components: properties: kuery: oneOf: - - type: string - - type: object + - type: string + - type: object + + MonitoringEntitySourceNoId: + allOf: + - $ref: '#/components/schemas/MonitoringEntitySourceProperties' + - type: object + required: [type, name, managed] + + MonitoringEntitySource: + allOf: + - $ref: '#/components/schemas/MonitoringEntitySourceProperties' + - type: object + required: [type, name, id, managed] + properties: + id: + type: string diff --git a/x-pack/solutions/security/plugins/security_solution/common/entity_analytics/privileged_user_monitoring/constants.ts b/x-pack/solutions/security/plugins/security_solution/common/entity_analytics/privileged_user_monitoring/constants.ts index 1babcca9eea67..9d4fa22e47f9e 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/entity_analytics/privileged_user_monitoring/constants.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/entity_analytics/privileged_user_monitoring/constants.ts @@ -9,6 +9,8 @@ export const PRIVMON_PUBLIC_URL = `/api/entity_analytics/monitoring` as const; export const PRIVMON_ENGINE_PUBLIC_URL = `${PRIVMON_PUBLIC_URL}/engine` as const; export const PRIVMON_USER_PUBLIC_CSV_UPLOAD_URL = `${PRIVMON_PUBLIC_URL}/users/_csv` as const; export const PRIVMON_PUBLIC_INIT = `${PRIVMON_PUBLIC_URL}/engine/init` as const; +export const getPrivmonMonitoringSourceByIdUrl = (id: string) => + `${PRIVMON_PUBLIC_URL}/entity_source/${id}` as const; export const PRIVMON_USERS_CSV_MAX_SIZE_BYTES = 1024 * 1024; // 1MB export const PRIVMON_USERS_CSV_SIZE_TOLERANCE_BYTES = 1024 * 50; // ~= 50kb diff --git a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/api/api.ts b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/api/api.ts index d0a8c0a3175da..042ff8026acc1 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/api/api.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/api/api.ts @@ -16,6 +16,7 @@ import type { CreatePrivilegesImportIndexResponse } from '../../../common/api/en import type { PrivMonHealthResponse } from '../../../common/api/entity_analytics/privilege_monitoring/health.gen'; import type { InitMonitoringEngineResponse } from '../../../common/api/entity_analytics/privilege_monitoring/engine/init.gen'; import { + getPrivmonMonitoringSourceByIdUrl, PRIVMON_PUBLIC_INIT, PRIVMON_USER_PUBLIC_CSV_UPLOAD_URL, } from '../../../common/entity_analytics/privileged_user_monitoring/constants'; @@ -266,11 +267,10 @@ export const useEntityAnalyticsRoutes = () => { * Update a data source for privilege monitoring engine */ const updatePrivMonMonitoredIndices = async (id: string, indexPattern: string | undefined) => - http.fetch('/api/entity_analytics/monitoring/entity_source', { + http.fetch(getPrivmonMonitoringSourceByIdUrl(id), { version: API_VERSIONS.public.v1, method: 'PUT', body: JSON.stringify({ - id, type: 'index', name: ENTITY_SOURCE_NAME, indexPattern, @@ -279,6 +279,13 @@ export const useEntityAnalyticsRoutes = () => { /** * Create asset criticality + /** + * + * + * @param {(Pick & { + * refresh?: 'wait_for'; + * })} params + * @return {*} {Promise} */ const createAssetCriticality = async ( params: Pick & { diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/monitoring_entity_source_data_client.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/monitoring_entity_source_data_client.test.ts index a45b3ac370c78..bebdea60136e9 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/monitoring_entity_source_data_client.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/monitoring_entity_source_data_client.test.ts @@ -12,17 +12,12 @@ import { loggingSystemMock, } from '@kbn/core/server/mocks'; import { monitoringEntitySourceTypeName } from './saved_objects'; -import type { - SavedObject, - SavedObjectsClientContract, - SavedObjectsFindResponse, -} from '@kbn/core/server'; +import type { SavedObject, SavedObjectsClientContract } from '@kbn/core/server'; describe('MonitoringEntitySourceDataClient', () => { const mockSavedObjectClient = savedObjectsClientMock.create(); const clusterClientMock = elasticsearchServiceMock.createScopedClusterClient(); const loggerMock = loggingSystemMock.createLogger(); - const namespace = 'test-namespace'; loggerMock.debug = jest.fn(); const defaultOpts = { @@ -69,73 +64,20 @@ describe('MonitoringEntitySourceDataClient', () => { } as unknown as SavedObjectsClientContract); defaultOpts.soClient.create.mockResolvedValue({ - id: 'temp-id', // TODO: update to use dynamic ID + id: 'abcdefg', type: monitoringEntitySourceTypeName, attributes: testDescriptor, references: [], }); const result = await dataClient.init(testDescriptor); - const id = `entity-analytics-monitoring-entity-source-${namespace}-${testDescriptor.type}-${testDescriptor.indexPattern}`; expect(defaultOpts.soClient.create).toHaveBeenCalledWith( monitoringEntitySourceTypeName, - testDescriptor, - { - id, - } + testDescriptor ); - expect(result).toEqual({ ...testDescriptor, managed: false, id }); - }); - - it('should update Monitoring Entity Source Sync Config Successfully when calling init when the SO already exists', async () => { - const existingDescriptor = { - total: 1, - saved_objects: [ - { - attributes: testDescriptor, - id: 'entity-analytics-monitoring-entity-source-test-namespace-test-type-test-index-pattern', - }, - ], - } as unknown as SavedObjectsFindResponse; - - const testSourceObject = { - filter: {}, - indexPattern: 'test-index-pattern', - matchers: [ - { - fields: ['user.role'], - values: ['admin'], - }, - ], - name: 'Test Source', - type: 'test-type', - managed: false, - }; - - defaultOpts.soClient.asScopedToNamespace.mockReturnValue({ - find: jest.fn().mockResolvedValue(existingDescriptor), - } as unknown as SavedObjectsClientContract); - - defaultOpts.soClient.update.mockResolvedValue({ - id: 'entity-analytics-monitoring-entity-source-test-namespace-test-type-test-index-pattern', - type: monitoringEntitySourceTypeName, - attributes: { ...testDescriptor, name: 'Updated Source' }, - references: [], - }); - - const updatedDescriptor = { ...testDescriptor, name: 'Updated Source' }; - const result = await dataClient.init(testDescriptor); - - expect(defaultOpts.soClient.update).toHaveBeenCalledWith( - monitoringEntitySourceTypeName, - `entity-analytics-monitoring-entity-source-${namespace}-${testDescriptor.type}-${testDescriptor.indexPattern}`, - testSourceObject, - { refresh: 'wait_for' } - ); - - expect(result).toEqual(updatedDescriptor); + expect(result).toEqual({ ...testDescriptor, managed: false, id: 'abcdefg' }); }); it('should not create Monitoring Entity Source Sync Config when a SO already exist with the same name', async () => { @@ -165,10 +107,10 @@ describe('MonitoringEntitySourceDataClient', () => { references: [], }; defaultOpts.soClient.get.mockResolvedValue(getResponse as unknown as SavedObject); - const result = await dataClient.get(); + const result = await dataClient.get('abcdefg'); expect(defaultOpts.soClient.get).toHaveBeenCalledWith( monitoringEntitySourceTypeName, - `temp-id` // TODO: https://github.com/elastic/security-team/issues/12851 + `abcdefg` ); expect(result).toEqual(getResponse.attributes); }); @@ -176,7 +118,7 @@ describe('MonitoringEntitySourceDataClient', () => { describe('update', () => { it('should update Monitoring Entity Source Sync Config Successfully', async () => { - const id = 'temp-id'; // TODO: https://github.com/elastic/security-team/issues/12851 + const id = 'abcdefg'; const updateDescriptor = { ...testDescriptor, managed: false, @@ -212,10 +154,10 @@ describe('MonitoringEntitySourceDataClient', () => { describe('delete', () => { it('should delete Monitoring Entity Source Sync Config Successfully', async () => { - await dataClient.delete(); + await dataClient.delete('abcdefg'); expect(mockSavedObjectClient.delete).toHaveBeenCalledWith( monitoringEntitySourceTypeName, - `temp-id` // TODO: https://github.com/elastic/security-team/issues/12851 + 'abcdefg' ); }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/monitoring_entity_source_data_client.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/monitoring_entity_source_data_client.ts index e01408d898907..481f22c0b391e 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/monitoring_entity_source_data_client.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/monitoring_entity_source_data_client.ts @@ -37,13 +37,13 @@ export class MonitoringEntitySourceDataClient { return descriptor; } - public async get(): Promise { - this.log('debug', 'Getting Monitoring Entity Source Sync saved object'); - return this.monitoringEntitySourceClient.get(); + public async get(id: string): Promise { + this.log('debug', `Getting Monitoring Entity Source Sync saved object with id: ${id}`); + return this.monitoringEntitySourceClient.get(id); } public async update(update: Partial & { id: string }) { - this.log('debug', 'Updating Monitoring Entity Source Sync saved object'); + this.log('debug', `Updating Monitoring Entity Source Sync saved object with id: ${update.id}`); const sanitizedUpdate = { ...update, @@ -56,9 +56,9 @@ export class MonitoringEntitySourceDataClient { return this.monitoringEntitySourceClient.update(sanitizedUpdate); } - public async delete() { - this.log('debug', 'Deleting Monitoring Entity Source Sync saved object'); - return this.monitoringEntitySourceClient.delete(); + public async delete(id: string) { + this.log('debug', `Deleting Monitoring Entity Source Sync saved object with id: ${id}`); + return this.monitoringEntitySourceClient.delete(id); } public async list(query: ListEntitySourcesRequestQuery): Promise { diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/routes/monitoring_entity_source/monitoring_entity_source.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/routes/monitoring_entity_source/monitoring_entity_source.ts index c6362bb8f178f..59c38f8b54d09 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/routes/monitoring_entity_source/monitoring_entity_source.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/routes/monitoring_entity_source/monitoring_entity_source.ts @@ -20,6 +20,8 @@ import { CreateEntitySourceRequestBody, UpdateEntitySourceRequestBody, type CreateEntitySourceResponse, + GetEntitySourceRequestParams, + UpdateEntitySourceRequestParams, } from '../../../../../../common/api/entity_analytics/privilege_monitoring/monitoring_entity_source/monitoring_entity_source.gen'; export const monitoringEntitySourceRoute = ( @@ -69,7 +71,7 @@ export const monitoringEntitySourceRoute = ( router.versioned .get({ access: 'public', - path: '/api/entity_analytics/monitoring/entity_source', + path: '/api/entity_analytics/monitoring/entity_source/{id}', security: { authz: { requiredPrivileges: ['securitySolution', `${APP_ID}-entity-analytics`], @@ -79,7 +81,11 @@ export const monitoringEntitySourceRoute = ( .addVersion( { version: API_VERSIONS.public.v1, - validate: {}, + validate: { + request: { + params: GetEntitySourceRequestParams, + }, + }, }, async (context, request, response): Promise> => { const siemResponse = buildSiemResponse(response); @@ -87,7 +93,7 @@ export const monitoringEntitySourceRoute = ( try { const secSol = await context.securitySolution; const client = secSol.getMonitoringEntitySourceDataClient(); - const body = await client.get(); + const body = await client.get(request.params.id); return response.ok({ body }); } catch (e) { const error = transformError(e); @@ -103,7 +109,7 @@ export const monitoringEntitySourceRoute = ( router.versioned .put({ access: 'public', - path: '/api/entity_analytics/monitoring/entity_source', + path: '/api/entity_analytics/monitoring/entity_source/{id}', security: { authz: { requiredPrivileges: ['securitySolution', `${APP_ID}-entity-analytics`], @@ -116,6 +122,7 @@ export const monitoringEntitySourceRoute = ( validate: { request: { body: UpdateEntitySourceRequestBody, + params: UpdateEntitySourceRequestParams, }, }, }, @@ -125,7 +132,7 @@ export const monitoringEntitySourceRoute = ( try { const secSol = await context.securitySolution; const client = secSol.getMonitoringEntitySourceDataClient(); - const body = await client.update(request.body); + const body = await client.update({ ...request.body, id: request.params.id }); return response.ok({ body }); } catch (e) { diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/saved_objects/monitoring_entity_source.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/saved_objects/monitoring_entity_source.ts index 6a6b8aa974730..b419d5d7a0947 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/saved_objects/monitoring_entity_source.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/saved_objects/monitoring_entity_source.ts @@ -20,40 +20,16 @@ export interface MonitoringEntitySourceDependencies { export class MonitoringEntitySourceDescriptorClient { constructor(private readonly dependencies: MonitoringEntitySourceDependencies) {} - getDynamicSavedObjectId(attributes: CreateMonitoringEntitySource) { - const { type, indexPattern, integrationName } = this.assertValidIdFields(attributes); - const sourceName = indexPattern || integrationName; - return `entity-analytics-monitoring-entity-source-${this.dependencies.namespace}-${type}${ - sourceName ? `-${sourceName}` : '' - }`; - } - async create(attributes: CreateMonitoringEntitySource) { - const savedObjectId = this.getDynamicSavedObjectId(attributes); - await this.assertNameUniqueness({ ...attributes, id: savedObjectId }); - - try { - // If exists, update it. - const { attributes: updated } = - await this.dependencies.soClient.update( - monitoringEntitySourceTypeName, - savedObjectId, - attributes, - { refresh: 'wait_for' } - ); - return updated; - } catch (e) { - if (e.output?.statusCode !== 404) throw e; - - // Does not exist, create it. - const { attributes: created } = - await this.dependencies.soClient.create( - monitoringEntitySourceTypeName, - { ...attributes, managed: attributes.managed ?? false }, // Ensure managed is set to true on creation - { id: savedObjectId } - ); - return { ...created, id: savedObjectId }; - } + await this.assertNameUniqueness(attributes); + + const { id, attributes: created } = + await this.dependencies.soClient.create( + monitoringEntitySourceTypeName, + { ...attributes, managed: attributes.managed ?? false } // Ensure managed is set to true on creation + ); + + return { ...created, id }; } async update(monitoringEntitySource: Partial & { id: string }) { @@ -86,30 +62,16 @@ export class MonitoringEntitySourceDescriptorClient { .join(' and '); }; - /** - * Need to update to understand the id based on the - * type and indexPattern or integrationName. - * - * Two options: create a getById method that takes the id, - * or use a dynamic ID based on the type and indexPattern/integrationName. - */ - async get() { + async get(id: string): Promise { const { attributes } = await this.dependencies.soClient.get( monitoringEntitySourceTypeName, - 'temp-id' // TODO: https://github.com/elastic/security-team/issues/12851 + id ); return attributes; } - /** - * Need to update to understand the id based on the - * type and indexPattern or integrationName. - * - * * Two options: create a getById method that takes the id, - * or use a dynamic ID based on the type and indexPattern/integrationName. - */ - async delete() { - await this.dependencies.soClient.delete(monitoringEntitySourceTypeName, 'temp-id'); // TODO: https://github.com/elastic/security-team/issues/12851 + async delete(id: string) { + await this.dependencies.soClient.delete(monitoringEntitySourceTypeName, id); } public async findByIndex(): Promise { @@ -126,13 +88,6 @@ export class MonitoringEntitySourceDescriptorClient { .map((so) => ({ ...so.attributes, id: so.id })); } - private assertValidIdFields(source: Partial): MonitoringEntitySource { - if (!source.type || (!source.indexPattern && !source.integrationName)) { - throw new Error('Missing required fields for ID generation'); - } - return source as MonitoringEntitySource; - } - private async assertNameUniqueness(attributes: Partial): Promise { if (attributes.name) { const { saved_objects: savedObjects } = await this.find({