diff --git a/public/app/features/alerting/unified/hooks/useFilteredRules.test.ts b/public/app/features/alerting/unified/hooks/useFilteredRules.test.ts index b102a5b20c6..db277cea39d 100644 --- a/public/app/features/alerting/unified/hooks/useFilteredRules.test.ts +++ b/public/app/features/alerting/unified/hooks/useFilteredRules.test.ts @@ -256,4 +256,26 @@ describe('filterRules', function () { expect(filtered[0]?.groups[0]?.rules).toHaveLength(1); expect(filtered[0]?.groups[0]?.rules[0]?.name).toBe('CPU too high'); }); + + it('does not crash when trying to filter with regex-like strings', () => { + const rules = [mockCombinedRule({ name: '[alongnameinthefirstgroup]' })]; + + const ns = mockCombinedRuleNamespace({ + name: 'foo|bar', + groups: [ + // Create group with regex-like name so we can test that searching for it doesn't crash, + // and so we can test further paths of the filtering + // (we need some a group to be matched so we can test filtering by rule name as well) + mockCombinedRuleGroup('some|group', rules), + ], + }); + + const ruleQuery = '[alongnameinthefirstgroup][thishas spaces][somethingelse]'; + const namespaceQuery = 'foo|bar'; + const groupQuery = 'some|group'; + + const performFilter = () => + filterRules([ns], getFilter({ groupName: groupQuery, ruleName: ruleQuery, namespace: namespaceQuery })); + expect(performFilter).not.toThrow(); + }); }); diff --git a/public/app/features/alerting/unified/hooks/useFilteredRules.ts b/public/app/features/alerting/unified/hooks/useFilteredRules.ts index 3ceacb11b07..eaa13b43840 100644 --- a/public/app/features/alerting/unified/hooks/useFilteredRules.ts +++ b/public/app/features/alerting/unified/hooks/useFilteredRules.ts @@ -28,6 +28,19 @@ import { useURLSearchParams } from './useURLSearchParams'; const MAX_NEEDLE_SIZE = 25; const INFO_THRESHOLD = Infinity; +/** + * Escape query strings so that regex characters don't interfere + * with uFuzzy search methods. + * + * The fuzzy searching will take the query and generate a regex - but if the query + * contains a regex itself, then it can easily end up being split in a bad place + * and end up creating an invalid expression + */ +const escapeQueryRegex = (query: string) => { + // see https://stackoverflow.com/a/6969486 + return query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +}; + export function useRulesFilter() { const [queryParams, updateQueryParams] = useURLSearchParams(); const searchQuery = queryParams.get('search') ?? ''; @@ -130,10 +143,12 @@ export const filterRules = ( if (namespaceFilter) { const namespaceHaystack = filteredNamespaces.map((ns) => ns.name); + const escapedQuery = escapeQueryRegex(namespaceFilter); + const ufuzzy = getSearchInstance(namespaceFilter); const [idxs, info, order] = ufuzzy.search( namespaceHaystack, - namespaceFilter, + escapedQuery, getOutOfOrderLimit(namespaceFilter), INFO_THRESHOLD ); @@ -157,9 +172,11 @@ const reduceNamespaces = (filterState: RulesFilter) => { const groupsHaystack = filteredGroups.map((g) => g.name); const ufuzzy = getSearchInstance(groupNameFilter); + const escapedQuery = escapeQueryRegex(groupNameFilter); + const [idxs, info, order] = ufuzzy.search( groupsHaystack, - groupNameFilter, + escapedQuery, getOutOfOrderLimit(groupNameFilter), INFO_THRESHOLD ); @@ -193,10 +210,11 @@ const reduceGroups = (filterState: RulesFilter) => { if (ruleNameQuery) { const rulesHaystack = filteredRules.map((r) => r.name); const ufuzzy = getSearchInstance(ruleNameQuery); + const escapedQuery = escapeQueryRegex(ruleNameQuery); const [idxs, info, order] = ufuzzy.search( rulesHaystack, - ruleNameQuery, + escapedQuery, getOutOfOrderLimit(ruleNameQuery), INFO_THRESHOLD );