[8.17] [Security Solution] Fixes exception item comment validation on newline chars \n (#202063) (#203708)

# Backport

This will backport the following commits from `main` to `8.17`:
- [[Security Solution] Fixes exception item comment validation on
newline chars `\n`
(#202063)](https://github.com/elastic/kibana/pull/202063)

<!--- Backport version: 8.9.8 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Devin W.
Hurley","email":"devin.hurley@elastic.co"},"sourceCommit":{"committedDate":"2024-12-10T22:19:32Z","message":"[Security
Solution] Fixes exception item comment validation on newline chars `\\n`
(#202063)\n\n## Summary\r\n\r\nFixes:
https://github.com/elastic/kibana/issues/201820\r\n\r\nCo-authored-by:
kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"35aeac104359eae81a233d0b8a9acaa97119d006","branchLabelMapping":{"^v9.0.0$":"main","^v8.18.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["bug","review","release_note:fix","v9.0.0","Team:Detections
and Resp","Feature:Rule
Exceptions","backport:version","v8.18.0","v8.16.2","v8.17.1"],"number":202063,"url":"https://github.com/elastic/kibana/pull/202063","mergeCommit":{"message":"[Security
Solution] Fixes exception item comment validation on newline chars `\\n`
(#202063)\n\n## Summary\r\n\r\nFixes:
https://github.com/elastic/kibana/issues/201820\r\n\r\nCo-authored-by:
kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"35aeac104359eae81a233d0b8a9acaa97119d006"}},"sourceBranch":"main","suggestedTargetBranches":["8.x","8.16","8.17"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","labelRegex":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/202063","number":202063,"mergeCommit":{"message":"[Security
Solution] Fixes exception item comment validation on newline chars `\\n`
(#202063)\n\n## Summary\r\n\r\nFixes:
https://github.com/elastic/kibana/issues/201820\r\n\r\nCo-authored-by:
kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"35aeac104359eae81a233d0b8a9acaa97119d006"}},{"branch":"8.x","label":"v8.18.0","labelRegex":"^v8.18.0$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"8.16","label":"v8.16.2","labelRegex":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"8.17","label":"v8.17.1","labelRegex":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->
This commit is contained in:
Devin W. Hurley 2024-12-12 19:51:00 -05:00 committed by GitHub
parent 2159816fce
commit 1619e73cfc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
33 changed files with 268 additions and 75 deletions

View file

@ -32086,9 +32086,9 @@ components:
- assistant
type: string
Security_AI_Assistant_API_NonEmptyString:
description: A string that is not empty and does not contain only whitespace
description: A string that does not contain only whitespace characters
format: nonempty
minLength: 1
pattern: ^(?! *$).+$
type: string
Security_AI_Assistant_API_NormalizedAnonymizationFieldError:
type: object
@ -35032,9 +35032,9 @@ components:
- severity
- $ref: '#/components/schemas/Security_Detections_API_NewTermsRuleCreateFields'
Security_Detections_API_NonEmptyString:
description: A string that is not empty and does not contain only whitespace
description: A string that does not contain only whitespace characters
format: nonempty
minLength: 1
pattern: ^(?! *$).+$
type: string
Security_Detections_API_NormalizedRuleAction:
additionalProperties: false
@ -38243,9 +38243,9 @@ components:
- text
type: string
Security_Endpoint_Exceptions_API_NonEmptyString:
description: A string that is not empty and does not contain only whitespace
description: A string that does not contain only whitespace characters
format: nonempty
minLength: 1
pattern: ^(?! *$).+$
type: string
Security_Endpoint_Exceptions_API_PlatformErrorResponse:
type: object
@ -38557,9 +38557,9 @@ components:
required:
- hostStatuses
Security_Endpoint_Management_API_NonEmptyString:
description: A string that is not empty and does not contain only whitespace
description: A string that does not contain only whitespace characters
format: nonempty
minLength: 1
pattern: ^(?! *$).+$
type: string
Security_Endpoint_Management_API_NoParametersRequestSchema:
type: object
@ -39697,9 +39697,9 @@ components:
- text
type: string
Security_Exceptions_API_NonEmptyString:
description: A string that is not empty and does not contain only whitespace
description: A string that does not contain only whitespace characters
format: nonempty
minLength: 1
pattern: ^(?! *$).+$
type: string
Security_Exceptions_API_PlatformErrorResponse:
type: object
@ -39943,9 +39943,9 @@ components:
- text
type: string
Security_Lists_API_NonEmptyString:
description: A string that is not empty and does not contain only whitespace
description: A string that does not contain only whitespace characters
format: nonempty
minLength: 1
pattern: ^(?! *$).+$
type: string
Security_Lists_API_PlatformErrorResponse:
type: object

View file

@ -0,0 +1,14 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
module.exports = {
preset: '@kbn/test/jest_node',
rootDir: '../..',
roots: ['<rootDir>/packages/kbn-openapi-common'],
};

View file

@ -17,15 +17,13 @@
*/
import { z } from '@kbn/zod';
import { isNonEmptyString } from '@kbn/zod-helpers';
/**
* A string that is not empty and does not contain only whitespace
* A string that does not contain only whitespace characters
*/
export type NonEmptyString = z.infer<typeof NonEmptyString>;
export const NonEmptyString = z
.string()
.min(1)
.regex(/^(?! *$).+$/);
export const NonEmptyString = z.string().min(1).superRefine(isNonEmptyString);
/**
* A universally unique identifier

View file

@ -8,9 +8,9 @@ components:
schemas:
NonEmptyString:
type: string
pattern: ^(?! *$).+$
minLength: 1
description: A string that is not empty and does not contain only whitespace
format: nonempty
description: A string that does not contain only whitespace characters
UUID:
type: string

View file

@ -0,0 +1,44 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { NonEmptyString } from './primitives.gen';
describe('NonEmptyString', () => {
describe('accepts ', () => {
// \t\r\n\f
test('accepts newline chars', () => {
expect(() => NonEmptyString.parse('hello \nworld')).not.toThrow();
});
test('accepts tab chars', () => {
expect(() => NonEmptyString.parse('hello \tworld')).not.toThrow();
});
test('accepts carriage return chars', () => {
expect(() => NonEmptyString.parse('hello \rworld')).not.toThrow();
});
test('accepts form feed return chars', () => {
expect(() => NonEmptyString.parse('hello \fworld')).not.toThrow();
});
});
describe('rejects', () => {
test('rejects only tab chars chars', () => {
expect(() => NonEmptyString.parse('\t\t\t\t')).toThrow();
});
test('rejects only newline chars chars', () => {
expect(() => NonEmptyString.parse('\n\n\n\n\n')).toThrow();
});
test('rejects only carriage return chars chars', () => {
expect(() => NonEmptyString.parse('\r\r\r\r')).toThrow();
});
test('rejects only form feed chars chars', () => {
expect(() => NonEmptyString.parse('\f\f\f\f\f')).toThrow();
});
test('rejects comment with just spaces', () => {
expect(() => NonEmptyString.parse(' ')).toThrow();
});
});
});

View file

@ -8,5 +8,6 @@
"include": ["**/*.ts"],
"kbn_references": [
"@kbn/zod",
"@kbn/zod-helpers",
]
}

View file

@ -9,7 +9,7 @@
import type { ZodTypeDef } from '@kbn/zod';
import { z } from '@kbn/zod';
import { requiredOptional, isValidDateMath, ArrayFromString, BooleanFromString } from '@kbn/zod-helpers';
import { requiredOptional, isValidDateMath, isNonEmptyString, ArrayFromString, BooleanFromString } from '@kbn/zod-helpers';
{{#each imports}}
import {

View file

@ -124,5 +124,8 @@ z.unknown()
{{~#if (eq format 'date-math')}}.superRefine(isValidDateMath){{/if~}}
{{~#if (eq format 'uuid')}}.uuid(){{/if~}}
{{~#if pattern}}.regex(/{{pattern}}/){{/if~}}
{{~#if (eq format 'trim')}}.trim(){{/if~}}
{{~#if (eq format 'nonempty')}}.superRefine(isNonEmptyString){{/if~}}
{{~/if~}}
{{~/inline~}}

View file

@ -830,9 +830,9 @@ components:
- text
type: string
NonEmptyString:
description: A string that is not empty and does not contain only whitespace
description: A string that does not contain only whitespace characters
format: nonempty
minLength: 1
pattern: ^(?! *$).+$
type: string
PlatformErrorResponse:
type: object

View file

@ -830,9 +830,9 @@ components:
- text
type: string
NonEmptyString:
description: A string that is not empty and does not contain only whitespace
description: A string that does not contain only whitespace characters
format: nonempty
minLength: 1
pattern: ^(?! *$).+$
type: string
PlatformErrorResponse:
type: object

View file

@ -1800,9 +1800,9 @@ components:
- text
type: string
NonEmptyString:
description: A string that is not empty and does not contain only whitespace
description: A string that does not contain only whitespace characters
format: nonempty
minLength: 1
pattern: ^(?! *$).+$
type: string
PlatformErrorResponse:
type: object

View file

@ -1800,9 +1800,9 @@ components:
- text
type: string
NonEmptyString:
description: A string that is not empty and does not contain only whitespace
description: A string that does not contain only whitespace characters
format: nonempty
minLength: 1
pattern: ^(?! *$).+$
type: string
PlatformErrorResponse:
type: object

View file

@ -1487,9 +1487,9 @@ components:
- text
type: string
NonEmptyString:
description: A string that is not empty and does not contain only whitespace
description: A string that does not contain only whitespace characters
format: nonempty
minLength: 1
pattern: ^(?! *$).+$
type: string
PlatformErrorResponse:
type: object

View file

@ -1487,9 +1487,9 @@ components:
- text
type: string
NonEmptyString:
description: A string that is not empty and does not contain only whitespace
description: A string that does not contain only whitespace characters
format: nonempty
minLength: 1
pattern: ^(?! *$).+$
type: string
PlatformErrorResponse:
type: object

View file

@ -16,3 +16,4 @@ export * from './src/required_optional';
export * from './src/safe_parse_result';
export * from './src/stringify_zod_error';
export * from './src/build_route_validation_with_zod';
export * from './src/non_empty_string';

View file

@ -0,0 +1,19 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import * as z from '@kbn/zod';
export function isNonEmptyString(input: string, ctx: z.RefinementCtx): void {
if (input.trim() === '') {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'No empty strings allowed',
});
}
}

View file

@ -978,9 +978,9 @@ components:
- assistant
type: string
NonEmptyString:
description: A string that is not empty and does not contain only whitespace
description: A string that does not contain only whitespace characters
format: nonempty
minLength: 1
pattern: ^(?! *$).+$
type: string
NormalizedAnonymizationFieldError:
type: object

View file

@ -978,9 +978,9 @@ components:
- assistant
type: string
NonEmptyString:
description: A string that is not empty and does not contain only whitespace
description: A string that does not contain only whitespace characters
format: nonempty
minLength: 1
pattern: ^(?! *$).+$
type: string
NormalizedAnonymizationFieldError:
type: object

View file

@ -15,15 +15,13 @@
*/
import { z } from '@kbn/zod';
import { isNonEmptyString } from '@kbn/zod-helpers';
/**
* A string that is not empty and does not contain only whitespace
* A string that does not contain only whitespace characters
*/
export type NonEmptyString = z.infer<typeof NonEmptyString>;
export const NonEmptyString = z
.string()
.min(1)
.regex(/^(?! *$).+$/);
export const NonEmptyString = z.string().min(1).superRefine(isNonEmptyString);
/**
* A universally unique identifier

View file

@ -8,9 +8,9 @@ components:
schemas:
NonEmptyString:
type: string
pattern: ^(?! *$).+$
format: nonempty
minLength: 1
description: A string that is not empty and does not contain only whitespace
description: A string that does not contain only whitespace characters
UUID:
type: string
@ -33,4 +33,3 @@ components:
enum:
- 'asc'
- 'desc'

View file

@ -5737,7 +5737,6 @@ Object {
"field_names": Object {
"items": Object {
"minLength": 1,
"pattern": "^(?! *$).+$",
"type": "string",
},
"minItems": 1,
@ -6405,7 +6404,6 @@ Object {
"field_names": Object {
"items": Object {
"minLength": 1,
"pattern": "^(?! *$).+$",
"type": "string",
},
"minItems": 1,
@ -7136,7 +7134,6 @@ Object {
"field_names": Object {
"items": Object {
"minLength": 1,
"pattern": "^(?! *$).+$",
"type": "string",
},
"minItems": 1,
@ -7785,7 +7782,6 @@ Object {
"field_names": Object {
"items": Object {
"minLength": 1,
"pattern": "^(?! *$).+$",
"type": "string",
},
"minItems": 1,
@ -8489,7 +8485,6 @@ Object {
"field_names": Object {
"items": Object {
"minLength": 1,
"pattern": "^(?! *$).+$",
"type": "string",
},
"minItems": 1,
@ -9195,7 +9190,6 @@ Object {
"field_names": Object {
"items": Object {
"minLength": 1,
"pattern": "^(?! *$).+$",
"type": "string",
},
"minItems": 1,
@ -9901,7 +9895,6 @@ Object {
"field_names": Object {
"items": Object {
"minLength": 1,
"pattern": "^(?! *$).+$",
"type": "string",
},
"minItems": 1,

View file

@ -15,15 +15,13 @@
*/
import { z } from '@kbn/zod';
import { isNonEmptyString } from '@kbn/zod-helpers';
/**
* A string that is not empty and does not contain only whitespace
* A string that does not contain only whitespace characters
*/
export type NonEmptyString = z.infer<typeof NonEmptyString>;
export const NonEmptyString = z
.string()
.min(1)
.regex(/^(?! *$).+$/);
export const NonEmptyString = z.string().min(1).superRefine(isNonEmptyString);
/**
* A universally unique identifier

View file

@ -8,9 +8,9 @@ components:
schemas:
NonEmptyString:
type: string
pattern: ^(?! *$).+$
format: nonempty
minLength: 1
description: A string that is not empty and does not contain only whitespace
description: A string that does not contain only whitespace characters
UUID:
type: string

View file

@ -15,15 +15,13 @@
*/
import { z } from '@kbn/zod';
import { isNonEmptyString } from '@kbn/zod-helpers';
/**
* A string that is not empty and does not contain only whitespace
* A string that does not contain only whitespace characters
*/
export type NonEmptyString = z.infer<typeof NonEmptyString>;
export const NonEmptyString = z
.string()
.min(1)
.regex(/^(?! *$).+$/);
export const NonEmptyString = z.string().min(1).superRefine(isNonEmptyString);
/**
* The GenAI connector id to use.

View file

@ -8,9 +8,9 @@ components:
schemas:
NonEmptyString:
type: string
pattern: ^(?! *$).+$
format: nonempty
minLength: 1
description: A string that is not empty and does not contain only whitespace
description: A string that does not contain only whitespace characters
ConnectorId:
type: string
description: The GenAI connector id to use.

View file

@ -4182,9 +4182,9 @@ components:
- severity
- $ref: '#/components/schemas/NewTermsRuleCreateFields'
NonEmptyString:
description: A string that is not empty and does not contain only whitespace
description: A string that does not contain only whitespace characters
format: nonempty
minLength: 1
pattern: ^(?! *$).+$
type: string
NormalizedRuleAction:
additionalProperties: false

View file

@ -920,9 +920,9 @@ components:
required:
- hostStatuses
NonEmptyString:
description: A string that is not empty and does not contain only whitespace
description: A string that does not contain only whitespace characters
format: nonempty
minLength: 1
pattern: ^(?! *$).+$
type: string
NoParametersRequestSchema:
type: object

View file

@ -3335,9 +3335,9 @@ components:
- severity
- $ref: '#/components/schemas/NewTermsRuleCreateFields'
NonEmptyString:
description: A string that is not empty and does not contain only whitespace
description: A string that does not contain only whitespace characters
format: nonempty
minLength: 1
pattern: ^(?! *$).+$
type: string
NormalizedRuleAction:
additionalProperties: false

View file

@ -820,9 +820,9 @@ components:
required:
- hostStatuses
NonEmptyString:
description: A string that is not empty and does not contain only whitespace
description: A string that does not contain only whitespace characters
format: nonempty
minLength: 1
pattern: ^(?! *$).+$
type: string
NoParametersRequestSchema:
type: object

View file

@ -92,7 +92,7 @@ describe('setAlertAssigneesRoute', () => {
const result = server.validate(request);
expect(result.badRequest).toHaveBeenCalledWith(
'ids.0: String must contain at least 1 character(s), ids.0: Invalid'
'ids.0: String must contain at least 1 character(s), ids.0: No empty strings allowed'
);
});
});

View file

@ -65,7 +65,7 @@ export default ({ getService }: FtrProviderContext) => {
expect(body).to.eql({
error: 'Bad Request',
message:
'[request body]: ids.1: String must contain at least 1 character(s), ids.1: Invalid',
'[request body]: ids.1: String must contain at least 1 character(s), ids.1: No empty strings allowed',
statusCode: 400,
});
});

View file

@ -68,6 +68,86 @@ export default ({ getService }: FtrProviderContext) => {
);
});
it('should create a simple exception list item with a list item id and a comment containing newline chars', async () => {
await supertest
.post(EXCEPTION_LIST_URL)
.set('kbn-xsrf', 'true')
.send(getCreateExceptionListMinimalSchemaMock())
.expect(200);
const { body } = await supertest
.post(EXCEPTION_LIST_ITEM_URL)
.set('kbn-xsrf', 'true')
.send({
...getCreateExceptionListItemMinimalSchemaMock(),
comments: [{ comment: 'hello\nworld' }],
})
.expect(200);
const { comments } = removeExceptionListItemServerGeneratedProperties(body);
expect(comments?.[0]?.comment).to.eql('hello\nworld');
});
it('should not create an item when the comment is empty', async () => {
await supertest
.post(EXCEPTION_LIST_URL)
.set('kbn-xsrf', 'true')
.send(getCreateExceptionListMinimalSchemaMock())
.expect(200);
const { body } = await supertest
.post(EXCEPTION_LIST_ITEM_URL)
.set('kbn-xsrf', 'true')
.send({
...getCreateExceptionListItemMinimalSchemaMock(),
comments: [{ comment: '' }],
})
.expect(400);
expect(body.message).to.contain('No empty strings allowed');
});
it('should not create an item when the comment is only newline chars', async () => {
await supertest
.post(EXCEPTION_LIST_URL)
.set('kbn-xsrf', 'true')
.send(getCreateExceptionListMinimalSchemaMock())
.expect(200);
const { body } = await supertest
.post(EXCEPTION_LIST_ITEM_URL)
.set('kbn-xsrf', 'true')
.send({
...getCreateExceptionListItemMinimalSchemaMock(),
comments: [{ comment: '\n\n\n\n' }],
})
.expect(400);
expect(body.message).to.contain('No empty strings allowed');
});
it('should create an item when the comments array is empty', async () => {
await supertest
.post(EXCEPTION_LIST_URL)
.set('kbn-xsrf', 'true')
.send(getCreateExceptionListMinimalSchemaMock())
.expect(200);
const { body } = await supertest
.post(EXCEPTION_LIST_ITEM_URL)
.set('kbn-xsrf', 'true')
.send({
...getCreateExceptionListItemMinimalSchemaMock(),
comments: [],
})
.expect(200);
const bodyToCompare = removeExceptionListItemServerGeneratedProperties(body);
expect(bodyToCompare).to.eql(
getExceptionListItemResponseMockWithoutAutoGeneratedValues(await utils.getUsername())
);
});
it('should create a match any exception item with multiple case sensitive values', async () => {
const entries = [
{

View file

@ -135,6 +135,53 @@ export default ({ getService }: FtrProviderContext): void => {
});
});
it('should return matching items when search is passed in and comments have newline chars', async () => {
// create exception list
await supertest
.post(EXCEPTION_LIST_URL)
.set('kbn-xsrf', 'true')
.send(getCreateExceptionListDetectionSchemaMock())
.expect(200);
// create exception list items
await supertest
.post(EXCEPTION_LIST_ITEM_URL)
.set('kbn-xsrf', 'true')
.send({
...getCreateExceptionListItemMinimalSchemaMockWithoutId(),
list_id: getCreateExceptionListDetectionSchemaMock().list_id,
item_id: '1',
entries: [
{ field: 'host.name', value: 'some host', operator: 'included', type: 'match' },
],
comments: [{ comment: 'hello\nworld' }],
})
.expect(200);
await supertest
.post(EXCEPTION_LIST_ITEM_URL)
.set('kbn-xsrf', 'true')
.send({
...getCreateExceptionListItemMinimalSchemaMockWithoutId(),
item_id: '2',
list_id: getCreateExceptionListDetectionSchemaMock().list_id,
entries: [{ field: 'foo', operator: 'included', type: 'exists' }],
})
.expect(200);
const { body } = await supertest
.get(
`${EXCEPTION_LIST_ITEM_URL}/_find?list_id=${
getCreateExceptionListMinimalSchemaMock().list_id
}&namespace_type=single&page=1&per_page=25&search=host&sort_field=exception-list.created_at&sort_order=desc`
)
.set('kbn-xsrf', 'true')
.send()
.expect(200);
body.data = [removeExceptionListItemServerGeneratedProperties(body.data[0])];
expect(body.data[0].comments[0].comment).to.eql('hello\nworld');
});
it('should return 404 if given a list_id that does not exist', async () => {
const { body } = await supertest
.get(`${EXCEPTION_LIST_ITEM_URL}/_find?list_id=non_exist`)