diff --git a/x-pack/plugins/case/server/client/alerts/get.ts b/x-pack/plugins/case/server/client/alerts/get.ts index a7ca5d9742c6bb..0b2663b7372041 100644 --- a/x-pack/plugins/case/server/client/alerts/get.ts +++ b/x-pack/plugins/case/server/client/alerts/get.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ElasticsearchClient } from 'kibana/server'; +import { ElasticsearchClient, Logger } from 'kibana/server'; import { AlertServiceContract } from '../../services'; import { CaseClientGetAlertsResponse } from './types'; @@ -14,6 +14,7 @@ interface GetParams { ids: string[]; indices: Set; scopedClusterClient: ElasticsearchClient; + logger: Logger; } export const get = async ({ @@ -21,12 +22,13 @@ export const get = async ({ ids, indices, scopedClusterClient, + logger, }: GetParams): Promise => { if (ids.length === 0 || indices.size <= 0) { return []; } - const alerts = await alertsService.getAlerts({ ids, indices, scopedClusterClient }); + const alerts = await alertsService.getAlerts({ ids, indices, scopedClusterClient, logger }); if (!alerts) { return []; } diff --git a/x-pack/plugins/case/server/client/alerts/update_status.test.ts b/x-pack/plugins/case/server/client/alerts/update_status.test.ts index c8df1c8ab74f36..b3ed3c2b84a993 100644 --- a/x-pack/plugins/case/server/client/alerts/update_status.test.ts +++ b/x-pack/plugins/case/server/client/alerts/update_status.test.ts @@ -22,6 +22,7 @@ describe('updateAlertsStatus', () => { expect(caseClient.services.alertsService.updateAlertsStatus).toHaveBeenCalledWith({ scopedClusterClient: expect.anything(), + logger: expect.anything(), ids: ['alert-id-1'], indices: new Set(['.siem-signals']), status: CaseStatuses.closed, diff --git a/x-pack/plugins/case/server/client/alerts/update_status.ts b/x-pack/plugins/case/server/client/alerts/update_status.ts index cb18bd4fc16e3e..2194c3a18afdd6 100644 --- a/x-pack/plugins/case/server/client/alerts/update_status.ts +++ b/x-pack/plugins/case/server/client/alerts/update_status.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ElasticsearchClient } from 'src/core/server'; +import { ElasticsearchClient, Logger } from 'src/core/server'; import { CaseStatuses } from '../../../common/api'; import { AlertServiceContract } from '../../services'; @@ -15,6 +15,7 @@ interface UpdateAlertsStatusArgs { status: CaseStatuses; indices: Set; scopedClusterClient: ElasticsearchClient; + logger: Logger; } export const updateAlertsStatus = async ({ @@ -23,6 +24,7 @@ export const updateAlertsStatus = async ({ status, indices, scopedClusterClient, + logger, }: UpdateAlertsStatusArgs): Promise => { - await alertsService.updateAlertsStatus({ ids, status, indices, scopedClusterClient }); + await alertsService.updateAlertsStatus({ ids, status, indices, scopedClusterClient, logger }); }; diff --git a/x-pack/plugins/case/server/client/cases/create.test.ts b/x-pack/plugins/case/server/client/cases/create.test.ts index 3016a57f218757..e8cc1a8898f047 100644 --- a/x-pack/plugins/case/server/client/cases/create.test.ts +++ b/x-pack/plugins/case/server/client/cases/create.test.ts @@ -6,6 +6,7 @@ */ import { ConnectorTypes, CaseStatuses, CaseType, CaseClientPostRequest } from '../../../common/api'; +import { isCaseError } from '../../common/error'; import { createMockSavedObjectsRepository, @@ -276,14 +277,16 @@ describe('create', () => { caseSavedObject: mockCases, }); const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); - caseClient.client - // @ts-expect-error - .create({ theCase: postCase }) - .catch((e) => { - expect(e).not.toBeNull(); - expect(e.isBoom).toBe(true); - expect(e.output.statusCode).toBe(400); - }); + return ( + caseClient.client + // @ts-expect-error + .create({ theCase: postCase }) + .catch((e) => { + expect(e).not.toBeNull(); + expect(e.isBoom).toBe(true); + expect(e.output.statusCode).toBe(400); + }) + ); }); test('it throws when missing description', async () => { @@ -303,14 +306,16 @@ describe('create', () => { caseSavedObject: mockCases, }); const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); - caseClient.client - // @ts-expect-error - .create({ theCase: postCase }) - .catch((e) => { - expect(e).not.toBeNull(); - expect(e.isBoom).toBe(true); - expect(e.output.statusCode).toBe(400); - }); + return ( + caseClient.client + // @ts-expect-error + .create({ theCase: postCase }) + .catch((e) => { + expect(e).not.toBeNull(); + expect(e.isBoom).toBe(true); + expect(e.output.statusCode).toBe(400); + }) + ); }); test('it throws when missing tags', async () => { @@ -330,14 +335,16 @@ describe('create', () => { caseSavedObject: mockCases, }); const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); - caseClient.client - // @ts-expect-error - .create({ theCase: postCase }) - .catch((e) => { - expect(e).not.toBeNull(); - expect(e.isBoom).toBe(true); - expect(e.output.statusCode).toBe(400); - }); + return ( + caseClient.client + // @ts-expect-error + .create({ theCase: postCase }) + .catch((e) => { + expect(e).not.toBeNull(); + expect(e.isBoom).toBe(true); + expect(e.output.statusCode).toBe(400); + }) + ); }); test('it throws when missing connector ', async () => { @@ -352,14 +359,16 @@ describe('create', () => { caseSavedObject: mockCases, }); const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); - caseClient.client - // @ts-expect-error - .create({ theCase: postCase }) - .catch((e) => { - expect(e).not.toBeNull(); - expect(e.isBoom).toBe(true); - expect(e.output.statusCode).toBe(400); - }); + return ( + caseClient.client + // @ts-expect-error + .create({ theCase: postCase }) + .catch((e) => { + expect(e).not.toBeNull(); + expect(e.isBoom).toBe(true); + expect(e.output.statusCode).toBe(400); + }) + ); }); test('it throws when connector missing the right fields', async () => { @@ -380,14 +389,16 @@ describe('create', () => { caseSavedObject: mockCases, }); const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); - caseClient.client - // @ts-expect-error - .create({ theCase: postCase }) - .catch((e) => { - expect(e).not.toBeNull(); - expect(e.isBoom).toBe(true); - expect(e.output.statusCode).toBe(400); - }); + return ( + caseClient.client + // @ts-expect-error + .create({ theCase: postCase }) + .catch((e) => { + expect(e).not.toBeNull(); + expect(e.isBoom).toBe(true); + expect(e.output.statusCode).toBe(400); + }) + ); }); test('it throws if you passing status for a new case', async () => { @@ -413,7 +424,7 @@ describe('create', () => { caseSavedObject: mockCases, }); const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); - caseClient.client.create(postCase).catch((e) => { + return caseClient.client.create(postCase).catch((e) => { expect(e).not.toBeNull(); expect(e.isBoom).toBe(true); expect(e.output.statusCode).toBe(400); @@ -441,10 +452,12 @@ describe('create', () => { }); const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); - caseClient.client.create(postCase).catch((e) => { + return caseClient.client.create(postCase).catch((e) => { expect(e).not.toBeNull(); - expect(e.isBoom).toBe(true); - expect(e.output.statusCode).toBe(400); + expect(isCaseError(e)).toBeTruthy(); + const boomErr = e.boomify(); + expect(boomErr.isBoom).toBe(true); + expect(boomErr.output.statusCode).toBe(400); }); }); }); diff --git a/x-pack/plugins/case/server/client/cases/create.ts b/x-pack/plugins/case/server/client/cases/create.ts index ee47c59072fdd9..f88924483e0b87 100644 --- a/x-pack/plugins/case/server/client/cases/create.ts +++ b/x-pack/plugins/case/server/client/cases/create.ts @@ -10,7 +10,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; -import { SavedObjectsClientContract } from 'src/core/server'; +import { SavedObjectsClientContract, Logger } from 'src/core/server'; import { flattenCaseSavedObject, transformNewCase } from '../../routes/api/utils'; import { @@ -34,6 +34,7 @@ import { CaseServiceSetup, CaseUserActionServiceSetup, } from '../../services'; +import { createCaseError } from '../../common/error'; interface CreateCaseArgs { caseConfigureService: CaseConfigureServiceSetup; @@ -42,8 +43,12 @@ interface CreateCaseArgs { savedObjectsClient: SavedObjectsClientContract; userActionService: CaseUserActionServiceSetup; theCase: CasePostRequest; + logger: Logger; } +/** + * Creates a new case. + */ export const create = async ({ savedObjectsClient, caseService, @@ -51,6 +56,7 @@ export const create = async ({ userActionService, user, theCase, + logger, }: CreateCaseArgs): Promise => { // default to an individual case if the type is not defined. const { type = CaseType.individual, ...nonTypeCaseFields } = theCase; @@ -60,41 +66,45 @@ export const create = async ({ fold(throwErrors(Boom.badRequest), identity) ); - // eslint-disable-next-line @typescript-eslint/naming-convention - const { username, full_name, email } = user; - const createdDate = new Date().toISOString(); - const myCaseConfigure = await caseConfigureService.find({ client: savedObjectsClient }); - const caseConfigureConnector = getConnectorFromConfiguration(myCaseConfigure); - - const newCase = await caseService.postNewCase({ - client: savedObjectsClient, - attributes: transformNewCase({ - createdDate, - newCase: query, - username, - full_name, - email, - connector: transformCaseConnectorToEsConnector(query.connector ?? caseConfigureConnector), - }), - }); + try { + // eslint-disable-next-line @typescript-eslint/naming-convention + const { username, full_name, email } = user; + const createdDate = new Date().toISOString(); + const myCaseConfigure = await caseConfigureService.find({ client: savedObjectsClient }); + const caseConfigureConnector = getConnectorFromConfiguration(myCaseConfigure); - await userActionService.postUserActions({ - client: savedObjectsClient, - actions: [ - buildCaseUserActionItem({ - action: 'create', - actionAt: createdDate, - actionBy: { username, full_name, email }, - caseId: newCase.id, - fields: ['description', 'status', 'tags', 'title', 'connector', 'settings'], - newValue: JSON.stringify(query), + const newCase = await caseService.postNewCase({ + client: savedObjectsClient, + attributes: transformNewCase({ + createdDate, + newCase: query, + username, + full_name, + email, + connector: transformCaseConnectorToEsConnector(query.connector ?? caseConfigureConnector), }), - ], - }); + }); - return CaseResponseRt.encode( - flattenCaseSavedObject({ - savedObject: newCase, - }) - ); + await userActionService.postUserActions({ + client: savedObjectsClient, + actions: [ + buildCaseUserActionItem({ + action: 'create', + actionAt: createdDate, + actionBy: { username, full_name, email }, + caseId: newCase.id, + fields: ['description', 'status', 'tags', 'title', 'connector', 'settings'], + newValue: JSON.stringify(query), + }), + ], + }); + + return CaseResponseRt.encode( + flattenCaseSavedObject({ + savedObject: newCase, + }) + ); + } catch (error) { + throw createCaseError({ message: `Failed to create case: ${error}`, error, logger }); + } }; diff --git a/x-pack/plugins/case/server/client/cases/get.ts b/x-pack/plugins/case/server/client/cases/get.ts index ab0b97abbcb76a..fa556986ee8d34 100644 --- a/x-pack/plugins/case/server/client/cases/get.ts +++ b/x-pack/plugins/case/server/client/cases/get.ts @@ -5,11 +5,12 @@ * 2.0. */ -import { SavedObjectsClientContract } from 'kibana/server'; +import { SavedObjectsClientContract, Logger } from 'kibana/server'; import { flattenCaseSavedObject } from '../../routes/api/utils'; import { CaseResponseRt, CaseResponse } from '../../../common/api'; import { CaseServiceSetup } from '../../services'; import { countAlertsForID } from '../../common'; +import { createCaseError } from '../../common/error'; interface GetParams { savedObjectsClient: SavedObjectsClientContract; @@ -17,50 +18,59 @@ interface GetParams { id: string; includeComments?: boolean; includeSubCaseComments?: boolean; + logger: Logger; } +/** + * Retrieves a case and optionally its comments and sub case comments. + */ export const get = async ({ savedObjectsClient, caseService, id, + logger, includeComments = false, includeSubCaseComments = false, }: GetParams): Promise => { - const [theCase, subCasesForCaseId] = await Promise.all([ - caseService.getCase({ + try { + const [theCase, subCasesForCaseId] = await Promise.all([ + caseService.getCase({ + client: savedObjectsClient, + id, + }), + caseService.findSubCasesByCaseId({ client: savedObjectsClient, ids: [id] }), + ]); + + const subCaseIds = subCasesForCaseId.saved_objects.map((so) => so.id); + + if (!includeComments) { + return CaseResponseRt.encode( + flattenCaseSavedObject({ + savedObject: theCase, + subCaseIds, + }) + ); + } + const theComments = await caseService.getAllCaseComments({ client: savedObjectsClient, id, - }), - caseService.findSubCasesByCaseId({ client: savedObjectsClient, ids: [id] }), - ]); + options: { + sortField: 'created_at', + sortOrder: 'asc', + }, + includeSubCaseComments, + }); - const subCaseIds = subCasesForCaseId.saved_objects.map((so) => so.id); - - if (!includeComments) { return CaseResponseRt.encode( flattenCaseSavedObject({ savedObject: theCase, + comments: theComments.saved_objects, subCaseIds, + totalComment: theComments.total, + totalAlerts: countAlertsForID({ comments: theComments, id }), }) ); + } catch (error) { + throw createCaseError({ message: `Failed to get case id: ${id}: ${error}`, error, logger }); } - const theComments = await caseService.getAllCaseComments({ - client: savedObjectsClient, - id, - options: { - sortField: 'created_at', - sortOrder: 'asc', - }, - includeSubCaseComments, - }); - - return CaseResponseRt.encode( - flattenCaseSavedObject({ - savedObject: theCase, - comments: theComments.saved_objects, - subCaseIds, - totalComment: theComments.total, - totalAlerts: countAlertsForID({ comments: theComments, id }), - }) - ); }; diff --git a/x-pack/plugins/case/server/client/cases/push.ts b/x-pack/plugins/case/server/client/cases/push.ts index 1e0c246855d88b..352328ed1dd40d 100644 --- a/x-pack/plugins/case/server/client/cases/push.ts +++ b/x-pack/plugins/case/server/client/cases/push.ts @@ -5,11 +5,12 @@ * 2.0. */ -import Boom, { isBoom, Boom as BoomType } from '@hapi/boom'; +import Boom from '@hapi/boom'; import { SavedObjectsBulkUpdateResponse, SavedObjectsClientContract, SavedObjectsUpdateResponse, + Logger, } from 'kibana/server'; import { ActionResult, ActionsClient } from '../../../../actions/server'; import { flattenCaseSavedObject, getAlertIndicesAndIDs } from '../../routes/api/utils'; @@ -34,16 +35,7 @@ import { CaseUserActionServiceSetup, } from '../../services'; import { CaseClientHandler } from '../client'; - -const createError = (e: Error | BoomType, message: string): Error | BoomType => { - if (isBoom(e)) { - e.message = message; - e.output.payload.message = message; - return e; - } - - return Error(message); -}; +import { createCaseError } from '../../common/error'; interface PushParams { savedObjectsClient: SavedObjectsClientContract; @@ -55,6 +47,7 @@ interface PushParams { connectorId: string; caseClient: CaseClientHandler; actionsClient: ActionsClient; + logger: Logger; } export const push = async ({ @@ -67,6 +60,7 @@ export const push = async ({ connectorId, caseId, user, + logger, }: PushParams): Promise => { /* Start of push to external service */ let theCase: CaseResponse; @@ -84,7 +78,7 @@ export const push = async ({ ]); } catch (e) { const message = `Error getting case and/or connector and/or user actions: ${e.message}`; - throw createError(e, message); + throw createCaseError({ message, error: e, logger }); } // We need to change the logic when we support subcases @@ -102,7 +96,11 @@ export const push = async ({ indices, }); } catch (e) { - throw new Error(`Error getting alerts for case with id ${theCase.id}: ${e.message}`); + throw createCaseError({ + message: `Error getting alerts for case with id ${theCase.id}: ${e.message}`, + logger, + error: e, + }); } try { @@ -113,7 +111,7 @@ export const push = async ({ }); } catch (e) { const message = `Error getting mapping for connector with id ${connector.id}: ${e.message}`; - throw createError(e, message); + throw createCaseError({ message, error: e, logger }); } try { @@ -127,7 +125,7 @@ export const push = async ({ }); } catch (e) { const message = `Error creating incident for case with id ${theCase.id}: ${e.message}`; - throw createError(e, message); + throw createCaseError({ error: e, message, logger }); } const pushRes = await actionsClient.execute({ @@ -171,7 +169,7 @@ export const push = async ({ ]); } catch (e) { const message = `Error getting user and/or case and/or case configuration and/or case comments: ${e.message}`; - throw createError(e, message); + throw createCaseError({ error: e, message, logger }); } // eslint-disable-next-line @typescript-eslint/naming-convention @@ -257,7 +255,7 @@ export const push = async ({ ]); } catch (e) { const message = `Error updating case and/or comments and/or creating user action: ${e.message}`; - throw createError(e, message); + throw createCaseError({ error: e, message, logger }); } /* End of update case with push information */ diff --git a/x-pack/plugins/case/server/client/cases/update.test.ts b/x-pack/plugins/case/server/client/cases/update.test.ts index 7a3e4458f25c54..752b0ab369de09 100644 --- a/x-pack/plugins/case/server/client/cases/update.test.ts +++ b/x-pack/plugins/case/server/client/cases/update.test.ts @@ -6,6 +6,7 @@ */ import { ConnectorTypes, CasesPatchRequest, CaseStatuses } from '../../../common/api'; +import { isCaseError } from '../../common/error'; import { createMockSavedObjectsRepository, mockCaseNoConnectorId, @@ -640,14 +641,16 @@ describe('update', () => { }); const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); - caseClient.client - // @ts-expect-error - .update({ cases: patchCases }) - .catch((e) => { - expect(e).not.toBeNull(); - expect(e.isBoom).toBe(true); - expect(e.output.statusCode).toBe(400); - }); + return ( + caseClient.client + // @ts-expect-error + .update({ cases: patchCases }) + .catch((e) => { + expect(e).not.toBeNull(); + expect(e.isBoom).toBe(true); + expect(e.output.statusCode).toBe(400); + }) + ); }); test('it throws when missing version', async () => { @@ -671,18 +674,20 @@ describe('update', () => { }); const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); - caseClient.client - // @ts-expect-error - .update({ cases: patchCases }) - .catch((e) => { - expect(e).not.toBeNull(); - expect(e.isBoom).toBe(true); - expect(e.output.statusCode).toBe(400); - }); + return ( + caseClient.client + // @ts-expect-error + .update({ cases: patchCases }) + .catch((e) => { + expect(e).not.toBeNull(); + expect(e.isBoom).toBe(true); + expect(e.output.statusCode).toBe(400); + }) + ); }); test('it throws when fields are identical', async () => { - expect.assertions(4); + expect.assertions(5); const patchCases = { cases: [ { @@ -698,16 +703,18 @@ describe('update', () => { }); const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); - caseClient.client.update(patchCases).catch((e) => { + return caseClient.client.update(patchCases).catch((e) => { expect(e).not.toBeNull(); - expect(e.isBoom).toBe(true); - expect(e.output.statusCode).toBe(406); - expect(e.message).toBe('All update fields are identical to current version.'); + expect(isCaseError(e)).toBeTruthy(); + const boomErr = e.boomify(); + expect(boomErr.isBoom).toBe(true); + expect(boomErr.output.statusCode).toBe(406); + expect(boomErr.message).toContain('All update fields are identical to current version.'); }); }); test('it throws when case does not exist', async () => { - expect.assertions(4); + expect.assertions(5); const patchCases = { cases: [ { @@ -728,18 +735,20 @@ describe('update', () => { }); const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); - caseClient.client.update(patchCases).catch((e) => { + return caseClient.client.update(patchCases).catch((e) => { expect(e).not.toBeNull(); - expect(e.isBoom).toBe(true); - expect(e.output.statusCode).toBe(404); - expect(e.message).toBe( + expect(isCaseError(e)).toBeTruthy(); + const boomErr = e.boomify(); + expect(boomErr.isBoom).toBe(true); + expect(boomErr.output.statusCode).toBe(404); + expect(boomErr.message).toContain( 'These cases not-exists do not exist. Please check you have the correct ids.' ); }); }); test('it throws when cases conflicts', async () => { - expect.assertions(4); + expect.assertions(5); const patchCases = { cases: [ { @@ -755,11 +764,13 @@ describe('update', () => { }); const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); - caseClient.client.update(patchCases).catch((e) => { + return caseClient.client.update(patchCases).catch((e) => { expect(e).not.toBeNull(); - expect(e.isBoom).toBe(true); - expect(e.output.statusCode).toBe(409); - expect(e.message).toBe( + expect(isCaseError(e)).toBeTruthy(); + const boomErr = e.boomify(); + expect(boomErr.isBoom).toBe(true); + expect(boomErr.output.statusCode).toBe(409); + expect(boomErr.message).toContain( 'These cases mock-id-1 has been updated. Please refresh before saving additional updates.' ); }); diff --git a/x-pack/plugins/case/server/client/cases/update.ts b/x-pack/plugins/case/server/client/cases/update.ts index a4ca2b4cbdef98..36318f03bd33f5 100644 --- a/x-pack/plugins/case/server/client/cases/update.ts +++ b/x-pack/plugins/case/server/client/cases/update.ts @@ -15,6 +15,7 @@ import { SavedObjectsClientContract, SavedObjectsFindResponse, SavedObjectsFindResult, + Logger, } from 'kibana/server'; import { AlertInfo, @@ -53,6 +54,7 @@ import { } from '../../saved_object_types'; import { CaseClientHandler } from '..'; import { addAlertInfoToStatusMap } from '../../common'; +import { createCaseError } from '../../common/error'; /** * Throws an error if any of the requests attempt to update a collection style cases' status field. @@ -325,6 +327,7 @@ interface UpdateArgs { user: User; caseClient: CaseClientHandler; cases: CasesPatchRequest; + logger: Logger; } export const update = async ({ @@ -334,175 +337,189 @@ export const update = async ({ user, caseClient, cases, + logger, }: UpdateArgs): Promise => { const query = pipe( excess(CasesPatchRequestRt).decode(cases), fold(throwErrors(Boom.badRequest), identity) ); - const myCases = await caseService.getCases({ - client: savedObjectsClient, - caseIds: query.cases.map((q) => q.id), - }); - - let nonExistingCases: CasePatchRequest[] = []; - const conflictedCases = query.cases.filter((q) => { - const myCase = myCases.saved_objects.find((c) => c.id === q.id); + try { + const myCases = await caseService.getCases({ + client: savedObjectsClient, + caseIds: query.cases.map((q) => q.id), + }); - if (myCase && myCase.error) { - nonExistingCases = [...nonExistingCases, q]; - return false; - } - return myCase == null || myCase?.version !== q.version; - }); + let nonExistingCases: CasePatchRequest[] = []; + const conflictedCases = query.cases.filter((q) => { + const myCase = myCases.saved_objects.find((c) => c.id === q.id); - if (nonExistingCases.length > 0) { - throw Boom.notFound( - `These cases ${nonExistingCases - .map((c) => c.id) - .join(', ')} do not exist. Please check you have the correct ids.` - ); - } + if (myCase && myCase.error) { + nonExistingCases = [...nonExistingCases, q]; + return false; + } + return myCase == null || myCase?.version !== q.version; + }); - if (conflictedCases.length > 0) { - throw Boom.conflict( - `These cases ${conflictedCases - .map((c) => c.id) - .join(', ')} has been updated. Please refresh before saving additional updates.` - ); - } + if (nonExistingCases.length > 0) { + throw Boom.notFound( + `These cases ${nonExistingCases + .map((c) => c.id) + .join(', ')} do not exist. Please check you have the correct ids.` + ); + } - const updateCases: ESCasePatchRequest[] = query.cases.map((updateCase) => { - const currentCase = myCases.saved_objects.find((c) => c.id === updateCase.id); - const { connector, ...thisCase } = updateCase; - return currentCase != null - ? getCaseToUpdate(currentCase.attributes, { - ...thisCase, - ...(connector != null - ? { connector: transformCaseConnectorToEsConnector(connector) } - : {}), - }) - : { id: thisCase.id, version: thisCase.version }; - }); + if (conflictedCases.length > 0) { + throw Boom.conflict( + `These cases ${conflictedCases + .map((c) => c.id) + .join(', ')} has been updated. Please refresh before saving additional updates.` + ); + } - const updateFilterCases = updateCases.filter((updateCase) => { - const { id, version, ...updateCaseAttributes } = updateCase; - return Object.keys(updateCaseAttributes).length > 0; - }); + const updateCases: ESCasePatchRequest[] = query.cases.map((updateCase) => { + const currentCase = myCases.saved_objects.find((c) => c.id === updateCase.id); + const { connector, ...thisCase } = updateCase; + return currentCase != null + ? getCaseToUpdate(currentCase.attributes, { + ...thisCase, + ...(connector != null + ? { connector: transformCaseConnectorToEsConnector(connector) } + : {}), + }) + : { id: thisCase.id, version: thisCase.version }; + }); - if (updateFilterCases.length <= 0) { - throw Boom.notAcceptable('All update fields are identical to current version.'); - } + const updateFilterCases = updateCases.filter((updateCase) => { + const { id, version, ...updateCaseAttributes } = updateCase; + return Object.keys(updateCaseAttributes).length > 0; + }); - const casesMap = myCases.saved_objects.reduce((acc, so) => { - acc.set(so.id, so); - return acc; - }, new Map>()); + if (updateFilterCases.length <= 0) { + throw Boom.notAcceptable('All update fields are identical to current version.'); + } - throwIfUpdateStatusOfCollection(updateFilterCases, casesMap); - throwIfUpdateTypeCollectionToIndividual(updateFilterCases, casesMap); - await throwIfInvalidUpdateOfTypeWithAlerts({ - requests: updateFilterCases, - caseService, - client: savedObjectsClient, - }); + const casesMap = myCases.saved_objects.reduce((acc, so) => { + acc.set(so.id, so); + return acc; + }, new Map>()); + + throwIfUpdateStatusOfCollection(updateFilterCases, casesMap); + throwIfUpdateTypeCollectionToIndividual(updateFilterCases, casesMap); + await throwIfInvalidUpdateOfTypeWithAlerts({ + requests: updateFilterCases, + caseService, + client: savedObjectsClient, + }); - // eslint-disable-next-line @typescript-eslint/naming-convention - const { username, full_name, email } = user; - const updatedDt = new Date().toISOString(); - const updatedCases = await caseService.patchCases({ - client: savedObjectsClient, - cases: updateFilterCases.map((thisCase) => { - const { id: caseId, version, ...updateCaseAttributes } = thisCase; - let closedInfo = {}; - if (updateCaseAttributes.status && updateCaseAttributes.status === CaseStatuses.closed) { - closedInfo = { - closed_at: updatedDt, - closed_by: { email, full_name, username }, - }; - } else if ( - updateCaseAttributes.status && - (updateCaseAttributes.status === CaseStatuses.open || - updateCaseAttributes.status === CaseStatuses['in-progress']) - ) { - closedInfo = { - closed_at: null, - closed_by: null, + // eslint-disable-next-line @typescript-eslint/naming-convention + const { username, full_name, email } = user; + const updatedDt = new Date().toISOString(); + const updatedCases = await caseService.patchCases({ + client: savedObjectsClient, + cases: updateFilterCases.map((thisCase) => { + const { id: caseId, version, ...updateCaseAttributes } = thisCase; + let closedInfo = {}; + if (updateCaseAttributes.status && updateCaseAttributes.status === CaseStatuses.closed) { + closedInfo = { + closed_at: updatedDt, + closed_by: { email, full_name, username }, + }; + } else if ( + updateCaseAttributes.status && + (updateCaseAttributes.status === CaseStatuses.open || + updateCaseAttributes.status === CaseStatuses['in-progress']) + ) { + closedInfo = { + closed_at: null, + closed_by: null, + }; + } + return { + caseId, + updatedAttributes: { + ...updateCaseAttributes, + ...closedInfo, + updated_at: updatedDt, + updated_by: { email, full_name, username }, + }, + version, }; - } - return { - caseId, - updatedAttributes: { - ...updateCaseAttributes, - ...closedInfo, - updated_at: updatedDt, - updated_by: { email, full_name, username }, - }, - version, - }; - }), - }); + }), + }); - // If a status update occurred and the case is synced then we need to update all alerts' status - // attached to the case to the new status. - const casesWithStatusChangedAndSynced = updateFilterCases.filter((caseToUpdate) => { - const currentCase = myCases.saved_objects.find((c) => c.id === caseToUpdate.id); - return ( - currentCase != null && - caseToUpdate.status != null && - currentCase.attributes.status !== caseToUpdate.status && - currentCase.attributes.settings.syncAlerts - ); - }); + // If a status update occurred and the case is synced then we need to update all alerts' status + // attached to the case to the new status. + const casesWithStatusChangedAndSynced = updateFilterCases.filter((caseToUpdate) => { + const currentCase = myCases.saved_objects.find((c) => c.id === caseToUpdate.id); + return ( + currentCase != null && + caseToUpdate.status != null && + currentCase.attributes.status !== caseToUpdate.status && + currentCase.attributes.settings.syncAlerts + ); + }); - // If syncAlerts setting turned on we need to update all alerts' status - // attached to the case to the current status. - const casesWithSyncSettingChangedToOn = updateFilterCases.filter((caseToUpdate) => { - const currentCase = myCases.saved_objects.find((c) => c.id === caseToUpdate.id); - return ( - currentCase != null && - caseToUpdate.settings?.syncAlerts != null && - currentCase.attributes.settings.syncAlerts !== caseToUpdate.settings.syncAlerts && - caseToUpdate.settings.syncAlerts - ); - }); + // If syncAlerts setting turned on we need to update all alerts' status + // attached to the case to the current status. + const casesWithSyncSettingChangedToOn = updateFilterCases.filter((caseToUpdate) => { + const currentCase = myCases.saved_objects.find((c) => c.id === caseToUpdate.id); + return ( + currentCase != null && + caseToUpdate.settings?.syncAlerts != null && + currentCase.attributes.settings.syncAlerts !== caseToUpdate.settings.syncAlerts && + caseToUpdate.settings.syncAlerts + ); + }); - // Update the alert's status to match any case status or sync settings changes - await updateAlerts({ - casesWithStatusChangedAndSynced, - casesWithSyncSettingChangedToOn, - caseService, - client: savedObjectsClient, - caseClient, - casesMap, - }); + // Update the alert's status to match any case status or sync settings changes + await updateAlerts({ + casesWithStatusChangedAndSynced, + casesWithSyncSettingChangedToOn, + caseService, + client: savedObjectsClient, + caseClient, + casesMap, + }); - const returnUpdatedCase = myCases.saved_objects - .filter((myCase) => - updatedCases.saved_objects.some((updatedCase) => updatedCase.id === myCase.id) - ) - .map((myCase) => { - const updatedCase = updatedCases.saved_objects.find((c) => c.id === myCase.id); - return flattenCaseSavedObject({ - savedObject: { - ...myCase, - ...updatedCase, - attributes: { ...myCase.attributes, ...updatedCase?.attributes }, - references: myCase.references, - version: updatedCase?.version ?? myCase.version, - }, + const returnUpdatedCase = myCases.saved_objects + .filter((myCase) => + updatedCases.saved_objects.some((updatedCase) => updatedCase.id === myCase.id) + ) + .map((myCase) => { + const updatedCase = updatedCases.saved_objects.find((c) => c.id === myCase.id); + return flattenCaseSavedObject({ + savedObject: { + ...myCase, + ...updatedCase, + attributes: { ...myCase.attributes, ...updatedCase?.attributes }, + references: myCase.references, + version: updatedCase?.version ?? myCase.version, + }, + }); }); - }); - await userActionService.postUserActions({ - client: savedObjectsClient, - actions: buildCaseUserActions({ - originalCases: myCases.saved_objects, - updatedCases: updatedCases.saved_objects, - actionDate: updatedDt, - actionBy: { email, full_name, username }, - }), - }); + await userActionService.postUserActions({ + client: savedObjectsClient, + actions: buildCaseUserActions({ + originalCases: myCases.saved_objects, + updatedCases: updatedCases.saved_objects, + actionDate: updatedDt, + actionBy: { email, full_name, username }, + }), + }); - return CasesResponseRt.encode(returnUpdatedCase); + return CasesResponseRt.encode(returnUpdatedCase); + } catch (error) { + const idVersions = cases.cases.map((caseInfo) => ({ + id: caseInfo.id, + version: caseInfo.version, + })); + + throw createCaseError({ + message: `Failed to update case, ids: ${JSON.stringify(idVersions)}: ${error}`, + error, + logger, + }); + } }; diff --git a/x-pack/plugins/case/server/client/client.ts b/x-pack/plugins/case/server/client/client.ts index c684548decbe6f..c34c3942b18d0e 100644 --- a/x-pack/plugins/case/server/client/client.ts +++ b/x-pack/plugins/case/server/client/client.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ElasticsearchClient, SavedObjectsClientContract } from 'src/core/server'; +import { ElasticsearchClient, SavedObjectsClientContract, Logger } from 'src/core/server'; import { CaseClientFactoryArguments, CaseClient, @@ -36,6 +36,7 @@ import { get } from './cases/get'; import { get as getUserActions } from './user_actions/get'; import { get as getAlerts } from './alerts/get'; import { push } from './cases/push'; +import { createCaseError } from '../common/error'; /** * This class is a pass through for common case functionality (like creating, get a case). @@ -49,6 +50,7 @@ export class CaseClientHandler implements CaseClient { private readonly _savedObjectsClient: SavedObjectsClientContract; private readonly _userActionService: CaseUserActionServiceSetup; private readonly _alertsService: AlertServiceContract; + private readonly logger: Logger; constructor(clientArgs: CaseClientFactoryArguments) { this._scopedClusterClient = clientArgs.scopedClusterClient; @@ -59,96 +61,190 @@ export class CaseClientHandler implements CaseClient { this._savedObjectsClient = clientArgs.savedObjectsClient; this._userActionService = clientArgs.userActionService; this._alertsService = clientArgs.alertsService; + this.logger = clientArgs.logger; } public async create(caseInfo: CasePostRequest) { - return create({ - savedObjectsClient: this._savedObjectsClient, - caseService: this._caseService, - caseConfigureService: this._caseConfigureService, - userActionService: this._userActionService, - user: this.user, - theCase: caseInfo, - }); + try { + return create({ + savedObjectsClient: this._savedObjectsClient, + caseService: this._caseService, + caseConfigureService: this._caseConfigureService, + userActionService: this._userActionService, + user: this.user, + theCase: caseInfo, + logger: this.logger, + }); + } catch (error) { + throw createCaseError({ + message: `Failed to create a new case using client: ${error}`, + error, + logger: this.logger, + }); + } } public async update(cases: CasesPatchRequest) { - return update({ - savedObjectsClient: this._savedObjectsClient, - caseService: this._caseService, - userActionService: this._userActionService, - user: this.user, - cases, - caseClient: this, - }); + try { + return update({ + savedObjectsClient: this._savedObjectsClient, + caseService: this._caseService, + userActionService: this._userActionService, + user: this.user, + cases, + caseClient: this, + logger: this.logger, + }); + } catch (error) { + const caseIDVersions = cases.cases.map((caseInfo) => ({ + id: caseInfo.id, + version: caseInfo.version, + })); + throw createCaseError({ + message: `Failed to update cases using client: ${JSON.stringify(caseIDVersions)}: ${error}`, + error, + logger: this.logger, + }); + } } public async addComment({ caseId, comment }: CaseClientAddComment) { - return addComment({ - savedObjectsClient: this._savedObjectsClient, - caseService: this._caseService, - userActionService: this._userActionService, - caseClient: this, - caseId, - comment, - user: this.user, - }); + try { + return addComment({ + savedObjectsClient: this._savedObjectsClient, + caseService: this._caseService, + userActionService: this._userActionService, + caseClient: this, + caseId, + comment, + user: this.user, + logger: this.logger, + }); + } catch (error) { + throw createCaseError({ + message: `Failed to add comment using client case id: ${caseId}: ${error}`, + error, + logger: this.logger, + }); + } } public async getFields(fields: ConfigureFields) { - return getFields(fields); + try { + return getFields(fields); + } catch (error) { + throw createCaseError({ + message: `Failed to retrieve fields using client: ${error}`, + error, + logger: this.logger, + }); + } } public async getMappings(args: MappingsClient) { - return getMappings({ - ...args, - savedObjectsClient: this._savedObjectsClient, - connectorMappingsService: this._connectorMappingsService, - caseClient: this, - }); + try { + return getMappings({ + ...args, + savedObjectsClient: this._savedObjectsClient, + connectorMappingsService: this._connectorMappingsService, + caseClient: this, + logger: this.logger, + }); + } catch (error) { + throw createCaseError({ + message: `Failed to get mappings using client: ${error}`, + error, + logger: this.logger, + }); + } } public async updateAlertsStatus(args: CaseClientUpdateAlertsStatus) { - return updateAlertsStatus({ - ...args, - alertsService: this._alertsService, - scopedClusterClient: this._scopedClusterClient, - }); + try { + return updateAlertsStatus({ + ...args, + alertsService: this._alertsService, + scopedClusterClient: this._scopedClusterClient, + logger: this.logger, + }); + } catch (error) { + throw createCaseError({ + message: `Failed to update alerts status using client ids: ${JSON.stringify( + args.ids + )} \nindices: ${JSON.stringify([...args.indices])} \nstatus: ${args.status}: ${error}`, + error, + logger: this.logger, + }); + } } public async get(args: CaseClientGet) { - return get({ - ...args, - caseService: this._caseService, - savedObjectsClient: this._savedObjectsClient, - }); + try { + return get({ + ...args, + caseService: this._caseService, + savedObjectsClient: this._savedObjectsClient, + logger: this.logger, + }); + } catch (error) { + this.logger.error(`Failed to get case using client id: ${args.id}: ${error}`); + throw error; + } } public async getUserActions(args: CaseClientGetUserActions) { - return getUserActions({ - ...args, - savedObjectsClient: this._savedObjectsClient, - userActionService: this._userActionService, - }); + try { + return getUserActions({ + ...args, + savedObjectsClient: this._savedObjectsClient, + userActionService: this._userActionService, + }); + } catch (error) { + throw createCaseError({ + message: `Failed to get user actions using client id: ${args.caseId}: ${error}`, + error, + logger: this.logger, + }); + } } public async getAlerts(args: CaseClientGetAlerts) { - return getAlerts({ - ...args, - alertsService: this._alertsService, - scopedClusterClient: this._scopedClusterClient, - }); + try { + return getAlerts({ + ...args, + alertsService: this._alertsService, + scopedClusterClient: this._scopedClusterClient, + logger: this.logger, + }); + } catch (error) { + throw createCaseError({ + message: `Failed to get alerts using client ids: ${JSON.stringify( + args.ids + )} \nindices: ${JSON.stringify([...args.indices])}: ${error}`, + error, + logger: this.logger, + }); + } } public async push(args: CaseClientPush) { - return push({ - ...args, - savedObjectsClient: this._savedObjectsClient, - caseService: this._caseService, - userActionService: this._userActionService, - user: this.user, - caseClient: this, - caseConfigureService: this._caseConfigureService, - }); + try { + return push({ + ...args, + savedObjectsClient: this._savedObjectsClient, + caseService: this._caseService, + userActionService: this._userActionService, + user: this.user, + caseClient: this, + caseConfigureService: this._caseConfigureService, + logger: this.logger, + }); + } catch (error) { + throw createCaseError({ + message: `Failed to push case using client id: ${args.caseId}: ${error}`, + error, + logger: this.logger, + }); + } } } diff --git a/x-pack/plugins/case/server/client/comments/add.test.ts b/x-pack/plugins/case/server/client/comments/add.test.ts index c9b1e4fd132722..123ecec6abea33 100644 --- a/x-pack/plugins/case/server/client/comments/add.test.ts +++ b/x-pack/plugins/case/server/client/comments/add.test.ts @@ -7,6 +7,7 @@ import { omit } from 'lodash/fp'; import { CommentType } from '../../../common/api'; +import { isCaseError } from '../../common/error'; import { createMockSavedObjectsRepository, mockCaseComments, @@ -297,7 +298,7 @@ describe('addComment', () => { caseCommentSavedObject: mockCaseComments, }); const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); - caseClient.client + return caseClient.client .addComment({ caseId: 'mock-id-1', // @ts-expect-error @@ -326,7 +327,7 @@ describe('addComment', () => { ['comment'].forEach((attribute) => { const requestAttributes = omit(attribute, allRequestAttributes); - caseClient.client + return caseClient.client .addComment({ caseId: 'mock-id-1', // @ts-expect-error @@ -353,7 +354,7 @@ describe('addComment', () => { const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); ['alertId', 'index'].forEach((attribute) => { - caseClient.client + return caseClient.client .addComment({ caseId: 'mock-id-1', comment: { @@ -387,7 +388,7 @@ describe('addComment', () => { ['alertId', 'index'].forEach((attribute) => { const requestAttributes = omit(attribute, allRequestAttributes); - caseClient.client + return caseClient.client .addComment({ caseId: 'mock-id-1', // @ts-expect-error @@ -414,7 +415,7 @@ describe('addComment', () => { const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); ['comment'].forEach((attribute) => { - caseClient.client + return caseClient.client .addComment({ caseId: 'mock-id-1', comment: { @@ -437,14 +438,14 @@ describe('addComment', () => { }); test('it throws when the case does not exists', async () => { - expect.assertions(3); + expect.assertions(4); const savedObjectsClient = createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, }); const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); - caseClient.client + return caseClient.client .addComment({ caseId: 'not-exists', comment: { @@ -454,20 +455,22 @@ describe('addComment', () => { }) .catch((e) => { expect(e).not.toBeNull(); - expect(e.isBoom).toBe(true); - expect(e.output.statusCode).toBe(404); + expect(isCaseError(e)).toBeTruthy(); + const boomErr = e.boomify(); + expect(boomErr.isBoom).toBe(true); + expect(boomErr.output.statusCode).toBe(404); }); }); test('it throws when postNewCase throws', async () => { - expect.assertions(3); + expect.assertions(4); const savedObjectsClient = createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, }); const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); - caseClient.client + return caseClient.client .addComment({ caseId: 'mock-id-1', comment: { @@ -477,13 +480,15 @@ describe('addComment', () => { }) .catch((e) => { expect(e).not.toBeNull(); - expect(e.isBoom).toBe(true); - expect(e.output.statusCode).toBe(400); + expect(isCaseError(e)).toBeTruthy(); + const boomErr = e.boomify(); + expect(boomErr.isBoom).toBe(true); + expect(boomErr.output.statusCode).toBe(400); }); }); test('it throws when the case is closed and the comment is of type alert', async () => { - expect.assertions(3); + expect.assertions(4); const savedObjectsClient = createMockSavedObjectsRepository({ caseSavedObject: mockCases, @@ -491,7 +496,7 @@ describe('addComment', () => { }); const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); - caseClient.client + return caseClient.client .addComment({ caseId: 'mock-id-4', comment: { @@ -506,8 +511,10 @@ describe('addComment', () => { }) .catch((e) => { expect(e).not.toBeNull(); - expect(e.isBoom).toBe(true); - expect(e.output.statusCode).toBe(400); + expect(isCaseError(e)).toBeTruthy(); + const boomErr = e.boomify(); + expect(boomErr.isBoom).toBe(true); + expect(boomErr.output.statusCode).toBe(400); }); }); }); diff --git a/x-pack/plugins/case/server/client/comments/add.ts b/x-pack/plugins/case/server/client/comments/add.ts index 4c1cc59a957509..d3d7047e71bd3c 100644 --- a/x-pack/plugins/case/server/client/comments/add.ts +++ b/x-pack/plugins/case/server/client/comments/add.ts @@ -10,7 +10,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; -import { SavedObject, SavedObjectsClientContract } from 'src/core/server'; +import { SavedObject, SavedObjectsClientContract, Logger } from 'src/core/server'; import { decodeCommentRequest, getAlertIds, @@ -38,6 +38,7 @@ import { import { CaseServiceSetup, CaseUserActionServiceSetup } from '../../services'; import { CommentableCase } from '../../common'; import { CaseClientHandler } from '..'; +import { createCaseError } from '../../common/error'; import { CASE_COMMENT_SAVED_OBJECT } from '../../saved_object_types'; import { MAX_GENERATED_ALERTS_PER_SUB_CASE } from '../../../common/constants'; @@ -104,6 +105,7 @@ interface AddCommentFromRuleArgs { savedObjectsClient: SavedObjectsClientContract; caseService: CaseServiceSetup; userActionService: CaseUserActionServiceSetup; + logger: Logger; } const addGeneratedAlerts = async ({ @@ -113,6 +115,7 @@ const addGeneratedAlerts = async ({ caseClient, caseId, comment, + logger, }: AddCommentFromRuleArgs): Promise => { const query = pipe( AlertCommentRequestRt.decode(comment), @@ -125,88 +128,104 @@ const addGeneratedAlerts = async ({ if (comment.type !== CommentType.generatedAlert) { throw Boom.internal('Attempting to add a non generated alert in the wrong context'); } - const createdDate = new Date().toISOString(); - const caseInfo = await caseService.getCase({ - client: savedObjectsClient, - id: caseId, - }); + try { + const createdDate = new Date().toISOString(); - if ( - query.type === CommentType.generatedAlert && - caseInfo.attributes.type !== CaseType.collection - ) { - throw Boom.badRequest('Sub case style alert comment cannot be added to an individual case'); - } + const caseInfo = await caseService.getCase({ + client: savedObjectsClient, + id: caseId, + }); - const userDetails: User = { - username: caseInfo.attributes.created_by?.username, - full_name: caseInfo.attributes.created_by?.full_name, - email: caseInfo.attributes.created_by?.email, - }; + if ( + query.type === CommentType.generatedAlert && + caseInfo.attributes.type !== CaseType.collection + ) { + throw Boom.badRequest('Sub case style alert comment cannot be added to an individual case'); + } - const subCase = await getSubCase({ - caseService, - savedObjectsClient, - caseId, - createdAt: createdDate, - userActionService, - user: userDetails, - }); + const userDetails: User = { + username: caseInfo.attributes.created_by?.username, + full_name: caseInfo.attributes.created_by?.full_name, + email: caseInfo.attributes.created_by?.email, + }; - const commentableCase = new CommentableCase({ - collection: caseInfo, - subCase, - soClient: savedObjectsClient, - service: caseService, - }); + const subCase = await getSubCase({ + caseService, + savedObjectsClient, + caseId, + createdAt: createdDate, + userActionService, + user: userDetails, + }); - const { - comment: newComment, - commentableCase: updatedCase, - } = await commentableCase.createComment({ createdDate, user: userDetails, commentReq: query }); - - if ( - (newComment.attributes.type === CommentType.alert || - newComment.attributes.type === CommentType.generatedAlert) && - caseInfo.attributes.settings.syncAlerts - ) { - const ids = getAlertIds(query); - await caseClient.updateAlertsStatus({ - ids, - status: subCase.attributes.status, - indices: new Set([ - ...(Array.isArray(newComment.attributes.index) - ? newComment.attributes.index - : [newComment.attributes.index]), - ]), + const commentableCase = new CommentableCase({ + logger, + collection: caseInfo, + subCase, + soClient: savedObjectsClient, + service: caseService, }); - } - await userActionService.postUserActions({ - client: savedObjectsClient, - actions: [ - buildCommentUserActionItem({ - action: 'create', - actionAt: createdDate, - actionBy: { ...userDetails }, - caseId: updatedCase.caseId, - subCaseId: updatedCase.subCaseId, - commentId: newComment.id, - fields: ['comment'], - newValue: JSON.stringify(query), - }), - ], - }); + const { + comment: newComment, + commentableCase: updatedCase, + } = await commentableCase.createComment({ createdDate, user: userDetails, commentReq: query }); + + if ( + (newComment.attributes.type === CommentType.alert || + newComment.attributes.type === CommentType.generatedAlert) && + caseInfo.attributes.settings.syncAlerts + ) { + const ids = getAlertIds(query); + await caseClient.updateAlertsStatus({ + ids, + status: subCase.attributes.status, + indices: new Set([ + ...(Array.isArray(newComment.attributes.index) + ? newComment.attributes.index + : [newComment.attributes.index]), + ]), + }); + } + + await userActionService.postUserActions({ + client: savedObjectsClient, + actions: [ + buildCommentUserActionItem({ + action: 'create', + actionAt: createdDate, + actionBy: { ...userDetails }, + caseId: updatedCase.caseId, + subCaseId: updatedCase.subCaseId, + commentId: newComment.id, + fields: ['comment'], + newValue: JSON.stringify(query), + }), + ], + }); - return updatedCase.encode(); + return updatedCase.encode(); + } catch (error) { + throw createCaseError({ + message: `Failed while adding a generated alert to case id: ${caseId} error: ${error}`, + error, + logger, + }); + } }; -async function getCombinedCase( - service: CaseServiceSetup, - client: SavedObjectsClientContract, - id: string -): Promise { +async function getCombinedCase({ + service, + client, + id, + logger, +}: { + service: CaseServiceSetup; + client: SavedObjectsClientContract; + id: string; + logger: Logger; +}): Promise { const [casePromise, subCasePromise] = await Promise.allSettled([ service.getCase({ client, @@ -225,6 +244,7 @@ async function getCombinedCase( id: subCasePromise.value.references[0].id, }); return new CommentableCase({ + logger, collection: caseValue, subCase: subCasePromise.value, service, @@ -238,7 +258,12 @@ async function getCombinedCase( if (casePromise.status === 'rejected') { throw casePromise.reason; } else { - return new CommentableCase({ collection: casePromise.value, service, soClient: client }); + return new CommentableCase({ + logger, + collection: casePromise.value, + service, + soClient: client, + }); } } @@ -250,6 +275,7 @@ interface AddCommentArgs { caseService: CaseServiceSetup; userActionService: CaseUserActionServiceSetup; user: User; + logger: Logger; } export const addComment = async ({ @@ -260,6 +286,7 @@ export const addComment = async ({ caseId, comment, user, + logger, }: AddCommentArgs): Promise => { const query = pipe( CommentRequestRt.decode(comment), @@ -274,56 +301,70 @@ export const addComment = async ({ savedObjectsClient, userActionService, caseService, + logger, }); } decodeCommentRequest(comment); - const createdDate = new Date().toISOString(); - - const combinedCase = await getCombinedCase(caseService, savedObjectsClient, caseId); - - // eslint-disable-next-line @typescript-eslint/naming-convention - const { username, full_name, email } = user; - const userInfo: User = { - username, - full_name, - email, - }; - - const { comment: newComment, commentableCase: updatedCase } = await combinedCase.createComment({ - createdDate, - user: userInfo, - commentReq: query, - }); + try { + const createdDate = new Date().toISOString(); - if (newComment.attributes.type === CommentType.alert && updatedCase.settings.syncAlerts) { - const ids = getAlertIds(query); - await caseClient.updateAlertsStatus({ - ids, - status: updatedCase.status, - indices: new Set([ - ...(Array.isArray(newComment.attributes.index) - ? newComment.attributes.index - : [newComment.attributes.index]), - ]), + const combinedCase = await getCombinedCase({ + service: caseService, + client: savedObjectsClient, + id: caseId, + logger, }); - } - await userActionService.postUserActions({ - client: savedObjectsClient, - actions: [ - buildCommentUserActionItem({ - action: 'create', - actionAt: createdDate, - actionBy: { username, full_name, email }, - caseId: updatedCase.caseId, - subCaseId: updatedCase.subCaseId, - commentId: newComment.id, - fields: ['comment'], - newValue: JSON.stringify(query), - }), - ], - }); + // eslint-disable-next-line @typescript-eslint/naming-convention + const { username, full_name, email } = user; + const userInfo: User = { + username, + full_name, + email, + }; + + const { comment: newComment, commentableCase: updatedCase } = await combinedCase.createComment({ + createdDate, + user: userInfo, + commentReq: query, + }); + + if (newComment.attributes.type === CommentType.alert && updatedCase.settings.syncAlerts) { + const ids = getAlertIds(query); + await caseClient.updateAlertsStatus({ + ids, + status: updatedCase.status, + indices: new Set([ + ...(Array.isArray(newComment.attributes.index) + ? newComment.attributes.index + : [newComment.attributes.index]), + ]), + }); + } + + await userActionService.postUserActions({ + client: savedObjectsClient, + actions: [ + buildCommentUserActionItem({ + action: 'create', + actionAt: createdDate, + actionBy: { username, full_name, email }, + caseId: updatedCase.caseId, + subCaseId: updatedCase.subCaseId, + commentId: newComment.id, + fields: ['comment'], + newValue: JSON.stringify(query), + }), + ], + }); - return updatedCase.encode(); + return updatedCase.encode(); + } catch (error) { + throw createCaseError({ + message: `Failed while adding a comment to case id: ${caseId} error: ${error}`, + error, + logger, + }); + } }; diff --git a/x-pack/plugins/case/server/client/configure/get_mappings.ts b/x-pack/plugins/case/server/client/configure/get_mappings.ts index 5dd90efd8a2d74..5553580a41560f 100644 --- a/x-pack/plugins/case/server/client/configure/get_mappings.ts +++ b/x-pack/plugins/case/server/client/configure/get_mappings.ts @@ -5,13 +5,14 @@ * 2.0. */ -import { SavedObjectsClientContract } from 'src/core/server'; +import { SavedObjectsClientContract, Logger } from 'src/core/server'; import { ActionsClient } from '../../../../actions/server'; import { ConnectorMappingsAttributes, ConnectorTypes } from '../../../common/api'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server/saved_objects'; import { ConnectorMappingsServiceSetup } from '../../services'; import { CaseClientHandler } from '..'; +import { createCaseError } from '../../common/error'; interface GetMappingsArgs { savedObjectsClient: SavedObjectsClientContract; @@ -20,6 +21,7 @@ interface GetMappingsArgs { caseClient: CaseClientHandler; connectorType: string; connectorId: string; + logger: Logger; } export const getMappings = async ({ @@ -29,42 +31,51 @@ export const getMappings = async ({ caseClient, connectorType, connectorId, + logger, }: GetMappingsArgs): Promise => { - if (connectorType === ConnectorTypes.none) { - return []; - } - const myConnectorMappings = await connectorMappingsService.find({ - client: savedObjectsClient, - options: { - hasReference: { - type: ACTION_SAVED_OBJECT_TYPE, - id: connectorId, - }, - }, - }); - let theMapping; - // Create connector mappings if there are none - if (myConnectorMappings.total === 0) { - const res = await caseClient.getFields({ - actionsClient, - connectorId, - connectorType, - }); - theMapping = await connectorMappingsService.post({ + try { + if (connectorType === ConnectorTypes.none) { + return []; + } + const myConnectorMappings = await connectorMappingsService.find({ client: savedObjectsClient, - attributes: { - mappings: res.defaultMappings, - }, - references: [ - { + options: { + hasReference: { type: ACTION_SAVED_OBJECT_TYPE, - name: `associated-${ACTION_SAVED_OBJECT_TYPE}`, id: connectorId, }, - ], + }, + }); + let theMapping; + // Create connector mappings if there are none + if (myConnectorMappings.total === 0) { + const res = await caseClient.getFields({ + actionsClient, + connectorId, + connectorType, + }); + theMapping = await connectorMappingsService.post({ + client: savedObjectsClient, + attributes: { + mappings: res.defaultMappings, + }, + references: [ + { + type: ACTION_SAVED_OBJECT_TYPE, + name: `associated-${ACTION_SAVED_OBJECT_TYPE}`, + id: connectorId, + }, + ], + }); + } else { + theMapping = myConnectorMappings.saved_objects[0]; + } + return theMapping ? theMapping.attributes.mappings : []; + } catch (error) { + throw createCaseError({ + message: `Failed to retrieve mapping connector id: ${connectorId} type: ${connectorType}: ${error}`, + error, + logger, }); - } else { - theMapping = myConnectorMappings.saved_objects[0]; } - return theMapping ? theMapping.attributes.mappings : []; }; diff --git a/x-pack/plugins/case/server/client/index.test.ts b/x-pack/plugins/case/server/client/index.test.ts index 8a085bf29f2147..6f4b4b136f68fb 100644 --- a/x-pack/plugins/case/server/client/index.test.ts +++ b/x-pack/plugins/case/server/client/index.test.ts @@ -7,6 +7,7 @@ import { elasticsearchServiceMock, + loggingSystemMock, savedObjectsClientMock, } from '../../../../../src/core/server/mocks'; import { nullUser } from '../common'; @@ -22,6 +23,7 @@ jest.mock('./client'); import { CaseClientHandler } from './client'; import { createExternalCaseClient } from './index'; +const logger = loggingSystemMock.create().get('case'); const esClient = elasticsearchServiceMock.createElasticsearchClient(); const caseConfigureService = createConfigureServiceMock(); const alertsService = createAlertServiceMock(); @@ -41,6 +43,7 @@ describe('createExternalCaseClient()', () => { user: nullUser, savedObjectsClient, userActionService, + logger, }); expect(CaseClientHandler).toHaveBeenCalledTimes(1); }); diff --git a/x-pack/plugins/case/server/client/mocks.ts b/x-pack/plugins/case/server/client/mocks.ts index 302745913babbd..98ffed0eaf8c5e 100644 --- a/x-pack/plugins/case/server/client/mocks.ts +++ b/x-pack/plugins/case/server/client/mocks.ts @@ -47,7 +47,6 @@ export const createCaseClientWithMockSavedObjectsClient = async ({ }; }> => { const esClient = elasticsearchServiceMock.createElasticsearchClient(); - // const actionsMock = createActionsClient(); const log = loggingSystemMock.create().get('case'); const auth = badAuth ? authenticationMock.createInvalid() : authenticationMock.create(); @@ -78,6 +77,7 @@ export const createCaseClientWithMockSavedObjectsClient = async ({ userActionService, alertsService, scopedClusterClient: esClient, + logger: log, }); return { client: caseClient, diff --git a/x-pack/plugins/case/server/client/types.ts b/x-pack/plugins/case/server/client/types.ts index d6a8f6b5d706cd..adc66d8b1ea77b 100644 --- a/x-pack/plugins/case/server/client/types.ts +++ b/x-pack/plugins/case/server/client/types.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ElasticsearchClient, SavedObjectsClientContract } from 'kibana/server'; +import { ElasticsearchClient, SavedObjectsClientContract, Logger } from 'kibana/server'; import { ActionsClient } from '../../../actions/server'; import { CasePostRequest, @@ -76,6 +76,7 @@ export interface CaseClientFactoryArguments { savedObjectsClient: SavedObjectsClientContract; userActionService: CaseUserActionServiceSetup; alertsService: AlertServiceContract; + logger: Logger; } export interface ConfigureFields { diff --git a/x-pack/plugins/case/server/common/error.ts b/x-pack/plugins/case/server/common/error.ts new file mode 100644 index 00000000000000..95b05fd612e602 --- /dev/null +++ b/x-pack/plugins/case/server/common/error.ts @@ -0,0 +1,73 @@ +/* + * 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 { Boom, isBoom } from '@hapi/boom'; +import { Logger } from 'src/core/server'; + +/** + * Helper class for wrapping errors while preserving the original thrown error. + */ +class CaseError extends Error { + public readonly wrappedError?: Error; + constructor(message?: string, originalError?: Error) { + super(message); + this.name = this.constructor.name; // for stack traces + if (isCaseError(originalError)) { + this.wrappedError = originalError.wrappedError; + } else { + this.wrappedError = originalError; + } + } + + /** + * This function creates a boom representation of the error. If the wrapped error is a boom we'll grab the statusCode + * and data from that. + */ + public boomify(): Boom { + const message = this.message ?? this.wrappedError?.message; + let statusCode = 500; + let data: unknown | undefined; + + if (isBoom(this.wrappedError)) { + data = this.wrappedError?.data; + statusCode = this.wrappedError?.output?.statusCode ?? 500; + } + + return new Boom(message, { + data, + statusCode, + }); + } +} + +/** + * Type guard for determining if an error is a CaseError + */ +export function isCaseError(error: unknown): error is CaseError { + return error instanceof CaseError; +} + +/** + * Create a CaseError that wraps the original thrown error. This also logs the message that will be placed in the CaseError + * if the logger was defined. + */ +export function createCaseError({ + message, + error, + logger, +}: { + message?: string; + error?: Error; + logger?: Logger; +}) { + const logMessage: string | undefined = message ?? error?.toString(); + if (logMessage !== undefined) { + logger?.error(logMessage); + } + + return new CaseError(message, error); +} diff --git a/x-pack/plugins/case/server/common/models/commentable_case.ts b/x-pack/plugins/case/server/common/models/commentable_case.ts index 3ae225999db4ee..1ff5b7beadcaf1 100644 --- a/x-pack/plugins/case/server/common/models/commentable_case.ts +++ b/x-pack/plugins/case/server/common/models/commentable_case.ts @@ -11,6 +11,7 @@ import { SavedObjectReference, SavedObjectsClientContract, SavedObjectsUpdateResponse, + Logger, } from 'src/core/server'; import { AssociationType, @@ -35,6 +36,7 @@ import { } from '../../routes/api/utils'; import { CASE_SAVED_OBJECT, SUB_CASE_SAVED_OBJECT } from '../../saved_object_types'; import { CaseServiceSetup } from '../../services'; +import { createCaseError } from '../error'; import { countAlertsForID } from '../index'; interface UpdateCommentResp { @@ -52,6 +54,7 @@ interface CommentableCaseParams { subCase?: SavedObject; soClient: SavedObjectsClientContract; service: CaseServiceSetup; + logger: Logger; } /** @@ -63,11 +66,14 @@ export class CommentableCase { private readonly subCase?: SavedObject; private readonly soClient: SavedObjectsClientContract; private readonly service: CaseServiceSetup; - constructor({ collection, subCase, soClient, service }: CommentableCaseParams) { + private readonly logger: Logger; + + constructor({ collection, subCase, soClient, service, logger }: CommentableCaseParams) { this.collection = collection; this.subCase = subCase; this.soClient = soClient; this.service = service; + this.logger = logger; } public get status(): CaseStatuses { @@ -119,55 +125,64 @@ export class CommentableCase { } private async update({ date, user }: { date: string; user: User }): Promise { - let updatedSubCaseAttributes: SavedObject | undefined; + try { + let updatedSubCaseAttributes: SavedObject | undefined; + + if (this.subCase) { + const updatedSubCase = await this.service.patchSubCase({ + client: this.soClient, + subCaseId: this.subCase.id, + updatedAttributes: { + updated_at: date, + updated_by: { + ...user, + }, + }, + version: this.subCase.version, + }); + + updatedSubCaseAttributes = { + ...this.subCase, + attributes: { + ...this.subCase.attributes, + ...updatedSubCase.attributes, + }, + version: updatedSubCase.version ?? this.subCase.version, + }; + } - if (this.subCase) { - const updatedSubCase = await this.service.patchSubCase({ + const updatedCase = await this.service.patchCase({ client: this.soClient, - subCaseId: this.subCase.id, + caseId: this.collection.id, updatedAttributes: { updated_at: date, - updated_by: { - ...user, - }, + updated_by: { ...user }, }, - version: this.subCase.version, + version: this.collection.version, }); - updatedSubCaseAttributes = { - ...this.subCase, - attributes: { - ...this.subCase.attributes, - ...updatedSubCase.attributes, + // this will contain the updated sub case information if the sub case was defined initially + return new CommentableCase({ + collection: { + ...this.collection, + attributes: { + ...this.collection.attributes, + ...updatedCase.attributes, + }, + version: updatedCase.version ?? this.collection.version, }, - version: updatedSubCase.version ?? this.subCase.version, - }; + subCase: updatedSubCaseAttributes, + soClient: this.soClient, + service: this.service, + logger: this.logger, + }); + } catch (error) { + throw createCaseError({ + message: `Failed to update commentable case, sub case id: ${this.subCaseId} case id: ${this.caseId}: ${error}`, + error, + logger: this.logger, + }); } - - const updatedCase = await this.service.patchCase({ - client: this.soClient, - caseId: this.collection.id, - updatedAttributes: { - updated_at: date, - updated_by: { ...user }, - }, - version: this.collection.version, - }); - - // this will contain the updated sub case information if the sub case was defined initially - return new CommentableCase({ - collection: { - ...this.collection, - attributes: { - ...this.collection.attributes, - ...updatedCase.attributes, - }, - version: updatedCase.version ?? this.collection.version, - }, - subCase: updatedSubCaseAttributes, - soClient: this.soClient, - service: this.service, - }); } /** @@ -182,25 +197,33 @@ export class CommentableCase { updatedAt: string; user: User; }): Promise { - const { id, version, ...queryRestAttributes } = updateRequest; + try { + const { id, version, ...queryRestAttributes } = updateRequest; - const [comment, commentableCase] = await Promise.all([ - this.service.patchComment({ - client: this.soClient, - commentId: id, - updatedAttributes: { - ...queryRestAttributes, - updated_at: updatedAt, - updated_by: user, - }, - version, - }), - this.update({ date: updatedAt, user }), - ]); - return { - comment, - commentableCase, - }; + const [comment, commentableCase] = await Promise.all([ + this.service.patchComment({ + client: this.soClient, + commentId: id, + updatedAttributes: { + ...queryRestAttributes, + updated_at: updatedAt, + updated_by: user, + }, + version, + }), + this.update({ date: updatedAt, user }), + ]); + return { + comment, + commentableCase, + }; + } catch (error) { + throw createCaseError({ + message: `Failed to update comment in commentable case, sub case id: ${this.subCaseId} case id: ${this.caseId}: ${error}`, + error, + logger: this.logger, + }); + } } /** @@ -215,33 +238,41 @@ export class CommentableCase { user: User; commentReq: CommentRequest; }): Promise { - if (commentReq.type === CommentType.alert) { - if (this.status === CaseStatuses.closed) { - throw Boom.badRequest('Alert cannot be attached to a closed case'); - } + try { + if (commentReq.type === CommentType.alert) { + if (this.status === CaseStatuses.closed) { + throw Boom.badRequest('Alert cannot be attached to a closed case'); + } - if (!this.subCase && this.collection.attributes.type === CaseType.collection) { - throw Boom.badRequest('Alert cannot be attached to a collection case'); + if (!this.subCase && this.collection.attributes.type === CaseType.collection) { + throw Boom.badRequest('Alert cannot be attached to a collection case'); + } } - } - const [comment, commentableCase] = await Promise.all([ - this.service.postNewComment({ - client: this.soClient, - attributes: transformNewComment({ - associationType: this.subCase ? AssociationType.subCase : AssociationType.case, - createdDate, - ...commentReq, - ...user, + const [comment, commentableCase] = await Promise.all([ + this.service.postNewComment({ + client: this.soClient, + attributes: transformNewComment({ + associationType: this.subCase ? AssociationType.subCase : AssociationType.case, + createdDate, + ...commentReq, + ...user, + }), + references: this.buildRefsToCase(), }), - references: this.buildRefsToCase(), - }), - this.update({ date: createdDate, user }), - ]); - return { - comment, - commentableCase, - }; + this.update({ date: createdDate, user }), + ]); + return { + comment, + commentableCase, + }; + } catch (error) { + throw createCaseError({ + message: `Failed creating a comment on a commentable case, sub case id: ${this.subCaseId} case id: ${this.caseId}: ${error}`, + error, + logger: this.logger, + }); + } } private formatCollectionForEncoding(totalComment: number) { @@ -255,65 +286,74 @@ export class CommentableCase { } public async encode(): Promise { - const collectionCommentStats = await this.service.getAllCaseComments({ - client: this.soClient, - id: this.collection.id, - options: { - fields: [], - page: 1, - perPage: 1, - }, - }); + try { + const collectionCommentStats = await this.service.getAllCaseComments({ + client: this.soClient, + id: this.collection.id, + options: { + fields: [], + page: 1, + perPage: 1, + }, + }); - const collectionComments = await this.service.getAllCaseComments({ - client: this.soClient, - id: this.collection.id, - options: { - fields: [], - page: 1, - perPage: collectionCommentStats.total, - }, - }); + const collectionComments = await this.service.getAllCaseComments({ + client: this.soClient, + id: this.collection.id, + options: { + fields: [], + page: 1, + perPage: collectionCommentStats.total, + }, + }); - const collectionTotalAlerts = - countAlertsForID({ comments: collectionComments, id: this.collection.id }) ?? 0; + const collectionTotalAlerts = + countAlertsForID({ comments: collectionComments, id: this.collection.id }) ?? 0; - const caseResponse = { - comments: flattenCommentSavedObjects(collectionComments.saved_objects), - totalAlerts: collectionTotalAlerts, - ...this.formatCollectionForEncoding(collectionCommentStats.total), - }; + const caseResponse = { + comments: flattenCommentSavedObjects(collectionComments.saved_objects), + totalAlerts: collectionTotalAlerts, + ...this.formatCollectionForEncoding(collectionCommentStats.total), + }; - if (this.subCase) { - const subCaseComments = await this.service.getAllSubCaseComments({ - client: this.soClient, - id: this.subCase.id, - }); - const totalAlerts = countAlertsForID({ comments: subCaseComments, id: this.subCase.id }) ?? 0; + if (this.subCase) { + const subCaseComments = await this.service.getAllSubCaseComments({ + client: this.soClient, + id: this.subCase.id, + }); + const totalAlerts = + countAlertsForID({ comments: subCaseComments, id: this.subCase.id }) ?? 0; - return CaseResponseRt.encode({ - ...caseResponse, - /** - * For now we need the sub case comments and totals to be exposed on the top level of the response so that the UI - * functionality can stay the same. Ideally in the future we can refactor this so that the UI will look for the - * comments either in the top level for a case or a collection or in the subCases field if it is a sub case. - * - * If we ever need to return both the collection's comments and the sub case comments we'll need to refactor it then - * as well. - */ - comments: flattenCommentSavedObjects(subCaseComments.saved_objects), - totalComment: subCaseComments.saved_objects.length, - totalAlerts, - subCases: [ - flattenSubCaseSavedObject({ - savedObject: this.subCase, - totalComment: subCaseComments.saved_objects.length, - totalAlerts, - }), - ], + return CaseResponseRt.encode({ + ...caseResponse, + /** + * For now we need the sub case comments and totals to be exposed on the top level of the response so that the UI + * functionality can stay the same. Ideally in the future we can refactor this so that the UI will look for the + * comments either in the top level for a case or a collection or in the subCases field if it is a sub case. + * + * If we ever need to return both the collection's comments and the sub case comments we'll need to refactor it then + * as well. + */ + comments: flattenCommentSavedObjects(subCaseComments.saved_objects), + totalComment: subCaseComments.saved_objects.length, + totalAlerts, + subCases: [ + flattenSubCaseSavedObject({ + savedObject: this.subCase, + totalComment: subCaseComments.saved_objects.length, + totalAlerts, + }), + ], + }); + } + + return CaseResponseRt.encode(caseResponse); + } catch (error) { + throw createCaseError({ + message: `Failed encoding the commentable case, sub case id: ${this.subCaseId} case id: ${this.caseId}: ${error}`, + error, + logger: this.logger, }); } - - return CaseResponseRt.encode(caseResponse); } } diff --git a/x-pack/plugins/case/server/connectors/case/index.ts b/x-pack/plugins/case/server/connectors/case/index.ts index fd2fc7389f9ca9..4a1d0569bde6df 100644 --- a/x-pack/plugins/case/server/connectors/case/index.ts +++ b/x-pack/plugins/case/server/connectors/case/index.ts @@ -6,7 +6,7 @@ */ import { curry } from 'lodash'; - +import { Logger } from 'src/core/server'; import { ActionTypeExecutorResult } from '../../../../actions/common'; import { CasePatchRequest, @@ -26,6 +26,7 @@ import * as i18n from './translations'; import { GetActionTypeParams, isCommentGeneratedAlert, separator } from '..'; import { nullUser } from '../../common'; +import { createCaseError } from '../../common/error'; const supportedSubActions: string[] = ['create', 'update', 'addComment']; @@ -84,6 +85,7 @@ async function executor( connectorMappingsService, userActionService, alertsService, + logger, }); if (!supportedSubActions.includes(subAction)) { @@ -93,9 +95,17 @@ async function executor( } if (subAction === 'create') { - data = await caseClient.create({ - ...(subActionParams as CasePostRequest), - }); + try { + data = await caseClient.create({ + ...(subActionParams as CasePostRequest), + }); + } catch (error) { + throw createCaseError({ + message: `Failed to create a case using connector: ${error}`, + error, + logger, + }); + } } if (subAction === 'update') { @@ -107,13 +117,29 @@ async function executor( {} as CasePatchRequest ); - data = await caseClient.update({ cases: [updateParamsWithoutNullValues] }); + try { + data = await caseClient.update({ cases: [updateParamsWithoutNullValues] }); + } catch (error) { + throw createCaseError({ + message: `Failed to update case using connector id: ${updateParamsWithoutNullValues?.id} version: ${updateParamsWithoutNullValues?.version}: ${error}`, + error, + logger, + }); + } } if (subAction === 'addComment') { const { caseId, comment } = subActionParams as ExecutorSubActionAddCommentParams; - const formattedComment = transformConnectorComment(comment); - data = await caseClient.addComment({ caseId, comment: formattedComment }); + try { + const formattedComment = transformConnectorComment(comment, logger); + data = await caseClient.addComment({ caseId, comment: formattedComment }); + } catch (error) { + throw createCaseError({ + message: `Failed to create comment using connector case id: ${caseId}: ${error}`, + error, + logger, + }); + } } return { status: 'ok', data: data ?? {}, actionId }; @@ -127,7 +153,19 @@ interface AttachmentAlerts { indices: string[]; rule: { id: string | null; name: string | null }; } -export const transformConnectorComment = (comment: CommentSchemaType): CommentRequest => { + +/** + * Convert a connector style comment passed through the action plugin to the expected format for the add comment functionality. + * + * @param comment an object defining the comment to be attached to a case/sub case + * @param logger an optional logger to handle logging an error if parsing failed + * + * Note: This is exported so that the integration tests can use it. + */ +export const transformConnectorComment = ( + comment: CommentSchemaType, + logger?: Logger +): CommentRequest => { if (isCommentGeneratedAlert(comment)) { try { const genAlerts: Array<{ @@ -162,7 +200,11 @@ export const transformConnectorComment = (comment: CommentSchemaType): CommentRe rule, }; } catch (e) { - throw new Error(`Error parsing generated alert in case connector -> ${e.message}`); + throw createCaseError({ + message: `Error parsing generated alert in case connector -> ${e}`, + error: e, + logger, + }); } } else { return comment; diff --git a/x-pack/plugins/case/server/plugin.ts b/x-pack/plugins/case/server/plugin.ts index 1c00c26a7c0b09..43daa519584297 100644 --- a/x-pack/plugins/case/server/plugin.ts +++ b/x-pack/plugins/case/server/plugin.ts @@ -97,11 +97,13 @@ export class CasePlugin { connectorMappingsService: this.connectorMappingsService, userActionService: this.userActionService, alertsService: this.alertsService, + logger: this.log, }) ); const router = core.http.createRouter(); initCaseApi({ + logger: this.log, caseService: this.caseService, caseConfigureService: this.caseConfigureService, connectorMappingsService: this.connectorMappingsService, @@ -137,6 +139,7 @@ export class CasePlugin { connectorMappingsService: this.connectorMappingsService!, userActionService: this.userActionService!, alertsService: this.alertsService!, + logger: this.log, }); }; @@ -156,6 +159,7 @@ export class CasePlugin { connectorMappingsService, userActionService, alertsService, + logger, }: { core: CoreSetup; caseService: CaseServiceSetup; @@ -163,6 +167,7 @@ export class CasePlugin { connectorMappingsService: ConnectorMappingsServiceSetup; userActionService: CaseUserActionServiceSetup; alertsService: AlertServiceContract; + logger: Logger; }): IContextProvider => { return async (context, request, response) => { const [{ savedObjects }] = await core.getStartServices(); @@ -178,6 +183,7 @@ export class CasePlugin { userActionService, alertsService, user, + logger, }); }, }; diff --git a/x-pack/plugins/case/server/routes/api/__fixtures__/mock_router.ts b/x-pack/plugins/case/server/routes/api/__fixtures__/mock_router.ts index b4230a05749a15..6b0c4adf9a6804 100644 --- a/x-pack/plugins/case/server/routes/api/__fixtures__/mock_router.ts +++ b/x-pack/plugins/case/server/routes/api/__fixtures__/mock_router.ts @@ -35,6 +35,7 @@ export const createRoute = async ( postUserActions: jest.fn(), getUserActions: jest.fn(), }, + logger: log, }); return router[method].mock.calls[0][1]; diff --git a/x-pack/plugins/case/server/routes/api/__fixtures__/route_contexts.ts b/x-pack/plugins/case/server/routes/api/__fixtures__/route_contexts.ts index 492be96fb4aa92..7f66602c61fffc 100644 --- a/x-pack/plugins/case/server/routes/api/__fixtures__/route_contexts.ts +++ b/x-pack/plugins/case/server/routes/api/__fixtures__/route_contexts.ts @@ -57,6 +57,7 @@ export const createRouteContext = async (client: any, badAuth = false) => { userActionService, alertsService, scopedClusterClient: esClient, + logger: log, }); return { context, services: { userActionService } }; diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/delete_all_comments.ts b/x-pack/plugins/case/server/routes/api/cases/comments/delete_all_comments.ts index e0b3a4420f4b5d..fd250b74fff1e3 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/delete_all_comments.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/delete_all_comments.ts @@ -13,7 +13,12 @@ import { wrapError } from '../../utils'; import { CASE_COMMENTS_URL } from '../../../../../common/constants'; import { AssociationType } from '../../../../../common/api'; -export function initDeleteAllCommentsApi({ caseService, router, userActionService }: RouteDeps) { +export function initDeleteAllCommentsApi({ + caseService, + router, + userActionService, + logger, +}: RouteDeps) { router.delete( { path: CASE_COMMENTS_URL, @@ -70,6 +75,9 @@ export function initDeleteAllCommentsApi({ caseService, router, userActionServic return response.noContent(); } catch (error) { + logger.error( + `Failed to delete all comments in route case id: ${request.params.case_id} sub case id: ${request.query?.subCaseId}: ${error}` + ); return response.customError(wrapError(error)); } } diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/delete_comment.ts b/x-pack/plugins/case/server/routes/api/cases/comments/delete_comment.ts index cae0809ea5f0bf..f1c5fdc2b7cc82 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/delete_comment.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/delete_comment.ts @@ -14,7 +14,12 @@ import { RouteDeps } from '../../types'; import { wrapError } from '../../utils'; import { CASE_COMMENT_DETAILS_URL } from '../../../../../common/constants'; -export function initDeleteCommentApi({ caseService, router, userActionService }: RouteDeps) { +export function initDeleteCommentApi({ + caseService, + router, + userActionService, + logger, +}: RouteDeps) { router.delete( { path: CASE_COMMENT_DETAILS_URL, @@ -78,6 +83,9 @@ export function initDeleteCommentApi({ caseService, router, userActionService }: return response.noContent(); } catch (error) { + logger.error( + `Failed to delete comment in route case id: ${request.params.case_id} comment id: ${request.params.comment_id} sub case id: ${request.query?.subCaseId}: ${error}` + ); return response.customError(wrapError(error)); } } diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/find_comments.ts b/x-pack/plugins/case/server/routes/api/cases/comments/find_comments.ts index 0ec0f1871c7adf..57ddd84e8742c5 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/find_comments.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/find_comments.ts @@ -30,7 +30,7 @@ const FindQueryParamsRt = rt.partial({ subCaseId: rt.string, }); -export function initFindCaseCommentsApi({ caseService, router }: RouteDeps) { +export function initFindCaseCommentsApi({ caseService, router, logger }: RouteDeps) { router.get( { path: `${CASE_COMMENTS_URL}/_find`, @@ -82,6 +82,9 @@ export function initFindCaseCommentsApi({ caseService, router }: RouteDeps) { const theComments = await caseService.getCommentsByAssociation(args); return response.ok({ body: CommentsResponseRt.encode(transformComments(theComments)) }); } catch (error) { + logger.error( + `Failed to find comments in route case id: ${request.params.case_id}: ${error}` + ); return response.customError(wrapError(error)); } } diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/get_all_comment.ts b/x-pack/plugins/case/server/routes/api/cases/comments/get_all_comment.ts index 8bf49ec3e27a12..770efe0109744c 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/get_all_comment.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/get_all_comment.ts @@ -14,7 +14,7 @@ import { flattenCommentSavedObjects, wrapError } from '../../utils'; import { CASE_COMMENTS_URL } from '../../../../../common/constants'; import { defaultSortField } from '../../../../common'; -export function initGetAllCommentsApi({ caseService, router }: RouteDeps) { +export function initGetAllCommentsApi({ caseService, router, logger }: RouteDeps) { router.get( { path: CASE_COMMENTS_URL, @@ -58,6 +58,9 @@ export function initGetAllCommentsApi({ caseService, router }: RouteDeps) { body: AllCommentsResponseRt.encode(flattenCommentSavedObjects(comments.saved_objects)), }); } catch (error) { + logger.error( + `Failed to get all comments in route case id: ${request.params.case_id} include sub case comments: ${request.query?.includeSubCaseComments} sub case id: ${request.query?.subCaseId}: ${error}` + ); return response.customError(wrapError(error)); } } diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/get_comment.ts b/x-pack/plugins/case/server/routes/api/cases/comments/get_comment.ts index 6484472af0c3c1..9dedfccd3a250e 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/get_comment.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/get_comment.ts @@ -12,7 +12,7 @@ import { RouteDeps } from '../../types'; import { flattenCommentSavedObject, wrapError } from '../../utils'; import { CASE_COMMENT_DETAILS_URL } from '../../../../../common/constants'; -export function initGetCommentApi({ caseService, router }: RouteDeps) { +export function initGetCommentApi({ caseService, router, logger }: RouteDeps) { router.get( { path: CASE_COMMENT_DETAILS_URL, @@ -35,6 +35,9 @@ export function initGetCommentApi({ caseService, router }: RouteDeps) { body: CommentResponseRt.encode(flattenCommentSavedObject(comment)), }); } catch (error) { + logger.error( + `Failed to get comment in route case id: ${request.params.case_id} comment id: ${request.params.comment_id}: ${error}` + ); return response.customError(wrapError(error)); } } diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.ts b/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.ts index 01b0e174640537..f5db2dc004a1d7 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.ts @@ -12,7 +12,7 @@ import { identity } from 'fp-ts/lib/function'; import { schema } from '@kbn/config-schema'; import Boom from '@hapi/boom'; -import { SavedObjectsClientContract } from 'kibana/server'; +import { SavedObjectsClientContract, Logger } from 'kibana/server'; import { CommentableCase } from '../../../../common'; import { CommentPatchRequestRt, throwErrors, User } from '../../../../../common/api'; import { CASE_SAVED_OBJECT, SUB_CASE_SAVED_OBJECT } from '../../../../saved_object_types'; @@ -26,10 +26,17 @@ interface CombinedCaseParams { service: CaseServiceSetup; client: SavedObjectsClientContract; caseID: string; + logger: Logger; subCaseId?: string; } -async function getCommentableCase({ service, client, caseID, subCaseId }: CombinedCaseParams) { +async function getCommentableCase({ + service, + client, + caseID, + subCaseId, + logger, +}: CombinedCaseParams) { if (subCaseId) { const [caseInfo, subCase] = await Promise.all([ service.getCase({ @@ -41,22 +48,23 @@ async function getCommentableCase({ service, client, caseID, subCaseId }: Combin id: subCaseId, }), ]); - return new CommentableCase({ collection: caseInfo, service, subCase, soClient: client }); + return new CommentableCase({ + collection: caseInfo, + service, + subCase, + soClient: client, + logger, + }); } else { const caseInfo = await service.getCase({ client, id: caseID, }); - return new CommentableCase({ collection: caseInfo, service, soClient: client }); + return new CommentableCase({ collection: caseInfo, service, soClient: client, logger }); } } -export function initPatchCommentApi({ - caseConfigureService, - caseService, - router, - userActionService, -}: RouteDeps) { +export function initPatchCommentApi({ caseService, router, userActionService, logger }: RouteDeps) { router.patch( { path: CASE_COMMENTS_URL, @@ -88,6 +96,7 @@ export function initPatchCommentApi({ client, caseID: request.params.case_id, subCaseId: request.query?.subCaseId, + logger, }); const myComment = await caseService.getComment({ @@ -161,6 +170,9 @@ export function initPatchCommentApi({ body: await updatedCase.encode(), }); } catch (error) { + logger.error( + `Failed to patch comment in route case id: ${request.params.case_id} sub case id: ${request.query?.subCaseId}: ${error}` + ); return response.customError(wrapError(error)); } } diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.ts b/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.ts index 607f3f381f0675..b8dc43dbf3fae6 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.ts @@ -11,7 +11,7 @@ import { RouteDeps } from '../../types'; import { CASE_COMMENTS_URL } from '../../../../../common/constants'; import { CommentRequest } from '../../../../../common/api'; -export function initPostCommentApi({ router }: RouteDeps) { +export function initPostCommentApi({ router, logger }: RouteDeps) { router.post( { path: CASE_COMMENTS_URL, @@ -41,6 +41,9 @@ export function initPostCommentApi({ router }: RouteDeps) { body: await caseClient.addComment({ caseId, comment }), }); } catch (error) { + logger.error( + `Failed to post comment in route case id: ${request.params.case_id} sub case id: ${request.query?.subCaseId}: ${error}` + ); return response.customError(wrapError(error)); } } diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.ts b/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.ts index 33226d39a25957..2ca34d25482dd4 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.ts @@ -12,7 +12,7 @@ import { wrapError } from '../../utils'; import { CASE_CONFIGURE_URL } from '../../../../../common/constants'; import { transformESConnectorToCaseConnector } from '../helpers'; -export function initGetCaseConfigure({ caseConfigureService, router }: RouteDeps) { +export function initGetCaseConfigure({ caseConfigureService, router, logger }: RouteDeps) { router.get( { path: CASE_CONFIGURE_URL, @@ -63,6 +63,7 @@ export function initGetCaseConfigure({ caseConfigureService, router }: RouteDeps : {}, }); } catch (error) { + logger.error(`Failed to get case configure in route: ${error}`); return response.customError(wrapError(error)); } } diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts b/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts index 0a368e0276bb5c..81ffc06355ff5e 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts @@ -28,7 +28,7 @@ const isConnectorSupported = ( * Be aware that this api will only return 20 connectors */ -export function initCaseConfigureGetActionConnector({ router }: RouteDeps) { +export function initCaseConfigureGetActionConnector({ router, logger }: RouteDeps) { router.get( { path: `${CASE_CONFIGURE_CONNECTORS_URL}/_find`, @@ -52,6 +52,7 @@ export function initCaseConfigureGetActionConnector({ router }: RouteDeps) { ); return response.ok({ body: results }); } catch (error) { + logger.error(`Failed to get connectors in route: ${error}`); return response.customError(wrapError(error)); } } diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.ts b/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.ts index 02d39465373f98..cd764bb0e8a3e5 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.ts @@ -24,7 +24,12 @@ import { transformESConnectorToCaseConnector, } from '../helpers'; -export function initPatchCaseConfigure({ caseConfigureService, caseService, router }: RouteDeps) { +export function initPatchCaseConfigure({ + caseConfigureService, + caseService, + router, + logger, +}: RouteDeps) { router.patch( { path: CASE_CONFIGURE_URL, @@ -107,6 +112,7 @@ export function initPatchCaseConfigure({ caseConfigureService, caseService, rout }), }); } catch (error) { + logger.error(`Failed to get patch configure in route: ${error}`); return response.customError(wrapError(error)); } } diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.ts b/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.ts index db3d5cd6a2e56e..f619a727e2e7aa 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.ts @@ -24,7 +24,12 @@ import { transformESConnectorToCaseConnector, } from '../helpers'; -export function initPostCaseConfigure({ caseConfigureService, caseService, router }: RouteDeps) { +export function initPostCaseConfigure({ + caseConfigureService, + caseService, + router, + logger, +}: RouteDeps) { router.post( { path: CASE_CONFIGURE_URL, @@ -96,6 +101,7 @@ export function initPostCaseConfigure({ caseConfigureService, caseService, route }), }); } catch (error) { + logger.error(`Failed to post case configure in route: ${error}`); return response.customError(wrapError(error)); } } diff --git a/x-pack/plugins/case/server/routes/api/cases/delete_cases.ts b/x-pack/plugins/case/server/routes/api/cases/delete_cases.ts index 497e33d7feb308..5f2a6c67220c3f 100644 --- a/x-pack/plugins/case/server/routes/api/cases/delete_cases.ts +++ b/x-pack/plugins/case/server/routes/api/cases/delete_cases.ts @@ -46,7 +46,7 @@ async function deleteSubCases({ ); } -export function initDeleteCasesApi({ caseService, router, userActionService }: RouteDeps) { +export function initDeleteCasesApi({ caseService, router, userActionService, logger }: RouteDeps) { router.delete( { path: CASES_URL, @@ -111,6 +111,9 @@ export function initDeleteCasesApi({ caseService, router, userActionService }: R return response.noContent(); } catch (error) { + logger.error( + `Failed to delete cases in route ids: ${JSON.stringify(request.query.ids)}: ${error}` + ); return response.customError(wrapError(error)); } } diff --git a/x-pack/plugins/case/server/routes/api/cases/find_cases.ts b/x-pack/plugins/case/server/routes/api/cases/find_cases.ts index 8ba83b42c06d70..d04f01eb735379 100644 --- a/x-pack/plugins/case/server/routes/api/cases/find_cases.ts +++ b/x-pack/plugins/case/server/routes/api/cases/find_cases.ts @@ -22,7 +22,7 @@ import { RouteDeps } from '../types'; import { CASES_URL } from '../../../../common/constants'; import { constructQueryOptions } from './helpers'; -export function initFindCasesApi({ caseService, caseConfigureService, router }: RouteDeps) { +export function initFindCasesApi({ caseService, router, logger }: RouteDeps) { router.get( { path: `${CASES_URL}/_find`, @@ -77,6 +77,7 @@ export function initFindCasesApi({ caseService, caseConfigureService, router }: ), }); } catch (error) { + logger.error(`Failed to find cases in route: ${error}`); return response.customError(wrapError(error)); } } diff --git a/x-pack/plugins/case/server/routes/api/cases/get_case.ts b/x-pack/plugins/case/server/routes/api/cases/get_case.ts index a3311796fa5cd9..8a34e3a5b24316 100644 --- a/x-pack/plugins/case/server/routes/api/cases/get_case.ts +++ b/x-pack/plugins/case/server/routes/api/cases/get_case.ts @@ -11,7 +11,7 @@ import { RouteDeps } from '../types'; import { wrapError } from '../utils'; import { CASE_DETAILS_URL } from '../../../../common/constants'; -export function initGetCaseApi({ caseConfigureService, caseService, router }: RouteDeps) { +export function initGetCaseApi({ router, logger }: RouteDeps) { router.get( { path: CASE_DETAILS_URL, @@ -26,10 +26,10 @@ export function initGetCaseApi({ caseConfigureService, caseService, router }: Ro }, }, async (context, request, response) => { - const caseClient = context.case.getCaseClient(); - const id = request.params.case_id; - try { + const caseClient = context.case.getCaseClient(); + const id = request.params.case_id; + return response.ok({ body: await caseClient.get({ id, @@ -38,6 +38,9 @@ export function initGetCaseApi({ caseConfigureService, caseService, router }: Ro }), }); } catch (error) { + logger.error( + `Failed to retrieve case in route case id: ${request.params.case_id} \ninclude comments: ${request.query.includeComments} \ninclude sub comments: ${request.query.includeSubCaseComments}: ${error}` + ); return response.customError(wrapError(error)); } } diff --git a/x-pack/plugins/case/server/routes/api/cases/patch_cases.ts b/x-pack/plugins/case/server/routes/api/cases/patch_cases.ts index 67d4d21a576340..2bff6000d5d6a9 100644 --- a/x-pack/plugins/case/server/routes/api/cases/patch_cases.ts +++ b/x-pack/plugins/case/server/routes/api/cases/patch_cases.ts @@ -10,7 +10,7 @@ import { RouteDeps } from '../types'; import { CASES_URL } from '../../../../common/constants'; import { CasesPatchRequest } from '../../../../common/api'; -export function initPatchCasesApi({ router }: RouteDeps) { +export function initPatchCasesApi({ router, logger }: RouteDeps) { router.patch( { path: CASES_URL, @@ -19,18 +19,19 @@ export function initPatchCasesApi({ router }: RouteDeps) { }, }, async (context, request, response) => { - if (!context.case) { - return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' }); - } + try { + if (!context.case) { + return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' }); + } - const caseClient = context.case.getCaseClient(); - const cases = request.body as CasesPatchRequest; + const caseClient = context.case.getCaseClient(); + const cases = request.body as CasesPatchRequest; - try { return response.ok({ body: await caseClient.update(cases), }); } catch (error) { + logger.error(`Failed to patch cases in route: ${error}`); return response.customError(wrapError(error)); } } diff --git a/x-pack/plugins/case/server/routes/api/cases/post_case.ts b/x-pack/plugins/case/server/routes/api/cases/post_case.ts index 349ed6c3e5af93..1328d958261302 100644 --- a/x-pack/plugins/case/server/routes/api/cases/post_case.ts +++ b/x-pack/plugins/case/server/routes/api/cases/post_case.ts @@ -11,7 +11,7 @@ import { RouteDeps } from '../types'; import { CASES_URL } from '../../../../common/constants'; import { CasePostRequest } from '../../../../common/api'; -export function initPostCaseApi({ router }: RouteDeps) { +export function initPostCaseApi({ router, logger }: RouteDeps) { router.post( { path: CASES_URL, @@ -20,17 +20,18 @@ export function initPostCaseApi({ router }: RouteDeps) { }, }, async (context, request, response) => { - if (!context.case) { - return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' }); - } - const caseClient = context.case.getCaseClient(); - const theCase = request.body as CasePostRequest; - try { + if (!context.case) { + return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' }); + } + const caseClient = context.case.getCaseClient(); + const theCase = request.body as CasePostRequest; + return response.ok({ body: await caseClient.create({ ...theCase }), }); } catch (error) { + logger.error(`Failed to post case in route: ${error}`); return response.customError(wrapError(error)); } } diff --git a/x-pack/plugins/case/server/routes/api/cases/push_case.ts b/x-pack/plugins/case/server/routes/api/cases/push_case.ts index c1f0a2cb59cb1d..cfd2f6b9a61ad5 100644 --- a/x-pack/plugins/case/server/routes/api/cases/push_case.ts +++ b/x-pack/plugins/case/server/routes/api/cases/push_case.ts @@ -16,7 +16,7 @@ import { throwErrors, CasePushRequestParamsRt } from '../../../../common/api'; import { RouteDeps } from '../types'; import { CASE_PUSH_URL } from '../../../../common/constants'; -export function initPushCaseApi({ router }: RouteDeps) { +export function initPushCaseApi({ router, logger }: RouteDeps) { router.post( { path: CASE_PUSH_URL, @@ -26,18 +26,18 @@ export function initPushCaseApi({ router }: RouteDeps) { }, }, async (context, request, response) => { - if (!context.case) { - return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' }); - } + try { + if (!context.case) { + return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' }); + } - const caseClient = context.case.getCaseClient(); - const actionsClient = context.actions?.getActionsClient(); + const caseClient = context.case.getCaseClient(); + const actionsClient = context.actions?.getActionsClient(); - if (actionsClient == null) { - return response.badRequest({ body: 'Action client not found' }); - } + if (actionsClient == null) { + return response.badRequest({ body: 'Action client not found' }); + } - try { const params = pipe( CasePushRequestParamsRt.decode(request.params), fold(throwErrors(Boom.badRequest), identity) @@ -51,6 +51,7 @@ export function initPushCaseApi({ router }: RouteDeps) { }), }); } catch (error) { + logger.error(`Failed to push case in route: ${error}`); return response.customError(wrapError(error)); } } diff --git a/x-pack/plugins/case/server/routes/api/cases/reporters/get_reporters.ts b/x-pack/plugins/case/server/routes/api/cases/reporters/get_reporters.ts index 3d724ca5fc966f..e5433f49722395 100644 --- a/x-pack/plugins/case/server/routes/api/cases/reporters/get_reporters.ts +++ b/x-pack/plugins/case/server/routes/api/cases/reporters/get_reporters.ts @@ -10,7 +10,7 @@ import { RouteDeps } from '../../types'; import { wrapError } from '../../utils'; import { CASE_REPORTERS_URL } from '../../../../../common/constants'; -export function initGetReportersApi({ caseService, router }: RouteDeps) { +export function initGetReportersApi({ caseService, router, logger }: RouteDeps) { router.get( { path: CASE_REPORTERS_URL, @@ -24,6 +24,7 @@ export function initGetReportersApi({ caseService, router }: RouteDeps) { }); return response.ok({ body: UsersRt.encode(reporters) }); } catch (error) { + logger.error(`Failed to get reporters in route: ${error}`); return response.customError(wrapError(error)); } } diff --git a/x-pack/plugins/case/server/routes/api/cases/status/get_status.ts b/x-pack/plugins/case/server/routes/api/cases/status/get_status.ts index f3cd0e2bdda5c2..d0addfff091243 100644 --- a/x-pack/plugins/case/server/routes/api/cases/status/get_status.ts +++ b/x-pack/plugins/case/server/routes/api/cases/status/get_status.ts @@ -12,7 +12,7 @@ import { CasesStatusResponseRt, caseStatuses } from '../../../../../common/api'; import { CASE_STATUS_URL } from '../../../../../common/constants'; import { constructQueryOptions } from '../helpers'; -export function initGetCasesStatusApi({ caseService, router }: RouteDeps) { +export function initGetCasesStatusApi({ caseService, router, logger }: RouteDeps) { router.get( { path: CASE_STATUS_URL, @@ -41,6 +41,7 @@ export function initGetCasesStatusApi({ caseService, router }: RouteDeps) { }), }); } catch (error) { + logger.error(`Failed to get status stats in route: ${error}`); return response.customError(wrapError(error)); } } diff --git a/x-pack/plugins/case/server/routes/api/cases/sub_case/delete_sub_cases.ts b/x-pack/plugins/case/server/routes/api/cases/sub_case/delete_sub_cases.ts index db701dd0fc82b5..fd33afbd7df8ee 100644 --- a/x-pack/plugins/case/server/routes/api/cases/sub_case/delete_sub_cases.ts +++ b/x-pack/plugins/case/server/routes/api/cases/sub_case/delete_sub_cases.ts @@ -13,7 +13,12 @@ import { wrapError } from '../../utils'; import { SUB_CASES_PATCH_DEL_URL } from '../../../../../common/constants'; import { CASE_SAVED_OBJECT } from '../../../../saved_object_types'; -export function initDeleteSubCasesApi({ caseService, router, userActionService }: RouteDeps) { +export function initDeleteSubCasesApi({ + caseService, + router, + userActionService, + logger, +}: RouteDeps) { router.delete( { path: SUB_CASES_PATCH_DEL_URL, @@ -80,6 +85,9 @@ export function initDeleteSubCasesApi({ caseService, router, userActionService } return response.noContent(); } catch (error) { + logger.error( + `Failed to delete sub cases in route ids: ${JSON.stringify(request.query.ids)}: ${error}` + ); return response.customError(wrapError(error)); } } diff --git a/x-pack/plugins/case/server/routes/api/cases/sub_case/find_sub_cases.ts b/x-pack/plugins/case/server/routes/api/cases/sub_case/find_sub_cases.ts index 98052ccaeaba8e..c24dde1944f832 100644 --- a/x-pack/plugins/case/server/routes/api/cases/sub_case/find_sub_cases.ts +++ b/x-pack/plugins/case/server/routes/api/cases/sub_case/find_sub_cases.ts @@ -24,7 +24,7 @@ import { SUB_CASES_URL } from '../../../../../common/constants'; import { constructQueryOptions } from '../helpers'; import { defaultPage, defaultPerPage } from '../..'; -export function initFindSubCasesApi({ caseService, router }: RouteDeps) { +export function initFindSubCasesApi({ caseService, router, logger }: RouteDeps) { router.get( { path: `${SUB_CASES_URL}/_find`, @@ -88,6 +88,9 @@ export function initFindSubCasesApi({ caseService, router }: RouteDeps) { ), }); } catch (error) { + logger.error( + `Failed to find sub cases in route case id: ${request.params.case_id}: ${error}` + ); return response.customError(wrapError(error)); } } diff --git a/x-pack/plugins/case/server/routes/api/cases/sub_case/get_sub_case.ts b/x-pack/plugins/case/server/routes/api/cases/sub_case/get_sub_case.ts index b6d9a7345dbdd1..32dcc924e1a083 100644 --- a/x-pack/plugins/case/server/routes/api/cases/sub_case/get_sub_case.ts +++ b/x-pack/plugins/case/server/routes/api/cases/sub_case/get_sub_case.ts @@ -13,7 +13,7 @@ import { flattenSubCaseSavedObject, wrapError } from '../../utils'; import { SUB_CASE_DETAILS_URL } from '../../../../../common/constants'; import { countAlertsForID } from '../../../../common'; -export function initGetSubCaseApi({ caseService, router }: RouteDeps) { +export function initGetSubCaseApi({ caseService, router, logger }: RouteDeps) { router.get( { path: SUB_CASE_DETAILS_URL, @@ -70,6 +70,9 @@ export function initGetSubCaseApi({ caseService, router }: RouteDeps) { ), }); } catch (error) { + logger.error( + `Failed to get sub case in route case id: ${request.params.case_id} sub case id: ${request.params.sub_case_id} include comments: ${request.query?.includeComments}: ${error}` + ); return response.customError(wrapError(error)); } } diff --git a/x-pack/plugins/case/server/routes/api/cases/sub_case/patch_sub_cases.ts b/x-pack/plugins/case/server/routes/api/cases/sub_case/patch_sub_cases.ts index 4b8e4920852c27..73aacc2c2b0ba4 100644 --- a/x-pack/plugins/case/server/routes/api/cases/sub_case/patch_sub_cases.ts +++ b/x-pack/plugins/case/server/routes/api/cases/sub_case/patch_sub_cases.ts @@ -14,6 +14,7 @@ import { KibanaRequest, SavedObject, SavedObjectsFindResponse, + Logger, } from 'kibana/server'; import { CaseClient } from '../../../../client'; @@ -47,6 +48,7 @@ import { import { getCaseToUpdate } from '../helpers'; import { buildSubCaseUserActions } from '../../../../services/user_actions/helpers'; import { addAlertInfoToStatusMap } from '../../../../common'; +import { createCaseError } from '../../../../common/error'; interface UpdateArgs { client: SavedObjectsClientContract; @@ -55,6 +57,7 @@ interface UpdateArgs { request: KibanaRequest; caseClient: CaseClient; subCases: SubCasesPatchRequest; + logger: Logger; } function checkNonExistingOrConflict( @@ -216,41 +219,53 @@ async function updateAlerts({ caseService, client, caseClient, + logger, }: { subCasesToSync: SubCasePatchRequest[]; caseService: CaseServiceSetup; client: SavedObjectsClientContract; caseClient: CaseClient; + logger: Logger; }) { - const subCasesToSyncMap = subCasesToSync.reduce((acc, subCase) => { - acc.set(subCase.id, subCase); - return acc; - }, new Map()); - // get all the alerts for all sub cases that need to be synced - const totalAlerts = await getAlertComments({ caseService, client, subCasesToSync }); - // create a map of the status (open, closed, etc) to alert info that needs to be updated - const alertsToUpdate = totalAlerts.saved_objects.reduce((acc, alertComment) => { - if (isCommentRequestTypeAlertOrGenAlert(alertComment.attributes)) { - const id = getID(alertComment); - const status = - id !== undefined - ? subCasesToSyncMap.get(id)?.status ?? CaseStatuses.open - : CaseStatuses.open; - - addAlertInfoToStatusMap({ comment: alertComment.attributes, statusMap: acc, status }); - } - return acc; - }, new Map()); - - // This does at most 3 calls to Elasticsearch to update the status of the alerts to either open, closed, or in-progress - for (const [status, alertInfo] of alertsToUpdate.entries()) { - if (alertInfo.ids.length > 0 && alertInfo.indices.size > 0) { - caseClient.updateAlertsStatus({ - ids: alertInfo.ids, - status, - indices: alertInfo.indices, - }); + try { + const subCasesToSyncMap = subCasesToSync.reduce((acc, subCase) => { + acc.set(subCase.id, subCase); + return acc; + }, new Map()); + // get all the alerts for all sub cases that need to be synced + const totalAlerts = await getAlertComments({ caseService, client, subCasesToSync }); + // create a map of the status (open, closed, etc) to alert info that needs to be updated + const alertsToUpdate = totalAlerts.saved_objects.reduce((acc, alertComment) => { + if (isCommentRequestTypeAlertOrGenAlert(alertComment.attributes)) { + const id = getID(alertComment); + const status = + id !== undefined + ? subCasesToSyncMap.get(id)?.status ?? CaseStatuses.open + : CaseStatuses.open; + + addAlertInfoToStatusMap({ comment: alertComment.attributes, statusMap: acc, status }); + } + return acc; + }, new Map()); + + // This does at most 3 calls to Elasticsearch to update the status of the alerts to either open, closed, or in-progress + for (const [status, alertInfo] of alertsToUpdate.entries()) { + if (alertInfo.ids.length > 0 && alertInfo.indices.size > 0) { + caseClient.updateAlertsStatus({ + ids: alertInfo.ids, + status, + indices: alertInfo.indices, + }); + } } + } catch (error) { + throw createCaseError({ + message: `Failed to update alert status while updating sub cases: ${JSON.stringify( + subCasesToSync + )}: ${error}`, + logger, + error, + }); } } @@ -261,133 +276,152 @@ async function update({ request, caseClient, subCases, + logger, }: UpdateArgs): Promise { const query = pipe( excess(SubCasesPatchRequestRt).decode(subCases), fold(throwErrors(Boom.badRequest), identity) ); - const bulkSubCases = await caseService.getSubCases({ - client, - ids: query.subCases.map((q) => q.id), - }); + try { + const bulkSubCases = await caseService.getSubCases({ + client, + ids: query.subCases.map((q) => q.id), + }); - const subCasesMap = bulkSubCases.saved_objects.reduce((acc, so) => { - acc.set(so.id, so); - return acc; - }, new Map>()); + const subCasesMap = bulkSubCases.saved_objects.reduce((acc, so) => { + acc.set(so.id, so); + return acc; + }, new Map>()); - checkNonExistingOrConflict(query.subCases, subCasesMap); + checkNonExistingOrConflict(query.subCases, subCasesMap); - const nonEmptySubCaseRequests = getValidUpdateRequests(query.subCases, subCasesMap); + const nonEmptySubCaseRequests = getValidUpdateRequests(query.subCases, subCasesMap); - if (nonEmptySubCaseRequests.length <= 0) { - throw Boom.notAcceptable('All update fields are identical to current version.'); - } + if (nonEmptySubCaseRequests.length <= 0) { + throw Boom.notAcceptable('All update fields are identical to current version.'); + } - const subIDToParentCase = await getParentCases({ - client, - caseService, - subCaseIDs: nonEmptySubCaseRequests.map((subCase) => subCase.id), - subCasesMap, - }); + const subIDToParentCase = await getParentCases({ + client, + caseService, + subCaseIDs: nonEmptySubCaseRequests.map((subCase) => subCase.id), + subCasesMap, + }); - // eslint-disable-next-line @typescript-eslint/naming-convention - const { username, full_name, email } = await caseService.getUser({ request }); - const updatedAt = new Date().toISOString(); - const updatedCases = await caseService.patchSubCases({ - client, - subCases: nonEmptySubCaseRequests.map((thisCase) => { - const { id: subCaseId, version, ...updateSubCaseAttributes } = thisCase; - let closedInfo: { closed_at: string | null; closed_by: User | null } = { - closed_at: null, - closed_by: null, - }; - - if ( - updateSubCaseAttributes.status && - updateSubCaseAttributes.status === CaseStatuses.closed - ) { - closedInfo = { - closed_at: updatedAt, - closed_by: { email, full_name, username }, - }; - } else if ( - updateSubCaseAttributes.status && - (updateSubCaseAttributes.status === CaseStatuses.open || - updateSubCaseAttributes.status === CaseStatuses['in-progress']) - ) { - closedInfo = { + // eslint-disable-next-line @typescript-eslint/naming-convention + const { username, full_name, email } = await caseService.getUser({ request }); + const updatedAt = new Date().toISOString(); + const updatedCases = await caseService.patchSubCases({ + client, + subCases: nonEmptySubCaseRequests.map((thisCase) => { + const { id: subCaseId, version, ...updateSubCaseAttributes } = thisCase; + let closedInfo: { closed_at: string | null; closed_by: User | null } = { closed_at: null, closed_by: null, }; - } - return { - subCaseId, - updatedAttributes: { - ...updateSubCaseAttributes, - ...closedInfo, - updated_at: updatedAt, - updated_by: { email, full_name, username }, - }, - version, - }; - }), - }); - const subCasesToSyncAlertsFor = nonEmptySubCaseRequests.filter((subCaseToUpdate) => { - const storedSubCase = subCasesMap.get(subCaseToUpdate.id); - const parentCase = subIDToParentCase.get(subCaseToUpdate.id); - return ( - storedSubCase !== undefined && - subCaseToUpdate.status !== undefined && - storedSubCase.attributes.status !== subCaseToUpdate.status && - parentCase?.attributes.settings.syncAlerts - ); - }); + if ( + updateSubCaseAttributes.status && + updateSubCaseAttributes.status === CaseStatuses.closed + ) { + closedInfo = { + closed_at: updatedAt, + closed_by: { email, full_name, username }, + }; + } else if ( + updateSubCaseAttributes.status && + (updateSubCaseAttributes.status === CaseStatuses.open || + updateSubCaseAttributes.status === CaseStatuses['in-progress']) + ) { + closedInfo = { + closed_at: null, + closed_by: null, + }; + } + return { + subCaseId, + updatedAttributes: { + ...updateSubCaseAttributes, + ...closedInfo, + updated_at: updatedAt, + updated_by: { email, full_name, username }, + }, + version, + }; + }), + }); - await updateAlerts({ - caseService, - client, - caseClient, - subCasesToSync: subCasesToSyncAlertsFor, - }); + const subCasesToSyncAlertsFor = nonEmptySubCaseRequests.filter((subCaseToUpdate) => { + const storedSubCase = subCasesMap.get(subCaseToUpdate.id); + const parentCase = subIDToParentCase.get(subCaseToUpdate.id); + return ( + storedSubCase !== undefined && + subCaseToUpdate.status !== undefined && + storedSubCase.attributes.status !== subCaseToUpdate.status && + parentCase?.attributes.settings.syncAlerts + ); + }); - const returnUpdatedSubCases = updatedCases.saved_objects.reduce( - (acc, updatedSO) => { - const originalSubCase = subCasesMap.get(updatedSO.id); - if (originalSubCase) { - acc.push( - flattenSubCaseSavedObject({ - savedObject: { - ...originalSubCase, - ...updatedSO, - attributes: { ...originalSubCase.attributes, ...updatedSO.attributes }, - references: originalSubCase.references, - version: updatedSO.version ?? originalSubCase.version, - }, - }) - ); - } - return acc; - }, - [] - ); + await updateAlerts({ + caseService, + client, + caseClient, + subCasesToSync: subCasesToSyncAlertsFor, + logger, + }); - await userActionService.postUserActions({ - client, - actions: buildSubCaseUserActions({ - originalSubCases: bulkSubCases.saved_objects, - updatedSubCases: updatedCases.saved_objects, - actionDate: updatedAt, - actionBy: { email, full_name, username }, - }), - }); + const returnUpdatedSubCases = updatedCases.saved_objects.reduce( + (acc, updatedSO) => { + const originalSubCase = subCasesMap.get(updatedSO.id); + if (originalSubCase) { + acc.push( + flattenSubCaseSavedObject({ + savedObject: { + ...originalSubCase, + ...updatedSO, + attributes: { ...originalSubCase.attributes, ...updatedSO.attributes }, + references: originalSubCase.references, + version: updatedSO.version ?? originalSubCase.version, + }, + }) + ); + } + return acc; + }, + [] + ); + + await userActionService.postUserActions({ + client, + actions: buildSubCaseUserActions({ + originalSubCases: bulkSubCases.saved_objects, + updatedSubCases: updatedCases.saved_objects, + actionDate: updatedAt, + actionBy: { email, full_name, username }, + }), + }); - return SubCasesResponseRt.encode(returnUpdatedSubCases); + return SubCasesResponseRt.encode(returnUpdatedSubCases); + } catch (error) { + const idVersions = query.subCases.map((subCase) => ({ + id: subCase.id, + version: subCase.version, + })); + throw createCaseError({ + message: `Failed to update sub cases: ${JSON.stringify(idVersions)}: ${error}`, + error, + logger, + }); + } } -export function initPatchSubCasesApi({ router, caseService, userActionService }: RouteDeps) { +export function initPatchSubCasesApi({ + router, + caseService, + userActionService, + logger, +}: RouteDeps) { router.patch( { path: SUB_CASES_PATCH_DEL_URL, @@ -396,10 +430,10 @@ export function initPatchSubCasesApi({ router, caseService, userActionService }: }, }, async (context, request, response) => { - const caseClient = context.case.getCaseClient(); - const subCases = request.body as SubCasesPatchRequest; - try { + const caseClient = context.case.getCaseClient(); + const subCases = request.body as SubCasesPatchRequest; + return response.ok({ body: await update({ request, @@ -408,9 +442,11 @@ export function initPatchSubCasesApi({ router, caseService, userActionService }: client: context.core.savedObjects.client, caseService, userActionService, + logger, }), }); } catch (error) { + logger.error(`Failed to patch sub cases in route: ${error}`); return response.customError(wrapError(error)); } } diff --git a/x-pack/plugins/case/server/routes/api/cases/user_actions/get_all_user_actions.ts b/x-pack/plugins/case/server/routes/api/cases/user_actions/get_all_user_actions.ts index 488f32a795811f..2efef9ac67f80b 100644 --- a/x-pack/plugins/case/server/routes/api/cases/user_actions/get_all_user_actions.ts +++ b/x-pack/plugins/case/server/routes/api/cases/user_actions/get_all_user_actions.ts @@ -11,7 +11,7 @@ import { RouteDeps } from '../../types'; import { wrapError } from '../../utils'; import { CASE_USER_ACTIONS_URL, SUB_CASE_USER_ACTIONS_URL } from '../../../../../common/constants'; -export function initGetAllCaseUserActionsApi({ router }: RouteDeps) { +export function initGetAllCaseUserActionsApi({ router, logger }: RouteDeps) { router.get( { path: CASE_USER_ACTIONS_URL, @@ -22,25 +22,28 @@ export function initGetAllCaseUserActionsApi({ router }: RouteDeps) { }, }, async (context, request, response) => { - if (!context.case) { - return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' }); - } + try { + if (!context.case) { + return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' }); + } - const caseClient = context.case.getCaseClient(); - const caseId = request.params.case_id; + const caseClient = context.case.getCaseClient(); + const caseId = request.params.case_id; - try { return response.ok({ body: await caseClient.getUserActions({ caseId }), }); } catch (error) { + logger.error( + `Failed to retrieve case user actions in route case id: ${request.params.case_id}: ${error}` + ); return response.customError(wrapError(error)); } } ); } -export function initGetAllSubCaseUserActionsApi({ router }: RouteDeps) { +export function initGetAllSubCaseUserActionsApi({ router, logger }: RouteDeps) { router.get( { path: SUB_CASE_USER_ACTIONS_URL, @@ -52,19 +55,22 @@ export function initGetAllSubCaseUserActionsApi({ router }: RouteDeps) { }, }, async (context, request, response) => { - if (!context.case) { - return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' }); - } + try { + if (!context.case) { + return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' }); + } - const caseClient = context.case.getCaseClient(); - const caseId = request.params.case_id; - const subCaseId = request.params.sub_case_id; + const caseClient = context.case.getCaseClient(); + const caseId = request.params.case_id; + const subCaseId = request.params.sub_case_id; - try { return response.ok({ body: await caseClient.getUserActions({ caseId, subCaseId }), }); } catch (error) { + logger.error( + `Failed to retrieve sub case user actions in route case id: ${request.params.case_id} sub case id: ${request.params.sub_case_id}: ${error}` + ); return response.customError(wrapError(error)); } } diff --git a/x-pack/plugins/case/server/routes/api/types.ts b/x-pack/plugins/case/server/routes/api/types.ts index 395880e5c14101..6ce40e01c77520 100644 --- a/x-pack/plugins/case/server/routes/api/types.ts +++ b/x-pack/plugins/case/server/routes/api/types.ts @@ -5,6 +5,8 @@ * 2.0. */ +import type { Logger } from 'kibana/server'; + import type { CaseConfigureServiceSetup, CaseServiceSetup, @@ -20,6 +22,7 @@ export interface RouteDeps { connectorMappingsService: ConnectorMappingsServiceSetup; router: CasesRouter; userActionService: CaseUserActionServiceSetup; + logger: Logger; } export enum SortFieldCase { diff --git a/x-pack/plugins/case/server/routes/api/utils.ts b/x-pack/plugins/case/server/routes/api/utils.ts index 084b1a17a1434a..298f8bb877cda5 100644 --- a/x-pack/plugins/case/server/routes/api/utils.ts +++ b/x-pack/plugins/case/server/routes/api/utils.ts @@ -6,7 +6,7 @@ */ import { isEmpty } from 'lodash'; -import { badRequest, boomify, isBoom } from '@hapi/boom'; +import { badRequest, Boom, boomify, isBoom } from '@hapi/boom'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; import { pipe } from 'fp-ts/lib/pipeable'; @@ -45,6 +45,7 @@ import { import { transformESConnectorToCaseConnector } from './cases/helpers'; import { SortFieldCase } from './types'; +import { isCaseError } from '../../common/error'; export const transformNewSubCase = ({ createdAt, @@ -182,9 +183,19 @@ export const transformNewComment = ({ }; }; +/** + * Transforms an error into the correct format for a kibana response. + */ export function wrapError(error: any): CustomHttpResponseOptions { - const options = { statusCode: error.statusCode ?? 500 }; - const boom = isBoom(error) ? error : boomify(error, options); + let boom: Boom; + + if (isCaseError(error)) { + boom = error.boomify(); + } else { + const options = { statusCode: error.statusCode ?? 500 }; + boom = isBoom(error) ? error : boomify(error, options); + } + return { body: boom, headers: boom.output.headers as { [key: string]: string }, diff --git a/x-pack/plugins/case/server/services/alerts/index.test.ts b/x-pack/plugins/case/server/services/alerts/index.test.ts index 35aa3ff80efc1e..3b1020d3ef5569 100644 --- a/x-pack/plugins/case/server/services/alerts/index.test.ts +++ b/x-pack/plugins/case/server/services/alerts/index.test.ts @@ -8,10 +8,11 @@ import { KibanaRequest } from 'kibana/server'; import { CaseStatuses } from '../../../common/api'; import { AlertService, AlertServiceContract } from '.'; -import { elasticsearchServiceMock } from 'src/core/server/mocks'; +import { elasticsearchServiceMock, loggingSystemMock } from 'src/core/server/mocks'; describe('updateAlertsStatus', () => { const esClient = elasticsearchServiceMock.createElasticsearchClient(); + const logger = loggingSystemMock.create().get('case'); describe('happy path', () => { let alertService: AlertServiceContract; @@ -21,6 +22,7 @@ describe('updateAlertsStatus', () => { request: {} as KibanaRequest, status: CaseStatuses.closed, scopedClusterClient: esClient, + logger, }; beforeEach(async () => { @@ -50,6 +52,7 @@ describe('updateAlertsStatus', () => { status: CaseStatuses.closed, indices: new Set(['']), scopedClusterClient: esClient, + logger, }) ).toBeUndefined(); }); diff --git a/x-pack/plugins/case/server/services/alerts/index.ts b/x-pack/plugins/case/server/services/alerts/index.ts index a19e533418bc9a..45245b86ba2d51 100644 --- a/x-pack/plugins/case/server/services/alerts/index.ts +++ b/x-pack/plugins/case/server/services/alerts/index.ts @@ -9,9 +9,10 @@ import _ from 'lodash'; import type { PublicMethodsOf } from '@kbn/utility-types'; -import { ElasticsearchClient } from 'kibana/server'; +import { ElasticsearchClient, Logger } from 'kibana/server'; import { CaseStatuses } from '../../../common/api'; import { MAX_ALERTS_PER_SUB_CASE } from '../../../common/constants'; +import { createCaseError } from '../../common/error'; export type AlertServiceContract = PublicMethodsOf; @@ -20,12 +21,14 @@ interface UpdateAlertsStatusArgs { status: CaseStatuses; indices: Set; scopedClusterClient: ElasticsearchClient; + logger: Logger; } interface GetAlertsArgs { ids: string[]; indices: Set; scopedClusterClient: ElasticsearchClient; + logger: Logger; } interface Alert { @@ -57,56 +60,75 @@ export class AlertService { status, indices, scopedClusterClient, + logger, }: UpdateAlertsStatusArgs) { const sanitizedIndices = getValidIndices(indices); if (sanitizedIndices.length <= 0) { - // log that we only had invalid indices + logger.warn(`Empty alert indices when updateAlertsStatus ids: ${JSON.stringify(ids)}`); return; } - const result = await scopedClusterClient.updateByQuery({ - index: sanitizedIndices, - conflicts: 'abort', - body: { - script: { - source: `ctx._source.signal.status = '${status}'`, - lang: 'painless', + try { + const result = await scopedClusterClient.updateByQuery({ + index: sanitizedIndices, + conflicts: 'abort', + body: { + script: { + source: `ctx._source.signal.status = '${status}'`, + lang: 'painless', + }, + query: { ids: { values: ids } }, }, - query: { ids: { values: ids } }, - }, - ignore_unavailable: true, - }); - - return result; + ignore_unavailable: true, + }); + + return result; + } catch (error) { + throw createCaseError({ + message: `Failed to update alert status ids: ${JSON.stringify(ids)}: ${error}`, + error, + logger, + }); + } } public async getAlerts({ scopedClusterClient, ids, indices, + logger, }: GetAlertsArgs): Promise { const index = getValidIndices(indices); if (index.length <= 0) { + logger.warn(`Empty alert indices when retrieving alerts ids: ${JSON.stringify(ids)}`); return; } - const result = await scopedClusterClient.search({ - index, - body: { - query: { - bool: { - filter: { - ids: { - values: ids, + try { + const result = await scopedClusterClient.search({ + index, + body: { + query: { + bool: { + filter: { + ids: { + values: ids, + }, }, }, }, }, - }, - size: MAX_ALERTS_PER_SUB_CASE, - ignore_unavailable: true, - }); - - return result.body; + size: MAX_ALERTS_PER_SUB_CASE, + ignore_unavailable: true, + }); + + return result.body; + } catch (error) { + throw createCaseError({ + message: `Failed to retrieve alerts ids: ${JSON.stringify(ids)}: ${error}`, + error, + logger, + }); + } } } diff --git a/x-pack/plugins/case/server/services/connector_mappings/index.ts b/x-pack/plugins/case/server/services/connector_mappings/index.ts index cc387d8e6fe3ee..d4fda10276d2b5 100644 --- a/x-pack/plugins/case/server/services/connector_mappings/index.ts +++ b/x-pack/plugins/case/server/services/connector_mappings/index.ts @@ -41,7 +41,7 @@ export class ConnectorMappingsService { this.log.debug(`Attempting to find all connector mappings`); return await client.find({ ...options, type: CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT }); } catch (error) { - this.log.debug(`Attempting to find all connector mappings`); + this.log.error(`Attempting to find all connector mappings: ${error}`); throw error; } }, @@ -52,7 +52,7 @@ export class ConnectorMappingsService { references, }); } catch (error) { - this.log.debug(`Error on POST a new connector mappings: ${error}`); + this.log.error(`Error on POST a new connector mappings: ${error}`); throw error; } }, diff --git a/x-pack/plugins/case/server/services/index.ts b/x-pack/plugins/case/server/services/index.ts index a9e5c269608308..f74e91ca102243 100644 --- a/x-pack/plugins/case/server/services/index.ts +++ b/x-pack/plugins/case/server/services/index.ts @@ -621,7 +621,7 @@ export class CaseService implements CaseServiceSetup { ], }); } catch (error) { - this.log.debug(`Error on POST a new sub case: ${error}`); + this.log.error(`Error on POST a new sub case for id ${caseId}: ${error}`); throw error; } } @@ -642,7 +642,7 @@ export class CaseService implements CaseServiceSetup { return subCases.saved_objects[0]; } catch (error) { - this.log.debug(`Error finding the most recent sub case for case: ${caseId}`); + this.log.error(`Error finding the most recent sub case for case: ${caseId}: ${error}`); throw error; } } @@ -652,7 +652,7 @@ export class CaseService implements CaseServiceSetup { this.log.debug(`Attempting to DELETE sub case ${id}`); return await client.delete(SUB_CASE_SAVED_OBJECT, id); } catch (error) { - this.log.debug(`Error on DELETE sub case ${id}: ${error}`); + this.log.error(`Error on DELETE sub case ${id}: ${error}`); throw error; } } @@ -662,7 +662,7 @@ export class CaseService implements CaseServiceSetup { this.log.debug(`Attempting to DELETE case ${caseId}`); return await client.delete(CASE_SAVED_OBJECT, caseId); } catch (error) { - this.log.debug(`Error on DELETE case ${caseId}: ${error}`); + this.log.error(`Error on DELETE case ${caseId}: ${error}`); throw error; } } @@ -671,7 +671,7 @@ export class CaseService implements CaseServiceSetup { this.log.debug(`Attempting to GET comment ${commentId}`); return await client.delete(CASE_COMMENT_SAVED_OBJECT, commentId); } catch (error) { - this.log.debug(`Error on GET comment ${commentId}: ${error}`); + this.log.error(`Error on GET comment ${commentId}: ${error}`); throw error; } } @@ -683,7 +683,7 @@ export class CaseService implements CaseServiceSetup { this.log.debug(`Attempting to GET case ${caseId}`); return await client.get(CASE_SAVED_OBJECT, caseId); } catch (error) { - this.log.debug(`Error on GET case ${caseId}: ${error}`); + this.log.error(`Error on GET case ${caseId}: ${error}`); throw error; } } @@ -692,7 +692,7 @@ export class CaseService implements CaseServiceSetup { this.log.debug(`Attempting to GET sub case ${id}`); return await client.get(SUB_CASE_SAVED_OBJECT, id); } catch (error) { - this.log.debug(`Error on GET sub case ${id}: ${error}`); + this.log.error(`Error on GET sub case ${id}: ${error}`); throw error; } } @@ -705,7 +705,7 @@ export class CaseService implements CaseServiceSetup { this.log.debug(`Attempting to GET sub cases ${ids.join(', ')}`); return await client.bulkGet(ids.map((id) => ({ type: SUB_CASE_SAVED_OBJECT, id }))); } catch (error) { - this.log.debug(`Error on GET cases ${ids.join(', ')}: ${error}`); + this.log.error(`Error on GET cases ${ids.join(', ')}: ${error}`); throw error; } } @@ -720,7 +720,7 @@ export class CaseService implements CaseServiceSetup { caseIds.map((caseId) => ({ type: CASE_SAVED_OBJECT, id: caseId })) ); } catch (error) { - this.log.debug(`Error on GET cases ${caseIds.join(', ')}: ${error}`); + this.log.error(`Error on GET cases ${caseIds.join(', ')}: ${error}`); throw error; } } @@ -732,7 +732,7 @@ export class CaseService implements CaseServiceSetup { this.log.debug(`Attempting to GET comment ${commentId}`); return await client.get(CASE_COMMENT_SAVED_OBJECT, commentId); } catch (error) { - this.log.debug(`Error on GET comment ${commentId}: ${error}`); + this.log.error(`Error on GET comment ${commentId}: ${error}`); throw error; } } @@ -749,7 +749,7 @@ export class CaseService implements CaseServiceSetup { type: CASE_SAVED_OBJECT, }); } catch (error) { - this.log.debug(`Error on find cases: ${error}`); + this.log.error(`Error on find cases: ${error}`); throw error; } } @@ -786,7 +786,7 @@ export class CaseService implements CaseServiceSetup { type: SUB_CASE_SAVED_OBJECT, }); } catch (error) { - this.log.debug(`Error on find sub cases: ${error}`); + this.log.error(`Error on find sub cases: ${error}`); throw error; } } @@ -824,7 +824,7 @@ export class CaseService implements CaseServiceSetup { }, }); } catch (error) { - this.log.debug( + this.log.error( `Error on GET all sub cases for case collection id ${ids.join(', ')}: ${error}` ); throw error; @@ -847,7 +847,7 @@ export class CaseService implements CaseServiceSetup { options, }: FindCommentsArgs): Promise> { try { - this.log.debug(`Attempting to GET all comments for id ${id}`); + this.log.debug(`Attempting to GET all comments for id ${JSON.stringify(id)}`); if (options?.page !== undefined || options?.perPage !== undefined) { return client.find({ type: CASE_COMMENT_SAVED_OBJECT, @@ -874,7 +874,7 @@ export class CaseService implements CaseServiceSetup { ...options, }); } catch (error) { - this.log.debug(`Error on GET all comments for ${id}: ${error}`); + this.log.error(`Error on GET all comments for ${JSON.stringify(id)}: ${error}`); throw error; } } @@ -915,7 +915,7 @@ export class CaseService implements CaseServiceSetup { ); } - this.log.debug(`Attempting to GET all comments for case caseID ${id}`); + this.log.debug(`Attempting to GET all comments for case caseID ${JSON.stringify(id)}`); return this.getAllComments({ client, id, @@ -927,7 +927,7 @@ export class CaseService implements CaseServiceSetup { }, }); } catch (error) { - this.log.debug(`Error on GET all comments for case ${id}: ${error}`); + this.log.error(`Error on GET all comments for case ${JSON.stringify(id)}: ${error}`); throw error; } } @@ -948,7 +948,7 @@ export class CaseService implements CaseServiceSetup { }; } - this.log.debug(`Attempting to GET all comments for sub case caseID ${id}`); + this.log.debug(`Attempting to GET all comments for sub case caseID ${JSON.stringify(id)}`); return this.getAllComments({ client, id, @@ -959,7 +959,7 @@ export class CaseService implements CaseServiceSetup { }, }); } catch (error) { - this.log.debug(`Error on GET all comments for sub case ${id}: ${error}`); + this.log.error(`Error on GET all comments for sub case ${JSON.stringify(id)}: ${error}`); throw error; } } @@ -969,7 +969,7 @@ export class CaseService implements CaseServiceSetup { this.log.debug(`Attempting to GET all reporters`); return await readReporters({ client }); } catch (error) { - this.log.debug(`Error on GET all reporters: ${error}`); + this.log.error(`Error on GET all reporters: ${error}`); throw error; } } @@ -978,7 +978,7 @@ export class CaseService implements CaseServiceSetup { this.log.debug(`Attempting to GET all cases`); return await readTags({ client }); } catch (error) { - this.log.debug(`Error on GET cases: ${error}`); + this.log.error(`Error on GET cases: ${error}`); throw error; } } @@ -1003,7 +1003,7 @@ export class CaseService implements CaseServiceSetup { email: null, }; } catch (error) { - this.log.debug(`Error on GET cases: ${error}`); + this.log.error(`Error on GET cases: ${error}`); throw error; } } @@ -1012,7 +1012,7 @@ export class CaseService implements CaseServiceSetup { this.log.debug(`Attempting to POST a new case`); return await client.create(CASE_SAVED_OBJECT, { ...attributes }); } catch (error) { - this.log.debug(`Error on POST a new case: ${error}`); + this.log.error(`Error on POST a new case: ${error}`); throw error; } } @@ -1021,7 +1021,7 @@ export class CaseService implements CaseServiceSetup { this.log.debug(`Attempting to POST a new comment`); return await client.create(CASE_COMMENT_SAVED_OBJECT, attributes, { references }); } catch (error) { - this.log.debug(`Error on POST a new comment: ${error}`); + this.log.error(`Error on POST a new comment: ${error}`); throw error; } } @@ -1030,7 +1030,7 @@ export class CaseService implements CaseServiceSetup { this.log.debug(`Attempting to UPDATE case ${caseId}`); return await client.update(CASE_SAVED_OBJECT, caseId, { ...updatedAttributes }, { version }); } catch (error) { - this.log.debug(`Error on UPDATE case ${caseId}: ${error}`); + this.log.error(`Error on UPDATE case ${caseId}: ${error}`); throw error; } } @@ -1046,7 +1046,7 @@ export class CaseService implements CaseServiceSetup { })) ); } catch (error) { - this.log.debug(`Error on UPDATE case ${cases.map((c) => c.caseId).join(', ')}: ${error}`); + this.log.error(`Error on UPDATE case ${cases.map((c) => c.caseId).join(', ')}: ${error}`); throw error; } } @@ -1062,7 +1062,7 @@ export class CaseService implements CaseServiceSetup { { version } ); } catch (error) { - this.log.debug(`Error on UPDATE comment ${commentId}: ${error}`); + this.log.error(`Error on UPDATE comment ${commentId}: ${error}`); throw error; } } @@ -1080,7 +1080,7 @@ export class CaseService implements CaseServiceSetup { })) ); } catch (error) { - this.log.debug( + this.log.error( `Error on UPDATE comments ${comments.map((c) => c.commentId).join(', ')}: ${error}` ); throw error; @@ -1096,7 +1096,7 @@ export class CaseService implements CaseServiceSetup { { version } ); } catch (error) { - this.log.debug(`Error on UPDATE sub case ${subCaseId}: ${error}`); + this.log.error(`Error on UPDATE sub case ${subCaseId}: ${error}`); throw error; } } @@ -1115,7 +1115,7 @@ export class CaseService implements CaseServiceSetup { })) ); } catch (error) { - this.log.debug( + this.log.error( `Error on UPDATE sub case ${subCases.map((c) => c.subCaseId).join(', ')}: ${error}` ); throw error; diff --git a/x-pack/plugins/case/server/services/user_actions/index.ts b/x-pack/plugins/case/server/services/user_actions/index.ts index d05ada0dba30cd..785c81021b584f 100644 --- a/x-pack/plugins/case/server/services/user_actions/index.ts +++ b/x-pack/plugins/case/server/services/user_actions/index.ts @@ -66,7 +66,7 @@ export class CaseUserActionService { sortOrder: 'asc', }); } catch (error) { - this.log.debug(`Error on GET case user action: ${error}`); + this.log.error(`Error on GET case user action case id: ${caseId}: ${error}`); throw error; } }, @@ -77,7 +77,7 @@ export class CaseUserActionService { actions.map((action) => ({ type: CASE_USER_ACTION_SAVED_OBJECT, ...action })) ); } catch (error) { - this.log.debug(`Error on POST a new case user action: ${error}`); + this.log.error(`Error on POST a new case user action: ${error}`); throw error; } },