diff --git a/x-pack/plugins/cases/common/types/api/case/v1.test.ts b/x-pack/plugins/cases/common/types/api/case/v1.test.ts index 40c980d633545..91676b7ca6510 100644 --- a/x-pack/plugins/cases/common/types/api/case/v1.test.ts +++ b/x-pack/plugins/cases/common/types/api/case/v1.test.ts @@ -5,6 +5,18 @@ * 2.0. */ +import { + MAX_CATEGORY_FILTER_LENGTH, + MAX_TAGS_FILTER_LENGTH, + MAX_ASSIGNEES_FILTER_LENGTH, + MAX_REPORTERS_FILTER_LENGTH, + MAX_ASSIGNEES_PER_CASE, + MAX_DESCRIPTION_LENGTH, + MAX_TAGS_PER_CASE, + MAX_LENGTH_PER_TAG, + MAX_TITLE_LENGTH, + MAX_CATEGORY_LENGTH, +} from '../../../constants'; import { PathReporter } from 'io-ts/lib/PathReporter'; import { AttachmentType } from '../../domain/attachment/v1'; import { CaseSeverity, CaseStatuses } from '../../domain/case/v1'; @@ -187,6 +199,66 @@ describe('Status', () => { right: defaultRequest, }); }); + + it(`throws an error when the assignees are more than ${MAX_ASSIGNEES_PER_CASE}`, async () => { + const assignees = Array(MAX_ASSIGNEES_PER_CASE + 1).fill({ uid: 'foobar' }); + + expect( + PathReporter.report(CasePostRequestRt.decode({ ...defaultRequest, assignees })) + ).toContain('The length of the field assignees is too long. Array must be of length <= 10.'); + }); + + it('does not throw an error with empty assignees', async () => { + expect( + PathReporter.report(CasePostRequestRt.decode({ ...defaultRequest, assignees: [] })) + ).toContain('No errors!'); + }); + + it('does not throw an error with undefined assignees', async () => { + const { assignees, ...rest } = defaultRequest; + + expect(PathReporter.report(CasePostRequestRt.decode(rest))).toContain('No errors!'); + }); + + it(`throws an error when the description contains more than ${MAX_DESCRIPTION_LENGTH} characters`, async () => { + const description = 'a'.repeat(MAX_DESCRIPTION_LENGTH + 1); + + expect( + PathReporter.report(CasePostRequestRt.decode({ ...defaultRequest, description })) + ).toContain('The length of the description is too long. The maximum length is 30000.'); + }); + + it(`throws an error when there are more than ${MAX_TAGS_PER_CASE} tags`, async () => { + const tags = Array(MAX_TAGS_PER_CASE + 1).fill('foobar'); + + expect(PathReporter.report(CasePostRequestRt.decode({ ...defaultRequest, tags }))).toContain( + 'The length of the field tags is too long. Array must be of length <= 200.' + ); + }); + + it(`throws an error when the a tag is more than ${MAX_LENGTH_PER_TAG} characters`, async () => { + const tag = 'a'.repeat(MAX_LENGTH_PER_TAG + 1); + + expect( + PathReporter.report(CasePostRequestRt.decode({ ...defaultRequest, tags: [tag] })) + ).toContain('The length of the tag is too long. The maximum length is 256.'); + }); + + it(`throws an error when the title contains more than ${MAX_TITLE_LENGTH} characters`, async () => { + const title = 'a'.repeat(MAX_TITLE_LENGTH + 1); + + expect(PathReporter.report(CasePostRequestRt.decode({ ...defaultRequest, title }))).toContain( + 'The length of the title is too long. The maximum length is 160.' + ); + }); + + it(`throws an error when the category contains more than ${MAX_CATEGORY_LENGTH} characters`, async () => { + const category = 'a'.repeat(MAX_CATEGORY_LENGTH + 1); + + expect( + PathReporter.report(CasePostRequestRt.decode({ ...defaultRequest, category })) + ).toContain('The length of the category is too long. The maximum length is 50.'); + }); }); describe('CasesFindRequestRt', () => { @@ -276,6 +348,38 @@ describe('Status', () => { 'No errors!' ); }); + + it(`throws an error when the category array has ${MAX_CATEGORY_FILTER_LENGTH} items`, async () => { + const category = Array(MAX_CATEGORY_FILTER_LENGTH + 1).fill('foobar'); + + expect(PathReporter.report(CasesFindRequestRt.decode({ category }))).toContain( + 'The length of the field category is too long. Array must be of length <= 100.' + ); + }); + + it(`throws an error when the tags array has ${MAX_TAGS_FILTER_LENGTH} items`, async () => { + const tags = Array(MAX_TAGS_FILTER_LENGTH + 1).fill('foobar'); + + expect(PathReporter.report(CasesFindRequestRt.decode({ tags }))).toContain( + 'The length of the field tags is too long. Array must be of length <= 100.' + ); + }); + + it(`throws an error when the assignees array has ${MAX_ASSIGNEES_FILTER_LENGTH} items`, async () => { + const assignees = Array(MAX_ASSIGNEES_FILTER_LENGTH + 1).fill('foobar'); + + expect(PathReporter.report(CasesFindRequestRt.decode({ assignees }))).toContain( + 'The length of the field assignees is too long. Array must be of length <= 100.' + ); + }); + + it(`throws an error when the reporters array has ${MAX_REPORTERS_FILTER_LENGTH} items`, async () => { + const reporters = Array(MAX_REPORTERS_FILTER_LENGTH + 1).fill('foobar'); + + expect(PathReporter.report(CasesFindRequestRt.decode({ reporters }))).toContain( + 'The length of the field reporters is too long. Array must be of length <= 100.' + ); + }); }); }); }); @@ -393,6 +497,64 @@ describe('CasePatchRequestRt', () => { right: defaultRequest, }); }); + + it(`throws an error when the assignees are more than ${MAX_ASSIGNEES_PER_CASE}`, async () => { + const assignees = Array(MAX_ASSIGNEES_PER_CASE + 1).fill({ uid: 'foobar' }); + + expect( + PathReporter.report(CasePatchRequestRt.decode({ ...defaultRequest, assignees })) + ).toContain('The length of the field assignees is too long. Array must be of length <= 10.'); + }); + + it('does not throw an error with empty assignees', async () => { + expect( + PathReporter.report(CasePatchRequestRt.decode({ ...defaultRequest, assignees: [] })) + ).toContain('No errors!'); + }); + + it('does not throw an error with undefined assignees', async () => { + expect(PathReporter.report(CasePatchRequestRt.decode(defaultRequest))).toContain('No errors!'); + }); + + it(`throws an error when the description contains more than ${MAX_DESCRIPTION_LENGTH} characters`, async () => { + const description = 'a'.repeat(MAX_DESCRIPTION_LENGTH + 1); + + expect( + PathReporter.report(CasePatchRequestRt.decode({ ...defaultRequest, description })) + ).toContain('The length of the description is too long. The maximum length is 30000.'); + }); + + it(`throws an error when there are more than ${MAX_TAGS_PER_CASE} tags`, async () => { + const tags = Array(MAX_TAGS_PER_CASE + 1).fill('foobar'); + + expect(PathReporter.report(CasePatchRequestRt.decode({ ...defaultRequest, tags }))).toContain( + 'The length of the field tags is too long. Array must be of length <= 200.' + ); + }); + + it(`throws an error when the a tag is more than ${MAX_LENGTH_PER_TAG} characters`, async () => { + const tag = 'a'.repeat(MAX_LENGTH_PER_TAG + 1); + + expect( + PathReporter.report(CasePatchRequestRt.decode({ ...defaultRequest, tags: [tag] })) + ).toContain('The length of the tag is too long. The maximum length is 256.'); + }); + + it(`throws an error when the title contains more than ${MAX_TITLE_LENGTH} characters`, async () => { + const title = 'a'.repeat(MAX_TITLE_LENGTH + 1); + + expect(PathReporter.report(CasePatchRequestRt.decode({ ...defaultRequest, title }))).toContain( + 'The length of the title is too long. The maximum length is 160.' + ); + }); + + it(`throws an error when the category contains more than ${MAX_CATEGORY_LENGTH} characters`, async () => { + const category = 'a'.repeat(MAX_CATEGORY_LENGTH + 1); + + expect( + PathReporter.report(CasePatchRequestRt.decode({ ...defaultRequest, category })) + ).toContain('The length of the category is too long. The maximum length is 50.'); + }); }); describe('CasesPatchRequestRt', () => { @@ -423,6 +585,24 @@ describe('CasesPatchRequestRt', () => { right: defaultRequest, }); }); + + it(`throws an error when the assignees are more than ${MAX_ASSIGNEES_PER_CASE}`, async () => { + const assignees = Array(MAX_ASSIGNEES_PER_CASE + 1).fill({ uid: 'foobar' }); + + expect( + PathReporter.report( + CasesPatchRequestRt.decode({ + cases: [ + { + id: 'basic-case-id', + version: 'WzQ3LDFd', + assignees, + }, + ], + }) + ) + ).toContain('The length of the field assignees is too long. Array must be of length <= 10.'); + }); }); describe('CasePushRequestParamsRt', () => { diff --git a/x-pack/plugins/cases/common/types/api/case/v1.ts b/x-pack/plugins/cases/common/types/api/case/v1.ts index 97981f90fb8dc..7a92c1f32ca12 100644 --- a/x-pack/plugins/cases/common/types/api/case/v1.ts +++ b/x-pack/plugins/cases/common/types/api/case/v1.ts @@ -19,6 +19,8 @@ import { MAX_TAGS_FILTER_LENGTH, MAX_CASES_TO_UPDATE, MAX_BULK_GET_CASES, + MAX_CATEGORY_FILTER_LENGTH, + MAX_ASSIGNEES_PER_CASE, } from '../../../constants'; import { limitedStringSchema, @@ -35,7 +37,7 @@ import { RelatedCaseRt, } from '../../domain/case/v1'; import { CaseConnectorRt } from '../../domain/connector/v1'; -import { CaseAssigneesRt, UserRt } from '../../domain/user/v1'; +import { CaseUserProfileRt, UserRt } from '../../domain/user/v1'; import { CasesStatusResponseRt } from '../stats/v1'; /** @@ -84,7 +86,12 @@ export const CasePostRequestRt = rt.intersection([ /** * The users assigned to the case */ - assignees: CaseAssigneesRt, + assignees: limitedArraySchema({ + codec: CaseUserProfileRt, + fieldName: 'assignees', + min: 0, + max: MAX_ASSIGNEES_PER_CASE, + }), /** * The severity of the case. The severity is * default it to "low" if not provided. @@ -214,7 +221,15 @@ export const CasesFindRequestRt = rt.intersection([ /** * The category of the case. */ - category: rt.union([rt.array(rt.string), rt.string]), + category: rt.union([ + limitedArraySchema({ + codec: rt.string, + fieldName: 'category', + min: 0, + max: MAX_CATEGORY_FILTER_LENGTH, + }), + rt.string, + ]), }) ), paginationSchema({ maxPerPage: MAX_CASES_PER_PAGE }), @@ -330,7 +345,12 @@ export const CasePatchRequestRt = rt.intersection([ /** * The users assigned to this case */ - assignees: CaseAssigneesRt, + assignees: limitedArraySchema({ + codec: CaseUserProfileRt, + fieldName: 'assignees', + min: 0, + max: MAX_ASSIGNEES_PER_CASE, + }), /** * The category of the case. */ diff --git a/x-pack/plugins/cases/server/client/cases/create.test.ts b/x-pack/plugins/cases/server/client/cases/create.test.ts index 9918cc302c033..61672e70d8e3d 100644 --- a/x-pack/plugins/cases/server/client/cases/create.test.ts +++ b/x-pack/plugins/cases/server/client/cases/create.test.ts @@ -10,6 +10,7 @@ import { MAX_TAGS_PER_CASE, MAX_LENGTH_PER_TAG, MAX_TITLE_LENGTH, + MAX_ASSIGNEES_PER_CASE, } from '../../../common/constants'; import { SECURITY_SOLUTION_OWNER } from '../../../common'; import { mockCases } from '../../mocks'; @@ -82,6 +83,14 @@ describe('create', () => { theCase: caseSO, }); }); + + it('should throw an error if the assignees array length is too long', async () => { + const assignees = Array(MAX_ASSIGNEES_PER_CASE + 1).fill({ uid: 'foo' }); + + await expect(create({ ...theCase, assignees }, clientArgs)).rejects.toThrow( + `Failed to create case: Error: The length of the field assignees is too long. Array must be of length <= ${MAX_ASSIGNEES_PER_CASE}.` + ); + }); }); describe('Attributes', () => { diff --git a/x-pack/plugins/cases/server/client/cases/create.ts b/x-pack/plugins/cases/server/client/cases/create.ts index 9467fe9ecc887..30524c1db6595 100644 --- a/x-pack/plugins/cases/server/client/cases/create.ts +++ b/x-pack/plugins/cases/server/client/cases/create.ts @@ -12,8 +12,6 @@ import { SavedObjectsUtils } from '@kbn/core/server'; import type { Case } from '../../../common/types/domain'; import { CaseSeverity, UserActionTypes, CaseRt } from '../../../common/types/domain'; import { decodeWithExcessOrThrow } from '../../../common/api'; -import { MAX_ASSIGNEES_PER_CASE } from '../../../common/constants'; -import { areTotalAssigneesInvalid } from '../../../common/utils/validators'; import { Operations } from '../../authorization'; import { createCaseError } from '../../common/error'; @@ -63,12 +61,6 @@ export const create = async (data: CasePostRequest, clientArgs: CasesClientArgs) licensingService.notifyUsage(LICENSING_CASE_ASSIGNMENT_FEATURE); } - if (areTotalAssigneesInvalid(query.assignees)) { - throw Boom.badRequest( - `You cannot assign more than ${MAX_ASSIGNEES_PER_CASE} assignees to a case.` - ); - } - const newCase = await caseService.postNewCase({ attributes: transformNewCase({ user, diff --git a/x-pack/plugins/cases/server/client/cases/find.test.ts b/x-pack/plugins/cases/server/client/cases/find.test.ts index 0a09fa8b5b7b2..8acb91b40c383 100644 --- a/x-pack/plugins/cases/server/client/cases/find.test.ts +++ b/x-pack/plugins/cases/server/client/cases/find.test.ts @@ -128,7 +128,7 @@ describe('find', () => { const findRequest = createCasesClientMockFindRequest({ category }); await expect(find(findRequest, clientArgs)).rejects.toThrow( - `Error: Too many categories provided. The maximum allowed is ${MAX_CATEGORY_FILTER_LENGTH}` + `Error: The length of the field category is too long. Array must be of length <= ${MAX_CATEGORY_FILTER_LENGTH}` ); }); diff --git a/x-pack/plugins/cases/server/client/cases/find.ts b/x-pack/plugins/cases/server/client/cases/find.ts index 9c8699ce9db84..c61c4ba71ffde 100644 --- a/x-pack/plugins/cases/server/client/cases/find.ts +++ b/x-pack/plugins/cases/server/client/cases/find.ts @@ -10,7 +10,6 @@ import Boom from '@hapi/boom'; import type { CasesFindRequest, CasesFindResponse } from '../../../common/types/api'; import { CasesFindRequestRt, CasesFindResponseRt } from '../../../common/types/api'; -import { MAX_CATEGORY_FILTER_LENGTH } from '../../../common/constants'; import { decodeWithExcessOrThrow } from '../../../common/api'; import { createCaseError } from '../../common/error'; @@ -22,16 +21,6 @@ import { LICENSING_CASE_ASSIGNMENT_FEATURE } from '../../common/constants'; import type { CasesFindQueryParams } from '../types'; import { decodeOrThrow } from '../../../common/api/runtime_types'; -/** - * Throws an error if the user tries to filter by more than MAX_CATEGORY_FILTER_LENGTH categories. - */ -function throwIfCategoryParamTooLong(category?: string[] | string) { - if (Array.isArray(category) && category.length > MAX_CATEGORY_FILTER_LENGTH) - throw Boom.badRequest( - `Too many categories provided. The maximum allowed is ${MAX_CATEGORY_FILTER_LENGTH}` - ); -} - /** * Retrieves a case and optionally its comments. * @@ -52,8 +41,6 @@ export const find = async ( try { const queryParams = decodeWithExcessOrThrow(CasesFindRequestRt)(params); - throwIfCategoryParamTooLong(queryParams.category); - /** * Assign users to a case is only available to Platinum+ */ diff --git a/x-pack/plugins/cases/server/client/cases/update.test.ts b/x-pack/plugins/cases/server/client/cases/update.test.ts index 039f4e8582804..17b51658b1233 100644 --- a/x-pack/plugins/cases/server/client/cases/update.test.ts +++ b/x-pack/plugins/cases/server/client/cases/update.test.ts @@ -13,6 +13,7 @@ import { MAX_TITLE_LENGTH, MAX_CASES_TO_UPDATE, MAX_USER_ACTIONS_PER_CASE, + MAX_ASSIGNEES_PER_CASE, } from '../../../common/constants'; import { mockCases } from '../../mocks'; import { createCasesClientMockArgs } from '../mocks'; @@ -279,6 +280,27 @@ describe('update', () => { `"Failed to update case, ids: [{\\"id\\":\\"mock-id-1\\",\\"version\\":\\"WzAsMV0=\\"}]: Error: invalid keys \\"foo\\""` ); }); + + it('should throw an error if the assignees array length is too long', async () => { + const assignees = Array(MAX_ASSIGNEES_PER_CASE + 1).fill({ uid: 'foo' }); + + await expect( + update( + { + cases: [ + { + id: mockCases[0].id, + version: mockCases[0].version ?? '', + assignees, + }, + ], + }, + clientArgs + ) + ).rejects.toThrow( + 'Failed to update case, ids: [{"id":"mock-id-1","version":"WzAsMV0="}]: Error: The length of the field assignees is too long. Array must be of length <= 10.' + ); + }); }); describe('Category', () => { diff --git a/x-pack/plugins/cases/server/client/cases/update.ts b/x-pack/plugins/cases/server/client/cases/update.ts index 1844b12b3ab07..121fd2a8e3aa5 100644 --- a/x-pack/plugins/cases/server/client/cases/update.ts +++ b/x-pack/plugins/cases/server/client/cases/update.ts @@ -25,11 +25,9 @@ import type { PatchCasesArgs } from '../../services/cases/types'; import type { UserActionEvent, UserActionsDict } from '../../services/user_actions/types'; import type { CasePatchRequest, CasesPatchRequest } from '../../../common/types/api'; -import { areTotalAssigneesInvalid } from '../../../common/utils/validators'; import { CASE_COMMENT_SAVED_OBJECT, CASE_SAVED_OBJECT, - MAX_ASSIGNEES_PER_CASE, MAX_USER_ACTIONS_PER_CASE, } from '../../../common/constants'; import { Operations } from '../../authorization'; @@ -138,27 +136,6 @@ function notifyPlatinumUsage( } } -/** - * Throws an error if any of the requests attempt to add more than - * MAX_ASSIGNEES_PER_CASE to a case - */ -function throwIfTotalAssigneesAreInvalid(requests: UpdateRequestWithOriginalCase[]) { - const requestsUpdatingAssignees = requests.filter( - ({ updateReq }) => updateReq.assignees !== undefined - ); - - if ( - requestsUpdatingAssignees.some(({ updateReq }) => areTotalAssigneesInvalid(updateReq.assignees)) - ) { - const ids = requestsUpdatingAssignees.map(({ updateReq }) => updateReq.id); - throw Boom.badRequest( - `You cannot assign more than ${MAX_ASSIGNEES_PER_CASE} assignees to a case, ids: [${ids.join( - ', ' - )}]` - ); - } -} - /** * Get the id from a reference in a comment for a specific type. */ @@ -394,7 +371,6 @@ export const update = async ( throwIfUpdateOwner(casesToUpdate); throwIfUpdateAssigneesWithoutValidLicense(casesToUpdate, hasPlatinumLicense); - throwIfTotalAssigneesAreInvalid(casesToUpdate); const patchCasesPayload = createPatchCasesPayload({ user, casesToUpdate }); const userActionsDict = userActionService.creator.buildUserActions({ diff --git a/x-pack/plugins/cases/server/routes/api/comments/find_comments.ts b/x-pack/plugins/cases/server/routes/api/comments/find_comments.ts index 7d0ba2af2a061..05e4662f27292 100644 --- a/x-pack/plugins/cases/server/routes/api/comments/find_comments.ts +++ b/x-pack/plugins/cases/server/routes/api/comments/find_comments.ts @@ -7,8 +7,7 @@ import { schema } from '@kbn/config-schema'; -import { FindAttachmentsQueryParamsRt } from '../../../../common/types/api'; -import { decodeWithExcessOrThrow } from '../../../../common/api'; +import type { attachmentApiV1 } from '../../../../common/types/api'; import { CASE_FIND_ATTACHMENTS_URL } from '../../../../common/constants'; import { createCasesRoute } from '../create_cases_route'; import { createCaseError } from '../../../common/error'; @@ -23,15 +22,17 @@ export const findCommentsRoute = createCasesRoute({ }, handler: async ({ context, request, response }) => { try { - const query = decodeWithExcessOrThrow(FindAttachmentsQueryParamsRt)(request.query); - const caseContext = await context.cases; const client = await caseContext.getCasesClient(); + const query = request.query as attachmentApiV1.FindAttachmentsQueryParams; + + const res: attachmentApiV1.AttachmentsFindResponse = await client.attachments.find({ + caseID: request.params.case_id, + findQueryParams: query, + }); + return response.ok({ - body: await client.attachments.find({ - caseID: request.params.case_id, - findQueryParams: query, - }), + body: res, }); } catch (error) { throw createCaseError({ diff --git a/x-pack/plugins/cases/server/routes/api/comments/get_all_comment.ts b/x-pack/plugins/cases/server/routes/api/comments/get_all_comment.ts index 72b8f7e6ac98b..fffb23921e2d3 100644 --- a/x-pack/plugins/cases/server/routes/api/comments/get_all_comment.ts +++ b/x-pack/plugins/cases/server/routes/api/comments/get_all_comment.ts @@ -10,6 +10,7 @@ import { schema } from '@kbn/config-schema'; import { CASE_COMMENTS_URL } from '../../../../common/constants'; import { createCaseError } from '../../../common/error'; import { createCasesRoute } from '../create_cases_route'; +import type { attachmentDomainV1 } from '../../../../common/types/domain'; /** * @deprecated since version 8.1.0 @@ -27,11 +28,12 @@ export const getAllCommentsRoute = createCasesRoute({ try { const caseContext = await context.cases; const client = await caseContext.getCasesClient(); + const res: attachmentDomainV1.Attachments = await client.attachments.getAll({ + caseID: request.params.case_id, + }); return response.ok({ - body: await client.attachments.getAll({ - caseID: request.params.case_id, - }), + body: res, }); } catch (error) { throw createCaseError({ diff --git a/x-pack/plugins/cases/server/routes/api/comments/get_comment.ts b/x-pack/plugins/cases/server/routes/api/comments/get_comment.ts index 2a863f3ee4df4..6267895f587ce 100644 --- a/x-pack/plugins/cases/server/routes/api/comments/get_comment.ts +++ b/x-pack/plugins/cases/server/routes/api/comments/get_comment.ts @@ -10,6 +10,7 @@ import { schema } from '@kbn/config-schema'; import { CASE_COMMENT_DETAILS_URL } from '../../../../common/constants'; import { createCaseError } from '../../../common/error'; import { createCasesRoute } from '../create_cases_route'; +import type { attachmentDomainV1 } from '../../../../common/types/domain'; export const getCommentRoute = createCasesRoute({ method: 'get', @@ -24,12 +25,13 @@ export const getCommentRoute = createCasesRoute({ try { const caseContext = await context.cases; const client = await caseContext.getCasesClient(); + const res: attachmentDomainV1.Attachment = await client.attachments.get({ + attachmentID: request.params.comment_id, + caseID: request.params.case_id, + }); return response.ok({ - body: await client.attachments.get({ - attachmentID: request.params.comment_id, - caseID: request.params.case_id, - }), + body: res, }); } catch (error) { throw createCaseError({ diff --git a/x-pack/plugins/cases/server/routes/api/comments/patch_comment.ts b/x-pack/plugins/cases/server/routes/api/comments/patch_comment.ts index 8130808e51dcb..99a697cb0bc9a 100644 --- a/x-pack/plugins/cases/server/routes/api/comments/patch_comment.ts +++ b/x-pack/plugins/cases/server/routes/api/comments/patch_comment.ts @@ -11,6 +11,7 @@ import { decodeWithExcessOrThrow } from '../../../../common/api'; import { CASE_COMMENTS_URL } from '../../../../common/constants'; import { createCaseError } from '../../../common/error'; import { createCasesRoute } from '../create_cases_route'; +import type { caseDomainV1 } from '../../../../common/types/domain'; export const patchCommentRoute = createCasesRoute({ method: 'patch', @@ -26,12 +27,13 @@ export const patchCommentRoute = createCasesRoute({ const caseContext = await context.cases; const client = await caseContext.getCasesClient(); + const res: caseDomainV1.Case = await client.attachments.update({ + caseID: request.params.case_id, + updateRequest: query, + }); return response.ok({ - body: await client.attachments.update({ - caseID: request.params.case_id, - updateRequest: query, - }), + body: res, }); } catch (error) { throw createCaseError({ diff --git a/x-pack/plugins/cases/server/routes/api/comments/post_comment.ts b/x-pack/plugins/cases/server/routes/api/comments/post_comment.ts index 80ce9386252d7..d4d8f4f4f8db2 100644 --- a/x-pack/plugins/cases/server/routes/api/comments/post_comment.ts +++ b/x-pack/plugins/cases/server/routes/api/comments/post_comment.ts @@ -7,9 +7,10 @@ import { schema } from '@kbn/config-schema'; import { CASE_COMMENTS_URL } from '../../../../common/constants'; -import type { AttachmentRequest } from '../../../../common/types/api'; import { createCaseError } from '../../../common/error'; import { createCasesRoute } from '../create_cases_route'; +import type { caseDomainV1 } from '../../../../common/types/domain'; +import type { attachmentApiV1 } from '../../../../common/types/api'; export const postCommentRoute = createCasesRoute({ method: 'post', @@ -24,10 +25,11 @@ export const postCommentRoute = createCasesRoute({ const caseContext = await context.cases; const casesClient = await caseContext.getCasesClient(); const caseId = request.params.case_id; - const comment = request.body as AttachmentRequest; + const comment = request.body as attachmentApiV1.AttachmentRequest; + const res: caseDomainV1.Case = await casesClient.attachments.add({ caseId, comment }); return response.ok({ - body: await casesClient.attachments.add({ caseId, comment }), + body: res, }); } catch (error) { throw createCaseError({ diff --git a/x-pack/plugins/cases/server/routes/api/internal/bulk_create_attachments.ts b/x-pack/plugins/cases/server/routes/api/internal/bulk_create_attachments.ts index e0abb28a58efe..8074d8e626ea7 100644 --- a/x-pack/plugins/cases/server/routes/api/internal/bulk_create_attachments.ts +++ b/x-pack/plugins/cases/server/routes/api/internal/bulk_create_attachments.ts @@ -6,11 +6,12 @@ */ import { schema } from '@kbn/config-schema'; -import type { BulkCreateAttachmentsRequest } from '../../../../common/types/api'; import { INTERNAL_BULK_CREATE_ATTACHMENTS_URL } from '../../../../common/constants'; import { createCaseError } from '../../../common/error'; import { createCasesRoute } from '../create_cases_route'; import { escapeHatch } from '../utils'; +import type { attachmentApiV1 } from '../../../../common/types/api'; +import type { caseDomainV1 } from '../../../../common/types/domain'; export const bulkCreateAttachmentsRoute = createCasesRoute({ method: 'post', @@ -26,10 +27,14 @@ export const bulkCreateAttachmentsRoute = createCasesRoute({ const casesContext = await context.cases; const casesClient = await casesContext.getCasesClient(); const caseId = request.params.case_id; - const attachments = request.body as BulkCreateAttachmentsRequest; + const attachments = request.body as attachmentApiV1.BulkCreateAttachmentsRequest; + const res: caseDomainV1.Case = await casesClient.attachments.bulkCreate({ + caseId, + attachments, + }); return response.ok({ - body: await casesClient.attachments.bulkCreate({ caseId, attachments }), + body: res, }); } catch (error) { throw createCaseError({ diff --git a/x-pack/plugins/cases/server/routes/api/internal/bulk_delete_file_attachments.ts b/x-pack/plugins/cases/server/routes/api/internal/bulk_delete_file_attachments.ts index c719d16e7ab16..d1e62f13c633b 100644 --- a/x-pack/plugins/cases/server/routes/api/internal/bulk_delete_file_attachments.ts +++ b/x-pack/plugins/cases/server/routes/api/internal/bulk_delete_file_attachments.ts @@ -7,12 +7,13 @@ import { schema } from '@kbn/config-schema'; -import { BulkDeleteFileAttachmentsRequestRt } from '../../../../common/types/api'; +import { decodeWithExcessOrThrow } from '../../../../common/api'; import { INTERNAL_DELETE_FILE_ATTACHMENTS_URL } from '../../../../common/constants'; import { createCasesRoute } from '../create_cases_route'; import { createCaseError } from '../../../common/error'; import { escapeHatch } from '../utils'; -import { decodeWithExcessOrThrow } from '../../../../common/api'; +import type { attachmentApiV1 } from '../../../../common/types/api'; +import { BulkDeleteFileAttachmentsRequestRt } from '../../../../common/types/api/attachment/v1'; export const bulkDeleteFileAttachments = createCasesRoute({ method: 'post', @@ -27,8 +28,9 @@ export const bulkDeleteFileAttachments = createCasesRoute({ try { const caseContext = await context.cases; const client = await caseContext.getCasesClient(); - - const requestBody = decodeWithExcessOrThrow(BulkDeleteFileAttachmentsRequestRt)(request.body); + const requestBody: attachmentApiV1.BulkDeleteFileAttachmentsRequest = decodeWithExcessOrThrow( + BulkDeleteFileAttachmentsRequestRt + )(request.body); await client.attachments.bulkDeleteFileAttachments({ caseId: request.params.case_id, diff --git a/x-pack/plugins/cases/server/routes/api/internal/bulk_get_attachments.ts b/x-pack/plugins/cases/server/routes/api/internal/bulk_get_attachments.ts index 1e59df016e8c6..a5cb22f536caa 100644 --- a/x-pack/plugins/cases/server/routes/api/internal/bulk_get_attachments.ts +++ b/x-pack/plugins/cases/server/routes/api/internal/bulk_get_attachments.ts @@ -6,8 +6,9 @@ */ import { schema } from '@kbn/config-schema'; -import { BulkGetAttachmentsRequestRt } from '../../../../common/types/api'; +import { BulkGetAttachmentsRequestRt } from '../../../../common/types/api/attachment/v1'; import { decodeWithExcessOrThrow } from '../../../../common/api'; +import type { attachmentApiV1 } from '../../../../common/types/api'; import { INTERNAL_BULK_GET_ATTACHMENTS_URL } from '../../../../common/constants'; import { createCaseError } from '../../../common/error'; @@ -28,13 +29,17 @@ export const bulkGetAttachmentsRoute = createCasesRoute({ const caseContext = await context.cases; const client = await caseContext.getCasesClient(); - const requestBody = decodeWithExcessOrThrow(BulkGetAttachmentsRequestRt)(request.body); + const requestBody: attachmentApiV1.BulkGetAttachmentsRequest = decodeWithExcessOrThrow( + BulkGetAttachmentsRequestRt + )(request.body); + + const res: attachmentApiV1.BulkGetAttachmentsResponse = await client.attachments.bulkGet({ + caseID: request.params.case_id, + attachmentIDs: requestBody.ids, + }); return response.ok({ - body: await client.attachments.bulkGet({ - caseID: request.params.case_id, - attachmentIDs: requestBody.ids, - }), + body: res, }); } catch (error) { throw createCaseError({ diff --git a/x-pack/plugins/cloud_security_posture/common/constants.ts b/x-pack/plugins/cloud_security_posture/common/constants.ts index 5e5bcdcbd1ef3..91d42c2544191 100644 --- a/x-pack/plugins/cloud_security_posture/common/constants.ts +++ b/x-pack/plugins/cloud_security_posture/common/constants.ts @@ -84,6 +84,8 @@ export const CLOUDBEAT_AZURE = 'cloudbeat/cis_azure'; export const CLOUDBEAT_VULN_MGMT_AWS = 'cloudbeat/vuln_mgmt_aws'; export const CLOUDBEAT_VULN_MGMT_GCP = 'cloudbeat/vuln_mgmt_gcp'; export const CLOUDBEAT_VULN_MGMT_AZURE = 'cloudbeat/vuln_mgmt_azure'; +export const CIS_AWS = 'cis_aws'; +export const CIS_GCP = 'cis_gcp'; export const KSPM_POLICY_TEMPLATE = 'kspm'; export const CSPM_POLICY_TEMPLATE = 'cspm'; export const VULN_MGMT_POLICY_TEMPLATE = 'vuln_mgmt'; diff --git a/x-pack/plugins/cloud_security_posture/public/components/accounts_evaluated_widget.tsx b/x-pack/plugins/cloud_security_posture/public/components/accounts_evaluated_widget.tsx new file mode 100644 index 0000000000000..2709163c22a84 --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/components/accounts_evaluated_widget.tsx @@ -0,0 +1,64 @@ +/* + * 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 React from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { CIS_AWS, CIS_GCP } from '../../common/constants'; +import { Cluster } from '../../common/types'; +import { CISBenchmarkIcon } from './cis_benchmark_icon'; +import { CompactFormattedNumber } from './compact_formatted_number'; + +export const AccountsEvaluatedWidget = ({ + clusters, + benchmarkAbbreviateAbove = 999, +}: { + clusters: Cluster[]; + /** numbers higher than the value of this field will be abbreviated using compact notation and have a tooltip displaying the full value */ + benchmarkAbbreviateAbove?: number; +}) => { + const filterClustersById = (benchmarkId: string) => { + return clusters?.filter((obj) => obj?.meta.benchmark.id === benchmarkId) || []; + }; + + const cisAwsClusterAmount = filterClustersById(CIS_AWS).length; + const cisGcpClusterAmount = filterClustersById(CIS_GCP).length; + + const cisAwsBenchmarkName = filterClustersById(CIS_AWS)[0]?.meta.benchmark.name || ''; + const cisGcpBenchmarkName = filterClustersById(CIS_GCP)[0]?.meta.benchmark.name || ''; + + return ( + <> + + + + + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/x-pack/plugins/cloud_security_posture/public/components/cis_benchmark_icon.tsx b/x-pack/plugins/cloud_security_posture/public/components/cis_benchmark_icon.tsx index 4751335c3debd..6ca379ffb24a4 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/cis_benchmark_icon.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/cis_benchmark_icon.tsx @@ -23,6 +23,8 @@ const getBenchmarkIdIconType = (props: Props): string => { return cisEksIcon; case 'cis_aws': return 'logoAWS'; + case 'cis_gcp': + return 'logoGCP'; case 'cis_k8s': default: return 'logoKubernetes'; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/summary_section.tsx b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/summary_section.tsx index bd6e46f018063..9837c531021f2 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/summary_section.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/summary_section.tsx @@ -31,6 +31,7 @@ import { KSPM_POLICY_TEMPLATE, RULE_FAILED, } from '../../../../common/constants'; +import { AccountsEvaluatedWidget } from '../../../components/accounts_evaluated_widget'; export const dashboardColumnsGrow: Record = { first: 3, @@ -86,7 +87,12 @@ export const SummarySection = ({ 'xpack.csp.dashboard.summarySection.counterCard.accountsEvaluatedDescription', { defaultMessage: 'Accounts Evaluated' } ), - title: , + title: + dashboardType === KSPM_POLICY_TEMPLATE ? ( + + ) : ( + + ), }, { id: DASHBOARD_COUNTER_CARDS.RESOURCES_EVALUATED, @@ -116,7 +122,7 @@ export const SummarySection = ({ }, ], [ - complianceData.clusters.length, + complianceData.clusters, complianceData.stats.resourcesEvaluated, complianceData.stats.totalFailed, dashboardType,