[Entity Analytics] Remove nested message.message property from risk engine API error responses (#170645)

## Summary

**TLDR:** change risk engine API error response bodies from `{ message :
{ message : 'blah', full_error : 'something'}}` to `{ message : 'blah',
full_error : 'something'}`

I noticed a UI bug when the risk engine "init" call returns an error,
this was because the UI was expecting `error.message` to be a string but
it was an object with another nested message property.

This lead me to investigate why this was the case, turns out our error
wrapper was always putting things under a `message` key which in our
case we do not want.

### UI crash before

```
Uncaught Error: Objects are not valid as a React child (found: object with keys {message}). If you meant to render a collection of children, use an array instead.
```
<img width="806" alt="Screenshot 2023-11-06 at 14 02 17"
src="25066a14-dabf-46a0-9741-a81f886f64fb">


### Correct error display after
<img width="1171" alt="Screenshot 2023-11-06 at 13 51 04"
src="af8db564-a119-4fc8-9821-bafcfe19b421">


### Checklist

Delete any items that are not applicable to this PR.

- [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
This commit is contained in:
Mark Hopkin 2023-11-08 15:38:21 +00:00 committed by GitHub
parent 5fab32d28f
commit 71f1dc7bd5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 51 additions and 40 deletions

View file

@ -49,7 +49,7 @@ export class SiemResponseFactory {
constructor(private response: KibanaResponseFactory) {}
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
error<T>({ statusCode, body, headers }: CustomHttpResponseOptions<T>) {
error<T>({ statusCode, body, headers, bypassErrorFormat }: CustomHttpResponseOptions<T>) {
// KibanaResponse is not exported so we cannot use a return type here and that is why the linter is turned off above
const contentType: CustomHttpResponseOptions<T>['headers'] = {
'content-type': 'application/json',
@ -59,10 +59,14 @@ export class SiemResponseFactory {
...(headers ?? {}),
};
const formattedBody = bypassErrorFormat
? body
: { message: body ?? statusToErrorMessage(statusCode) };
return this.response.custom({
body: Buffer.from(
JSON.stringify({
message: body ?? statusToErrorMessage(statusCode),
...formattedBody,
status_code: statusCode,
})
),

View file

@ -235,11 +235,11 @@ export const RiskScoreEnableSection = () => {
let initRiskEngineErrors: string[] = [];
if (initRiskEngineMutation.isError) {
const errorBody = initRiskEngineMutation.error.body.message;
const errorBody = initRiskEngineMutation.error.body;
if (errorBody?.full_error?.errors) {
initRiskEngineErrors = errorBody.full_error?.errors;
} else {
initRiskEngineErrors = [errorBody];
initRiskEngineErrors = [errorBody.message];
}
}
@ -266,10 +266,10 @@ export const RiskScoreEnableSection = () => {
</EuiTitle>
{initRiskEngineMutation.isError && <RiskScoreErrorPanel errors={initRiskEngineErrors} />}
{disableRiskEngineMutation.isError && (
<RiskScoreErrorPanel errors={[disableRiskEngineMutation.error.body.message.message]} />
<RiskScoreErrorPanel errors={[disableRiskEngineMutation.error.body.message]} />
)}
{enableRiskEngineMutation.isError && (
<RiskScoreErrorPanel errors={[enableRiskEngineMutation.error.body.message.message]} />
<RiskScoreErrorPanel errors={[enableRiskEngineMutation.error.body.message]} />
)}
<EuiSpacer size="m" />

View file

@ -16,7 +16,7 @@ import {
} from '../../detection_engine/routes/__mocks__';
import { riskEngineDataClientMock } from '../risk_engine_data_client.mock';
describe('risk score calculation route', () => {
describe('risk score disable route', () => {
let server: ReturnType<typeof serverMock.create>;
let context: ReturnType<typeof requestContextMock.convertContext>;
let mockTaskManagerStart: ReturnType<typeof taskManagerMock.createStart>;
@ -78,7 +78,7 @@ describe('risk score calculation route', () => {
const response = await server.inject(request, context);
expect(response.status).toEqual(500);
expect(response.body.message.message).toEqual('something went wrong');
expect(response.body.message).toEqual('something went wrong');
});
});
@ -94,10 +94,8 @@ describe('risk score calculation route', () => {
expect(response.status).toEqual(400);
expect(response.body).toEqual({
message: {
message:
'Task Manager is unavailable, but is required to disable the risk engine. Please enable the taskManager plugin and try again.',
},
message:
'Task Manager is unavailable, but is required by the risk engine. Please enable the taskManager plugin and try again.',
status_code: 400,
});
});

View file

@ -9,6 +9,7 @@ import type { StartServicesAccessor } from '@kbn/core/server';
import { buildSiemResponse } from '@kbn/lists-plugin/server/routes/utils';
import { transformError } from '@kbn/securitysolution-es-utils';
import { RISK_ENGINE_DISABLE_URL, APP_ID } from '../../../../common/constants';
import { TASK_MANAGER_UNAVAILABLE_ERROR } from './translations';
import type { StartPlugins } from '../../../plugin';
import type { SecuritySolutionPluginRouter } from '../../../types';
@ -34,10 +35,7 @@ export const riskEngineDisableRoute = (
if (!taskManager) {
return siemResponse.error({
statusCode: 400,
body: {
message:
'Task Manager is unavailable, but is required to disable the risk engine. Please enable the taskManager plugin and try again.',
},
body: TASK_MANAGER_UNAVAILABLE_ERROR,
});
}
@ -50,6 +48,7 @@ export const riskEngineDisableRoute = (
return siemResponse.error({
statusCode: error.statusCode,
body: { message: error.message, full_error: JSON.stringify(e) },
bypassErrorFormat: true,
});
}
});

View file

@ -16,7 +16,7 @@ import {
} from '../../detection_engine/routes/__mocks__';
import { riskEngineDataClientMock } from '../risk_engine_data_client.mock';
describe('risk score calculation route', () => {
describe('risk score enable route', () => {
let server: ReturnType<typeof serverMock.create>;
let context: ReturnType<typeof requestContextMock.convertContext>;
let mockTaskManagerStart: ReturnType<typeof taskManagerMock.createStart>;
@ -78,7 +78,7 @@ describe('risk score calculation route', () => {
const response = await server.inject(request, context);
expect(response.status).toEqual(500);
expect(response.body.message.message).toEqual('something went wrong');
expect(response.body.message).toEqual('something went wrong');
});
});
@ -94,10 +94,8 @@ describe('risk score calculation route', () => {
expect(response.status).toEqual(400);
expect(response.body).toEqual({
message: {
message:
'Task Manager is unavailable, but is required to enable the risk engine. Please enable the taskManager plugin and try again.',
},
message:
'Task Manager is unavailable, but is required by the risk engine. Please enable the taskManager plugin and try again.',
status_code: 400,
});
});

View file

@ -9,6 +9,7 @@ import type { StartServicesAccessor } from '@kbn/core/server';
import { buildSiemResponse } from '@kbn/lists-plugin/server/routes/utils';
import { transformError } from '@kbn/securitysolution-es-utils';
import { RISK_ENGINE_ENABLE_URL, APP_ID } from '../../../../common/constants';
import { TASK_MANAGER_UNAVAILABLE_ERROR } from './translations';
import type { StartPlugins } from '../../../plugin';
import type { SecuritySolutionPluginRouter } from '../../../types';
@ -29,14 +30,10 @@ export const riskEngineEnableRoute = (
const [_, { taskManager }] = await getStartServices();
const securitySolution = await context.securitySolution;
const riskEngineClient = securitySolution.getRiskEngineDataClient();
if (!taskManager) {
return siemResponse.error({
statusCode: 400,
body: {
message:
'Task Manager is unavailable, but is required to enable the risk engine. Please enable the taskManager plugin and try again.',
},
body: TASK_MANAGER_UNAVAILABLE_ERROR,
});
}
@ -49,6 +46,7 @@ export const riskEngineEnableRoute = (
return siemResponse.error({
statusCode: error.statusCode,
body: { message: error.message, full_error: JSON.stringify(e) },
bypassErrorFormat: true,
});
}
});

View file

@ -10,7 +10,7 @@ import { buildSiemResponse } from '@kbn/lists-plugin/server/routes/utils';
import { transformError } from '@kbn/securitysolution-es-utils';
import { RISK_ENGINE_INIT_URL, APP_ID } from '../../../../common/constants';
import type { StartPlugins } from '../../../plugin';
import { TASK_MANAGER_UNAVAILABLE_ERROR } from './translations';
import type { SecuritySolutionPluginRouter } from '../../../types';
export const riskEngineInitRoute = (
@ -36,10 +36,7 @@ export const riskEngineInitRoute = (
if (!taskManager) {
return siemResponse.error({
statusCode: 400,
body: {
message:
'Task Manager is unavailable, but is required to initialize the risk engine. Please enable the taskManager plugin and try again.',
},
body: TASK_MANAGER_UNAVAILABLE_ERROR,
});
}
@ -67,6 +64,7 @@ export const riskEngineInitRoute = (
message: initResultResponse.errors.join('\n'),
full_error: initResultResponse,
},
bypassErrorFormat: true,
});
}
return response.ok({ body: { result: initResultResponse } });
@ -76,6 +74,7 @@ export const riskEngineInitRoute = (
return siemResponse.error({
statusCode: error.statusCode,
body: { message: error.message, full_error: JSON.stringify(e) },
bypassErrorFormat: true,
});
}
});

View file

@ -44,6 +44,7 @@ export const riskEngineStatusRoute = (router: SecuritySolutionPluginRouter) => {
return siemResponse.error({
statusCode: error.statusCode,
body: { message: error.message, full_error: JSON.stringify(e) },
bypassErrorFormat: true,
});
}
});

View file

@ -89,6 +89,7 @@ export const riskScoreCalculationRoute = (router: SecuritySolutionPluginRouter,
return siemResponse.error({
statusCode: error.statusCode,
body: { message: error.message, full_error: JSON.stringify(e) },
bypassErrorFormat: true,
});
}
}

View file

@ -91,6 +91,7 @@ export const riskScorePreviewRoute = (router: SecuritySolutionPluginRouter, logg
return siemResponse.error({
statusCode: error.statusCode,
body: { message: error.message, full_error: JSON.stringify(e) },
bypassErrorFormat: true,
});
}
}

View file

@ -0,0 +1,16 @@
/*
* 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 { i18n } from '@kbn/i18n';
export const TASK_MANAGER_UNAVAILABLE_ERROR = i18n.translate(
'xpack.securitySolution.api.riskEngine.taskManagerUnavailable',
{
defaultMessage:
'Task Manager is unavailable, but is required by the risk engine. Please enable the taskManager plugin and try again.',
}
);

View file

@ -78,19 +78,15 @@ export interface InitRiskEngineResponse {
export interface InitRiskEngineError {
body: {
message: {
message: string;
full_error: InitRiskEngineResultResponse | undefined;
} & string;
message: string;
full_error: InitRiskEngineResultResponse | undefined;
};
}
export interface EnableDisableRiskEngineErrorResponse {
body: {
message: {
message: string;
full_error: string;
};
message: string;
full_error: string;
};
}