From 991b5f6f8c4b2a0358915f3602a340d331b7ab7e Mon Sep 17 00:00:00 2001 From: Mark Hopkin Date: Fri, 1 Dec 2023 10:50:48 +0000 Subject: [PATCH] [Entity Analytics] Implement Asset Criticality Create, Read & Delete APIs (#172073) ## Summary Adds upsert, read and delete APIs for asset criticality records. I have used the OpenAPI code generation to create the types and zod schemas. The APIs added are as follows: **POST /internal/risk_score/criticality** Request Body: ``` { id_value: "host-1", id_field: "host.name", criticality_level: "very_important" } ``` If the record already exists it will be overwritten, otherwise created **GET /internal/risk_score/criticality?id_field=host.name&id_value=host-1** Response body: ``` { id_value: "host-1", id_field: "host.name", criticality_level: "very_important" @timestamp: "2023-11-29T11:43:43.175Z" } ``` **DELETE /internal/risk_score/criticality?id_field=host.name&id_value=host-1** --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .github/CODEOWNERS | 1 + .../api/asset_criticality/common.gen.ts | 50 +++++++ .../api/asset_criticality/common.schema.yaml | 66 +++++++++ .../create_asset_criticality.schema.yaml | 23 ++++ .../delete_asset_criticality.schema.yaml | 16 +++ .../get_asset_criticality.schema.yaml | 22 +++ .../get_asset_criticality_status.gen.ts | 18 +++ ... get_asset_criticality_status.schema.yaml} | 3 +- .../common/api/asset_criticality/index.ts | 9 ++ .../asset_criticality_data_client.ts | 65 ++++++++- .../asset_criticality/routes/delete.ts | 60 ++++++++ .../asset_criticality/routes/get.ts | 61 +++++++++ .../asset_criticality/routes/index.ts | 3 + .../asset_criticality/routes/status.ts | 8 +- .../asset_criticality/routes/upsert.ts | 66 +++++++++ .../security_solution/server/routes/index.ts | 11 +- .../risk_engine/asset_criticality.ts | 128 +++++++++++++++++- .../utils/asset_criticality.ts | 64 ++++++++- 18 files changed, 659 insertions(+), 15 deletions(-) create mode 100644 x-pack/plugins/security_solution/common/api/asset_criticality/common.gen.ts create mode 100644 x-pack/plugins/security_solution/common/api/asset_criticality/common.schema.yaml create mode 100644 x-pack/plugins/security_solution/common/api/asset_criticality/create_asset_criticality.schema.yaml create mode 100644 x-pack/plugins/security_solution/common/api/asset_criticality/delete_asset_criticality.schema.yaml create mode 100644 x-pack/plugins/security_solution/common/api/asset_criticality/get_asset_criticality.schema.yaml create mode 100644 x-pack/plugins/security_solution/common/api/asset_criticality/get_asset_criticality_status.gen.ts rename x-pack/plugins/security_solution/common/api/asset_criticality/{status.yaml => get_asset_criticality_status.schema.yaml} (92%) create mode 100644 x-pack/plugins/security_solution/common/api/asset_criticality/index.ts create mode 100644 x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/routes/delete.ts create mode 100644 x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/routes/get.ts create mode 100644 x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/routes/upsert.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index ad75ebb7710a7..a027612a8cb83 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1474,6 +1474,7 @@ x-pack/plugins/security_solution/server/lib/entity_analytics @elastic/security-e x-pack/plugins/security_solution/server/lib/risk_score @elastic/security-entity-analytics x-pack/test/security_solution_api_integration/test_suites/entity_analytics @elastic/security-entity-analytics x-pack/plugins/security_solution/public/flyout/entity_details @elastic/security-entity-analytics +x-pack/plugins/security_solution/common/api/asset_criticality @elastic/security-entity-analytics /x-pack/plugins/security_solution/public/entity_analytics @elastic/security-entity-analytics /x-pack/test/security_solution_cypress/cypress/e2e/entity_analytics @elastic/security-entity-analytics diff --git a/x-pack/plugins/security_solution/common/api/asset_criticality/common.gen.ts b/x-pack/plugins/security_solution/common/api/asset_criticality/common.gen.ts new file mode 100644 index 0000000000000..378aaa3098584 --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/asset_criticality/common.gen.ts @@ -0,0 +1,50 @@ +/* + * 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 { z } from 'zod'; + +/* + * NOTICE: Do not edit this file manually. + * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator. + */ + +export type IdField = z.infer; +export const IdField = z.enum(['host.name', 'user.name']); +export type IdFieldEnum = typeof IdField.enum; +export const IdFieldEnum = IdField.enum; + +export type AssetCriticalityRecordIdParts = z.infer; +export const AssetCriticalityRecordIdParts = z.object({ + /** + * The ID value of the asset. + */ + id_value: z.string(), + /** + * The field representing the ID. + */ + id_field: IdField, +}); + +export type CreateAssetCriticalityRecord = z.infer; +export const CreateAssetCriticalityRecord = AssetCriticalityRecordIdParts.merge( + z.object({ + /** + * The criticality level of the asset. + */ + criticality_level: z.enum(['very_important', 'important', 'normal', 'not_important']), + }) +); + +export type AssetCriticalityRecord = z.infer; +export const AssetCriticalityRecord = CreateAssetCriticalityRecord.merge( + z.object({ + /** + * The time the record was created or updated. + */ + '@timestamp': z.string().datetime(), + }) +); diff --git a/x-pack/plugins/security_solution/common/api/asset_criticality/common.schema.yaml b/x-pack/plugins/security_solution/common/api/asset_criticality/common.schema.yaml new file mode 100644 index 0000000000000..3271f990931ef --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/asset_criticality/common.schema.yaml @@ -0,0 +1,66 @@ +openapi: 3.0.0 +info: + title: Asset Criticality Common Schema + description: Common schema for asset criticality + version: 1.0.0 +paths: { } +components: + parameters: + id_value: + name: id_value + in: query + required: true + schema: + type: string + description: The ID value of the asset. + id_field: + name: id_field + in: query + required: true + schema: + $ref: '#/components/schemas/IdField' + example: 'host.name' + description: The field representing the ID. + + schemas: + IdField: + type: string + enum: + - 'host.name' + - 'user.name' + AssetCriticalityRecordIdParts: + type: object + properties: + id_value: + type: string + description: The ID value of the asset. + id_field: + $ref: '#/components/schemas/IdField' + example: 'host.name' + description: The field representing the ID. + required: + - id_value + - id_field + CreateAssetCriticalityRecord: + allOf: + - $ref: '#/components/schemas/AssetCriticalityRecordIdParts' + - type: object + properties: + criticality_level: + type: string + enum: [very_important, important, normal, not_important] + description: The criticality level of the asset. + required: + - criticality_level + AssetCriticalityRecord: + allOf: + - $ref: '#/components/schemas/CreateAssetCriticalityRecord' + - type: object + properties: + "@timestamp": + type: string + format: 'date-time' + example: '2017-07-21T17:32:28Z' + description: The time the record was created or updated. + required: + - "@timestamp" diff --git a/x-pack/plugins/security_solution/common/api/asset_criticality/create_asset_criticality.schema.yaml b/x-pack/plugins/security_solution/common/api/asset_criticality/create_asset_criticality.schema.yaml new file mode 100644 index 0000000000000..198fe9a35339b --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/asset_criticality/create_asset_criticality.schema.yaml @@ -0,0 +1,23 @@ +openapi: 3.0.0 +info: + version: 1.0.0 + title: Asset Criticality Create Record Schema +paths: + /internal/asset_criticality: + post: + summary: Create Criticality Record + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateAssetCriticalityRecord' + responses: + '200': + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/SingleAssetCriticality' + '400': + description: Invalid request \ No newline at end of file diff --git a/x-pack/plugins/security_solution/common/api/asset_criticality/delete_asset_criticality.schema.yaml b/x-pack/plugins/security_solution/common/api/asset_criticality/delete_asset_criticality.schema.yaml new file mode 100644 index 0000000000000..e8334c48205f4 --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/asset_criticality/delete_asset_criticality.schema.yaml @@ -0,0 +1,16 @@ +openapi: 3.0.0 +info: + version: 1.0.0 + title: Asset Criticality Delete Record Schema +paths: + /internal/asset_criticality: + delete: + summary: Delete Criticality Record + parameters: + - $ref: '#/components/parameters/id_value' + - $ref: '#/components/parameters/id_field' + responses: + '200': + description: Successful response + '400': + description: Invalid request \ No newline at end of file diff --git a/x-pack/plugins/security_solution/common/api/asset_criticality/get_asset_criticality.schema.yaml b/x-pack/plugins/security_solution/common/api/asset_criticality/get_asset_criticality.schema.yaml new file mode 100644 index 0000000000000..2aa88dcbc862c --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/asset_criticality/get_asset_criticality.schema.yaml @@ -0,0 +1,22 @@ +openapi: 3.0.0 +info: + version: 1.0.0 + title: Asset Criticality Get Record Schema +paths: + /internal/asset_criticality: + get: + summary: Get Criticality Record + parameters: + - $ref: '#/components/parameters/id_value' + - $ref: '#/components/parameters/id_field' + responses: + '200': + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/SingleAssetCriticality' + '400': + description: Invalid request + '404': + description: Criticality record not found \ No newline at end of file diff --git a/x-pack/plugins/security_solution/common/api/asset_criticality/get_asset_criticality_status.gen.ts b/x-pack/plugins/security_solution/common/api/asset_criticality/get_asset_criticality_status.gen.ts new file mode 100644 index 0000000000000..6e034ae654f6e --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/asset_criticality/get_asset_criticality_status.gen.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { z } from 'zod'; + +/* + * NOTICE: Do not edit this file manually. + * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator. + */ + +export type AssetCriticalityStatusResponse = z.infer; +export const AssetCriticalityStatusResponse = z.object({ + asset_criticality_resources_installed: z.boolean().optional(), +}); diff --git a/x-pack/plugins/security_solution/common/api/asset_criticality/status.yaml b/x-pack/plugins/security_solution/common/api/asset_criticality/get_asset_criticality_status.schema.yaml similarity index 92% rename from x-pack/plugins/security_solution/common/api/asset_criticality/status.yaml rename to x-pack/plugins/security_solution/common/api/asset_criticality/get_asset_criticality_status.schema.yaml index 7bdc5fd562c36..976833fae0706 100644 --- a/x-pack/plugins/security_solution/common/api/asset_criticality/status.yaml +++ b/x-pack/plugins/security_solution/common/api/asset_criticality/get_asset_criticality_status.schema.yaml @@ -15,7 +15,6 @@ paths: $ref: '#/components/schemas/AssetCriticalityStatusResponse' '400': description: Invalid request - responses: components: schemas: @@ -23,4 +22,4 @@ components: type: object properties: asset_criticality_resources_installed: - type: boolean \ No newline at end of file + type: boolean diff --git a/x-pack/plugins/security_solution/common/api/asset_criticality/index.ts b/x-pack/plugins/security_solution/common/api/asset_criticality/index.ts new file mode 100644 index 0000000000000..d908c931ad1dd --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/asset_criticality/index.ts @@ -0,0 +1,9 @@ +/* + * 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 * from './common.gen'; +export * from './get_asset_criticality_status.gen'; diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/asset_criticality_data_client.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/asset_criticality_data_client.ts index 4ac8f43627432..7e3d5dcd7b50c 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/asset_criticality_data_client.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/asset_criticality_data_client.ts @@ -6,6 +6,7 @@ */ import type { Logger, ElasticsearchClient } from '@kbn/core/server'; import { mappingFromFieldMap } from '@kbn/alerting-plugin/common'; +import type { AssetCriticalityRecord } from '../../../../common/api/asset_criticality'; import { createOrUpdateIndex } from '../utils/create_or_update_index'; import { getAssetCriticalityIndex } from '../../../../common/asset_criticality'; import { assetCriticalityFieldMap } from './configurations'; @@ -16,6 +17,15 @@ interface AssetCriticalityClientOpts { namespace: string; } +interface AssetCriticalityUpsert { + idField: AssetCriticalityRecord['id_field']; + idValue: AssetCriticalityRecord['id_value']; + criticalityLevel: AssetCriticalityRecord['criticality_level']; +} + +type AssetCriticalityIdParts = Pick; + +const createId = ({ idField, idValue }: AssetCriticalityIdParts) => `${idField}:${idValue}`; export class AssetCriticalityDataClient { constructor(private readonly options: AssetCriticalityClientOpts) {} /** @@ -27,16 +37,20 @@ export class AssetCriticalityDataClient { esClient: this.options.esClient, logger: this.options.logger, options: { - index: getAssetCriticalityIndex(this.options.namespace), + index: this.getIndex(), mappings: mappingFromFieldMap(assetCriticalityFieldMap, 'strict'), }, }); } + private getIndex() { + return getAssetCriticalityIndex(this.options.namespace); + } + public async doesIndexExist() { try { const result = await this.options.esClient.indices.exists({ - index: getAssetCriticalityIndex(this.options.namespace), + index: this.getIndex(), }); return result; } catch (e) { @@ -51,4 +65,51 @@ export class AssetCriticalityDataClient { isAssetCriticalityResourcesInstalled, }; } + + public async get(idParts: AssetCriticalityIdParts): Promise { + const id = createId(idParts); + + try { + const body = await this.options.esClient.get({ + id, + index: this.getIndex(), + }); + + return body._source; + } catch (err) { + if (err.statusCode === 404) { + return undefined; + } else { + throw err; + } + } + } + + public async upsert(record: AssetCriticalityUpsert): Promise { + const id = createId(record); + const doc = { + id_field: record.idField, + id_value: record.idValue, + criticality_level: record.criticalityLevel, + '@timestamp': new Date().toISOString(), + }; + + await this.options.esClient.update({ + id, + index: this.getIndex(), + body: { + doc, + doc_as_upsert: true, + }, + }); + + return doc; + } + + public async delete(idParts: AssetCriticalityIdParts) { + await this.options.esClient.delete({ + id: createId(idParts), + index: this.getIndex(), + }); + } } diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/routes/delete.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/routes/delete.ts new file mode 100644 index 0000000000000..45daef8a1ac1b --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/routes/delete.ts @@ -0,0 +1,60 @@ +/* + * 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 } from '@kbn/core/server'; +import { buildSiemResponse } from '@kbn/lists-plugin/server/routes/utils'; +import { transformError } from '@kbn/securitysolution-es-utils'; +import { ASSET_CRITICALITY_URL, APP_ID } from '../../../../../common/constants'; +import type { SecuritySolutionPluginRouter } from '../../../../types'; +import { AssetCriticalityRecordIdParts } from '../../../../../common/api/asset_criticality'; +import { buildRouteValidationWithZod } from '../../../../utils/build_validation/route_validation'; +import { checkAndInitAssetCriticalityResources } from '../check_and_init_asset_criticality_resources'; +export const assetCriticalityDeleteRoute = ( + router: SecuritySolutionPluginRouter, + logger: Logger +) => { + router.versioned + .delete({ + access: 'internal', + path: ASSET_CRITICALITY_URL, + options: { + tags: ['access:securitySolution', `access:${APP_ID}-entity-analytics`], + }, + }) + .addVersion( + { + version: '1', + validate: { + request: { + query: buildRouteValidationWithZod(AssetCriticalityRecordIdParts), + }, + }, + }, + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + await checkAndInitAssetCriticalityResources(context, logger); + + const securitySolution = await context.securitySolution; + const assetCriticalityClient = securitySolution.getAssetCriticalityDataClient(); + await assetCriticalityClient.delete({ + idField: request.query.id_field, + idValue: request.query.id_value, + }); + + return response.ok(); + } catch (e) { + const error = transformError(e); + + return siemResponse.error({ + statusCode: error.statusCode, + body: { message: error.message, full_error: JSON.stringify(e) }, + bypassErrorFormat: true, + }); + } + } + ); +}; diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/routes/get.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/routes/get.ts new file mode 100644 index 0000000000000..e3551aac1eef6 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/routes/get.ts @@ -0,0 +1,61 @@ +/* + * 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 } from '@kbn/core/server'; +import { buildSiemResponse } from '@kbn/lists-plugin/server/routes/utils'; +import { transformError } from '@kbn/securitysolution-es-utils'; +import { ASSET_CRITICALITY_URL, APP_ID } from '../../../../../common/constants'; +import type { SecuritySolutionPluginRouter } from '../../../../types'; +import { checkAndInitAssetCriticalityResources } from '../check_and_init_asset_criticality_resources'; +import { buildRouteValidationWithZod } from '../../../../utils/build_validation/route_validation'; +import { AssetCriticalityRecordIdParts } from '../../../../../common/api/asset_criticality'; +export const assetCriticalityGetRoute = (router: SecuritySolutionPluginRouter, logger: Logger) => { + router.versioned + .get({ + access: 'internal', + path: ASSET_CRITICALITY_URL, + options: { + tags: ['access:securitySolution', `access:${APP_ID}-entity-analytics`], + }, + }) + .addVersion( + { + version: '1', + validate: { + request: { + query: buildRouteValidationWithZod(AssetCriticalityRecordIdParts), + }, + }, + }, + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + await checkAndInitAssetCriticalityResources(context, logger); + + const securitySolution = await context.securitySolution; + const assetCriticalityClient = securitySolution.getAssetCriticalityDataClient(); + const record = await assetCriticalityClient.get({ + idField: request.query.id_field, + idValue: request.query.id_value, + }); + + if (!record) { + return response.notFound(); + } + + return response.ok({ body: record }); + } catch (e) { + const error = transformError(e); + + return siemResponse.error({ + statusCode: error.statusCode, + body: { message: error.message, full_error: JSON.stringify(e) }, + bypassErrorFormat: true, + }); + } + } + ); +}; diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/routes/index.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/routes/index.ts index c72249c3110e3..8a5f62ccff079 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/routes/index.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/routes/index.ts @@ -6,3 +6,6 @@ */ export { assetCriticalityStatusRoute } from './status'; +export { assetCriticalityUpsertRoute } from './upsert'; +export { assetCriticalityGetRoute } from './get'; +export { assetCriticalityDeleteRoute } from './delete'; diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/routes/status.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/routes/status.ts index 7605d2cb099cc..f53500aeb3a29 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/routes/status.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/routes/status.ts @@ -7,6 +7,7 @@ import type { Logger } from '@kbn/core/server'; import { buildSiemResponse } from '@kbn/lists-plugin/server/routes/utils'; import { transformError } from '@kbn/securitysolution-es-utils'; +import type { AssetCriticalityStatusResponse } from '../../../../../common/api/asset_criticality'; import { ASSET_CRITICALITY_STATUS_URL, APP_ID } from '../../../../../common/constants'; import type { SecuritySolutionPluginRouter } from '../../../../types'; import { checkAndInitAssetCriticalityResources } from '../check_and_init_asset_criticality_resources'; @@ -32,10 +33,11 @@ export const assetCriticalityStatusRoute = ( const assetCriticalityClient = securitySolution.getAssetCriticalityDataClient(); const result = await assetCriticalityClient.getStatus(); + const body: AssetCriticalityStatusResponse = { + asset_criticality_resources_installed: result.isAssetCriticalityResourcesInstalled, + }; return response.ok({ - body: { - asset_criticality_resources_installed: result.isAssetCriticalityResourcesInstalled, - }, + body, }); } catch (e) { const error = transformError(e); diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/routes/upsert.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/routes/upsert.ts new file mode 100644 index 0000000000000..6ad1e469ff0d4 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/routes/upsert.ts @@ -0,0 +1,66 @@ +/* + * 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 } from '@kbn/core/server'; +import { buildSiemResponse } from '@kbn/lists-plugin/server/routes/utils'; +import { transformError } from '@kbn/securitysolution-es-utils'; +import { ASSET_CRITICALITY_URL, APP_ID } from '../../../../../common/constants'; +import type { SecuritySolutionPluginRouter } from '../../../../types'; +import { checkAndInitAssetCriticalityResources } from '../check_and_init_asset_criticality_resources'; +import { buildRouteValidationWithZod } from '../../../../utils/build_validation/route_validation'; +import { CreateAssetCriticalityRecord } from '../../../../../common/api/asset_criticality'; +export const assetCriticalityUpsertRoute = ( + router: SecuritySolutionPluginRouter, + logger: Logger +) => { + router.versioned + .post({ + access: 'internal', + path: ASSET_CRITICALITY_URL, + options: { + tags: ['access:securitySolution', `access:${APP_ID}-entity-analytics`], + }, + }) + .addVersion( + { + version: '1', + validate: { + request: { + body: buildRouteValidationWithZod(CreateAssetCriticalityRecord), + }, + }, + }, + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + await checkAndInitAssetCriticalityResources(context, logger); + + const securitySolution = await context.securitySolution; + const assetCriticalityClient = securitySolution.getAssetCriticalityDataClient(); + + const assetCriticalityRecord = { + idField: request.body.id_field, + idValue: request.body.id_value, + criticalityLevel: request.body.criticality_level, + }; + + const result = await assetCriticalityClient.upsert(assetCriticalityRecord); + + return response.ok({ + body: result, + }); + } catch (e) { + const error = transformError(e); + + return siemResponse.error({ + statusCode: error.statusCode, + body: { message: error.message, full_error: JSON.stringify(e) }, + bypassErrorFormat: true, + }); + } + } + ); +}; diff --git a/x-pack/plugins/security_solution/server/routes/index.ts b/x-pack/plugins/security_solution/server/routes/index.ts index de1b9d8125f7f..a6db255d0d4f6 100644 --- a/x-pack/plugins/security_solution/server/routes/index.ts +++ b/x-pack/plugins/security_solution/server/routes/index.ts @@ -66,8 +66,12 @@ import { import { registerTimelineRoutes } from '../lib/timeline/routes'; import { riskScoreCalculationRoute } from '../lib/entity_analytics/risk_score/routes/calculation'; import { riskScorePreviewRoute } from '../lib/entity_analytics/risk_score/routes/preview'; -import { assetCriticalityStatusRoute } from '../lib/entity_analytics/asset_criticality/routes'; - +import { + assetCriticalityStatusRoute, + assetCriticalityUpsertRoute, + assetCriticalityGetRoute, + assetCriticalityDeleteRoute, +} from '../lib/entity_analytics/asset_criticality/routes'; export const initRoutes = ( router: SecuritySolutionPluginRouter, config: ConfigType, @@ -161,5 +165,8 @@ export const initRoutes = ( } if (config.experimentalFeatures.entityAnalyticsAssetCriticalityEnabled) { assetCriticalityStatusRoute(router, logger); + assetCriticalityUpsertRoute(router, logger); + assetCriticalityGetRoute(router, logger); + assetCriticalityDeleteRoute(router, logger); } }; diff --git a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/default_license/risk_engine/asset_criticality.ts b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/default_license/risk_engine/asset_criticality.ts index 8953d9986c035..de5f64aaaa3a1 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/default_license/risk_engine/asset_criticality.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/default_license/risk_engine/asset_criticality.ts @@ -10,6 +10,8 @@ import { cleanRiskEngine, cleanAssetCriticality, assetCriticalityRouteHelpersFactory, + getAssetCriticalityDoc, + getAssetCriticalityIndex, } from '../../utils'; import { FtrProviderContext } from '../../../../ftr_provider_context'; @@ -33,13 +35,11 @@ export default ({ getService }: FtrProviderContext) => { describe('initialisation of resources', () => { it('should has index installed on status api call', async () => { - const assetCriticalityIndex = '.asset-criticality.asset-criticality-default'; - let assetCriticalityIndexExist; try { assetCriticalityIndexExist = await es.indices.exists({ - index: assetCriticalityIndex, + index: getAssetCriticalityIndex(), }); } catch (e) { assetCriticalityIndexExist = false; @@ -54,7 +54,7 @@ export default ({ getService }: FtrProviderContext) => { }); const assetCriticalityIndexResult = await es.indices.get({ - index: assetCriticalityIndex, + index: getAssetCriticalityIndex(), }); expect( @@ -81,5 +81,125 @@ export default ({ getService }: FtrProviderContext) => { }); }); }); + + describe('create', () => { + it('should correctly create asset criticality', async () => { + const assetCriticality = { + id_field: 'host.name', + id_value: 'host-01', + criticality_level: 'important', + }; + + const { body: result } = await assetCriticalityRoutes.upsert(assetCriticality); + + expect(result.id_field).to.eql('host.name'); + expect(result.id_value).to.eql('host-01'); + expect(result.criticality_level).to.eql('important'); + expect(result['@timestamp']).to.be.a('string'); + + const doc = await getAssetCriticalityDoc({ idField: 'host.name', idValue: 'host-01', es }); + + expect(doc).to.eql(result); + }); + + it('should return 400 if criticality is invalid', async () => { + const assetCriticality = { + id_field: 'host.name', + id_value: 'host-01', + criticality_level: 'invalid', + }; + + await assetCriticalityRoutes.upsert(assetCriticality, { + expectStatusCode: 400, + }); + }); + + it('should return 400 if id_field is invalid', async () => { + const assetCriticality = { + id_field: 'invalid', + id_value: 'host-01', + criticality_level: 'important', + }; + + await assetCriticalityRoutes.upsert(assetCriticality, { + expectStatusCode: 400, + }); + }); + }); + + describe('read', () => { + it('should correctly get asset criticality', async () => { + const assetCriticality = { + id_field: 'host.name', + id_value: 'host-02', + criticality_level: 'important', + }; + + await assetCriticalityRoutes.upsert(assetCriticality); + + const { body: result } = await assetCriticalityRoutes.get('host.name', 'host-02'); + + expect(result.id_field).to.eql('host.name'); + expect(result.id_value).to.eql('host-02'); + expect(result.criticality_level).to.eql('important'); + expect(result['@timestamp']).to.be.a('string'); + }); + + it('should return a 400 if id_field is invalid', async () => { + await assetCriticalityRoutes.get('invalid', 'host-02', { + expectStatusCode: 400, + }); + }); + }); + + describe('update', () => { + it('should correctly update asset criticality', async () => { + const assetCriticality = { + id_field: 'host.name', + id_value: 'host-01', + criticality_level: 'important', + }; + + const { body: createdDoc } = await assetCriticalityRoutes.upsert(assetCriticality); + const updatedAssetCriticality = { + id_field: 'host.name', + id_value: 'host-01', + criticality_level: 'very_important', + }; + + const { body: updatedDoc } = await assetCriticalityRoutes.upsert(updatedAssetCriticality); + + expect(updatedDoc.id_field).to.eql('host.name'); + expect(updatedDoc.id_value).to.eql('host-01'); + expect(updatedDoc.criticality_level).to.eql('very_important'); + expect(updatedDoc['@timestamp']).to.be.a('string'); + expect(updatedDoc['@timestamp']).to.not.eql(createdDoc['@timestamp']); + + const doc = await getAssetCriticalityDoc({ idField: 'host.name', idValue: 'host-01', es }); + + expect(doc).to.eql(updatedDoc); + }); + }); + + describe('delete', () => { + it('should correctly delete asset criticality', async () => { + const assetCriticality = { + id_field: 'host.name', + id_value: 'delete-me', + criticality_level: 'important', + }; + + await assetCriticalityRoutes.upsert(assetCriticality); + + await assetCriticalityRoutes.delete('host.name', 'delete-me'); + const doc = await getAssetCriticalityDoc({ + idField: 'host.name', + idValue: 'delete-me', + es, + }); + + expect(doc).to.eql(undefined); + }); + }); }); }; diff --git a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/utils/asset_criticality.ts b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/utils/asset_criticality.ts index 2bc7de4a895f5..9318bc7d77991 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/utils/asset_criticality.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/utils/asset_criticality.ts @@ -10,11 +10,18 @@ import { ELASTIC_HTTP_VERSION_HEADER, X_ELASTIC_INTERNAL_ORIGIN_REQUEST, } from '@kbn/core-http-common'; -import { ASSET_CRITICALITY_STATUS_URL } from '@kbn/security-solution-plugin/common/constants'; +import { + ASSET_CRITICALITY_STATUS_URL, + ASSET_CRITICALITY_URL, +} from '@kbn/security-solution-plugin/common/constants'; import type { Client } from '@elastic/elasticsearch'; import type { ToolingLog } from '@kbn/tooling-log'; +import querystring from 'querystring'; import { routeWithNamespace } from '../../detections_response/utils'; +export const getAssetCriticalityIndex = (namespace?: string) => + `.asset-criticality.asset-criticality-${namespace ?? 'default'}`; + export const cleanAssetCriticality = async ({ log, es, @@ -27,7 +34,7 @@ export const cleanAssetCriticality = async ({ try { await Promise.allSettled([ es.indices.delete({ - index: [`.asset-criticality.asset-criticality-${namespace}`], + index: [getAssetCriticalityIndex(namespace)], }), ]); } catch (e) { @@ -35,6 +42,24 @@ export const cleanAssetCriticality = async ({ } }; +export const getAssetCriticalityDoc = async (opts: { + es: Client; + idField: string; + idValue: string; +}) => { + const { es, idField, idValue } = opts; + try { + const doc = await es.get({ + index: getAssetCriticalityIndex(), + id: `${idField}:${idValue}`, + }); + + return doc._source; + } catch (e) { + return undefined; + } +}; + export const assetCriticalityRouteHelpersFactory = ( supertest: SuperTest.SuperTest, namespace?: string @@ -47,4 +72,39 @@ export const assetCriticalityRouteHelpersFactory = ( .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .send() .expect(200), + upsert: async ( + body: Record, + { expectStatusCode }: { expectStatusCode: number } = { expectStatusCode: 200 } + ) => + await supertest + .post(routeWithNamespace(ASSET_CRITICALITY_URL, namespace)) + .set('kbn-xsrf', 'true') + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') + .send(body) + .expect(expectStatusCode), + delete: async (idField: string, idValue: string) => { + const qs = querystring.stringify({ id_field: idField, id_value: idValue }); + const route = `${routeWithNamespace(ASSET_CRITICALITY_URL, namespace)}?${qs}`; + return supertest + .delete(route) + .set('kbn-xsrf', 'true') + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') + .expect(200); + }, + get: async ( + idField: string, + idValue: string, + { expectStatusCode }: { expectStatusCode: number } = { expectStatusCode: 200 } + ) => { + const qs = querystring.stringify({ id_field: idField, id_value: idValue }); + const route = `${routeWithNamespace(ASSET_CRITICALITY_URL, namespace)}?${qs}`; + return supertest + .get(route) + .set('kbn-xsrf', 'true') + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') + .expect(expectStatusCode); + }, });