Skip to content

Commit

Permalink
[Security Solution][Case] Create comment types (#82715)
Browse files Browse the repository at this point in the history
  • Loading branch information
cnasikas committed Nov 20, 2020
1 parent 9e084f3 commit 52c6b7b
Show file tree
Hide file tree
Showing 36 changed files with 1,380 additions and 150 deletions.
11 changes: 6 additions & 5 deletions x-pack/plugins/case/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,12 @@ This action type has no `secrets` properties.

#### `subActionParams (addComment)`

| Property | Description | Type |
| -------- | --------------------------------------------------------- | ------ |
| comment | The case’s new comment. | string |
| type | The type of the comment, which can be: `user` or `alert`. | string |

| Property | Description | Type |
| -------- | ----------------------------------------------------------------------- | ----------------- |
| type | The type of the comment | `user` \| `alert` |
| comment | The comment. Valid only when type is `user`. | string |
| alertId | The alert ID. Valid only when the type is `alert` | string |
| index | The index where the alert is saved. Valid only when the type is `alert` | string |
#### `connector`

| Property | Description | Type |
Expand Down
57 changes: 42 additions & 15 deletions x-pack/plugins/case/common/api/cases/comment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,33 @@ import * as rt from 'io-ts';

import { UserRT } from '../user';

const CommentBasicRt = rt.type({
export const CommentAttributesBasicRt = rt.type({
created_at: rt.string,
created_by: UserRT,
pushed_at: rt.union([rt.string, rt.null]),
pushed_by: rt.union([UserRT, rt.null]),
updated_at: rt.union([rt.string, rt.null]),
updated_by: rt.union([UserRT, rt.null]),
});

export const ContextTypeUserRt = rt.type({
comment: rt.string,
type: rt.union([rt.literal('alert'), rt.literal('user')]),
type: rt.literal('user'),
});

export const CommentAttributesRt = rt.intersection([
CommentBasicRt,
rt.type({
created_at: rt.string,
created_by: UserRT,
pushed_at: rt.union([rt.string, rt.null]),
pushed_by: rt.union([UserRT, rt.null]),
updated_at: rt.union([rt.string, rt.null]),
updated_by: rt.union([UserRT, rt.null]),
}),
]);
export const ContextTypeAlertRt = rt.type({
type: rt.literal('alert'),
alertId: rt.string,
index: rt.string,
});

export const CommentRequestRt = CommentBasicRt;
const AttributesTypeUserRt = rt.intersection([ContextTypeUserRt, CommentAttributesBasicRt]);
const AttributesTypeAlertsRt = rt.intersection([ContextTypeAlertRt, CommentAttributesBasicRt]);
const CommentAttributesRt = rt.union([AttributesTypeUserRt, AttributesTypeAlertsRt]);

const ContextBasicRt = rt.union([ContextTypeUserRt, ContextTypeAlertRt]);

export const CommentRequestRt = ContextBasicRt;

export const CommentResponseRt = rt.intersection([
CommentAttributesRt,
Expand All @@ -38,10 +47,25 @@ export const CommentResponseRt = rt.intersection([
export const AllCommentsResponseRT = rt.array(CommentResponseRt);

export const CommentPatchRequestRt = rt.intersection([
rt.partial(CommentBasicRt.props),
/**
* Partial updates are not allowed.
* We want to prevent the user for changing the type without removing invalid fields.
*/
ContextBasicRt,
rt.type({ id: rt.string, version: rt.string }),
]);

/**
* This type is used by the CaseService.
* Because the type for the attributes of savedObjectClient update function is Partial<T>
* we need to make all of our attributes partial too.
* We ensure that partial updates of CommentContext is not going to happen inside the patch comment route.
*/
export const CommentPatchAttributesRt = rt.intersection([
rt.union([rt.partial(CommentAttributesBasicRt.props), rt.partial(ContextTypeAlertRt.props)]),
rt.partial(CommentAttributesBasicRt.props),
]);

export const CommentsResponseRt = rt.type({
comments: rt.array(CommentResponseRt),
page: rt.number,
Expand All @@ -62,3 +86,6 @@ export type CommentResponse = rt.TypeOf<typeof CommentResponseRt>;
export type AllCommentsResponse = rt.TypeOf<typeof AllCommentsResponseRt>;
export type CommentsResponse = rt.TypeOf<typeof CommentsResponseRt>;
export type CommentPatchRequest = rt.TypeOf<typeof CommentPatchRequestRt>;
export type CommentPatchAttributes = rt.TypeOf<typeof CommentPatchAttributesRt>;
export type CommentRequestUserType = rt.TypeOf<typeof ContextTypeUserRt>;
export type CommentRequestAlertType = rt.TypeOf<typeof ContextTypeAlertRt>;
197 changes: 177 additions & 20 deletions x-pack/plugins/case/server/client/comments/add.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { omit } from 'lodash/fp';
import { CommentType } from '../../../common/api';
import {
createMockSavedObjectsRepository,
Expand Down Expand Up @@ -31,7 +32,10 @@ describe('addComment', () => {
const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient);
const res = await caseClient.client.addComment({
caseId: 'mock-id-1',
comment: { comment: 'Wow, good luck catching that bad meanie!', type: CommentType.user },
comment: {
comment: 'Wow, good luck catching that bad meanie!',
type: CommentType.user,
},
});

expect(res.id).toEqual('mock-id-1');
Expand All @@ -54,6 +58,43 @@ describe('addComment', () => {
});
});

test('it adds a comment of type alert correctly', async () => {
const savedObjectsClient = createMockSavedObjectsRepository({
caseSavedObject: mockCases,
caseCommentSavedObject: mockCaseComments,
});

const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient);
const res = await caseClient.client.addComment({
caseId: 'mock-id-1',
comment: {
type: CommentType.alert,
alertId: 'test-id',
index: 'test-index',
},
});

expect(res.id).toEqual('mock-id-1');
expect(res.totalComment).toEqual(res.comments!.length);
expect(res.comments![res.comments!.length - 1]).toEqual({
type: CommentType.alert,
alertId: 'test-id',
index: 'test-index',
created_at: '2020-10-23T21:54:48.952Z',
created_by: {
email: 'd00d@awesome.com',
full_name: 'Awesome D00d',
username: 'awesome',
},
id: 'mock-comment',
pushed_at: null,
pushed_by: null,
updated_at: null,
updated_by: null,
version: 'WzksMV0=',
});
});

test('it updates the case correctly after adding a comment', async () => {
const savedObjectsClient = createMockSavedObjectsRepository({
caseSavedObject: mockCases,
Expand All @@ -63,7 +104,10 @@ describe('addComment', () => {
const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient);
const res = await caseClient.client.addComment({
caseId: 'mock-id-1',
comment: { comment: 'Wow, good luck catching that bad meanie!', type: CommentType.user },
comment: {
comment: 'Wow, good luck catching that bad meanie!',
type: CommentType.user,
},
});

expect(res.updated_at).toEqual('2020-10-23T21:54:48.952Z');
Expand All @@ -83,7 +127,10 @@ describe('addComment', () => {
const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient);
await caseClient.client.addComment({
caseId: 'mock-id-1',
comment: { comment: 'Wow, good luck catching that bad meanie!', type: CommentType.user },
comment: {
comment: 'Wow, good luck catching that bad meanie!',
type: CommentType.user,
},
});

expect(
Expand All @@ -99,7 +146,7 @@ describe('addComment', () => {
username: 'awesome',
},
action_field: ['comment'],
new_value: 'Wow, good luck catching that bad meanie!',
new_value: '{"comment":"Wow, good luck catching that bad meanie!","type":"user"}',
old_value: null,
},
references: [
Expand Down Expand Up @@ -127,7 +174,10 @@ describe('addComment', () => {
const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient, true);
const res = await caseClient.client.addComment({
caseId: 'mock-id-1',
comment: { comment: 'Wow, good luck catching that bad meanie!', type: CommentType.user },
comment: {
comment: 'Wow, good luck catching that bad meanie!',
type: CommentType.user,
},
});

expect(res.id).toEqual('mock-id-1');
Expand All @@ -151,7 +201,7 @@ describe('addComment', () => {
});

describe('unhappy path', () => {
test('it throws when missing comment', async () => {
test('it throws when missing type', async () => {
expect.assertions(3);

const savedObjectsClient = createMockSavedObjectsRepository({
Expand All @@ -172,25 +222,126 @@ describe('addComment', () => {
});
});

test('it throws when missing comment type', async () => {
test('it throws when missing attributes: type user', async () => {
expect.assertions(3);

const savedObjectsClient = createMockSavedObjectsRepository({
caseSavedObject: mockCases,
caseCommentSavedObject: mockCaseComments,
});

const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient);
caseClient.client
.addComment({
caseId: 'mock-id-1',
// @ts-expect-error
comment: { comment: 'a comment' },
})
.catch((e) => {
expect(e).not.toBeNull();
expect(e.isBoom).toBe(true);
expect(e.output.statusCode).toBe(400);
});
const allRequestAttributes = {
type: CommentType.user,
comment: 'a comment',
};

['comment'].forEach((attribute) => {
const requestAttributes = omit(attribute, allRequestAttributes);
caseClient.client
.addComment({
caseId: 'mock-id-1',
// @ts-expect-error
comment: {
...requestAttributes,
},
})
.catch((e) => {
expect(e).not.toBeNull();
expect(e.isBoom).toBe(true);
expect(e.output.statusCode).toBe(400);
});
});
});

test('it throws when excess attributes are provided: type user', async () => {
expect.assertions(6);

const savedObjectsClient = createMockSavedObjectsRepository({
caseSavedObject: mockCases,
caseCommentSavedObject: mockCaseComments,
});

const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient);

['alertId', 'index'].forEach((attribute) => {
caseClient.client
.addComment({
caseId: 'mock-id-1',
comment: {
[attribute]: attribute,
comment: 'a comment',
type: CommentType.user,
},
})
.catch((e) => {
expect(e).not.toBeNull();
expect(e.isBoom).toBe(true);
expect(e.output.statusCode).toBe(400);
});
});
});

test('it throws when missing attributes: type alert', async () => {
expect.assertions(6);

const savedObjectsClient = createMockSavedObjectsRepository({
caseSavedObject: mockCases,
caseCommentSavedObject: mockCaseComments,
});

const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient);
const allRequestAttributes = {
type: CommentType.alert,
index: 'test-index',
alertId: 'test-id',
};

['alertId', 'index'].forEach((attribute) => {
const requestAttributes = omit(attribute, allRequestAttributes);
caseClient.client
.addComment({
caseId: 'mock-id-1',
// @ts-expect-error
comment: {
...requestAttributes,
},
})
.catch((e) => {
expect(e).not.toBeNull();
expect(e.isBoom).toBe(true);
expect(e.output.statusCode).toBe(400);
});
});
});

test('it throws when excess attributes are provided: type alert', async () => {
expect.assertions(3);

const savedObjectsClient = createMockSavedObjectsRepository({
caseSavedObject: mockCases,
caseCommentSavedObject: mockCaseComments,
});

const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient);

['comment'].forEach((attribute) => {
caseClient.client
.addComment({
caseId: 'mock-id-1',
comment: {
[attribute]: attribute,
type: CommentType.alert,
index: 'test-index',
alertId: 'test-id',
},
})
.catch((e) => {
expect(e).not.toBeNull();
expect(e.isBoom).toBe(true);
expect(e.output.statusCode).toBe(400);
});
});
});

test('it throws when the case does not exists', async () => {
Expand All @@ -204,7 +355,10 @@ describe('addComment', () => {
caseClient.client
.addComment({
caseId: 'not-exists',
comment: { comment: 'Wow, good luck catching that bad meanie!', type: CommentType.user },
comment: {
comment: 'Wow, good luck catching that bad meanie!',
type: CommentType.user,
},
})
.catch((e) => {
expect(e).not.toBeNull();
Expand All @@ -224,7 +378,10 @@ describe('addComment', () => {
caseClient.client
.addComment({
caseId: 'mock-id-1',
comment: { comment: 'Throw an error', type: CommentType.user },
comment: {
comment: 'Throw an error',
type: CommentType.user,
},
})
.catch((e) => {
expect(e).not.toBeNull();
Expand Down
Loading

0 comments on commit 52c6b7b

Please sign in to comment.