diff --git a/changelog/unreleased/issue-21565.toml b/changelog/unreleased/issue-21565.toml new file mode 100644 index 000000000000..75d2958e908c --- /dev/null +++ b/changelog/unreleased/issue-21565.toml @@ -0,0 +1,5 @@ +type = "fixed" +message = "Fixed issue preventing quoted entity search values from finding exact matches on various pages throughout Graylog." + +issues = ["21565"] +pulls = ["21567"] diff --git a/graylog2-server/src/main/java/org/graylog2/search/SearchQueryParser.java b/graylog2-server/src/main/java/org/graylog2/search/SearchQueryParser.java index b6665072ece2..f145a0c121eb 100644 --- a/graylog2-server/src/main/java/org/graylog2/search/SearchQueryParser.java +++ b/graylog2-server/src/main/java/org/graylog2/search/SearchQueryParser.java @@ -77,8 +77,16 @@ public class SearchQueryParser { private static final Splitter FIELD_VALUE_SPLITTER = Splitter.on(":").limit(2).omitEmptyStrings().trimResults(); private static final Splitter VALUE_SPLITTER = Splitter.on(",").omitEmptyStrings().trimResults(); + // Pattern to split a search query into individual parsable elements terms. + private static final String TERM_SPLIT_PATTERN = + "(\\S+:(=|=~|<|<=|>|>=)?'(?:[^'\\\\]|\\\\.)*')|" + // Split field-specific terms with single-quotes: title:'value' + "(\\S+:(=|=~|<|<=|>|>=)?\"(?:[^\"\\\\]|\\\\.)*\")|" + // Split field-specific terms with double-quotes title:"value" + "['\"][^\\\\]*?(?:\\\\.[^\\\\]*)*?['\"]|" + // Split single quoted value: "value one" + "\\S+:(=|=~|<|<=|>|>=)?\\S+|" + // Split field-specific terms without quotes title:value + "\\S+"; // Split the words of any other string value + // This needs to be updated if more operators are added - private static final Pattern QUERY_SPLITTER_PATTERN = Pattern.compile("(\\S+:(=|=~|<|<=|>|>=)?'(?:[^'\\\\]|\\\\.)*')|(\\S+:(=|=~|<|<=|>|>=)?\"(?:[^\"\\\\]|\\\\.)*\")|\\S+|\\S+:(=|=~|<|<=|>|>=)?\\S+"); + private static final Pattern QUERY_SPLITTER_PATTERN = Pattern.compile(TERM_SPLIT_PATTERN); private static final String INVALID_ENTRY_MESSAGE = "Chunk [%s] is not a valid entry"; private static final String QUOTE_REPLACE_REGEX = "^[\"']|[\"']$"; public static final SearchQueryOperator DEFAULT_STRING_OPERATOR = SearchQueryOperators.REGEXP; diff --git a/graylog2-server/src/test/java/org/graylog2/search/SearchQueryParserTest.java b/graylog2-server/src/test/java/org/graylog2/search/SearchQueryParserTest.java index aa2aade2a2b1..67a53b1e392b 100644 --- a/graylog2-server/src/test/java/org/graylog2/search/SearchQueryParserTest.java +++ b/graylog2-server/src/test/java/org/graylog2/search/SearchQueryParserTest.java @@ -299,6 +299,31 @@ void emptyFieldPrefixDoesNotChangeDefaultBehavior() { assertThat(searchQuery.hasDisallowedKeys()).isFalse(); } + @Test + void unquotedEmptyFieldPrefixSingleSearchTerm() { + final SearchQueryParser parser = new SearchQueryParser("name", Set.of(), ""); + // Verify unquoted term is split into two search values. + final SearchQuery searchQuery = parser.parse("Bobby testerson"); + final Multimap queryMap = searchQuery.getQueryMap(); + assertThat(queryMap.keySet().size()).isEqualTo(1); + final Collection values = queryMap.get("name"); + assertThat(values.size()).isEqualTo(2); + assertThat(values).contains(new SearchQueryParser.FieldValue("Bobby", false)); + assertThat(values).contains(new SearchQueryParser.FieldValue("testerson", false)); + } + + @Test + void quotedEmptyFieldPrefixSingleSearchTerm() { + final SearchQueryParser parser = new SearchQueryParser("name", Set.of(), ""); + // Verify quoted term is maintained as a single search value. + final SearchQuery searchQuery = parser.parse("\"Bobby testerson\""); + final Multimap queryMap = searchQuery.getQueryMap(); + assertThat(queryMap.keySet().size()).isEqualTo(1); + final Collection values = queryMap.get("name"); + assertThat(values.size()).isEqualTo(1); + assertThat(values).containsOnly(new SearchQueryParser.FieldValue("Bobby testerson", false)); + } + @Test void booleanValuesSupported() { final SearchQueryParser parser = new SearchQueryParser("name",