Skip to content

Commit c62f9e8

Browse files
authored
[Security Solution][Exceptions] - Update exception item comments to include id (#73129) (#73375)
## Summary This PR is somewhat of an intermediary step. Comments on exception list items are denormalized. We initially decided that we would not add `uuid` to comments, but found that it is in fact necessary. This is intermediary in the sense that what we ideally want to have is a dedicated `comments` CRUD route. Also just note that I added a callout for when a version conflict occurs (ie: exception item was updated by someone else while a user is editing the same item). With this PR users are able to: - Create comments when creating exception list items - Add new comments on exception item update Users will currently be blocked from: - Deleting comments - Updating comments - Updating exception item if version conflict is found
1 parent 6d99816 commit c62f9e8

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+702
-783
lines changed

x-pack/plugins/lists/common/constants.mock.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import { EntriesArray } from './schemas/types';
77

88
export const DATE_NOW = '2020-04-20T15:25:31.830Z';
9+
export const OLD_DATE_RELATIVE_TO_DATE_NOW = '2020-04-19T15:25:31.830Z';
910
export const USER = 'some user';
1011
export const LIST_INDEX = '.lists';
1112
export const LIST_ITEM_INDEX = '.items';

x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.test.ts

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ import { left } from 'fp-ts/lib/Either';
88
import { pipe } from 'fp-ts/lib/pipeable';
99

1010
import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps';
11-
import { getCreateCommentsArrayMock } from '../types/create_comments.mock';
12-
import { getCommentsMock } from '../types/comments.mock';
11+
import { getCreateCommentsArrayMock } from '../types/create_comment.mock';
12+
import { getCommentsMock } from '../types/comment.mock';
1313
import { CommentsArray } from '../types';
1414

1515
import {
@@ -19,7 +19,7 @@ import {
1919
import { getCreateEndpointListItemSchemaMock } from './create_endpoint_list_item_schema.mock';
2020

2121
describe('create_endpoint_list_item_schema', () => {
22-
test('it should validate a typical list item request not counting the auto generated uuid', () => {
22+
test('it should pass validation when supplied a typical list item request not counting the auto generated uuid', () => {
2323
const payload = getCreateEndpointListItemSchemaMock();
2424
const decoded = createEndpointListItemSchema.decode(payload);
2525
const checked = exactCheck(payload, decoded);
@@ -29,7 +29,7 @@ describe('create_endpoint_list_item_schema', () => {
2929
expect(message.schema).toEqual(payload);
3030
});
3131

32-
test('it should not validate an undefined for "description"', () => {
32+
test('it should fail validation when supplied an undefined for "description"', () => {
3333
const payload = getCreateEndpointListItemSchemaMock();
3434
delete payload.description;
3535
const decoded = createEndpointListItemSchema.decode(payload);
@@ -41,7 +41,7 @@ describe('create_endpoint_list_item_schema', () => {
4141
expect(message.schema).toEqual({});
4242
});
4343

44-
test('it should not validate an undefined for "name"', () => {
44+
test('it should fail validation when supplied an undefined for "name"', () => {
4545
const payload = getCreateEndpointListItemSchemaMock();
4646
delete payload.name;
4747
const decoded = createEndpointListItemSchema.decode(payload);
@@ -53,7 +53,7 @@ describe('create_endpoint_list_item_schema', () => {
5353
expect(message.schema).toEqual({});
5454
});
5555

56-
test('it should not validate an undefined for "type"', () => {
56+
test('it should fail validation when supplied an undefined for "type"', () => {
5757
const payload = getCreateEndpointListItemSchemaMock();
5858
delete payload.type;
5959
const decoded = createEndpointListItemSchema.decode(payload);
@@ -65,7 +65,7 @@ describe('create_endpoint_list_item_schema', () => {
6565
expect(message.schema).toEqual({});
6666
});
6767

68-
test('it should not validate a "list_id" since it does not required one', () => {
68+
test('it should fail validation when supplied a "list_id" since it does not required one', () => {
6969
const inputPayload: CreateEndpointListItemSchema & { list_id: string } = {
7070
...getCreateEndpointListItemSchemaMock(),
7171
list_id: 'list-123',
@@ -77,7 +77,7 @@ describe('create_endpoint_list_item_schema', () => {
7777
expect(message.schema).toEqual({});
7878
});
7979

80-
test('it should not validate a "namespace_type" since it does not required one', () => {
80+
test('it should fail validation when supplied a "namespace_type" since it does not required one', () => {
8181
const inputPayload: CreateEndpointListItemSchema & { namespace_type: string } = {
8282
...getCreateEndpointListItemSchemaMock(),
8383
namespace_type: 'single',
@@ -89,7 +89,7 @@ describe('create_endpoint_list_item_schema', () => {
8989
expect(message.schema).toEqual({});
9090
});
9191

92-
test('it should validate an undefined for "meta" but strip it out and generate a correct body not counting the auto generated uuid', () => {
92+
test('it should pass validation when supplied an undefined for "meta" but strip it out and generate a correct body not counting the auto generated uuid', () => {
9393
const payload = getCreateEndpointListItemSchemaMock();
9494
const outputPayload = getCreateEndpointListItemSchemaMock();
9595
delete payload.meta;
@@ -102,7 +102,7 @@ describe('create_endpoint_list_item_schema', () => {
102102
expect(message.schema).toEqual(outputPayload);
103103
});
104104

105-
test('it should validate an undefined for "comments" but return an array and generate a correct body not counting the auto generated uuid', () => {
105+
test('it should pass validation when supplied an undefined for "comments" but return an array and generate a correct body not counting the auto generated uuid', () => {
106106
const inputPayload = getCreateEndpointListItemSchemaMock();
107107
const outputPayload = getCreateEndpointListItemSchemaMock();
108108
delete inputPayload.comments;
@@ -115,7 +115,7 @@ describe('create_endpoint_list_item_schema', () => {
115115
expect(message.schema).toEqual(outputPayload);
116116
});
117117

118-
test('it should validate "comments" array', () => {
118+
test('it should pass validation when supplied "comments" array', () => {
119119
const inputPayload = {
120120
...getCreateEndpointListItemSchemaMock(),
121121
comments: getCreateCommentsArrayMock(),
@@ -128,7 +128,7 @@ describe('create_endpoint_list_item_schema', () => {
128128
expect(message.schema).toEqual(inputPayload);
129129
});
130130

131-
test('it should NOT validate "comments" with "created_at" or "created_by" values', () => {
131+
test('it should fail validation when supplied "comments" with "created_at", "created_by", or "id" values', () => {
132132
const inputPayload: Omit<CreateEndpointListItemSchema, 'comments'> & {
133133
comments?: CommentsArray;
134134
} = {
@@ -138,11 +138,11 @@ describe('create_endpoint_list_item_schema', () => {
138138
const decoded = createEndpointListItemSchema.decode(inputPayload);
139139
const checked = exactCheck(inputPayload, decoded);
140140
const message = pipe(checked, foldLeftRight);
141-
expect(getPaths(left(message.errors))).toEqual(['invalid keys "created_at,created_by"']);
141+
expect(getPaths(left(message.errors))).toEqual(['invalid keys "created_at,created_by,id"']);
142142
expect(message.schema).toEqual({});
143143
});
144144

145-
test('it should NOT validate an undefined for "entries"', () => {
145+
test('it should fail validation when supplied an undefined for "entries"', () => {
146146
const inputPayload = getCreateEndpointListItemSchemaMock();
147147
const outputPayload = getCreateEndpointListItemSchemaMock();
148148
delete inputPayload.entries;
@@ -157,7 +157,7 @@ describe('create_endpoint_list_item_schema', () => {
157157
expect(message.schema).toEqual({});
158158
});
159159

160-
test('it should validate an undefined for "tags" but return an array and generate a correct body not counting the auto generated uuid', () => {
160+
test('it should pass validation when supplied an undefined for "tags" but return an array and generate a correct body not counting the auto generated uuid', () => {
161161
const inputPayload = getCreateEndpointListItemSchemaMock();
162162
const outputPayload = getCreateEndpointListItemSchemaMock();
163163
delete inputPayload.tags;
@@ -170,7 +170,7 @@ describe('create_endpoint_list_item_schema', () => {
170170
expect(message.schema).toEqual(outputPayload);
171171
});
172172

173-
test('it should validate an undefined for "_tags" but return an array and generate a correct body not counting the auto generated uuid', () => {
173+
test('it should pass validation when supplied an undefined for "_tags" but return an array and generate a correct body not counting the auto generated uuid', () => {
174174
const inputPayload = getCreateEndpointListItemSchemaMock();
175175
const outputPayload = getCreateEndpointListItemSchemaMock();
176176
delete inputPayload._tags;
@@ -183,7 +183,7 @@ describe('create_endpoint_list_item_schema', () => {
183183
expect(message.schema).toEqual(outputPayload);
184184
});
185185

186-
test('it should validate an undefined for "item_id" and auto generate a uuid', () => {
186+
test('it should pass validation when supplied an undefined for "item_id" and auto generate a uuid', () => {
187187
const inputPayload = getCreateEndpointListItemSchemaMock();
188188
delete inputPayload.item_id;
189189
const decoded = createEndpointListItemSchema.decode(inputPayload);
@@ -195,7 +195,7 @@ describe('create_endpoint_list_item_schema', () => {
195195
);
196196
});
197197

198-
test('it should validate an undefined for "item_id" and generate a correct body not counting the uuid', () => {
198+
test('it should pass validation when supplied an undefined for "item_id" and generate a correct body not counting the uuid', () => {
199199
const inputPayload = getCreateEndpointListItemSchemaMock();
200200
delete inputPayload.item_id;
201201
const decoded = createEndpointListItemSchema.decode(inputPayload);

x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.test.ts

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ import { left } from 'fp-ts/lib/Either';
88
import { pipe } from 'fp-ts/lib/pipeable';
99

1010
import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps';
11-
import { getCreateCommentsArrayMock } from '../types/create_comments.mock';
12-
import { getCommentsMock } from '../types/comments.mock';
11+
import { getCreateCommentsArrayMock } from '../types/create_comment.mock';
12+
import { getCommentsMock } from '../types/comment.mock';
1313
import { CommentsArray } from '../types';
1414

1515
import {
@@ -19,7 +19,7 @@ import {
1919
import { getCreateExceptionListItemSchemaMock } from './create_exception_list_item_schema.mock';
2020

2121
describe('create_exception_list_item_schema', () => {
22-
test('it should validate a typical exception list item request not counting the auto generated uuid', () => {
22+
test('it should pass validation when supplied a typical exception list item request not counting the auto generated uuid', () => {
2323
const payload = getCreateExceptionListItemSchemaMock();
2424
const decoded = createExceptionListItemSchema.decode(payload);
2525
const checked = exactCheck(payload, decoded);
@@ -29,7 +29,7 @@ describe('create_exception_list_item_schema', () => {
2929
expect(message.schema).toEqual(payload);
3030
});
3131

32-
test('it should not validate an undefined for "description"', () => {
32+
test('it should fail validation when supplied an undefined for "description"', () => {
3333
const payload = getCreateExceptionListItemSchemaMock();
3434
delete payload.description;
3535
const decoded = createExceptionListItemSchema.decode(payload);
@@ -41,7 +41,7 @@ describe('create_exception_list_item_schema', () => {
4141
expect(message.schema).toEqual({});
4242
});
4343

44-
test('it should not validate an undefined for "name"', () => {
44+
test('it should fail validation when supplied an undefined for "name"', () => {
4545
const payload = getCreateExceptionListItemSchemaMock();
4646
delete payload.name;
4747
const decoded = createExceptionListItemSchema.decode(payload);
@@ -53,7 +53,7 @@ describe('create_exception_list_item_schema', () => {
5353
expect(message.schema).toEqual({});
5454
});
5555

56-
test('it should not validate an undefined for "type"', () => {
56+
test('it should fail validation when supplied an undefined for "type"', () => {
5757
const payload = getCreateExceptionListItemSchemaMock();
5858
delete payload.type;
5959
const decoded = createExceptionListItemSchema.decode(payload);
@@ -65,7 +65,7 @@ describe('create_exception_list_item_schema', () => {
6565
expect(message.schema).toEqual({});
6666
});
6767

68-
test('it should not validate an undefined for "list_id"', () => {
68+
test('it should fail validation when supplied an undefined for "list_id"', () => {
6969
const inputPayload = getCreateExceptionListItemSchemaMock();
7070
delete inputPayload.list_id;
7171
const decoded = createExceptionListItemSchema.decode(inputPayload);
@@ -77,7 +77,7 @@ describe('create_exception_list_item_schema', () => {
7777
expect(message.schema).toEqual({});
7878
});
7979

80-
test('it should validate an undefined for "meta" but strip it out and generate a correct body not counting the auto generated uuid', () => {
80+
test('it should pass validation when supplied an undefined for "meta" but strip it out and generate a correct body not counting the auto generated uuid', () => {
8181
const payload = getCreateExceptionListItemSchemaMock();
8282
const outputPayload = getCreateExceptionListItemSchemaMock();
8383
delete payload.meta;
@@ -90,7 +90,7 @@ describe('create_exception_list_item_schema', () => {
9090
expect(message.schema).toEqual(outputPayload);
9191
});
9292

93-
test('it should validate an undefined for "comments" but return an array and generate a correct body not counting the auto generated uuid', () => {
93+
test('it should pass validation when supplied an undefined for "comments" but return an array and generate a correct body not counting the auto generated uuid', () => {
9494
const inputPayload = getCreateExceptionListItemSchemaMock();
9595
const outputPayload = getCreateExceptionListItemSchemaMock();
9696
delete inputPayload.comments;
@@ -103,7 +103,7 @@ describe('create_exception_list_item_schema', () => {
103103
expect(message.schema).toEqual(outputPayload);
104104
});
105105

106-
test('it should validate "comments" array', () => {
106+
test('it should pass validation when supplied "comments" array', () => {
107107
const inputPayload = {
108108
...getCreateExceptionListItemSchemaMock(),
109109
comments: getCreateCommentsArrayMock(),
@@ -116,7 +116,7 @@ describe('create_exception_list_item_schema', () => {
116116
expect(message.schema).toEqual(inputPayload);
117117
});
118118

119-
test('it should NOT validate "comments" with "created_at" or "created_by" values', () => {
119+
test('it should fail validation when supplied "comments" with "created_at" or "created_by" values', () => {
120120
const inputPayload: Omit<CreateExceptionListItemSchema, 'comments'> & {
121121
comments?: CommentsArray;
122122
} = {
@@ -126,11 +126,11 @@ describe('create_exception_list_item_schema', () => {
126126
const decoded = createExceptionListItemSchema.decode(inputPayload);
127127
const checked = exactCheck(inputPayload, decoded);
128128
const message = pipe(checked, foldLeftRight);
129-
expect(getPaths(left(message.errors))).toEqual(['invalid keys "created_at,created_by"']);
129+
expect(getPaths(left(message.errors))).toEqual(['invalid keys "created_at,created_by,id"']);
130130
expect(message.schema).toEqual({});
131131
});
132132

133-
test('it should NOT validate an undefined for "entries"', () => {
133+
test('it should fail validation when supplied an undefined for "entries"', () => {
134134
const inputPayload = getCreateExceptionListItemSchemaMock();
135135
const outputPayload = getCreateExceptionListItemSchemaMock();
136136
delete inputPayload.entries;
@@ -145,7 +145,7 @@ describe('create_exception_list_item_schema', () => {
145145
expect(message.schema).toEqual({});
146146
});
147147

148-
test('it should validate an undefined for "namespace_type" but return enum "single" and generate a correct body not counting the auto generated uuid', () => {
148+
test('it should pass validation when supplied an undefined for "namespace_type" but return enum "single" and generate a correct body not counting the auto generated uuid', () => {
149149
const inputPayload = getCreateExceptionListItemSchemaMock();
150150
const outputPayload = getCreateExceptionListItemSchemaMock();
151151
delete inputPayload.namespace_type;
@@ -158,7 +158,7 @@ describe('create_exception_list_item_schema', () => {
158158
expect(message.schema).toEqual(outputPayload);
159159
});
160160

161-
test('it should validate an undefined for "tags" but return an array and generate a correct body not counting the auto generated uuid', () => {
161+
test('it should pass validation when supplied an undefined for "tags" but return an array and generate a correct body not counting the auto generated uuid', () => {
162162
const inputPayload = getCreateExceptionListItemSchemaMock();
163163
const outputPayload = getCreateExceptionListItemSchemaMock();
164164
delete inputPayload.tags;
@@ -171,7 +171,7 @@ describe('create_exception_list_item_schema', () => {
171171
expect(message.schema).toEqual(outputPayload);
172172
});
173173

174-
test('it should validate an undefined for "_tags" but return an array and generate a correct body not counting the auto generated uuid', () => {
174+
test('it should pass validation when supplied an undefined for "_tags" but return an array and generate a correct body not counting the auto generated uuid', () => {
175175
const inputPayload = getCreateExceptionListItemSchemaMock();
176176
const outputPayload = getCreateExceptionListItemSchemaMock();
177177
delete inputPayload._tags;
@@ -184,7 +184,7 @@ describe('create_exception_list_item_schema', () => {
184184
expect(message.schema).toEqual(outputPayload);
185185
});
186186

187-
test('it should validate an undefined for "item_id" and auto generate a uuid', () => {
187+
test('it should pass validation when supplied an undefined for "item_id" and auto generate a uuid', () => {
188188
const inputPayload = getCreateExceptionListItemSchemaMock();
189189
delete inputPayload.item_id;
190190
const decoded = createExceptionListItemSchema.decode(inputPayload);
@@ -196,7 +196,7 @@ describe('create_exception_list_item_schema', () => {
196196
);
197197
});
198198

199-
test('it should validate an undefined for "item_id" and generate a correct body not counting the uuid', () => {
199+
test('it should pass validation when supplied an undefined for "item_id" and generate a correct body not counting the uuid', () => {
200200
const inputPayload = getCreateExceptionListItemSchemaMock();
201201
delete inputPayload.item_id;
202202
const decoded = createExceptionListItemSchema.decode(inputPayload);
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import { getUpdateExceptionListItemSchemaMock } from './update_exception_list_item_schema.mock';
8+
import { validateComments } from './update_exception_list_item_validation';
9+
10+
describe('update_exception_list_item_validation', () => {
11+
describe('#validateComments', () => {
12+
test('it returns no errors if comments is undefined', () => {
13+
const payload = getUpdateExceptionListItemSchemaMock();
14+
delete payload.comments;
15+
const output = validateComments(payload);
16+
17+
expect(output).toEqual([]);
18+
});
19+
20+
test('it returns no errors if new comments are append only', () => {
21+
const payload = getUpdateExceptionListItemSchemaMock();
22+
payload.comments = [
23+
{ comment: 'Im an old comment', id: '1' },
24+
{ comment: 'Im a new comment' },
25+
];
26+
const output = validateComments(payload);
27+
28+
expect(output).toEqual([]);
29+
});
30+
31+
test('it returns error if comments are not append only', () => {
32+
const payload = getUpdateExceptionListItemSchemaMock();
33+
payload.comments = [
34+
{ comment: 'Im an old comment', id: '1' },
35+
{ comment: 'Im a new comment modifying the order of existing comments' },
36+
{ comment: 'Im an old comment', id: '2' },
37+
];
38+
const output = validateComments(payload);
39+
40+
expect(output).toEqual(['item "comments" are append only']);
41+
});
42+
});
43+
});
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import { UpdateExceptionListItemSchema } from './update_exception_list_item_schema';
8+
9+
export const validateComments = (item: UpdateExceptionListItemSchema): string[] => {
10+
if (item.comments == null) {
11+
return [];
12+
}
13+
14+
const [appendOnly] = item.comments.reduce(
15+
(acc, comment) => {
16+
const [, hasNewComments] = acc;
17+
if (comment.id == null) {
18+
return [true, true];
19+
}
20+
21+
if (hasNewComments && comment.id != null) {
22+
return [false, true];
23+
}
24+
25+
return acc;
26+
},
27+
[true, false]
28+
);
29+
if (!appendOnly) {
30+
return ['item "comments" are append only'];
31+
} else {
32+
return [];
33+
}
34+
};
35+
36+
export const updateExceptionListItemValidate = (
37+
schema: UpdateExceptionListItemSchema
38+
): string[] => {
39+
return [...validateComments(schema)];
40+
};

0 commit comments

Comments
 (0)