Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ export * from './preview_route.gen';
export * from './entity_calculation_route.gen';
export * from './get_risk_engine_privileges.gen';
export * from './engine_cleanup_route.gen';
export * from './so_configure_route.gen';
Original file line number Diff line number Diff line change
@@ -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.
*/

/*
* NOTICE: Do not edit this file manually.
* This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator.
*
* info:
* title: Risk Engine API
* version: 2023-10-31
*/

import { z } from '@kbn/zod';

export type ConfigureRiskEngineRequest = z.infer<typeof ConfigureRiskEngineRequest>;
export const ConfigureRiskEngineRequest = z.object({
dataViewId: z.string().optional(),
enabled: z.boolean().optional(),
filter: z.object({}).strict().optional(),
identifierType: z.string().optional(),
interval: z.string().optional(),
pageSize: z.number().int().optional(),
alertSampleSizePerShard: z.number().int().optional(),
range: z
.object({
start: z.string().optional(),
end: z.string().optional(),
})
.optional(),
excludeAlertStatuses: z
.array(z.enum(['open', 'closed', 'in-progress', 'acknowledged']))
.optional(),
excludeAlertTags: z
.array(z.enum(['Duplicate', 'False Positive', 'Futher investigation required']))
.optional(),
});

export type ConfigureRiskEngineResponse = z.infer<typeof ConfigureRiskEngineResponse>;
export const ConfigureRiskEngineResponse = z.object({
configuration_successful: z.boolean().optional(),
});

export type ConfigureRiskEngineSavedObjectRequestBody = z.infer<
typeof ConfigureRiskEngineSavedObjectRequestBody
>;
export const ConfigureRiskEngineSavedObjectRequestBody = ConfigureRiskEngineRequest;
export type ConfigureRiskEngineSavedObjectRequestBodyInput = z.input<
typeof ConfigureRiskEngineSavedObjectRequestBody
>;

export type ConfigureRiskEngineSavedObjectResponse = z.infer<
typeof ConfigureRiskEngineSavedObjectResponse
>;
export const ConfigureRiskEngineSavedObjectResponse = ConfigureRiskEngineResponse;
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
openapi: 3.0.0
info:
version: '2023-10-31'
title: Risk Engine API
description: These APIs allow the consumer to configure the Risk Engine saved object.
paths:
/api/risk_engine/saved_object/configure:
post:
x-labels: [ess, serverless]
x-internal: false
x-codegen-enabled: true
operationId: ConfigureRiskEngineSavedObject
summary: Configure the Risk Engine as per user requirements
requestBody:
description: User defined configuration the risk engine
content:
application/json:
schema:
$ref: '#/components/schemas/ConfigureRiskEngineRequest'
required: true
responses:
'200':
description: Successful response
content:
application/json:
schema:
$ref: '#/components/schemas/ConfigureRiskEngineResponse'
'400':
description: Invalid request

components:
schemas:
ConfigureRiskEngineRequest:
type: object
properties:
dataViewId:
type: string
enabled:
type: boolean
filter:
type: object
additionalProperties: false
properties: {}
identifierType:
type: string
interval:
type: string
pageSize:
type: integer
alertSampleSizePerShard:
type: integer
range:
type: object
properties:
start:
type: string
end:
type: string
excludeAlertStatuses:
type: array
items:
type: string
enum:
- open
- closed
- in-progress
- acknowledged
excludeAlertTags:
type: array
items:
type: string
enum:
- 'Duplicate'
- 'False Positive'
- 'Futher investigation required'

