[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


![image](https://github.com/user-attachments/assets/56d69602-061d-4a01-9d2b-01a8398ffc76)


### 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:
Abhishek Bhatia 2024-09-25 17:05:24 +05:30 committed by GitHub
parent 8d7dad266d
commit 22f451b30d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 823 additions and 6 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 = [

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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')

View file

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

View file

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

View file

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