diff --git a/x-pack/plugins/cases/common/api/cases/comment/index.test.ts b/x-pack/plugins/cases/common/api/cases/comment/index.test.ts index b0f99125226c3..ab27b976a5f71 100644 --- a/x-pack/plugins/cases/common/api/cases/comment/index.test.ts +++ b/x-pack/plugins/cases/common/api/cases/comment/index.test.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { PathReporter } from 'io-ts/lib/PathReporter'; + import { CommentAttributesBasicRt, CommentType, @@ -29,6 +31,7 @@ import { BulkGetAttachmentsRequestRt, BulkGetAttachmentsResponseRt, } from '.'; +import { MAX_COMMENT_LENGTH } from '../../../constants'; describe('Comments', () => { describe('CommentAttributesBasicRt', () => { @@ -323,6 +326,7 @@ describe('Comments', () => { type: CommentType.user, owner: 'cases', }; + it('has expected attributes in request', () => { const query = CommentRequestRt.decode(defaultRequest); @@ -340,6 +344,68 @@ describe('Comments', () => { right: defaultRequest, }); }); + + describe('errors', () => { + describe('commentType: user', () => { + it('throws error when comment is too long', () => { + const longComment = 'x'.repeat(MAX_COMMENT_LENGTH + 1); + + expect( + PathReporter.report( + CommentRequestRt.decode({ ...defaultRequest, comment: longComment }) + ) + ).toContain('The length of the comment is too long. The maximum length is 30000.'); + }); + + it('throws error when comment is empty', () => { + expect( + PathReporter.report(CommentRequestRt.decode({ ...defaultRequest, comment: '' })) + ).toContain('The comment field cannot be an empty string.'); + }); + + it('throws error when comment string of empty characters', () => { + expect( + PathReporter.report(CommentRequestRt.decode({ ...defaultRequest, comment: ' ' })) + ).toContain('The comment field cannot be an empty string.'); + }); + }); + + describe('commentType: action', () => { + const request = { + type: CommentType.actions, + actions: { + targets: [ + { + hostname: 'host1', + endpointId: '001', + }, + ], + type: 'isolate', + }, + owner: 'cases', + }; + + it('throws error when comment is too long', () => { + const longComment = 'x'.repeat(MAX_COMMENT_LENGTH + 1); + + expect( + PathReporter.report(CommentRequestRt.decode({ ...request, comment: longComment })) + ).toContain('The length of the comment is too long. The maximum length is 30000.'); + }); + + it('throws error when comment is empty', () => { + expect( + PathReporter.report(CommentRequestRt.decode({ ...request, comment: '' })) + ).toContain('The comment field cannot be an empty string.'); + }); + + it('throws error when comment string of empty characters', () => { + expect( + PathReporter.report(CommentRequestRt.decode({ ...request, comment: ' ' })) + ).toContain('The comment field cannot be an empty string.'); + }); + }); + }); }); describe('CommentRt', () => { diff --git a/x-pack/plugins/cases/common/api/cases/comment/index.ts b/x-pack/plugins/cases/common/api/cases/comment/index.ts index f53e7d8828930..50b93a8c70461 100644 --- a/x-pack/plugins/cases/common/api/cases/comment/index.ts +++ b/x-pack/plugins/cases/common/api/cases/comment/index.ts @@ -6,8 +6,12 @@ */ import * as rt from 'io-ts'; -import { MAX_BULK_GET_ATTACHMENTS, MAX_COMMENTS_PER_PAGE } from '../../../constants'; -import { limitedArraySchema, paginationSchema } from '../../../schema'; +import { + MAX_BULK_GET_ATTACHMENTS, + MAX_COMMENTS_PER_PAGE, + MAX_COMMENT_LENGTH, +} from '../../../constants'; +import { limitedArraySchema, paginationSchema, limitedStringSchema } from '../../../schema'; import { jsonValueRt } from '../../runtime_types'; import { UserRt } from '../../user'; @@ -193,7 +197,31 @@ const BasicCommentRequestRt = rt.union([ PersistableStateAttachmentRt, ]); -export const CommentRequestRt = rt.union([BasicCommentRequestRt, ExternalReferenceSORt]); +export const CommentRequestRt = rt.union([ + rt.strict({ + comment: limitedStringSchema({ fieldName: 'comment', min: 1, max: MAX_COMMENT_LENGTH }), + type: rt.literal(CommentType.user), + owner: rt.string, + }), + AlertCommentRequestRt, + rt.strict({ + type: rt.literal(CommentType.actions), + comment: limitedStringSchema({ fieldName: 'comment', min: 1, max: MAX_COMMENT_LENGTH }), + actions: rt.strict({ + targets: rt.array( + rt.strict({ + hostname: rt.string, + endpointId: rt.string, + }) + ), + type: rt.string, + }), + owner: rt.string, + }), + ExternalReferenceNoSORt, + ExternalReferenceSORt, + PersistableStateAttachmentRt, +]); export const CommentRequestWithoutRefsRt = rt.union([ BasicCommentRequestRt, diff --git a/x-pack/plugins/cases/common/constants/index.ts b/x-pack/plugins/cases/common/constants/index.ts index ffd627c8de865..15b40edafdf27 100644 --- a/x-pack/plugins/cases/common/constants/index.ts +++ b/x-pack/plugins/cases/common/constants/index.ts @@ -119,6 +119,7 @@ export const MAX_REPORTERS_FILTER_LENGTH = 100 as const; export const MAX_TITLE_LENGTH = 160 as const; export const MAX_CATEGORY_LENGTH = 50 as const; export const MAX_DESCRIPTION_LENGTH = 30000 as const; +export const MAX_COMMENT_LENGTH = 30000 as const; export const MAX_LENGTH_PER_TAG = 256 as const; export const MAX_TAGS_PER_CASE = 200 as const; export const MAX_DELETE_IDS_LENGTH = 100 as const; diff --git a/x-pack/plugins/cases/docs/openapi/bundled.json b/x-pack/plugins/cases/docs/openapi/bundled.json index c504d966cb9d2..888f537d6d461 100644 --- a/x-pack/plugins/cases/docs/openapi/bundled.json +++ b/x-pack/plugins/cases/docs/openapi/bundled.json @@ -5553,6 +5553,7 @@ "comment": { "description": "The new comment. It is required only when `type` is `user`.", "type": "string", + "maxLength": 30000, "example": "A new comment." }, "owner": { @@ -5642,6 +5643,7 @@ "comment": { "description": "The new comment. It is required only when `type` is `user`.", "type": "string", + "maxLength": 30000, "example": "A new comment." }, "id": { diff --git a/x-pack/plugins/cases/docs/openapi/bundled.yaml b/x-pack/plugins/cases/docs/openapi/bundled.yaml index d393da4d334a0..ac1df4c1436e8 100644 --- a/x-pack/plugins/cases/docs/openapi/bundled.yaml +++ b/x-pack/plugins/cases/docs/openapi/bundled.yaml @@ -3597,6 +3597,7 @@ components: comment: description: The new comment. It is required only when `type` is `user`. type: string + maxLength: 30000 example: A new comment. owner: $ref: '#/components/schemas/owners' @@ -3663,6 +3664,7 @@ components: comment: description: The new comment. It is required only when `type` is `user`. type: string + maxLength: 30000 example: A new comment. id: type: string diff --git a/x-pack/plugins/cases/docs/openapi/components/schemas/add_user_comment_request_properties.yaml b/x-pack/plugins/cases/docs/openapi/components/schemas/add_user_comment_request_properties.yaml index 40efb7f945f45..beac63c377ade 100644 --- a/x-pack/plugins/cases/docs/openapi/components/schemas/add_user_comment_request_properties.yaml +++ b/x-pack/plugins/cases/docs/openapi/components/schemas/add_user_comment_request_properties.yaml @@ -5,6 +5,7 @@ properties: comment: description: The new comment. It is required only when `type` is `user`. type: string + maxLength: 30000 example: A new comment. owner: $ref: 'owners.yaml' diff --git a/x-pack/plugins/cases/docs/openapi/components/schemas/update_user_comment_request_properties.yaml b/x-pack/plugins/cases/docs/openapi/components/schemas/update_user_comment_request_properties.yaml index 22fb76d9bba74..a83050e93eaaf 100644 --- a/x-pack/plugins/cases/docs/openapi/components/schemas/update_user_comment_request_properties.yaml +++ b/x-pack/plugins/cases/docs/openapi/components/schemas/update_user_comment_request_properties.yaml @@ -5,6 +5,7 @@ properties: comment: description: The new comment. It is required only when `type` is `user`. type: string + maxLength: 30000 example: A new comment. id: type: string diff --git a/x-pack/plugins/cases/server/client/attachments/add.test.ts b/x-pack/plugins/cases/server/client/attachments/add.test.ts index 0e39ff30a65ec..b78ec1219088b 100644 --- a/x-pack/plugins/cases/server/client/attachments/add.test.ts +++ b/x-pack/plugins/cases/server/client/attachments/add.test.ts @@ -7,6 +7,7 @@ import { comment } from '../../mocks'; import { createCasesClientMockArgs } from '../mocks'; +import { MAX_COMMENT_LENGTH } from '../../../common/constants'; import { addComment } from './add'; describe('addComment', () => { @@ -22,4 +23,30 @@ describe('addComment', () => { addComment({ comment: { ...comment, foo: 'bar' }, caseId: 'test-case' }, clientArgs) ).rejects.toThrow('invalid keys "foo"'); }); + + it('should throw an error if the comment length is too long', async () => { + const longComment = 'x'.repeat(MAX_COMMENT_LENGTH + 1); + + await expect( + addComment({ comment: { ...comment, comment: longComment }, caseId: 'test-case' }, clientArgs) + ).rejects.toThrow( + `Failed while adding a comment to case id: test-case error: Error: The length of the comment is too long. The maximum length is ${MAX_COMMENT_LENGTH}.` + ); + }); + + it('should throw an error if the comment is an empty string', async () => { + await expect( + addComment({ comment: { ...comment, comment: '' }, caseId: 'test-case' }, clientArgs) + ).rejects.toThrow( + 'Failed while adding a comment to case id: test-case error: Error: The comment field cannot be an empty string.' + ); + }); + + it('should throw an error if the description is a string with empty characters', async () => { + await expect( + addComment({ comment: { ...comment, comment: ' ' }, caseId: 'test-case' }, clientArgs) + ).rejects.toThrow( + 'Failed while adding a comment to case id: test-case error: Error: The comment field cannot be an empty string.' + ); + }); }); diff --git a/x-pack/plugins/cases/server/client/attachments/bulk_create.test.ts b/x-pack/plugins/cases/server/client/attachments/bulk_create.test.ts index 7d9cdcf150a20..d9cb3d9ea190b 100644 --- a/x-pack/plugins/cases/server/client/attachments/bulk_create.test.ts +++ b/x-pack/plugins/cases/server/client/attachments/bulk_create.test.ts @@ -5,8 +5,9 @@ * 2.0. */ -import { comment } from '../../mocks'; +import { comment, actionComment } from '../../mocks'; import { createCasesClientMockArgs } from '../mocks'; +import { MAX_COMMENT_LENGTH } from '../../../common/constants'; import { bulkCreate } from './bulk_create'; describe('bulkCreate', () => { @@ -22,4 +23,79 @@ describe('bulkCreate', () => { bulkCreate({ attachments: [{ ...comment, foo: 'bar' }], caseId: 'test-case' }, clientArgs) ).rejects.toThrow('invalid keys "foo"'); }); + + describe('comments', () => { + it('should throw an error if the comment length is too long', async () => { + const longComment = Array(MAX_COMMENT_LENGTH + 1) + .fill('x') + .toString(); + + await expect( + bulkCreate( + { attachments: [{ ...comment, comment: longComment }], caseId: 'test-case' }, + clientArgs + ) + ).rejects.toThrow( + `Failed while bulk creating attachment to case id: test-case error: Error: The length of the comment is too long. The maximum length is ${MAX_COMMENT_LENGTH}.` + ); + }); + + it('should throw an error if the comment is an empty string', async () => { + await expect( + bulkCreate({ attachments: [{ ...comment, comment: '' }], caseId: 'test-case' }, clientArgs) + ).rejects.toThrow( + 'Failed while bulk creating attachment to case id: test-case error: Error: The comment field cannot be an empty string.' + ); + }); + + it('should throw an error if the description is a string with empty characters', async () => { + await expect( + bulkCreate( + { attachments: [{ ...comment, comment: ' ' }], caseId: 'test-case' }, + clientArgs + ) + ).rejects.toThrow( + 'Failed while bulk creating attachment to case id: test-case error: Error: The comment field cannot be an empty string.' + ); + }); + }); + + describe('actions', () => { + it('should throw an error if the comment length is too long', async () => { + const longComment = Array(MAX_COMMENT_LENGTH + 1) + .fill('x') + .toString(); + + await expect( + bulkCreate( + { attachments: [{ ...actionComment, comment: longComment }], caseId: 'test-case' }, + clientArgs + ) + ).rejects.toThrow( + `Failed while bulk creating attachment to case id: test-case error: Error: The length of the comment is too long. The maximum length is ${MAX_COMMENT_LENGTH}.` + ); + }); + + it('should throw an error if the comment is an empty string', async () => { + await expect( + bulkCreate( + { attachments: [{ ...actionComment, comment: '' }], caseId: 'test-case' }, + clientArgs + ) + ).rejects.toThrow( + 'Failed while bulk creating attachment to case id: test-case error: Error: The comment field cannot be an empty string.' + ); + }); + + it('should throw an error if the description is a string with empty characters', async () => { + await expect( + bulkCreate( + { attachments: [{ ...actionComment, comment: ' ' }], caseId: 'test-case' }, + clientArgs + ) + ).rejects.toThrow( + 'Failed while bulk creating attachment to case id: test-case error: Error: The comment field cannot be an empty string.' + ); + }); + }); }); diff --git a/x-pack/plugins/cases/server/client/attachments/update.test.ts b/x-pack/plugins/cases/server/client/attachments/update.test.ts new file mode 100644 index 0000000000000..c3ebb40d396d5 --- /dev/null +++ b/x-pack/plugins/cases/server/client/attachments/update.test.ts @@ -0,0 +1,100 @@ +/* + * 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 { comment, actionComment } from '../../mocks'; +import { createCasesClientMockArgs } from '../mocks'; +import { MAX_COMMENT_LENGTH } from '../../../common/constants'; +import { update } from './update'; + +describe('update', () => { + const clientArgs = createCasesClientMockArgs(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('comments', () => { + const updateComment = { ...comment, id: 'comment-id', version: 'WzAsMV0=' }; + it('should throw an error if the comment length is too long', async () => { + const longComment = Array(MAX_COMMENT_LENGTH + 1) + .fill('x') + .toString(); + + await expect( + update( + { updateRequest: { ...updateComment, comment: longComment }, caseID: 'test-case' }, + clientArgs + ) + ).rejects.toThrow( + `Failed to patch comment case id: test-case: Error: The length of the comment is too long. The maximum length is ${MAX_COMMENT_LENGTH}.` + ); + }); + + it('should throw an error if the comment is an empty string', async () => { + await expect( + update( + { updateRequest: { ...updateComment, comment: '' }, caseID: 'test-case' }, + clientArgs + ) + ).rejects.toThrow( + 'Failed to patch comment case id: test-case: Error: The comment field cannot be an empty string.' + ); + }); + + it('should throw an error if the description is a string with empty characters', async () => { + await expect( + update( + { updateRequest: { ...updateComment, comment: ' ' }, caseID: 'test-case' }, + clientArgs + ) + ).rejects.toThrow( + 'Failed to patch comment case id: test-case: Error: The comment field cannot be an empty string.' + ); + }); + }); + + describe('actions', () => { + const updateActionComment = { ...actionComment, id: 'comment-id', version: 'WzAsMV0=' }; + + it('should throw an error if the comment length is too long', async () => { + const longComment = Array(MAX_COMMENT_LENGTH + 1) + .fill('x') + .toString(); + + await expect( + update( + { updateRequest: { ...updateActionComment, comment: longComment }, caseID: 'test-case' }, + clientArgs + ) + ).rejects.toThrow( + `Failed to patch comment case id: test-case: Error: The length of the comment is too long. The maximum length is ${MAX_COMMENT_LENGTH}.` + ); + }); + + it('should throw an error if the comment is an empty string', async () => { + await expect( + update( + { updateRequest: { ...updateActionComment, comment: '' }, caseID: 'test-case' }, + clientArgs + ) + ).rejects.toThrow( + 'Failed to patch comment case id: test-case: Error: The comment field cannot be an empty string.' + ); + }); + + it('should throw an error if the description is a string with empty characters', async () => { + await expect( + update( + { updateRequest: { ...updateActionComment, comment: ' ' }, caseID: 'test-case' }, + clientArgs + ) + ).rejects.toThrow( + 'Failed to patch comment case id: test-case: Error: The comment field cannot be an empty string.' + ); + }); + }); +}); diff --git a/x-pack/plugins/cases/server/mocks.ts b/x-pack/plugins/cases/server/mocks.ts index 479873f7c0a9e..8eacf10c59e15 100644 --- a/x-pack/plugins/cases/server/mocks.ts +++ b/x-pack/plugins/cases/server/mocks.ts @@ -9,6 +9,7 @@ import type { SavedObject } from '@kbn/core/server'; import type { CasePostRequest, CommentAttributes, + CommentRequestActionsType, CommentRequestAlertType, CommentRequestUserType, ConnectorMappings, @@ -664,6 +665,21 @@ export const comment: CommentRequestUserType = { owner: SECURITY_SOLUTION_OWNER, }; +export const actionComment: CommentRequestActionsType = { + type: CommentType.actions, + comment: 'I just isolated the host!', + actions: { + targets: [ + { + hostname: 'host1', + endpointId: '001', + }, + ], + type: 'isolate', + }, + owner: 'cases', +}; + export const alertComment: CommentRequestAlertType = { alertId: 'alert-id-1', index: 'alert-index-1', diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/comments/patch_comment.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/comments/patch_comment.ts index fc7d86306fc41..16029e3f84615 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/comments/patch_comment.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/comments/patch_comment.ts @@ -173,6 +173,73 @@ export default ({ getService }: FtrProviderContext): void => { }); }); + it('unhappy path - 400s when comment is too long', async () => { + const postedCase = await createCase(supertest, postCaseReq); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + }); + const longComment = Array(30001).fill('a').toString(); + + await updateComment({ + supertest, + caseId: postedCase.id, + req: { + id: patchedCase.comments![0].id, + version: patchedCase.comments![0].version, + type: CommentType.user, + comment: longComment, + owner: 'securitySolutionFixture', + }, + expectedHttpCode: 400, + }); + }); + + it('unhappy path - 400s when comment is empty', async () => { + const postedCase = await createCase(supertest, postCaseReq); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + }); + + await updateComment({ + supertest, + caseId: postedCase.id, + req: { + id: patchedCase.comments![0].id, + version: patchedCase.comments![0].version, + type: CommentType.user, + comment: '', + owner: 'securitySolutionFixture', + }, + expectedHttpCode: 400, + }); + }); + + it('unhappy path - 400s when comment is a string of empty characters', async () => { + const postedCase = await createCase(supertest, postCaseReq); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + }); + + await updateComment({ + supertest, + caseId: postedCase.id, + req: { + id: patchedCase.comments![0].id, + version: patchedCase.comments![0].version, + type: CommentType.user, + comment: ' ', + owner: 'securitySolutionFixture', + }, + expectedHttpCode: 400, + }); + }); + it('unhappy path - 400s when trying to change comment type', async () => { const postedCase = await createCase(supertest, postCaseReq); const patchedCase = await createComment({ diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/comments/post_comment.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/comments/post_comment.ts index 34854939b4771..8851a95c6ebc3 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/comments/post_comment.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/comments/post_comment.ts @@ -274,6 +274,49 @@ export default ({ getService }: FtrProviderContext): void => { }); }); + it('400s when adding too long comment', async () => { + const postedCase = await createCase(supertest, postCaseReq); + const longComment = Array(30001).fill('a').toString(); + + await createComment({ + supertest, + caseId: postedCase.id, + // @ts-expect-error + params: { + comment: longComment, + }, + expectedHttpCode: 400, + }); + }); + + it('400s when adding empty comment', async () => { + const postedCase = await createCase(supertest, postCaseReq); + + await createComment({ + supertest, + caseId: postedCase.id, + // @ts-expect-error + params: { + comment: '', + }, + expectedHttpCode: 400, + }); + }); + + it('400s when adding a comment with only empty characters', async () => { + const postedCase = await createCase(supertest, postCaseReq); + + await createComment({ + supertest, + caseId: postedCase.id, + // @ts-expect-error + params: { + comment: ' ', + }, + expectedHttpCode: 400, + }); + }); + it('400s when adding excess attributes for type user', async () => { const postedCase = await createCase(supertest, postCaseReq); diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/internal/bulk_create_attachments.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/internal/bulk_create_attachments.ts index 6788cc26fef94..0df7e207081a0 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/internal/bulk_create_attachments.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/internal/bulk_create_attachments.ts @@ -441,6 +441,23 @@ export default ({ getService }: FtrProviderContext): void => { }); }); + it('400s when comment is too long', async () => { + const longComment = 'x'.repeat(30001); + + await bulkCreateAttachments({ + supertest, + caseId: 'case-id', + params: [ + { + type: CommentType.user, + comment: longComment, + owner: 'securitySolutionFixture', + }, + ], + expectedHttpCode: 400, + }); + }); + it('400s when adding excess attributes for type user', async () => { const postedCase = await createCase(supertest, postCaseReq);