ConfigureRiskEngineResponse:
type: object
properties:
configuration_successful:
type: boolean


Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,10 @@ import type {
PreviewRiskScoreRequestBodyInput,
PreviewRiskScoreResponse,
} from './entity_analytics/risk_engine/preview_route.gen';
import type {
ConfigureRiskEngineSavedObjectRequestBodyInput,
ConfigureRiskEngineSavedObjectResponse,
} from './entity_analytics/risk_engine/so_configure_route.gen';
import type {
CleanDraftTimelinesRequestBodyInput,
CleanDraftTimelinesResponse,
Expand Down Expand Up @@ -560,6 +564,19 @@ If asset criticality records already exist for the specified entities, those rec
})
.catch(catchAxiosErrorFormatAndThrow);
}
async configureRiskEngineSavedObject(props: ConfigureRiskEngineSavedObjectProps) {
this.log.info(`${new Date().toISOString()} Calling API ConfigureRiskEngineSavedObject`);
return this.kbnClient
.request<ConfigureRiskEngineSavedObjectResponse>({
path: '/api/risk_engine/saved_object/configure',
headers: {
[ELASTIC_HTTP_VERSION_HEADER]: '2023-10-31',
},
method: 'POST',
body: props.body,
})
.catch(catchAxiosErrorFormatAndThrow);
}
/**
* Copies and returns a timeline or timeline template.

Expand Down Expand Up @@ -2014,6 +2031,9 @@ export interface BulkUpsertAssetCriticalityRecordsProps {
export interface CleanDraftTimelinesProps {
body: CleanDraftTimelinesRequestBodyInput;
}
export interface ConfigureRiskEngineSavedObjectProps {
body: ConfigureRiskEngineSavedObjectRequestBodyInput;
}
export interface CopyTimelineProps {
body: CopyTimelineRequestBodyInput;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ export const RISK_ENGINE_SETTINGS_URL = `${RISK_ENGINE_URL}/settings` as const;
export const PUBLIC_RISK_ENGINE_URL = `${PUBLIC_RISK_SCORE_URL}/engine` as const;
export const RISK_ENGINE_SCHEDULE_NOW_URL = `${RISK_ENGINE_URL}/schedule_now` as const;
export const RISK_ENGINE_CLEANUP_URL = `${PUBLIC_RISK_ENGINE_URL}/dangerously_delete_data` as const;
export const RISK_ENGINE_SO_CONFIGURATION_URL =
`${PUBLIC_RISK_ENGINE_URL}/saved_object/config` as const;

type ClusterPrivilege = 'manage_index_templates' | 'manage_transform';
export const RISK_ENGINE_REQUIRED_ES_CLUSTER_PRIVILEGES = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ export enum RiskEngineAuditActions {
RISK_ENGINE_DISABLE_LEGACY_ENGINE = 'risk_engine_disable_legacy_engine',
RISK_ENGINE_REMOVE_TASK = 'risk_engine_remove_task',
RISK_ENGINE_SCHEDULE_NOW = 'risk_engine_schedule_now',
RISK_ENGINE_CONFIGURATION_UPDATE = 'risk_engine_configuration_update',
}
Original file line number Diff line number Diff line change
Expand Up @@ -402,5 +402,56 @@ describe('RiskEngineDataClient', () => {
expect(errors).toEqual([error]);
});
});

describe('updateSavedObjectConfiguration', () => {
it('should update the risk engine saved object configuration in the respective namespace', async () => {
const esClient = elasticsearchServiceMock.createScopedClusterClient().asCurrentUser;
const namespaces = {
default: 'default',
custom: 'space_2',
};
const options = {
logger,
kibanaVersion: '8.9.0',
esClient,
soClient: mockSavedObjectClient,
namespace: namespaces.default,
auditLogger: undefined,
};
riskEngineDataClient = new RiskEngineDataClient(options);
const attributes = {
enabled: true,
excludeAlertStatuses: ['closed'],
excludeAlertTags: ['Duplicate'],
};
mockSavedObjectClient.find.mockResolvedValueOnce(getSavedObjectConfiguration());

mockSavedObjectClient.update.mockResolvedValueOnce({
attributes,
} as unknown as SavedObject<RiskEngineConfiguration>);

const result = await riskEngineDataClient.updateSavedObjectConfiguration({ attributes });
expect(result.attributes).toEqual(attributes);

// Check for the saved object configuration in the non-default space

options.namespace = namespaces.custom;
const riskEngineDataClient2 = new RiskEngineDataClient(options);
const attributes2 = {
enabled: true,
excludeAlertStatuses: ['open', 'closed'],
excludeAlertTags: ['False Positive', 'Duplicate'],
};
mockSavedObjectClient.find.mockResolvedValueOnce(getSavedObjectConfiguration());
mockSavedObjectClient.update.mockResolvedValueOnce({
attributes,
} as unknown as SavedObject<RiskEngineConfiguration>);

const result2 = await riskEngineDataClient2.updateSavedObjectConfiguration({
attributes: attributes2,
});
expect(result2.attributes).toEqual(attributes2);
Comment on lines +421 to +453
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you split it into 2 different tests?

});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -319,4 +319,21 @@ export class RiskEngineDataClient {

return RiskEngineStatusEnum.ENABLED;
}

public async updateSavedObjectConfiguration({ attributes }: { attributes: {} }) {
this.options.auditLogger?.log({
message: 'User updates the Risk Engine savedObject',
event: {
action: RiskEngineAuditActions.RISK_ENGINE_CONFIGURATION_UPDATE,
category: AUDIT_CATEGORY.DATABASE,
type: AUDIT_TYPE.CHANGE,
outcome: AUDIT_OUTCOME.SUCCESS,
},
});

return updateSavedObjectAttribute({
savedObjectsClient: this.options.soClient,
attributes,
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { riskEngineSettingsRoute } from './settings';
import type { EntityAnalyticsRoutesDeps } from '../../types';
import { riskEngineScheduleNowRoute } from './schedule_now';
import { riskEngineCleanupRoute } from './delete';
import { riskEngineSOConfigurationRoute } from '../saved_object/routes/configure';

export const registerRiskEngineRoutes = ({
router,
Expand All @@ -26,4 +27,5 @@ export const registerRiskEngineRoutes = ({
riskEngineSettingsRoute(router);
riskEnginePrivilegesRoute(router, getStartServices);
riskEngineCleanupRoute(router, getStartServices);
riskEngineSOConfigurationRoute(router);
};
Original file line number Diff line number Diff line change
@@ -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.
*/

