mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Entity Analytics] New API endpoint to cleanup the risk engine installation and data (#191843)
## Summary 1. Create a new public API endpoint : `DELETE /api/risk_score/engine/dangerously_delete_data` ## Test cases Result ``` PASS x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/delete.test.ts (7.017 s) risk engine cleanup route invokes the risk engine cleanup route ✓ should call the router with the correct route and handler (71 ms) ✓ returns a 200 when cleanup is successful (64 ms) ✓ returns a 500 when cleanup is unsuccessful (57 ms) ✓ returns a 500 when cleanup is unsuccessful with multiple errors (53 ms) when task manager is unavailable ✓ returns a 400 when task manager is unavailable (55 ms) when user does not have the required privileges ✓ returns a 403 when user does not have the required privileges (88 ms) ``` ### API Responses ## When multiple errors encountered ``` { "risk_engine_cleanup": false, "errors": [ { "seq": 1, "error": "resource_not_found_exception\n\tRoot causes:\n\t\tresource_not_found_exception: Transform with id [risk_score_latest_transform_default] could not be found" }, { "seq": 2, "error": "index_not_found_exception\n\tRoot causes:\n\t\tindex_not_found_exception: no such index [risk-score.risk-score-default]" }, { "seq": 3, "error": "index_template_missing_exception\n\tRoot causes:\n\t\tindex_template_missing_exception: index_template [.risk-score.risk-score-default-index-template] missing" }, { "seq": 4, "error": "resource_not_found_exception\n\tRoot causes:\n\t\tresource_not_found_exception: .risk-score-mappings" } ], "status_code": 500 } ``` ## Success ``` { "risk_engine_cleanup": true } ``` ### Checklist Delete any items that are not applicable to this PR. - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [x] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios ### OpenAPI spec  ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
This commit is contained in:
parent
8d7dad266d
commit
22f451b30d
19 changed files with 823 additions and 6 deletions
|
@ -15392,6 +15392,39 @@ paths:
|
|||
tags:
|
||||
- Security Timeline API
|
||||
- access:securitySolution
|
||||
/api/risk_score/engine/dangerously_delete_data:
|
||||
delete:
|
||||
description: >-
|
||||
Cleaning up the the Risk Engine by removing the indices, mapping and
|
||||
transforms
|
||||
operationId: CleanUpRiskEngine
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json; Elastic-Api-Version=2023-10-31:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
cleanup_successful:
|
||||
type: boolean
|
||||
description: Successful response
|
||||
'400':
|
||||
content:
|
||||
application/json; Elastic-Api-Version=2023-10-31:
|
||||
schema:
|
||||
$ref: >-
|
||||
#/components/schemas/Security_Entity_Analytics_API_TaskManagerUnavailableResponse
|
||||
description: Task manager is unavailable
|
||||
default:
|
||||
content:
|
||||
application/json; Elastic-Api-Version=2023-10-31:
|
||||
schema:
|
||||
$ref: >-
|
||||
#/components/schemas/Security_Entity_Analytics_API_CleanUpRiskEngineErrorResponse
|
||||
description: Unexpected error
|
||||
summary: Cleanup the Risk Engine
|
||||
tags:
|
||||
- Security Entity Analytics API
|
||||
/api/risk_score/engine/schedule_now:
|
||||
post:
|
||||
description: >-
|
||||
|
@ -29750,6 +29783,27 @@ components:
|
|||
required:
|
||||
- id_value
|
||||
- id_field
|
||||
Security_Entity_Analytics_API_CleanUpRiskEngineErrorResponse:
|
||||
type: object
|
||||
properties:
|
||||
cleanup_successful:
|
||||
example: false
|
||||
type: boolean
|
||||
errors:
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
error:
|
||||
type: string
|
||||
seq:
|
||||
type: integer
|
||||
required:
|
||||
- seq
|
||||
- error
|
||||
type: array
|
||||
required:
|
||||
- cleanup_successful
|
||||
- errors
|
||||
Security_Entity_Analytics_API_CreateAssetCriticalityRecord:
|
||||
allOf:
|
||||
- $ref: >-
|
||||
|
|
|
@ -18822,6 +18822,39 @@ paths:
|
|||
tags:
|
||||
- Security Timeline API
|
||||
- access:securitySolution
|
||||
/api/risk_score/engine/dangerously_delete_data:
|
||||
delete:
|
||||
description: >-
|
||||
Cleaning up the the Risk Engine by removing the indices, mapping and
|
||||
transforms
|
||||
operationId: CleanUpRiskEngine
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json; Elastic-Api-Version=2023-10-31:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
cleanup_successful:
|
||||
type: boolean
|
||||
description: Successful response
|
||||
'400':
|
||||
content:
|
||||
application/json; Elastic-Api-Version=2023-10-31:
|
||||
schema:
|
||||
$ref: >-
|
||||
#/components/schemas/Security_Entity_Analytics_API_TaskManagerUnavailableResponse
|
||||
description: Task manager is unavailable
|
||||
default:
|
||||
content:
|
||||
application/json; Elastic-Api-Version=2023-10-31:
|
||||
schema:
|
||||
$ref: >-
|
||||
#/components/schemas/Security_Entity_Analytics_API_CleanUpRiskEngineErrorResponse
|
||||
description: Unexpected error
|
||||
summary: Cleanup the Risk Engine
|
||||
tags:
|
||||
- Security Entity Analytics API
|
||||
/api/risk_score/engine/schedule_now:
|
||||
post:
|
||||
description: >-
|
||||
|
@ -37759,6 +37792,27 @@ components:
|
|||
required:
|
||||
- id_value
|
||||
- id_field
|
||||
Security_Entity_Analytics_API_CleanUpRiskEngineErrorResponse:
|
||||
type: object
|
||||
properties:
|
||||
cleanup_successful:
|
||||
example: false
|
||||
type: boolean
|
||||
errors:
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
error:
|
||||
type: string
|
||||
seq:
|
||||
type: integer
|
||||
required:
|
||||
- seq
|
||||
- error
|
||||
type: array
|
||||
required:
|
||||
- cleanup_successful
|
||||
- errors
|
||||
Security_Entity_Analytics_API_CreateAssetCriticalityRecord:
|
||||
allOf:
|
||||
- $ref: >-
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* 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 Scoring API
|
||||
* version: 1
|
||||
*/
|
||||
|
||||
import { z } from '@kbn/zod';
|
||||
|
||||
export type CleanUpRiskEngineErrorResponse = z.infer<typeof CleanUpRiskEngineErrorResponse>;
|
||||
export const CleanUpRiskEngineErrorResponse = z.object({
|
||||
cleanup_successful: z.boolean(),
|
||||
errors: z.array(
|
||||
z.object({
|
||||
seq: z.number().int(),
|
||||
error: z.string(),
|
||||
})
|
||||
),
|
||||
});
|
||||
|
||||
export type CleanUpRiskEngineResponse = z.infer<typeof CleanUpRiskEngineResponse>;
|
||||
export const CleanUpRiskEngineResponse = z.object({
|
||||
cleanup_successful: z.boolean().optional(),
|
||||
});
|
|
@ -0,0 +1,60 @@
|
|||
openapi: 3.0.0
|
||||
info:
|
||||
version: '1'
|
||||
title: Risk Scoring API
|
||||
description: These APIs allow the consumer to manage Entity Risk Scores within Entity Analytics.
|
||||
paths:
|
||||
/api/risk_score/engine/dangerously_delete_data:
|
||||
delete:
|
||||
x-labels: [ess, serverless]
|
||||
x-codegen-enabled: true
|
||||
operationId: CleanUpRiskEngine
|
||||
summary: Cleanup the Risk Engine
|
||||
description: Cleaning up the the Risk Engine by removing the indices, mapping and transforms
|
||||
responses:
|
||||
'200':
|
||||
description: Successful response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
cleanup_successful:
|
||||
type: boolean
|
||||
'400':
|
||||
description: Task manager is unavailable
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '../common/common.schema.yaml#/components/schemas/TaskManagerUnavailableResponse'
|
||||
default:
|
||||
description: Unexpected error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/CleanUpRiskEngineErrorResponse'
|
||||
|
||||
components:
|
||||
schemas:
|
||||
CleanUpRiskEngineErrorResponse:
|
||||
type: object
|
||||
required:
|
||||
- cleanup_successful
|
||||
- errors
|
||||
properties:
|
||||
cleanup_successful:
|
||||
type: boolean
|
||||
example: false
|
||||
errors:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
required:
|
||||
- seq
|
||||
- error
|
||||
properties:
|
||||
seq:
|
||||
type: integer
|
||||
error:
|
||||
type: string
|
||||
|
|
@ -15,3 +15,4 @@ export * from './calculation_route.gen';
|
|||
export * from './preview_route.gen';
|
||||
export * from './entity_calculation_route.gen';
|
||||
export * from './get_risk_engine_privileges.gen';
|
||||
export * from './engine_cleanup_route.gen';
|
||||
|
|
|
@ -274,6 +274,7 @@ import type {
|
|||
ListEntitiesRequestQueryInput,
|
||||
ListEntitiesResponse,
|
||||
} from './entity_analytics/entity_store/entities/list_entities.gen';
|
||||
import type { CleanUpRiskEngineResponse } from './entity_analytics/risk_engine/engine_cleanup_route.gen';
|
||||
import type { DisableRiskEngineResponse } from './entity_analytics/risk_engine/engine_disable_route.gen';
|
||||
import type { EnableRiskEngineResponse } from './entity_analytics/risk_engine/engine_enable_route.gen';
|
||||
import type { InitRiskEngineResponse } from './entity_analytics/risk_engine/engine_init_route.gen';
|
||||
|
@ -540,6 +541,21 @@ If asset criticality records already exist for the specified entities, those rec
|
|||
})
|
||||
.catch(catchAxiosErrorFormatAndThrow);
|
||||
}
|
||||
/**
|
||||
* Cleaning up the the Risk Engine by removing the indices, mapping and transforms
|
||||
*/
|
||||
async cleanUpRiskEngine() {
|
||||
this.log.info(`${new Date().toISOString()} Calling API CleanUpRiskEngine`);
|
||||
return this.kbnClient
|
||||
.request<CleanUpRiskEngineResponse>({
|
||||
path: '/api/risk_score/engine/dangerously_delete_data',
|
||||
headers: {
|
||||
[ELASTIC_HTTP_VERSION_HEADER]: '1',
|
||||
},
|
||||
method: 'DELETE',
|
||||
})
|
||||
.catch(catchAxiosErrorFormatAndThrow);
|
||||
}
|
||||
async createAlertsIndex() {
|
||||
this.log.info(`${new Date().toISOString()} Calling API CreateAlertsIndex`);
|
||||
return this.kbnClient
|
||||
|
|
|
@ -16,6 +16,7 @@ export const RISK_ENGINE_SETTINGS_URL = `${RISK_ENGINE_URL}/settings` as const;
|
|||
// Public Risk Score routes
|
||||
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;
|
||||
|
||||
type ClusterPrivilege = 'manage_index_templates' | 'manage_transform';
|
||||
export const RISK_ENGINE_REQUIRED_ES_CLUSTER_PRIVILEGES = [
|
||||
|
|
|
@ -0,0 +1,88 @@
|
|||
openapi: 3.0.3
|
||||
info:
|
||||
description: ''
|
||||
title: Security Entity Analytics API (Elastic Cloud and self-hosted)
|
||||
version: '1'
|
||||
servers:
|
||||
- url: http://{kibana_host}:{port}
|
||||
variables:
|
||||
kibana_host:
|
||||
default: localhost
|
||||
port:
|
||||
default: '5601'
|
||||
paths:
|
||||
/api/risk_score/engine/dangerously_delete_data:
|
||||
delete:
|
||||
description: >-
|
||||
Cleaning up the the Risk Engine by removing the indices, mapping and
|
||||
transforms
|
||||
operationId: CleanUpRiskEngine
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
cleanup_successful:
|
||||
type: boolean
|
||||
description: Successful response
|
||||
'400':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/TaskManagerUnavailableResponse'
|
||||
description: Task manager is unavailable
|
||||
default:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/CleanUpRiskEngineErrorResponse'
|
||||
description: Unexpected error
|
||||
summary: Cleanup the Risk Engine
|
||||
tags:
|
||||
- Security Entity Analytics API
|
||||
components:
|
||||
schemas:
|
||||
CleanUpRiskEngineErrorResponse:
|
||||
type: object
|
||||
properties:
|
||||
cleanup_successful:
|
||||
example: false
|
||||
type: boolean
|
||||
errors:
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
error:
|
||||
type: string
|
||||
seq:
|
||||
type: integer
|
||||
required:
|
||||
- seq
|
||||
- error
|
||||
type: array
|
||||
required:
|
||||
- cleanup_successful
|
||||
- errors
|
||||
TaskManagerUnavailableResponse:
|
||||
description: Task manager is unavailable
|
||||
type: object
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
status_code:
|
||||
minimum: 400
|
||||
type: integer
|
||||
required:
|
||||
- status_code
|
||||
- message
|
||||
securitySchemes:
|
||||
BasicAuth:
|
||||
scheme: basic
|
||||
type: http
|
||||
security:
|
||||
- BasicAuth: []
|
||||
tags:
|
||||
- description: ''
|
||||
name: Security Entity Analytics API
|
|
@ -0,0 +1,88 @@
|
|||
openapi: 3.0.3
|
||||
info:
|
||||
description: ''
|
||||
title: Security Entity Analytics API (Elastic Cloud Serverless)
|
||||
version: '1'
|
||||
servers:
|
||||
- url: http://{kibana_host}:{port}
|
||||
variables:
|
||||
kibana_host:
|
||||
default: localhost
|
||||
port:
|
||||
default: '5601'
|
||||
paths:
|
||||
/api/risk_score/engine/dangerously_delete_data:
|
||||
delete:
|
||||
description: >-
|
||||
Cleaning up the the Risk Engine by removing the indices, mapping and
|
||||
transforms
|
||||
operationId: CleanUpRiskEngine
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
cleanup_successful:
|
||||
type: boolean
|
||||
description: Successful response
|
||||
'400':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/TaskManagerUnavailableResponse'
|
||||
description: Task manager is unavailable
|
||||
default:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/CleanUpRiskEngineErrorResponse'
|
||||
description: Unexpected error
|
||||
summary: Cleanup the Risk Engine
|
||||
tags:
|
||||
- Security Entity Analytics API
|
||||
components:
|
||||
schemas:
|
||||
CleanUpRiskEngineErrorResponse:
|
||||
type: object
|
||||
properties:
|
||||
cleanup_successful:
|
||||
example: false
|
||||
type: boolean
|
||||
errors:
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
error:
|
||||
type: string
|
||||
seq:
|
||||
type: integer
|
||||
required:
|
||||
- seq
|
||||
- error
|
||||
type: array
|
||||
required:
|
||||
- cleanup_successful
|
||||
- errors
|
||||
TaskManagerUnavailableResponse:
|
||||
description: Task manager is unavailable
|
||||
type: object
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
status_code:
|
||||
minimum: 400
|
||||
type: integer
|
||||
required:
|
||||
- status_code
|
||||
- message
|
||||
securitySchemes:
|
||||
BasicAuth:
|
||||
scheme: basic
|
||||
type: http
|
||||
security:
|
||||
- BasicAuth: []
|
||||
tags:
|
||||
- description: ''
|
||||
name: Security Entity Analytics API
|
|
@ -7,12 +7,12 @@
|
|||
|
||||
import { useMemo } from 'react';
|
||||
import { LIST_ENTITIES_URL } from '../../../common/entity_analytics/entity_store/constants';
|
||||
import type { RiskEngineScheduleNowResponse } from '../../../common/api/entity_analytics/risk_engine/engine_schedule_now_route.gen';
|
||||
import type { DisableRiskEngineResponse } from '../../../common/api/entity_analytics/risk_engine/engine_disable_route.gen';
|
||||
import type { UploadAssetCriticalityRecordsResponse } from '../../../common/api/entity_analytics/asset_criticality/upload_asset_criticality_csv.gen';
|
||||
import type { DisableRiskEngineResponse } from '../../../common/api/entity_analytics/risk_engine/engine_disable_route.gen';
|
||||
import type { RiskEngineStatusResponse } from '../../../common/api/entity_analytics/risk_engine/engine_status_route.gen';
|
||||
import type { InitRiskEngineResponse } from '../../../common/api/entity_analytics/risk_engine/engine_init_route.gen';
|
||||
import type { EnableRiskEngineResponse } from '../../../common/api/entity_analytics/risk_engine/engine_enable_route.gen';
|
||||
import type { RiskEngineScheduleNowResponse } from '../../../common/api/entity_analytics/risk_engine/engine_schedule_now_route.gen';
|
||||
import type {
|
||||
RiskScoresPreviewRequest,
|
||||
RiskScoresPreviewResponse,
|
||||
|
@ -40,6 +40,7 @@ import {
|
|||
ASSET_CRITICALITY_PUBLIC_CSV_UPLOAD_URL,
|
||||
RISK_SCORE_ENTITY_CALCULATION_URL,
|
||||
API_VERSIONS,
|
||||
RISK_ENGINE_CLEANUP_URL,
|
||||
RISK_ENGINE_SCHEDULE_NOW_URL,
|
||||
} from '../../../common/constants';
|
||||
import type { SnakeToCamelCase } from '../common/utils';
|
||||
|
@ -191,12 +192,18 @@ export const useEntityAnalyticsRoutes = () => {
|
|||
});
|
||||
|
||||
const deleteAssetCriticality = async (
|
||||
params: Pick<AssetCriticality, 'idField' | 'idValue'> & { refresh?: 'wait_for' }
|
||||
params: Pick<AssetCriticality, 'idField' | 'idValue'> & {
|
||||
refresh?: 'wait_for';
|
||||
}
|
||||
): Promise<{ deleted: true }> => {
|
||||
await http.fetch(ASSET_CRITICALITY_PUBLIC_URL, {
|
||||
version: API_VERSIONS.public.v1,
|
||||
method: 'DELETE',
|
||||
query: { id_value: params.idValue, id_field: params.idField, refresh: params.refresh },
|
||||
query: {
|
||||
id_value: params.idValue,
|
||||
id_field: params.idField,
|
||||
refresh: params.refresh,
|
||||
},
|
||||
});
|
||||
|
||||
// spoof a response to allow us to better distnguish a delete from a create in use_asset_criticality.ts
|
||||
|
@ -220,7 +227,9 @@ export const useEntityAnalyticsRoutes = () => {
|
|||
fileContent: string,
|
||||
fileName: string
|
||||
): Promise<UploadAssetCriticalityRecordsResponse> => {
|
||||
const file = new File([new Blob([fileContent])], fileName, { type: 'text/csv' });
|
||||
const file = new File([new Blob([fileContent])], fileName, {
|
||||
type: 'text/csv',
|
||||
});
|
||||
const body = new FormData();
|
||||
body.append('file', file);
|
||||
|
||||
|
@ -267,6 +276,16 @@ export const useEntityAnalyticsRoutes = () => {
|
|||
method: 'GET',
|
||||
});
|
||||
|
||||
/**
|
||||
* Deletes Risk engine installation and associated data
|
||||
*/
|
||||
|
||||
const cleanUpRiskEngine = () =>
|
||||
http.fetch(RISK_ENGINE_CLEANUP_URL, {
|
||||
version: '1',
|
||||
method: 'DELETE',
|
||||
});
|
||||
|
||||
return {
|
||||
fetchRiskScorePreview,
|
||||
fetchRiskEngineStatus,
|
||||
|
@ -283,6 +302,7 @@ export const useEntityAnalyticsRoutes = () => {
|
|||
getRiskScoreIndexStatus,
|
||||
fetchRiskEngineSettings,
|
||||
calculateEntityRiskScore,
|
||||
cleanUpRiskEngine,
|
||||
fetchEntitiesList,
|
||||
};
|
||||
}, [http]);
|
||||
|
|
|
@ -15,6 +15,9 @@ const createRiskEngineDataClientMock = () =>
|
|||
getConfiguration: jest.fn(),
|
||||
getStatus: jest.fn(),
|
||||
init: jest.fn(),
|
||||
tearDown: jest.fn(),
|
||||
} as unknown as jest.Mocked<RiskEngineDataClient>);
|
||||
|
||||
export const riskEngineDataClientMock = { create: createRiskEngineDataClientMock };
|
||||
export const riskEngineDataClientMock = {
|
||||
create: createRiskEngineDataClientMock,
|
||||
};
|
||||
|
|
|
@ -0,0 +1,186 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { taskManagerMock } from '@kbn/task-manager-plugin/server/mocks';
|
||||
import { RISK_ENGINE_CLEANUP_URL } from '../../../../../common/constants';
|
||||
import {
|
||||
serverMock,
|
||||
requestContextMock,
|
||||
requestMock,
|
||||
} from '../../../detection_engine/routes/__mocks__';
|
||||
import { riskEnginePrivilegesMock } from './risk_engine_privileges.mock';
|
||||
import { riskEngineDataClientMock } from '../risk_engine_data_client.mock';
|
||||
import { riskEngineCleanupRoute } from './delete';
|
||||
|
||||
describe('risk engine cleanup route', () => {
|
||||
let server: ReturnType<typeof serverMock.create>;
|
||||
let context: ReturnType<typeof requestContextMock.convertContext>;
|
||||
let mockTaskManagerStart: ReturnType<typeof taskManagerMock.createStart>;
|
||||
let mockRiskEngineDataClient: ReturnType<typeof riskEngineDataClientMock.create>;
|
||||
let getStartServicesMock: jest.Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
|
||||
server = serverMock.create();
|
||||
const { clients } = requestContextMock.createTools();
|
||||
mockRiskEngineDataClient = riskEngineDataClientMock.create();
|
||||
context = requestContextMock.convertContext(
|
||||
requestContextMock.create({
|
||||
...clients,
|
||||
riskEngineDataClient: mockRiskEngineDataClient,
|
||||
})
|
||||
);
|
||||
mockTaskManagerStart = taskManagerMock.createStart();
|
||||
});
|
||||
|
||||
const buildRequest = () => {
|
||||
return requestMock.create({
|
||||
method: 'delete',
|
||||
path: RISK_ENGINE_CLEANUP_URL,
|
||||
body: {},
|
||||
});
|
||||
};
|
||||
describe('invokes the risk engine cleanup route', () => {
|
||||
beforeEach(() => {
|
||||
getStartServicesMock = jest.fn().mockResolvedValue([
|
||||
{},
|
||||
{
|
||||
taskManager: mockTaskManagerStart,
|
||||
security: riskEnginePrivilegesMock.createMockSecurityStartWithFullRiskEngineAccess(),
|
||||
},
|
||||
]);
|
||||
riskEngineCleanupRoute(server.router, getStartServicesMock);
|
||||
});
|
||||
|
||||
it('should call the router with the correct route and handler', async () => {
|
||||
const request = buildRequest();
|
||||
await server.inject(request, context);
|
||||
expect(mockRiskEngineDataClient.tearDown).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns a 200 when cleanup is successful', async () => {
|
||||
const request = buildRequest();
|
||||
const response = await server.inject(request, context);
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toEqual({ cleanup_successful: true });
|
||||
});
|
||||
|
||||
it('returns a 400 when cleanup endpoint is called multiple times', async () => {
|
||||
mockRiskEngineDataClient.tearDown.mockImplementation(async () => {
|
||||
return [Error('Risk engine is disabled or deleted already.')];
|
||||
});
|
||||
const request = buildRequest();
|
||||
const response = await server.inject(request, context);
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body).toEqual({
|
||||
cleanup_successful: false,
|
||||
errors: [
|
||||
{
|
||||
seq: 1,
|
||||
error: 'Error: Risk engine is disabled or deleted already.',
|
||||
},
|
||||
],
|
||||
status_code: 400,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns a 500 when cleanup is unsuccessful', async () => {
|
||||
mockRiskEngineDataClient.tearDown.mockImplementation(() => {
|
||||
throw new Error('Error tearing down');
|
||||
});
|
||||
const request = buildRequest();
|
||||
const response = await server.inject(request, context);
|
||||
expect(response.status).toBe(500);
|
||||
expect(response.body).toEqual({
|
||||
errors: {
|
||||
error: '{}',
|
||||
seq: 1,
|
||||
},
|
||||
cleanup_successful: false,
|
||||
status_code: 500,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns a 500 when cleanup is unsuccessful with multiple errors', async () => {
|
||||
mockRiskEngineDataClient.tearDown.mockImplementation(async () => {
|
||||
return [
|
||||
Error('Error while removing risk scoring task'),
|
||||
Error('Error while deleting saved objects'),
|
||||
Error('Error while removing risk score index'),
|
||||
];
|
||||
});
|
||||
const request = buildRequest();
|
||||
const response = await server.inject(request, context);
|
||||
expect(response.status).toBe(500);
|
||||
expect(response.body).toEqual({
|
||||
errors: [
|
||||
{
|
||||
seq: 1,
|
||||
error: 'Error: Error while removing risk scoring task',
|
||||
},
|
||||
{
|
||||
seq: 2,
|
||||
error: 'Error: Error while deleting saved objects',
|
||||
},
|
||||
{
|
||||
seq: 3,
|
||||
error: 'Error: Error while removing risk score index',
|
||||
},
|
||||
],
|
||||
cleanup_successful: false,
|
||||
status_code: 500,
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('when task manager is unavailable', () => {
|
||||
beforeEach(() => {
|
||||
getStartServicesMock = jest.fn().mockResolvedValue([
|
||||
{},
|
||||
{
|
||||
security: riskEnginePrivilegesMock.createMockSecurityStartWithFullRiskEngineAccess(),
|
||||
},
|
||||
]);
|
||||
riskEngineCleanupRoute(server.router, getStartServicesMock);
|
||||
});
|
||||
|
||||
it('returns a 400 when task manager is unavailable', async () => {
|
||||
const request = buildRequest();
|
||||
const response = await server.inject(request, context);
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body).toEqual({
|
||||
message:
|
||||
'Task Manager is unavailable, but is required by the risk engine. Please enable the taskManager plugin and try again.',
|
||||
status_code: 400,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when user does not have the required privileges', () => {
|
||||
beforeEach(() => {
|
||||
getStartServicesMock = jest.fn().mockResolvedValue([
|
||||
{},
|
||||
{
|
||||
taskManager: mockTaskManagerStart,
|
||||
security: riskEnginePrivilegesMock.createMockSecurityStartWithNoRiskEngineAccess(),
|
||||
},
|
||||
]);
|
||||
riskEngineCleanupRoute(server.router, getStartServicesMock);
|
||||
});
|
||||
|
||||
it('returns a 403 when user does not have the required privileges', async () => {
|
||||
const request = buildRequest();
|
||||
const response = await server.inject(request, context);
|
||||
expect(response.status).toBe(403);
|
||||
expect(response.body).toEqual({
|
||||
message:
|
||||
'User is missing risk engine privileges. Missing cluster privileges: manage_index_templates, manage_transform.',
|
||||
status_code: 403,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,103 @@
|
|||
/*
|
||||
* 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 type { IKibanaResponse } from '@kbn/core-http-server';
|
||||
import { withRiskEnginePrivilegeCheck } from '../risk_engine_privileges';
|
||||
import { RISK_ENGINE_CLEANUP_URL, APP_ID, API_VERSIONS } from '../../../../../common/constants';
|
||||
import type { EntityAnalyticsRoutesDeps } from '../../types';
|
||||
import { RiskEngineAuditActions } from '../audit';
|
||||
import { AUDIT_CATEGORY, AUDIT_OUTCOME, AUDIT_TYPE } from '../../audit';
|
||||
import { TASK_MANAGER_UNAVAILABLE_ERROR } from './translations';
|
||||
import type { CleanUpRiskEngineResponse } from '../../../../../common/api/entity_analytics';
|
||||
|
||||
export const riskEngineCleanupRoute = (
|
||||
router: EntityAnalyticsRoutesDeps['router'],
|
||||
getStartServices: EntityAnalyticsRoutesDeps['getStartServices']
|
||||
) => {
|
||||
router.versioned
|
||||
.delete({
|
||||
access: 'public',
|
||||
path: RISK_ENGINE_CLEANUP_URL,
|
||||
options: {
|
||||
tags: ['access:securitySolution', `access:${APP_ID}-entity-analytics`],
|
||||
},
|
||||
})
|
||||
.addVersion(
|
||||
{ version: API_VERSIONS.public.v1, validate: {} },
|
||||
withRiskEnginePrivilegeCheck(
|
||||
getStartServices,
|
||||
async (context, request, response): Promise<IKibanaResponse<CleanUpRiskEngineResponse>> => {
|
||||
const siemResponse = buildSiemResponse(response);
|
||||
const securitySolution = await context.securitySolution;
|
||||
const [_, { taskManager }] = await getStartServices();
|
||||
const riskEngineClient = securitySolution.getRiskEngineDataClient();
|
||||
const riskScoreDataClient = securitySolution.getRiskScoreDataClient();
|
||||
|
||||
if (!taskManager) {
|
||||
securitySolution.getAuditLogger()?.log({
|
||||
message:
|
||||
'User attempted to perform a cleanup of risk engine, but the Kibana Task Manager was unavailable',
|
||||
event: {
|
||||
action: RiskEngineAuditActions.RISK_ENGINE_REMOVE_TASK,
|
||||
category: AUDIT_CATEGORY.DATABASE,
|
||||
type: AUDIT_TYPE.DELETION,
|
||||
outcome: AUDIT_OUTCOME.FAILURE,
|
||||
},
|
||||
error: {
|
||||
message:
|
||||
'User attempted to perform a cleanup of risk engine, but the Kibana Task Manager was unavailable',
|
||||
},
|
||||
});
|
||||
|
||||
return siemResponse.error({
|
||||
statusCode: 400,
|
||||
body: TASK_MANAGER_UNAVAILABLE_ERROR,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const errors = await riskEngineClient.tearDown({
|
||||
taskManager,
|
||||
riskScoreDataClient,
|
||||
});
|
||||
if (errors && errors.length > 0) {
|
||||
return siemResponse.error({
|
||||
statusCode: errors.some((error) =>
|
||||
error.message.includes('Risk engine is disabled or deleted already.')
|
||||
)
|
||||
? 400
|
||||
: 500,
|
||||
body: {
|
||||
cleanup_successful: false,
|
||||
errors: errors.map((error, seq) => ({
|
||||
seq: seq + 1,
|
||||
error: error.toString(),
|
||||
})),
|
||||
},
|
||||
bypassErrorFormat: true,
|
||||
});
|
||||
} else {
|
||||
return response.ok({ body: { cleanup_successful: true } });
|
||||
}
|
||||
} catch (error) {
|
||||
return siemResponse.error({
|
||||
statusCode: 500,
|
||||
body: {
|
||||
cleanup_successful: false,
|
||||
errors: {
|
||||
seq: 1,
|
||||
error: JSON.stringify(error),
|
||||
},
|
||||
},
|
||||
bypassErrorFormat: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
};
|
|
@ -12,6 +12,7 @@ import { riskEnginePrivilegesRoute } from './privileges';
|
|||
import { riskEngineSettingsRoute } from './settings';
|
||||
import type { EntityAnalyticsRoutesDeps } from '../../types';
|
||||
import { riskEngineScheduleNowRoute } from './schedule_now';
|
||||
import { riskEngineCleanupRoute } from './delete';
|
||||
|
||||
export const registerRiskEngineRoutes = ({
|
||||
router,
|
||||
|
@ -24,4 +25,5 @@ export const registerRiskEngineRoutes = ({
|
|||
riskEngineScheduleNowRoute(router, getStartServices);
|
||||
riskEngineSettingsRoute(router);
|
||||
riskEnginePrivilegesRoute(router, getStartServices);
|
||||
riskEngineCleanupRoute(router, getStartServices);
|
||||
};
|
||||
|
|
|
@ -29,6 +29,27 @@ const createMockSecurityStartWithFullRiskEngineAccess = () => {
|
|||
return mockSecurityStart;
|
||||
};
|
||||
|
||||
const createMockSecurityStartWithNoRiskEngineAccess = () => {
|
||||
const mockSecurityStart = securityMock.createStart();
|
||||
|
||||
const mockCheckPrivileges = jest.fn().mockResolvedValue({
|
||||
hasAllRequested: false,
|
||||
privileges: {
|
||||
elasticsearch: {
|
||||
cluster: [],
|
||||
index: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
mockSecurityStart.authz.checkPrivilegesDynamicallyWithRequest = jest
|
||||
.fn()
|
||||
.mockReturnValue(mockCheckPrivileges);
|
||||
|
||||
return mockSecurityStart;
|
||||
};
|
||||
|
||||
export const riskEnginePrivilegesMock = {
|
||||
createMockSecurityStartWithFullRiskEngineAccess,
|
||||
createMockSecurityStartWithNoRiskEngineAccess,
|
||||
};
|
||||
|
|
|
@ -256,6 +256,16 @@ If asset criticality records already exist for the specified entities, those rec
|
|||
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
|
||||
.send(props.body as object);
|
||||
},
|
||||
/**
|
||||
* Cleaning up the the Risk Engine by removing the indices, mapping and transforms
|
||||
*/
|
||||
cleanUpRiskEngine() {
|
||||
return supertest
|
||||
.delete('/api/risk_score/engine/dangerously_delete_data')
|
||||
.set('kbn-xsrf', 'true')
|
||||
.set(ELASTIC_HTTP_VERSION_HEADER, '1')
|
||||
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana');
|
||||
},
|
||||
createAlertsIndex() {
|
||||
return supertest
|
||||
.post('/api/detection_engine/index')
|
||||
|
|
|
@ -10,6 +10,7 @@ import { FtrProviderContext } from '../../../../ftr_provider_context';
|
|||
export default function ({ loadTestFile }: FtrProviderContext) {
|
||||
describe('Entity Analytics - Risk Engine', function () {
|
||||
loadTestFile(require.resolve('./init_and_status_apis'));
|
||||
loadTestFile(require.resolve('./risk_engine_cleanup_api'));
|
||||
loadTestFile(require.resolve('./risk_score_preview'));
|
||||
loadTestFile(require.resolve('./risk_scoring_task/task_execution'));
|
||||
loadTestFile(require.resolve('./risk_scoring_task/task_execution_nondefault_spaces'));
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* 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 { v4 as uuidv4 } from 'uuid';
|
||||
import expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../../../ftr_provider_context';
|
||||
import {
|
||||
buildDocument,
|
||||
riskEngineRouteHelpersFactory,
|
||||
waitForRiskScoresToBePresent,
|
||||
createAndSyncRuleAndAlertsFactory,
|
||||
} from '../../utils';
|
||||
import { dataGeneratorFactory } from '../../../detections_response/utils';
|
||||
|
||||
export default ({ getService }: FtrProviderContext) => {
|
||||
const supertest = getService('supertest');
|
||||
const riskEngineRoutes = riskEngineRouteHelpersFactory(supertest);
|
||||
const es = getService('es');
|
||||
const log = getService('log');
|
||||
const esArchiver = getService('esArchiver');
|
||||
|
||||
describe('@ess @ serverless @serverless QA risk_engine_cleanup_api', () => {
|
||||
const createAndSyncRuleAndAlerts = createAndSyncRuleAndAlertsFactory({ supertest, log });
|
||||
const { indexListOfDocuments } = dataGeneratorFactory({
|
||||
es,
|
||||
index: 'ecs_compliant',
|
||||
log,
|
||||
});
|
||||
before(async () => {
|
||||
await esArchiver.load('x-pack/test/functional/es_archives/security_solution/ecs_compliant');
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await esArchiver.unload('x-pack/test/functional/es_archives/security_solution/ecs_compliant');
|
||||
});
|
||||
|
||||
it('should return response with success status', async () => {
|
||||
const status1 = await riskEngineRoutes.getStatus();
|
||||
expect(status1.body.risk_engine_status).to.be('NOT_INSTALLED');
|
||||
expect(status1.body.legacy_risk_engine_status).to.be('NOT_INSTALLED');
|
||||
|
||||
const firstDocumentId = uuidv4();
|
||||
await indexListOfDocuments([buildDocument({ host: { name: 'host-1' } }, firstDocumentId)]);
|
||||
await createAndSyncRuleAndAlerts({ query: `id: ${firstDocumentId}` });
|
||||
|
||||
await riskEngineRoutes.init();
|
||||
await waitForRiskScoresToBePresent({ es, log, scoreCount: 1 });
|
||||
|
||||
const status2 = await riskEngineRoutes.getStatus();
|
||||
expect(status2.body.risk_engine_status).to.be('ENABLED');
|
||||
expect(status2.body.legacy_risk_engine_status).to.be('NOT_INSTALLED');
|
||||
|
||||
const response = await riskEngineRoutes.delete();
|
||||
expect(response.body).to.eql({
|
||||
cleanup_successful: true,
|
||||
});
|
||||
|
||||
const status3 = await riskEngineRoutes.getStatus();
|
||||
expect(status3.body.risk_engine_status).to.be('NOT_INSTALLED');
|
||||
expect(status3.body.legacy_risk_engine_status).to.be('NOT_INSTALLED');
|
||||
});
|
||||
});
|
||||
};
|
|
@ -22,6 +22,7 @@ import {
|
|||
RISK_ENGINE_ENABLE_URL,
|
||||
RISK_ENGINE_STATUS_URL,
|
||||
RISK_ENGINE_PRIVILEGES_URL,
|
||||
RISK_ENGINE_CLEANUP_URL,
|
||||
RISK_ENGINE_SCHEDULE_NOW_URL,
|
||||
} from '@kbn/security-solution-plugin/common/constants';
|
||||
import { MappingTypeMapping } from '@elastic/elasticsearch/lib/api/types';
|
||||
|
@ -574,6 +575,14 @@ export const riskEngineRouteHelpersFactory = (supertest: SuperTest.Agent, namesp
|
|||
.send()
|
||||
.expect(expectStatusCode),
|
||||
|
||||
delete: async (expectStatusCode: number = 200) =>
|
||||
await supertest
|
||||
.delete(routeWithNamespace(RISK_ENGINE_CLEANUP_URL, namespace))
|
||||
.set('kbn-xsrf', 'true')
|
||||
.set('elastic-api-version', '2023-10-31')
|
||||
.send()
|
||||
.expect(expectStatusCode),
|
||||
|
||||
scheduleNow: async (expectStatusCode: number = 200) =>
|
||||
await supertest
|
||||
.post(routeWithNamespace(RISK_ENGINE_SCHEDULE_NOW_URL, namespace))
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue