diff --git a/packages/kbn-es-query/src/kuery/index.ts b/packages/kbn-es-query/src/kuery/index.ts index d7b0c902e47f1..8d19a814ffd6e 100644 --- a/packages/kbn-es-query/src/kuery/index.ts +++ b/packages/kbn-es-query/src/kuery/index.ts @@ -24,3 +24,4 @@ export { KQLSyntaxError } from './kuery_syntax_error'; export { nodeTypes, nodeBuilder } from './node_types'; export { fromKueryExpression } from './ast'; export type { DslQuery, KueryNode, KueryQueryOptions } from './types'; +export { escapeKuery } from './utils/escape_kuery'; diff --git a/packages/kbn-es-query/src/kuery/utils/escape_kuery.ts b/packages/kbn-es-query/src/kuery/utils/escape_kuery.ts new file mode 100644 index 0000000000000..62eede9fe5340 --- /dev/null +++ b/packages/kbn-es-query/src/kuery/utils/escape_kuery.ts @@ -0,0 +1,42 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import { flow } from 'lodash'; + +/** + * Escapes backslashes and double-quotes. (Useful when putting a string in quotes to use as a value + * in a KQL expression. See the QuotedCharacter rule in kuery.peg.) + */ +export function escapeQuotes(str: string) { + return str.replace(/[\\"]/g, '\\$&'); +} + +/** + * Escapes a Kuery node value to ensure that special characters, operators, and whitespace do not result in a parsing error or unintended + * behavior when using the value as an argument for the `buildNode` function. + */ +export const escapeKuery = flow(escapeSpecialCharacters, escapeAndOr, escapeNot, escapeWhitespace); + +// See the SpecialCharacter rule in kuery.peg +function escapeSpecialCharacters(str: string) { + return str.replace(/[\\():<>"*]/g, '\\$&'); // $& means the whole matched string +} + +// See the Keyword rule in kuery.peg +function escapeAndOr(str: string) { + return str.replace(/(\s+)(and|or)(\s+)/gi, '$1\\$2$3'); +} + +function escapeNot(str: string) { + return str.replace(/not(\s+)/gi, '\\$&'); +} + +// See the Space rule in kuery.peg +function escapeWhitespace(str: string) { + return str.replace(/\t/g, '\\t').replace(/\r/g, '\\r').replace(/\n/g, '\\n'); +} diff --git a/x-pack/plugins/timelines/public/components/utils/keury/index.test.ts b/x-pack/plugins/timelines/public/components/utils/keury/index.test.ts index 692fa9303c8e8..b83ad9573028b 100644 --- a/x-pack/plugins/timelines/public/components/utils/keury/index.test.ts +++ b/x-pack/plugins/timelines/public/components/utils/keury/index.test.ts @@ -6,84 +6,74 @@ */ import expect from '@kbn/expect'; -import { escapeKuery } from '.'; +import { escapeQueryValue } from '.'; -describe('Kuery escape', () => { - it('should not remove white spaces quotes', () => { - const value = ' netcat'; - const expected = ' netcat'; - expect(escapeKuery(value)).to.be(expected); - }); - - it('should escape quotes', () => { - const value = 'I said, "Hello."'; - const expected = 'I said, \\"Hello.\\"'; - expect(escapeKuery(value)).to.be(expected); - }); - - it('should escape special characters', () => { - const value = `This \\ has (a lot of) characters, don't you *think*? "Yes."`; - const expected = `This \\ has (a lot of) characters, don't you *think*? \\"Yes.\\"`; - expect(escapeKuery(value)).to.be(expected); - }); - - it('should NOT escape keywords', () => { - const value = 'foo and bar or baz not qux'; - const expected = 'foo and bar or baz not qux'; - expect(escapeKuery(value)).to.be(expected); - }); - - it('should NOT escape keywords next to each other', () => { - const value = 'foo and bar or not baz'; - const expected = 'foo and bar or not baz'; - expect(escapeKuery(value)).to.be(expected); - }); - - it('should not escape keywords without surrounding spaces', () => { - const value = 'And this has keywords, or does it not?'; - const expected = 'And this has keywords, or does it not?'; - expect(escapeKuery(value)).to.be(expected); - }); +const TEST_QUERIES = [ + { + description: 'should not remove white spaces quotes', + value: ' netcat', + expected: ' netcat', + }, + { + description: 'should escape quotes', + value: 'I said, "Hello."', + expected: 'I said, \\"Hello.\\"', + }, + { + description: 'should escape special characters', + value: `This \\ has (a lot of) characters, don't you *think*? "Yes."`, + expected: `This \\\\ has \\(a lot of\\) \\ characters, don't you \\*think\\*? \\"Yes.\\"`, + }, + { + description: 'should NOT escape keywords', + value: 'foo and bar or baz not qux', + expected: 'foo and bar or baz not qux', + }, + { + description: 'should NOT escape keywords next to each other', + value: 'foo and bar or not baz', + expected: 'foo and bar or not baz', + }, + { + description: 'should not escape keywords without surrounding spaces', + value: 'And this has keywords, or does it not?', + expected: 'And this has keywords, or does it not?', + }, + { + description: 'should NOT escape uppercase keywords', + value: 'foo AND bar', + expected: 'foo AND bar', + }, + { + description: 'should escape special characters and NOT keywords', + value: 'Hello, "world", and to meet you!', + expected: 'Hello, \\"world\\", and to meet you!', + }, + { + description: 'should escape newlines and tabs', + value: 'This\nhas\tnewlines\r\nwith\ttabs', + expected: 'This\\nhas\\tnewlines\\r\\nwith\\ttabs', + }, + { + description: 'should escape backslashes', + value: 'This\\has\\backslashes', + expected: 'This\\\\has\\\\backslashes', + }, + { + description: 'should escape multiple backslashes and quotes', + value: 'This\\ has 2" quotes & \\ 2 "backslashes', + expected: 'This\\\\ has 2\\" quotes & \\\\ 2 \\"backslashes', + }, + { + description: 'should escape all special character according to kuery.peg SpecialCharacter rule', + value: '\\():"*', + expected: '\\\\\\(\\)\\:\\"\\*', + }, +]; - it('should NOT escape uppercase keywords', () => { - const value = 'foo AND bar'; - const expected = 'foo AND bar'; - expect(escapeKuery(value)).to.be(expected); - }); - - it('should escape special characters and NOT keywords', () => { - const value = 'Hello, "world", and to meet you!'; - const expected = 'Hello, \\"world\\", and to meet you!'; - expect(escapeKuery(value)).to.be(expected); - }); - - it('should escape newlines and tabs', () => { - const value = 'This\nhas\tnewlines\r\nwith\ttabs'; - const expected = 'This\\nhas\\tnewlines\\r\\nwith\\ttabs'; - expect(escapeKuery(value)).to.be(expected); - }); - - it('should escape backslashes', () => { - const value = 'This\\has\\backslashes'; - const expected = 'This\\\\has\\\\backslashes'; - expect(escapeKuery(value)).to.be(expected); - }); - - it('should escape multiple backslashes and quotes', () => { - const value = 'This\\ has 2" quotes & \\ 2 "backslashes'; - const expected = 'This\\\\ has 2\\" quotes & \\\\ 2 \\"backslashes'; - expect(escapeKuery(value)).to.be(expected); - }); - - it('should escape all special character according to kuery.peg SpecialCharacter rule', () => { - /* - * Ref: packages/kbn-es-query/grammar/grammar.peggy - * - * SpecialCharacter - * = [\\():<>"*{}] - */ - const value = `\\():"*{}`; - const expected = `\\\\\\(\\)\\:\\"\\*\\{\\}`; - expect(escapeKuery(value)).to.be(expected); +describe('Kuery escape', () => { + it.each(TEST_QUERIES)('$description', ({ description, value, expected }) => { + const result = escapeQueryValue(value); + expect(result).to.be(`"${expected}"`); }); }); diff --git a/x-pack/plugins/timelines/public/components/utils/keury/index.ts b/x-pack/plugins/timelines/public/components/utils/keury/index.ts index 6453463860f3b..485a4bdde9972 100644 --- a/x-pack/plugins/timelines/public/components/utils/keury/index.ts +++ b/x-pack/plugins/timelines/public/components/utils/keury/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { isEmpty, isString, flow } from 'lodash/fp'; +import { isEmpty, isString } from 'lodash/fp'; import { buildEsQuery, EsQueryConfig, @@ -14,6 +14,7 @@ import { IndexPatternBase, Query, toElasticsearchQuery, + escapeKuery, } from '@kbn/es-query'; export const convertKueryToElasticSearchQuery = ( @@ -53,20 +54,6 @@ export const escapeQueryValue = (val: number | string = ''): string | number => return val; }; -const escapeWhitespace = (val: string) => - val.replace(/\t/g, '\\t').replace(/\r/g, '\\r').replace(/\n/g, '\\n'); - -// See the SpecialCharacter rule in kuery.peg -const escapeSpecialCharacters = (val: string) => val.replace(/["\\\(\)\:\<\>\*\{\}]/g, '\\$&'); // $& means the whole matched string - -// See the Keyword rule in kuery.peg -// I do not think that we need that anymore since we are doing a full match_phrase all the time now => return `"${escapeKuery(val)}"`; -// const escapeAndOr = (val: string) => val.replace(/(\s+)(and|or)(\s+)/gi, '$1\\$2$3'); - -// const escapeNot = (val: string) => val.replace(/not(\s+)/gi, '\\$&'); - -export const escapeKuery = flow(escapeSpecialCharacters, escapeWhitespace); - export const convertToBuildEsQuery = ({ config, indexPattern,