/**
* Public Risk Engine Saved Object Configuration routes
*/

export const APP_ID = 'securitySolution' as const;
export const PUBLIC_RISK_ENGINE_SO_URL = '/api/risk_score/engine/saved_object' as const;
export const RISK_ENGINE_SAVED_OBJECT_CONFIG_URL = `${PUBLIC_RISK_ENGINE_SO_URL}/config` as const;
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@
*/

export * from './risk_engine_configuration_type';
export * from './routes/configure';
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* 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 { buildSiemResponse } from '@kbn/lists-plugin/server/routes/utils';
import { transformError } from '@kbn/securitysolution-es-utils';
import type { IKibanaResponse } from '@kbn/core-http-server';
import { buildRouteValidationWithZod } from '@kbn/zod-helpers';
import type { ConfigureRiskEngineResponse } from '../../../../../../common/api/entity_analytics/risk_engine';
import { ConfigureRiskEngineSavedObjectRequestBody } from '../../../../../../common/api/entity_analytics/risk_engine';
import { RISK_ENGINE_SAVED_OBJECT_CONFIG_URL, APP_ID } from '../constants';
import type { EntityAnalyticsRoutesDeps } from '../../../types';
import { API_VERSIONS } from '../../../../../../common/constants';

export const riskEngineSOConfigurationRoute = (router: EntityAnalyticsRoutesDeps['router']) => {
router.versioned
.post({
access: 'public',
path: RISK_ENGINE_SAVED_OBJECT_CONFIG_URL,
options: {
tags: ['access:securitySolution', `access:${APP_ID}-entity-analytics`],
},
})
.addVersion(
{
version: API_VERSIONS.public.v1,
validate: {
request: {
body: buildRouteValidationWithZod(ConfigureRiskEngineSavedObjectRequestBody),
},
},
},
async (context, request, response): Promise<IKibanaResponse<ConfigureRiskEngineResponse>> => {
const siemResponse = buildSiemResponse(response);

const attributes = request.body;

const securitySolution = await context.securitySolution;
const riskEngineClient = securitySolution.getRiskEngineDataClient();

try {
const result = await riskEngineClient.updateSavedObjectConfiguration({ attributes });
if (!result) {
throw new Error('Unable to update risk engine configuration');
}
return response.ok({
body: {
configuration_successful: true,
},
});
} catch (e) {
const error = transformError(e);

return siemResponse.error({
statusCode: error.statusCode,
body: { message: error.message, full_error: JSON.stringify(e) },
bypassErrorFormat: true,
});
}
}
);
};
Loading