From e9f592315af6a2fe7091c8184a59cab1c6f4a616 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Thu, 16 Feb 2023 12:47:51 +0100 Subject: [PATCH 01/76] feat: add a search bar component Features: - Supports multiple query languages - Decouple the business logic of query languages of the search bar component - Ability of query language to interact with the search bar Query language implementations - AQL: custom implementation of the Wazuh Query Language. Include suggestions. - UIQL: simple implementation (as another example) --- public/components/search-bar/README.md | 99 ++++ public/components/search-bar/index.tsx | 186 ++++++ .../search-bar/query-language/aql.test.tsx | 88 +++ .../search-bar/query-language/aql.tsx | 538 ++++++++++++++++++ .../search-bar/query-language/index.ts | 26 + .../search-bar/query-language/uiql.tsx | 71 +++ .../agent/components/agents-table.js | 90 ++- 7 files changed, 1095 insertions(+), 3 deletions(-) create mode 100644 public/components/search-bar/README.md create mode 100644 public/components/search-bar/index.tsx create mode 100644 public/components/search-bar/query-language/aql.test.tsx create mode 100644 public/components/search-bar/query-language/aql.tsx create mode 100644 public/components/search-bar/query-language/index.ts create mode 100644 public/components/search-bar/query-language/uiql.tsx diff --git a/public/components/search-bar/README.md b/public/components/search-bar/README.md new file mode 100644 index 0000000000..764b1848ba --- /dev/null +++ b/public/components/search-bar/README.md @@ -0,0 +1,99 @@ +# Component + +The `SearchBar` component is a base component of a search bar. + +It is designed to be extensible through the self-contained query language implementations. This means +the behavior of the search bar depends on the business logic of each query language. For example, a +query language can display suggestions according to the user input or prepend some buttons to the search bar. + +It is based on a custom `EuiSuggest` component defined in `public/components/eui-suggest/suggest.js`. So the +abilities are restricted by this one. + +## Features + +- Supports multiple query languages. +- Switch the selected query language. +- Self-contained query language implementation and ability to interact with the search bar component +- React to external changes to set the new input. This enables to change the input from external components. + +# Usage + +Basic usage: + +```tsx + +``` + +# Query languages + +The built-in query languages are: + +- AQL: API Query Language. Based on https://documentation.wazuh.com/current/user-manual/api/queries.html. + +## How to add a new query language + +### Definition + +The language expects to take the interface: + +```ts +type SearchBarQueryLanguage = { + description: string; + documentationLink?: string; + id: string; + label: string; + getConfiguration?: () => any; + run: (input: string | undefined, params: any) => any; + transformUnifiedQuery: (unifiedQuery) => any; +}; +``` + +where: + +- `description`: It is the description of the query language. This is displayed in a query language popover + on the right side of the search bar. Required. +- `documentationLink`: URL to the documentation link. Optional. +- `id`: identification of the query language. +- `label`: name +- `getConfiguration`: method that returns the configuration of the language. This allows custom behavior. +- `run`: method that returns the properties that will used by the base search bar component and the output used when searching + +Create a new file located in `public/components/search-bar/query-language` and define the expected interface; + +### Register + +Go to `public/components/search-bar/query-language/index.ts` and add the new query language: + +```ts +import { AQL } from './aql'; + +// Import the custom query language +import { CustomQL } from './custom'; + +// [...] + +// Register the query languages +export const searchBarQueryLanguages: { + [key: string]: SearchBarQueryLanguage; +} = [ + AQL, + CustomQL, // Add the new custom query language +].reduce((accum, item) => { + if (accum[item.id]) { + throw new Error(`Query language with id: ${item.id} already registered.`); + } + return { + ...accum, + [item.id]: item, + }; +}, {}); +``` diff --git a/public/components/search-bar/index.tsx b/public/components/search-bar/index.tsx new file mode 100644 index 0000000000..5497ac9965 --- /dev/null +++ b/public/components/search-bar/index.tsx @@ -0,0 +1,186 @@ +import React, { useEffect, useState } from 'react'; +import { + EuiButtonEmpty, + EuiLink, + EuiPopover, + EuiSpacer, + EuiSelect, + EuiText, +} from '@elastic/eui'; +import { EuiSuggest } from '../eui-suggest'; +import { searchBarQueryLanguages } from './query-language'; + +type Props = { + defaultMode?: string; + modes: { id: string; [key: string]: any }[]; + onChange?: (params: any) => void; + onSearch: (params: any) => void; + input?: string; +}; + +export const SearchBar = ({ + defaultMode, + modes, + onChange, + onSearch, + ...rest +}: Props) => { + // Query language ID and configuration + const [queryLanguage, setQueryLanguage] = useState<{ + id: string; + configuration: any; + }>({ + id: defaultMode || modes[0].id, + configuration: + searchBarQueryLanguages[ + defaultMode || modes[0].id + ]?.getConfiguration?.() || {}, + }); + // Popover query language is open + const [isOpenPopoverQueryLanguage, setIsOpenPopoverQueryLanguage] = + useState(false); + // Input field + const [input, setInput] = useState(''); + // Query language output of run method + const [queryLanguageOutputRun, setQueryLanguageOutputRun] = useState({ + searchBarProps: { suggestions: [] }, + output: undefined, + }); + // Controls when the suggestion popover is open/close + const [isOpenSuggestionPopover, setIsOpenSuggestionPopover] = + useState(false); + // Reference to the input + const [inputRef, setInputRef] = useState(); + + // Handler when searching + const _onSearch = (output: any) => { + // TODO: fix when searching + inputRef && inputRef.blur(); + setIsOpenSuggestionPopover(false); + onSearch(output); + }; + + // Handler on change the input field text + const onChangeInput = (event: React.ChangeEvent) => + setInput(event.target.value); + + // Handler when pressing a key + const onKeyPressHandler = event => { + if (event.key === 'Enter') { + _onSearch(queryLanguageOutputRun.output); + } + }; + + useEffect(() => { + // React to external changes and set the internal input text. Use the `transformUnifiedQuery` of + // the query language in use + setInput( + searchBarQueryLanguages[queryLanguage.id]?.transformUnifiedQuery?.( + rest.input, + ), + ); + }, [rest.input]); + + useEffect(() => { + (async () => { + // Set the query language output + setQueryLanguageOutputRun( + await searchBarQueryLanguages[queryLanguage.id].run(input, { + onSearch: _onSearch, + setInput, + closeSuggestionPopover: () => setIsOpenSuggestionPopover(false), + openSuggestionPopover: () => setIsOpenSuggestionPopover(true), + queryLanguage: { + configuration: queryLanguage.configuration, + parameters: modes.find(({ id }) => id === queryLanguage.id), + }, + setQueryLanguageConfiguration: (configuration: any) => + setQueryLanguage(state => ({ + ...state, + configuration: + configuration?.(state.configuration) || configuration, + })), + }), + ); + })(); + }, [input, queryLanguage]); + + useEffect(() => { + onChange && onChange(queryLanguageOutputRun.output); + }, [queryLanguageOutputRun.output]); + + const onQueryLanguagePopoverSwitch = () => + setIsOpenPopoverQueryLanguage(state => !state); + + return ( + {}} + isPopoverOpen={ + queryLanguageOutputRun.searchBarProps.suggestions.length > 0 && + isOpenSuggestionPopover + } + onClosePopover={() => setIsOpenSuggestionPopover(false)} + onPopoverFocus={() => setIsOpenSuggestionPopover(true)} + placeholder={'Search'} + append={ + + {searchBarQueryLanguages[queryLanguage.id].label} + + } + isOpen={isOpenPopoverQueryLanguage} + closePopover={onQueryLanguagePopoverSwitch} + > + + {searchBarQueryLanguages[queryLanguage.id].description} + + {searchBarQueryLanguages[queryLanguage.id].documentationLink && ( + <> + +
+ + Documentation + +
+ + )} + {modes?.length && modes.length > 1 && ( + <> + + ({ + value: id, + text: searchBarQueryLanguages[id].label, + }))} + value={queryLanguage.id} + onChange={(event: React.ChangeEvent) => { + const queryLanguageID: string = event.target.value; + setQueryLanguage({ + id: queryLanguageID, + configuration: + searchBarQueryLanguages[ + queryLanguageID + ]?.getConfiguration?.() || {}, + }); + setInput(''); + }} + aria-label='query-language-selector' + /> + + )} +
+ } + {...queryLanguageOutputRun.searchBarProps} + /> + ); +}; diff --git a/public/components/search-bar/query-language/aql.test.tsx b/public/components/search-bar/query-language/aql.test.tsx new file mode 100644 index 0000000000..c2af8efd9d --- /dev/null +++ b/public/components/search-bar/query-language/aql.test.tsx @@ -0,0 +1,88 @@ +import { getSuggestions, tokenizer } from './aql'; + +describe('Query language - AQL', () => { + // Tokenize the input + it.each` + input | tokens + ${''} | ${[]} + ${'f'} | ${[{ type: 'field', value: 'f' }]} + ${'field'} | ${[{ type: 'field', value: 'field' }]} + ${'field.subfield'} | ${[{ type: 'field', value: 'field.subfield' }]} + ${'field='} | ${[{ type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }]} + ${'field!='} | ${[{ type: 'field', value: 'field' }, { type: 'operator_compare', value: '!=' }]} + ${'field>'} | ${[{ type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }]} + ${'field<'} | ${[{ type: 'field', value: 'field' }, { type: 'operator_compare', value: '<' }]} + ${'field~'} | ${[{ type: 'field', value: 'field' }, { type: 'operator_compare', value: '~' }]} + ${'field=value'} | ${[{ type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value' }]} + ${'field=value;'} | ${[{ type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value' }, { type: 'conjunction', value: ';' }]} + ${'field=value,'} | ${[{ type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value' }, { type: 'conjunction', value: ',' }]} + ${'field=value,field2'} | ${[{ type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value' }, { type: 'conjunction', value: ',' }, { type: 'field', value: 'field2' }]} + ${'field=value,field2.subfield'} | ${[{ type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value' }, { type: 'conjunction', value: ',' }, { type: 'field', value: 'field2.subfield' }]} + ${'('} | ${[{ type: 'operator_group', value: '(' }]} + ${'(f'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'f' }]} + ${'(field'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }]} + ${'(field.subfield'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field.subfield' }]} + ${'(field='} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }]} + ${'(field!='} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '!=' }]} + ${'(field>'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }]} + ${'(field<'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '<' }]} + ${'(field~'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '~' }]} + ${'(field=value'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value' }]} + ${'(field=value,field2=value2)'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value' }, { type: 'conjunction', value: ',' }, { type: 'field', value: 'field2' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value2' }, { type: 'operator_group', value: ')' }]} + ${'(field=value;field2=value2)'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value' }, { type: 'conjunction', value: ';' }, { type: 'field', value: 'field2' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value2' }, { type: 'operator_group', value: ')' }]} + ${'(field=value;field2=value2),field3=value3'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value' }, { type: 'conjunction', value: ';' }, { type: 'field', value: 'field2' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value2' }, { type: 'operator_group', value: ')' }, { type: 'conjunction', value: ',' }, { type: 'field', value: 'field3' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value3' }]} + `('Tokenize the input: $input', ({ input, tokens }) => { + expect(tokenizer({ input })).toEqual(tokens); + }); + + // Get suggestions + it.each` + input | suggestions + ${''} | ${[]} + ${'w'} | ${[]} + ${'f'} | ${[{ description: 'Field', label: 'field', type: 'field' }, { description: 'Field2', label: 'field2', type: 'field' }]} + ${'field'} | ${[{ description: 'Field2', label: 'field2', type: 'field' }, { description: 'equality', label: '=', type: 'operator_compare' }, { description: 'not equality', label: '!=', type: 'operator_compare' }, { description: 'bigger', label: '>', type: 'operator_compare' }, { description: 'smaller', label: '<', type: 'operator_compare' }, { description: 'like as', label: '~', type: 'operator_compare' }]} + ${'field='} | ${[{ label: 'value', type: 'value' }, { label: 'value2', type: 'value' }, { label: 'value3', type: 'value' }, { label: 'value4', type: 'value' }]} + ${'field=v'} | ${[{ description: 'Run the search query', label: 'Search', type: 'function_search' }, { description: 'Current value', label: 'v', type: 'value' }, { label: 'value', type: 'value' }, { label: 'value2', type: 'value' }, { label: 'value3', type: 'value' }, { label: 'value4', type: 'value' }, { description: 'and', label: ';', type: 'conjunction' }, { description: 'or', label: ',', type: 'conjunction' }, { description: 'close group', label: ')', type: 'operator_group' }]} + ${'field=value'} | ${[{ description: 'Run the search query', label: 'Search', type: 'function_search' }, { description: 'Current value', label: 'value', type: 'value' }, { label: 'value', type: 'value' }, { label: 'value2', type: 'value' }, { label: 'value3', type: 'value' }, { label: 'value4', type: 'value' }, { description: 'and', label: ';', type: 'conjunction' }, { description: 'or', label: ',', type: 'conjunction' }, { description: 'close group', label: ')', type: 'operator_group' }]} + ${'field=value;'} | ${[{ description: 'Field', label: 'field', type: 'field' }, { description: 'Field2', label: 'field2', type: 'field' }, { description: 'open group', label: '(', type: 'operator_group' }]} + ${'field=value;field2'} | ${[{ description: 'equality', label: '=', type: 'operator_compare' }, { description: 'not equality', label: '!=', type: 'operator_compare' }, { description: 'bigger', label: '>', type: 'operator_compare' }, { description: 'smaller', label: '<', type: 'operator_compare' }, { description: 'like as', label: '~', type: 'operator_compare' }]} + ${'field=value;field2='} | ${[{ label: '127.0.0.1', type: 'value' }, { label: '127.0.0.2', type: 'value' }, { label: '190.0.0.1', type: 'value' }, { label: '190.0.0.2', type: 'value' }]} + ${'field=value;field2=127'} | ${[{ description: 'Run the search query', label: 'Search', type: 'function_search' }, { description: 'Current value', label: '127', type: 'value' }, { label: '127.0.0.1', type: 'value' }, { label: '127.0.0.2', type: 'value' }, { description: 'and', label: ';', type: 'conjunction' }, { description: 'or', label: ',', type: 'conjunction' }, { description: 'close group', label: ')', type: 'operator_group' }]} + `('Get suggestion from the input: $input', async ({ input, suggestions }) => { + expect( + await getSuggestions(tokenizer({ input }), { + id: 'aql', + suggestions: { + field(currentValue) { + return [ + { label: 'field', description: 'Field' }, + { label: 'field2', description: 'Field2' }, + ].map(({ label, description }) => ({ + type: 'field', + label, + description, + })); + }, + value(currentValue = '', { previousField }) { + switch (previousField) { + case 'field': + return ['value', 'value2', 'value3', 'value4'] + .filter(value => value.startsWith(currentValue)) + .map(value => ({ type: 'value', label: value })); + break; + case 'field2': + return ['127.0.0.1', '127.0.0.2', '190.0.0.1', '190.0.0.2'] + .filter(value => value.startsWith(currentValue)) + .map(value => ({ type: 'value', label: value })); + break; + default: + return []; + break; + } + }, + }, + }), + ).toEqual(suggestions); + }); +}); diff --git a/public/components/search-bar/query-language/aql.tsx b/public/components/search-bar/query-language/aql.tsx new file mode 100644 index 0000000000..d96f31ead8 --- /dev/null +++ b/public/components/search-bar/query-language/aql.tsx @@ -0,0 +1,538 @@ +import React from 'react'; +import { EuiButtonEmpty, EuiPopover, EuiText, EuiCode } from '@elastic/eui'; +import { webDocumentationLink } from '../../../../common/services/web_documentation'; + +type ITokenType = + | 'field' + | 'operator_compare' + | 'operator_group' + | 'value' + | 'conjunction'; +type IToken = { type: ITokenType; value: string }; +type ITokens = IToken[]; + +/* API Query Language +https://documentation.wazuh.com/current/user-manual/api/queries.html + +Syntax schema: +??? +*/ + +// Language definition +const language = { + // Tokens + tokens: { + field: { + regex: /[\w.]/, + }, + // eslint-disable-next-line camelcase + operator_compare: { + literal: { + '=': 'equality', + '!=': 'not equality', + '>': 'bigger', + '<': 'smaller', + '~': 'like as', + }, + }, + conjunction: { + literal: { + ';': 'and', + ',': 'or', + }, + }, + // eslint-disable-next-line camelcase + operator_group: { + literal: { + '(': 'open group', + ')': 'close group', + }, + }, + }, +}; + +// Suggestion mapper by language token type +const suggestionMappingLanguageTokenType = { + field: { iconType: 'kqlField', color: 'tint4' }, + // eslint-disable-next-line camelcase + operator_compare: { iconType: 'kqlOperand', color: 'tint1' }, + value: { iconType: 'kqlValue', color: 'tint0' }, + conjunction: { iconType: 'kqlSelector', color: 'tint3' }, + // eslint-disable-next-line camelcase + operator_group: { iconType: 'tokenDenseVector', color: 'tint3' }, + // eslint-disable-next-line camelcase + function_search: { iconType: 'search', color: 'tint5' }, +}; + +type ITokenizerInput = { input: string; output?: ITokens }; + +/** + * Tokenize the input string. Returns an array with the tokens. + * @param param0 + * @returns + */ +export function tokenizer({ input, output = [] }: ITokenizerInput): ITokens { + if (!input) { + return output; + } + const character = input[0]; + + // When there is no tokens, the first expected values are: + if (!output.length) { + // A literal `(` + if (character === '(') { + output.push({ type: 'operator_group', value: '(' }); + } + + // Any character that matches the regex for the field + if (language.tokens.field.regex.test(character)) { + output.push({ type: 'field', value: character }); + } + } else { + // Get the last token + const lastToken = output[output.length - 1]; + + switch (lastToken.type) { + // Token: field + case 'field': { + if ( + Object.keys(language.tokens.operator_compare.literal) + .map(str => str[0]) + .includes(character) + ) { + // If the character is the first character of an operator_compare token, + // add a new token with the input character + output.push({ type: 'operator_compare', value: character }); + } else if ( + Object.keys(language.tokens.operator_compare.literal).includes( + character, + ) + ) { + // If the character matches with an operator_compare token, + // add a new token with the input character + output.push({ type: 'operator_compare', value: character }); + } else if (language.tokens.field.regex.test(character)) { + // If the character matches with a character of field, + // appends the character to the current field token + lastToken.value = lastToken.value + character; + } + break; + } + + // Token: operator_compare + case 'operator_compare': { + if ( + Object.keys(language.tokens.operator_compare.literal) + .map(str => str[lastToken.value.length]) + .includes(character) + ) { + // If the character is included in the operator_compare token, + // appends the character to the current operator_compare token + lastToken.value = lastToken.value + character; + } else { + // If the character is not a operator_compare token, + // add a new value token with the character + output.push({ type: 'value', value: character }); + } + break; + } + + // Token: value + case 'value': { + if ( + Object.keys(language.tokens.conjunction.literal).includes(character) + ) { + // If the character is a conjunction, add a new conjunction token with the character + output.push({ type: 'conjunction', value: character }); + } else if (character === ')') { + // If the character is the ")" literal, then add a new operator_group token + output.push({ type: 'operator_group', value: character }); + } else { + // Else appends the character to the current value token + lastToken.value = lastToken.value + character; + } + break; + } + + // Token: conjunction + case 'conjunction': { + if (character === '(') { + // If the character is the "(" literal, then add a new operator_group token + output.push({ type: 'operator_group', value: character }); + } else if (language.tokens.field.regex.test(character)) { + // If the character matches with a character of field, + // appends the character to the current field token + output.push({ type: 'field', value: character }); + } + break; + } + + // Token: operator_group + case 'operator_group': { + if (lastToken.value === '(') { + // If the character is the "(" literal + if (language.tokens.field.regex.test(character)) { + // If the character matches with a character of field, + // appends the character to the current field token + output.push({ type: 'field', value: character }); + } + } else if (lastToken.value === ')') { + if ( + Object.keys(language.tokens.conjunction.literal).includes(character) + ) { + // If the character is a conjunction, add a new conjunction token with the character + output.push({ type: 'conjunction', value: character }); + } + } + break; + } + + default: + } + } + + // Split the string from the second character + const substring = input.substring(1); + + // Call recursively + return tokenizer({ input: substring, output }, language); +} + +/** + * Check the + * @param tokens + * @returns + */ +function validate(tokens: ITokens): boolean { + // TODO: enhance the validation + return tokens.every( + ({ type }, index) => + type === ['field', 'operator_compare', 'value', 'conjunction'][index % 4], + ); +} + +type OptionSuggestionHandler = ( + currentValue: string | undefined, + { + previousField, + previousOperatorCompare, + }: { previousField: string; previousOperatorCompare: string }, +) => Promise<{ description?: string; label: string; type: string }[]>; + +type optionsQL = { + suggestions: { + field: OptionSuggestionHandler; + value: OptionSuggestionHandler; + }; +}; + +/** + * Get the last token by type + * @param tokens Tokens + * @param tokenType token type to search + * @returns + */ +function getLastTokenByType( + tokens: ITokens, + tokenType: ITokenType, +): IToken | undefined { + // Find the last token by type + // Reverse the tokens array and use the Array.protorype.find method + const shallowCopyArray = Array.from([...tokens]); + const shallowCopyArrayReversed = shallowCopyArray.reverse(); + const tokenFound = shallowCopyArrayReversed.find( + ({ type }) => type === tokenType, + ); + return tokenFound; +} + +/** + * Get the suggestions from the tokens + * @param tokens + * @param language + * @param options + * @returns + */ +export async function getSuggestions(tokens: ITokens, options: optionsQL) { + if (!tokens.length) { + return []; + } + + // Get last token + const lastToken = tokens[tokens.length - 1]; + + switch (lastToken.type) { + case 'field': + return [ + // fields that starts with the input but is not equals + ...(await options.suggestions.field()).filter( + ({ label }) => + label.startsWith(lastToken.value) && label !== lastToken.value, + ), + // operators if the input field is exact + ...((await options.suggestions.field()).some( + ({ label }) => label === lastToken.value, + ) + ? [ + ...Object.keys(language.tokens.operator_compare.literal).map( + operator => ({ + type: 'operator_compare', + label: operator, + description: + language.tokens.operator_compare.literal[operator], + }), + ), + ] + : []), + ]; + break; + case 'operator_compare': + return [ + ...Object.keys(language.tokens.operator_compare.literal) + .filter( + operator => + operator.startsWith(lastToken.value) && + operator !== lastToken.value, + ) + .map(operator => ({ + type: 'operator_compare', + label: operator, + description: language.tokens.operator_compare.literal[operator], + })), + ...(Object.keys(language.tokens.operator_compare.literal).some( + operator => operator === lastToken.value, + ) + ? [ + ...(await options.suggestions.value(undefined, { + previousField: getLastTokenByType(tokens, 'field')!.value, + previousOperatorCompare: getLastTokenByType( + tokens, + 'operator_compare', + )!.value, + })), + ] + : []), + ]; + break; + case 'value': + return [ + ...(lastToken.value + ? [ + { + type: 'function_search', + label: 'Search', + description: 'Run the search query', + }, + ] + : []), + { + type: 'value', + label: lastToken.value, + description: 'Current value', + }, + ...(await options.suggestions.value(lastToken.value, { + previousField: getLastTokenByType(tokens, 'field')!.value, + previousOperatorCompare: getLastTokenByType( + tokens, + 'operator_compare', + )!.value, + })), + ...Object.entries(language.tokens.conjunction.literal).map( + ([conjunction, description]) => ({ + type: 'conjunction', + label: conjunction, + description, + }), + ), + { + type: 'operator_group', + label: ')', + description: language.tokens.operator_group.literal[')'], + }, + ]; + break; + case 'conjunction': + return [ + ...Object.keys(language.tokens.conjunction.literal) + .filter( + conjunction => + conjunction.startsWith(lastToken.value) && + conjunction !== lastToken.value, + ) + .map(conjunction => ({ + type: 'conjunction', + label: conjunction, + description: language.tokens.conjunction.literal[conjunction], + })), + // fields if the input field is exact + ...(Object.keys(language.tokens.conjunction.literal).some( + conjunction => conjunction === lastToken.value, + ) + ? [ + ...(await options.suggestions.field()).map( + ({ label, description }) => ({ + type: 'field', + label, + description, + }), + ), + ] + : []), + { + type: 'operator_group', + label: '(', + description: language.tokens.operator_group.literal['('], + }, + ]; + break; + case 'operator_group': + if (lastToken.value === '(') { + return [ + // fields + ...(await options.suggestions.field()).map( + ({ label, description }) => ({ type: 'field', label, description }), + ), + ]; + } else if (lastToken.value === ')') { + return [ + // conjunction + ...Object.keys(language.tokens.conjunction.literal).map( + conjunction => ({ + type: 'conjunction', + label: conjunction, + description: language.tokens.conjunction.literal[conjunction], + }), + ), + ]; + } + break; + default: + return []; + break; + } + + return []; +} + +/** + * Transform the suggestion object to the expected object by EuiSuggestItem + * @param suggestions + * @param options + * @returns + */ +function transformSuggestionsToUI( + suggestions: { type: string; label: string; description?: string }[], + mapSuggestionByLanguageToken: any, +) { + return suggestions.map(({ type, ...rest }) => ({ + type: { ...mapSuggestionByLanguageToken[type] }, + ...rest, + })); +} + +/** + * Get the output from the input + * @param input + * @returns + */ +function getOutput(input: string, options: {implicitQuery?: string} = {}) { + return { + language: AQL.id, + query: `${options?.implicitQuery ?? ''}${input}`, + }; +}; + +export const AQL = { + id: 'aql', + label: 'AQL', + description: 'API Query Language (AQL) allows to do queries.', + documentationLink: webDocumentationLink('user-manual/api/queries.html'), + getConfiguration() { + return { + isOpenPopoverImplicitFilter: false, + }; + }, + async run(input, params) { + // Get the tokens from the input + const tokens: ITokens = tokenizer({ input }, language); + + return { + searchBarProps: { + // Props that will be used by the EuiSuggest component + // Suggestions + suggestions: transformSuggestionsToUI( + await getSuggestions(tokens, params.queryLanguage.parameters), + suggestionMappingLanguageTokenType, + ), + // Handler to manage when clicking in a suggestion item + onItemClick: item => { + // When the clicked item has the `search` iconType, run the `onSearch` function + if (item.type.iconType === 'search') { + // Execute the search action + params.onSearch(getOutput(input, params.queryLanguage.parameters)); + } else { + // When the clicked item has another iconType + const lastToken: IToken = tokens[tokens.length - 1]; + // if the clicked suggestion is of same type of last token + if ( + suggestionMappingLanguageTokenType[lastToken.type].iconType === + item.type.iconType + ) { + // replace the value of last token + lastToken.value = item.label; + } else { + // add a new token of the selected type and value + tokens.push({ + type: Object.entries(suggestionMappingLanguageTokenType).find( + ([, { iconType }]) => iconType === item.type.iconType, + )[0], + value: item.label, + }); + } + } + // Change the input + params.setInput(tokens.map(({ value }) => value).join('')); + }, + prepend: params.queryLanguage.parameters.implicitQuery ? ( + + params.setQueryLanguageConfiguration(state => ({ + ...state, + isOpenPopoverImplicitFilter: + !state.isOpenPopoverImplicitFilter, + })) + } + iconType='filter' + > + + {params.queryLanguage.parameters.implicitQuery} + + + } + isOpen={ + params.queryLanguage.configuration.isOpenPopoverImplicitFilter + } + closePopover={() => + params.setQueryLanguageConfiguration(state => ({ + ...state, + isOpenPopoverImplicitFilter: false, + })) + } + > + + Implicit query:{' '} + {params.queryLanguage.parameters.implicitQuery} + + This query is added to the input. + + ) : null, + }, + output: getOutput(input, params.queryLanguage.parameters), + }; + }, + transformUnifiedQuery(unifiedQuery) { + return unifiedQuery; + }, +}; diff --git a/public/components/search-bar/query-language/index.ts b/public/components/search-bar/query-language/index.ts new file mode 100644 index 0000000000..1895aae4fb --- /dev/null +++ b/public/components/search-bar/query-language/index.ts @@ -0,0 +1,26 @@ +import { AQL } from './aql'; +import { UIQL } from './uiql'; + +type SearchBarQueryLanguage = { + description: string; + documentationLink?: string; + id: string; + label: string; + getConfiguration?: () => any; + run: (input: string | undefined, params: any) => any; + transformUnifiedQuery: (unifiedQuery) => any; +}; + +// Register the query languages +export const searchBarQueryLanguages: { + [key: string]: SearchBarQueryLanguage; +} = [AQL, UIQL].reduce((accum, item) => { + if (accum[item.id]) { + throw new Error(`Query language with id: ${item.id} already registered.`); + } + return { + ...accum, + [item.id]: item, + }; + ['hola']; +}, {}); diff --git a/public/components/search-bar/query-language/uiql.tsx b/public/components/search-bar/query-language/uiql.tsx new file mode 100644 index 0000000000..97b1927f54 --- /dev/null +++ b/public/components/search-bar/query-language/uiql.tsx @@ -0,0 +1,71 @@ +import React from 'react'; +import { EuiButtonEmpty, EuiPopover, EuiText, EuiCode } from '@elastic/eui'; + +/* UI Query language +https://documentation.wazuh.com/current/user-manual/api/queries.html + +// Example of another query language definition +*/ + +/** + * Get the output from the input + * @param input + * @returns + */ +function getOutput(input: string, options: {implicitQuery?: string} = {}) { + return { + language: UIQL.id, + query: `${options?.implicitQuery ?? ''}${input}`, + }; +}; + +export const UIQL = { + id: 'uiql', + label: 'UIQL', + description: 'UIQL allows to do queries.', + documentationLink: '', + getConfiguration() { + return { + anotherProp: false, + }; + }, + async run(input, params) { + // Get the tokens from the input + return { + searchBarProps: { + // Props that will be used by the EuiSuggest component + // Suggestions + suggestions: [], + // Handler to manage when clicking in a suggestion item + prepend: params.queryLanguage.parameters.implicitQuery ? ( + + params.setQueryLanguageConfiguration(state => ({ + ...state, + anotherProp: !state.anotherProp, + })) + } + iconType='filter' + > + } + isOpen={params.queryLanguage.configuration.anotherProp} + closePopover={() => + params.setQueryLanguageConfiguration(state => ({ + ...state, + anotherProp: false, + })) + } + > + + Implicit UIQL query:{' '} + {params.queryLanguage.parameters.implicitQuery} + + + ) : null, + }, + output: getOutput(input, params.queryLanguage.parameters), + }; + }, +}; diff --git a/public/controllers/agent/components/agents-table.js b/public/controllers/agent/components/agents-table.js index 55ce4d48d9..ddc7e68195 100644 --- a/public/controllers/agent/components/agents-table.js +++ b/public/controllers/agent/components/agents-table.js @@ -41,6 +41,7 @@ import { UI_ERROR_SEVERITIES } from '../../../react-services/error-orchestrator/ import { getErrorOrchestrator } from '../../../react-services/common-services'; import { AgentStatus } from '../../../components/agents/agent_status'; import { AgentSynced } from '../../../components/agents/agent-synced'; +import { SearchBar } from '../../../components/search-bar'; export const AgentsTable = withErrorBoundary( class AgentsTable extends Component { @@ -611,12 +612,95 @@ export const AgentsTable = withErrorBoundary( noDeleteFiltersOnUpdateSuggests filters={this.state.filters} suggestions={this.suggestions} - onFiltersChange={(filters) => this.setState({ filters, pageIndex: 0 })} - placeholder="Filter or search agent" + onFiltersChange={filters => + this.setState({ filters, pageIndex: 0 }) + } + placeholder='Filter or search agent' + /> + {/** Example implementation */} + ({ type: 'field', ...field })); + }, + value: async (currentValue, { previousField }) => { + switch (previousField) { + case 'status': + try { + const results = await this.props.wzReq( + 'GET', + `/agents/stats/distinct`, + { + params: { + fields: 'status', + limit: 30, + }, + }, + ); + + return results.data.data.affected_items.map( + ({ status }) => ({ + type: 'value', + label: status, + }), + ); + } catch (error) { + console.log({ error }); + return []; + } + break; + case 'ip': + try { + const results = await this.props.wzReq( + 'GET', + `/agents/stats/distinct`, + { + params: { + fields: 'ip', + limit: 30, + }, + }, + ); + + console.log({ results }); + return results.data.data.affected_items.map( + ({ ip }) => ({ type: 'value', label: ip }), + ); + } catch (error) { + console.log({ error }); + return []; + } + break; + default: + return []; + break; + } + }, + }, + }, + { + id: 'uiql', + implicitQuery: 'id!=000;', + }, + ]} + onChange={console.log} + onSearch={console.log} /> - this.reloadAgents()}> + this.reloadAgents()} + > Refresh From eff5d2b3d8181bd12c05d919ac73a3ea4294f2a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Wed, 1 Mar 2023 10:00:44 +0100 Subject: [PATCH 02/76] feat(search-bar): change the AQL implemenation to use the regular expression used in the Wazuh manager API - Change the implementation of AQL query language to use the regular expression decomposition defined in the Wazuh manager API - Adapt the tests for the tokenizer and getting the suggestions - Enchance documentation of search bar - Add documentation of AQL query language - Add more fields and values for the use example in Agents section - Add description to the query language select input --- public/components/search-bar/README.md | 131 ++++++- public/components/search-bar/index.tsx | 43 +- .../search-bar/query-language/aql.md | 178 +++++++++ .../search-bar/query-language/aql.test.tsx | 61 ++- .../search-bar/query-language/aql.tsx | 367 +++++++++--------- .../agent/components/agents-table.js | 168 +++++--- 6 files changed, 669 insertions(+), 279 deletions(-) create mode 100644 public/components/search-bar/query-language/aql.md diff --git a/public/components/search-bar/README.md b/public/components/search-bar/README.md index 764b1848ba..046157a9a0 100644 --- a/public/components/search-bar/README.md +++ b/public/components/search-bar/README.md @@ -13,7 +13,7 @@ abilities are restricted by this one. - Supports multiple query languages. - Switch the selected query language. -- Self-contained query language implementation and ability to interact with the search bar component +- Self-contained query language implementation and ability to interact with the search bar component. - React to external changes to set the new input. This enables to change the input from external components. # Usage @@ -28,6 +28,135 @@ Basic usage: { id: 'aql', // specific query language parameters + implicitQuery: 'id!=000;', + suggestions: { + field(currentValue) { + return [ + { label: 'configSum', description: 'Config sum' }, + { label: 'dateAdd', description: 'Date add' }, + { label: 'id', description: 'ID' }, + { label: 'ip', description: 'IP address' }, + { label: 'group', description: 'Group' }, + { label: 'group_config_status', description: 'Synced configuration status' }, + { label: 'lastKeepAline', description: 'Date add' }, + { label: 'manager', description: 'Manager' }, + { label: 'mergedSum', description: 'Merged sum' }, + { label: 'name', description: 'Agent name' }, + { label: 'node_name', description: 'Node name' }, + { label: 'os.platform', description: 'Operating system platform' }, + { label: 'status', description: 'Status' }, + { label: 'version', description: 'Version' }, + ] + .map(field => ({ type: 'field', ...field })); + }, + value: async (currentValue, { previousField }) => { + switch (previousField) { + case 'configSum': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + case 'dateAdd': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + case 'id': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + case 'ip': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + case 'group': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + case 'group_config_status': + return [AGENT_SYNCED_STATUS.SYNCED, AGENT_SYNCED_STATUS.NOT_SYNCED].map( + (status) => ({ + type: 'value', + label: status, + }), + ); + break; + case 'lastKeepAline': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + case 'manager': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + case 'mergedSum': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + case 'name': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + case 'node_name': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + case 'os.platform': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + case 'status': + return UI_ORDER_AGENT_STATUS.map( + (status) => ({ + type: 'value', + label: status, + }), + ); + break; + case 'version': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + default: + return []; + break; + } + }, + } }, ]} > diff --git a/public/components/search-bar/index.tsx b/public/components/search-bar/index.tsx index 5497ac9965..01a3764e14 100644 --- a/public/components/search-bar/index.tsx +++ b/public/components/search-bar/index.tsx @@ -1,6 +1,7 @@ import React, { useEffect, useState } from 'react'; import { EuiButtonEmpty, + EuiFormRow, EuiLink, EuiPopover, EuiSpacer, @@ -156,26 +157,28 @@ export const SearchBar = ({ {modes?.length && modes.length > 1 && ( <> - ({ - value: id, - text: searchBarQueryLanguages[id].label, - }))} - value={queryLanguage.id} - onChange={(event: React.ChangeEvent) => { - const queryLanguageID: string = event.target.value; - setQueryLanguage({ - id: queryLanguageID, - configuration: - searchBarQueryLanguages[ - queryLanguageID - ]?.getConfiguration?.() || {}, - }); - setInput(''); - }} - aria-label='query-language-selector' - /> + + ({ + value: id, + text: searchBarQueryLanguages[id].label, + }))} + value={queryLanguage.id} + onChange={(event: React.ChangeEvent) => { + const queryLanguageID: string = event.target.value; + setQueryLanguage({ + id: queryLanguageID, + configuration: + searchBarQueryLanguages[ + queryLanguageID + ]?.getConfiguration?.() || {}, + }); + setInput(''); + }} + aria-label='query-language-selector' + /> + )} diff --git a/public/components/search-bar/query-language/aql.md b/public/components/search-bar/query-language/aql.md new file mode 100644 index 0000000000..637e8c9147 --- /dev/null +++ b/public/components/search-bar/query-language/aql.md @@ -0,0 +1,178 @@ +# Query Language - AQL + +AQL (API Query Language) is a query language based in the `q` query parameters of the Wazuh API +endpoints. + +Documentation: https://wazuh.com/./user-manual/api/queries.html + +The implementation is adapted to work with the search bar component defined +`public/components/search-bar/index.tsx`. + +## Features +- Suggestions for `fields` (configurable), `operators` and `values` (configurable) +- Support implicit query + +## Options + +- `implicitQuery`: add an implicit query that is added to the user input. This can't be changed by +the user. If this is defined, will be displayed as a prepend of the search bar. + +```ts +// language options +implicitQuery: 'id!=000;' // ID is not 000 and +``` + +- `suggestions`: define the suggestion handlers. This is required. + + - `field`: method that returns the suggestions for the fields + + ```ts + // language options + field(currentValue) { + return [ + { label: 'configSum', description: 'Config sum' }, + { label: 'dateAdd', description: 'Date add' }, + { label: 'id', description: 'ID' }, + { label: 'ip', description: 'IP address' }, + { label: 'group', description: 'Group' }, + { label: 'group_config_status', description: 'Synced configuration status' }, + { label: 'lastKeepAline', description: 'Date add' }, + { label: 'manager', description: 'Manager' }, + { label: 'mergedSum', description: 'Merged sum' }, + { label: 'name', description: 'Agent name' }, + { label: 'node_name', description: 'Node name' }, + { label: 'os.platform', description: 'Operating system platform' }, + { label: 'status', description: 'Status' }, + { label: 'version', description: 'Version' }, + ] + .map(field => ({ type: 'field', ...field })); + } + ``` + + - `value`: method that returns the suggestion for the values + ```ts + // language options + value: async (currentValue, { previousField }) => { + switch (previousField) { + case 'configSum': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + case 'dateAdd': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + case 'id': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + case 'ip': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + case 'group': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + case 'group_config_status': + return [AGENT_SYNCED_STATUS.SYNCED, AGENT_SYNCED_STATUS.NOT_SYNCED].map( + (status) => ({ + type: 'value', + label: status, + }), + ); + break; + case 'lastKeepAline': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + case 'manager': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + case 'mergedSum': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + case 'name': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + case 'node_name': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + case 'os.platform': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + case 'status': + return UI_ORDER_AGENT_STATUS.map( + (status) => ({ + type: 'value', + label: status, + }), + ); + break; + case 'version': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + default: + return []; + break; + } + } + ``` + +## How to get the suggestions + +```mermaid +graph TD; + user_input[User input]-->tokenize; + subgraph tokenize + tokenize_regex + end + + tokenize-->suggestions[Get suggestions]; + subgraph suggestions[Get suggestions]; + get_last_token_with_value-->get_suggestions[Get suggestions] + end + suggestions-->EuiSuggestItem +``` \ No newline at end of file diff --git a/public/components/search-bar/query-language/aql.test.tsx b/public/components/search-bar/query-language/aql.test.tsx index c2af8efd9d..37c34904bc 100644 --- a/public/components/search-bar/query-language/aql.test.tsx +++ b/public/components/search-bar/query-language/aql.test.tsx @@ -1,38 +1,35 @@ -import { getSuggestions, tokenizer } from './aql'; +import { getSuggestionsAPI, tokenizerAPI, validate } from './aql'; describe('Query language - AQL', () => { // Tokenize the input it.each` - input | tokens - ${''} | ${[]} - ${'f'} | ${[{ type: 'field', value: 'f' }]} - ${'field'} | ${[{ type: 'field', value: 'field' }]} - ${'field.subfield'} | ${[{ type: 'field', value: 'field.subfield' }]} - ${'field='} | ${[{ type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }]} - ${'field!='} | ${[{ type: 'field', value: 'field' }, { type: 'operator_compare', value: '!=' }]} - ${'field>'} | ${[{ type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }]} - ${'field<'} | ${[{ type: 'field', value: 'field' }, { type: 'operator_compare', value: '<' }]} - ${'field~'} | ${[{ type: 'field', value: 'field' }, { type: 'operator_compare', value: '~' }]} - ${'field=value'} | ${[{ type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value' }]} - ${'field=value;'} | ${[{ type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value' }, { type: 'conjunction', value: ';' }]} - ${'field=value,'} | ${[{ type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value' }, { type: 'conjunction', value: ',' }]} - ${'field=value,field2'} | ${[{ type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value' }, { type: 'conjunction', value: ',' }, { type: 'field', value: 'field2' }]} - ${'field=value,field2.subfield'} | ${[{ type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value' }, { type: 'conjunction', value: ',' }, { type: 'field', value: 'field2.subfield' }]} - ${'('} | ${[{ type: 'operator_group', value: '(' }]} - ${'(f'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'f' }]} - ${'(field'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }]} - ${'(field.subfield'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field.subfield' }]} - ${'(field='} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }]} - ${'(field!='} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '!=' }]} - ${'(field>'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }]} - ${'(field<'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '<' }]} - ${'(field~'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '~' }]} - ${'(field=value'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value' }]} - ${'(field=value,field2=value2)'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value' }, { type: 'conjunction', value: ',' }, { type: 'field', value: 'field2' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value2' }, { type: 'operator_group', value: ')' }]} - ${'(field=value;field2=value2)'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value' }, { type: 'conjunction', value: ';' }, { type: 'field', value: 'field2' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value2' }, { type: 'operator_group', value: ')' }]} - ${'(field=value;field2=value2),field3=value3'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value' }, { type: 'conjunction', value: ';' }, { type: 'field', value: 'field2' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value2' }, { type: 'operator_group', value: ')' }, { type: 'conjunction', value: ',' }, { type: 'field', value: 'field3' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value3' }]} - `('Tokenize the input: $input', ({ input, tokens }) => { - expect(tokenizer({ input })).toEqual(tokens); + input | tokens + ${''} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'f'} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'f' }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'field'} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'field='} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'field=value'} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'field=value with spaces'} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value with spaces' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'field=value with spaces<'} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value with spaces<' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'field=value with (parenthesis)'} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value with (parenthesis)' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'field=value;'} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'field=value;field2'} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: 'field2' }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'field=value;field2!='} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: 'field2' }, { type: 'operator_compare', value: '!=' }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'field=value;field2!=value2'} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: 'field2' }, { type: 'operator_compare', value: '!=' }, { type: 'value', value: 'value2' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'('} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'(field'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'(field>'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'(field>2'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: '2' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'(field>2)'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: '2' }, { type: 'operator_group', value: ')' }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'(field>2);'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: '2' }, { type: 'operator_group', value: ')' }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'(field>2;'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: '2' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'(field>2;field2'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: '2' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: 'field2' }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'(field>2;field2='} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: '2' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: 'field2' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'(field>2;field2=value2'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: '2' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: 'field2' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value2' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'(field>2;field2=value2)'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: '2' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: 'field2' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value2' }, { type: 'operator_group', value: ')' }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'(field>2;field2=custom value())'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: '2' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: 'field2' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'custom value()' }, { type: 'operator_group', value: ')' }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + `(`Tokenizer API input $input`, ({input, tokens}) => { + expect(tokenizerAPI(input)).toEqual(tokens); }); // Get suggestions @@ -51,7 +48,7 @@ describe('Query language - AQL', () => { ${'field=value;field2=127'} | ${[{ description: 'Run the search query', label: 'Search', type: 'function_search' }, { description: 'Current value', label: '127', type: 'value' }, { label: '127.0.0.1', type: 'value' }, { label: '127.0.0.2', type: 'value' }, { description: 'and', label: ';', type: 'conjunction' }, { description: 'or', label: ',', type: 'conjunction' }, { description: 'close group', label: ')', type: 'operator_group' }]} `('Get suggestion from the input: $input', async ({ input, suggestions }) => { expect( - await getSuggestions(tokenizer({ input }), { + await getSuggestionsAPI(tokenizerAPI(input), { id: 'aql', suggestions: { field(currentValue) { diff --git a/public/components/search-bar/query-language/aql.tsx b/public/components/search-bar/query-language/aql.tsx index d96f31ead8..9569b807d0 100644 --- a/public/components/search-bar/query-language/aql.tsx +++ b/public/components/search-bar/query-language/aql.tsx @@ -12,10 +12,18 @@ type IToken = { type: ITokenType; value: string }; type ITokens = IToken[]; /* API Query Language +Define the API Query Language to use in the search bar. +It is based in the language used by the q query parameter. https://documentation.wazuh.com/current/user-manual/api/queries.html -Syntax schema: +Use the regular expression of API with some modifications to allow the decomposition of +input in entities that doesn't compose a valid query. It allows get not-completed queries. + +API schema: ??? + +Implemented schema: +?????? */ // Language definition @@ -64,151 +72,127 @@ const suggestionMappingLanguageTokenType = { function_search: { iconType: 'search', color: 'tint5' }, }; -type ITokenizerInput = { input: string; output?: ITokens }; /** * Tokenize the input string. Returns an array with the tokens. - * @param param0 + * @param input * @returns */ -export function tokenizer({ input, output = [] }: ITokenizerInput): ITokens { - if (!input) { - return output; - } - const character = input[0]; - - // When there is no tokens, the first expected values are: - if (!output.length) { - // A literal `(` - if (character === '(') { - output.push({ type: 'operator_group', value: '(' }); - } - - // Any character that matches the regex for the field - if (language.tokens.field.regex.test(character)) { - output.push({ type: 'field', value: character }); - } - } else { - // Get the last token - const lastToken = output[output.length - 1]; - - switch (lastToken.type) { - // Token: field - case 'field': { - if ( - Object.keys(language.tokens.operator_compare.literal) - .map(str => str[0]) - .includes(character) - ) { - // If the character is the first character of an operator_compare token, - // add a new token with the input character - output.push({ type: 'operator_compare', value: character }); - } else if ( - Object.keys(language.tokens.operator_compare.literal).includes( - character, - ) - ) { - // If the character matches with an operator_compare token, - // add a new token with the input character - output.push({ type: 'operator_compare', value: character }); - } else if (language.tokens.field.regex.test(character)) { - // If the character matches with a character of field, - // appends the character to the current field token - lastToken.value = lastToken.value + character; - } - break; - } - - // Token: operator_compare - case 'operator_compare': { - if ( - Object.keys(language.tokens.operator_compare.literal) - .map(str => str[lastToken.value.length]) - .includes(character) - ) { - // If the character is included in the operator_compare token, - // appends the character to the current operator_compare token - lastToken.value = lastToken.value + character; - } else { - // If the character is not a operator_compare token, - // add a new value token with the character - output.push({ type: 'value', value: character }); - } - break; - } - - // Token: value - case 'value': { - if ( - Object.keys(language.tokens.conjunction.literal).includes(character) - ) { - // If the character is a conjunction, add a new conjunction token with the character - output.push({ type: 'conjunction', value: character }); - } else if (character === ')') { - // If the character is the ")" literal, then add a new operator_group token - output.push({ type: 'operator_group', value: character }); - } else { - // Else appends the character to the current value token - lastToken.value = lastToken.value + character; - } - break; - } +export function tokenizerAPI(input: string): ITokens{ + // API regular expression + // https://github.com/wazuh/wazuh/blob/v4.4.0-rc1/framework/wazuh/core/utils.py#L1242-L1257 + // self.query_regex = re.compile( + // # A ( character. + // r"(\()?" + + // # Field name: name of the field to look on DB. + // r"([\w.]+)" + + // # Operator: looks for '=', '!=', '<', '>' or '~'. + // rf"([{''.join(self.query_operators.keys())}]{{1,2}})" + + // # Value: A string. + // r"((?:(?:\((?:\[[\[\]\w _\-.,:?\\/'\"=@%<>{}]*]|[\[\]\w _\-.:?\\/'\"=@%<>{}]*)\))*" + // r"(?:\[[\[\]\w _\-.,:?\\/'\"=@%<>{}]*]|[\[\]\w _\-.:?\\/'\"=@%<>{}]+)" + // r"(?:\((?:\[[\[\]\w _\-.,:?\\/'\"=@%<>{}]*]|[\[\]\w _\-.:?\\/'\"=@%<>{}]*)\))*)+)" + + // # A ) character. + // r"(\))?" + + // # Separator: looks for ';', ',' or nothing. + // rf"([{''.join(self.query_separators.keys())}])?" + // ) - // Token: conjunction - case 'conjunction': { - if (character === '(') { - // If the character is the "(" literal, then add a new operator_group token - output.push({ type: 'operator_group', value: character }); - } else if (language.tokens.field.regex.test(character)) { - // If the character matches with a character of field, - // appends the character to the current field token - output.push({ type: 'field', value: character }); - } - break; - } - - // Token: operator_group - case 'operator_group': { - if (lastToken.value === '(') { - // If the character is the "(" literal - if (language.tokens.field.regex.test(character)) { - // If the character matches with a character of field, - // appends the character to the current field token - output.push({ type: 'field', value: character }); - } - } else if (lastToken.value === ')') { - if ( - Object.keys(language.tokens.conjunction.literal).includes(character) - ) { - // If the character is a conjunction, add a new conjunction token with the character - output.push({ type: 'conjunction', value: character }); - } - } - break; - } - - default: - } - } - - // Split the string from the second character - const substring = input.substring(1); + const re = new RegExp( + // The following regular expression is based in API one but was modified to use named groups + // and added the optional operator to allow matching the entities when the query is not + // completed. This helps to tokenize the query and manage when the input is not completed. + // A ( character. + '(?\\()?' + + // Field name: name of the field to look on DB. + '(?[\\w.]+)?' + // Added a optional find + // Operator: looks for '=', '!=', '<', '>' or '~'. + // This seems to be a bug because is not searching the literal valid operators. + // I guess the operator is validated after the regular expression matches + `(?[${Object.keys(language.tokens.operator_compare.literal)}]{1,2})?` + // Added a optional find + // Value: A string. + '(?(?:(?:\\((?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|[\\[\\]\\w _\\-.:?\\/\'"=@%<>{}]*)\\))*' + + '(?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|[\\[\\]\\w _\\-.:?\\\\/\'"=@%<>{}]+)' + + '(?:\\((?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|[\\[\\]\\w _\\-.:?\\\\/\'"=@%<>{}]*)\\))*)+)?' + // Added a optional find + // A ) character. + '(?\\))?' + + `(?[${Object.keys(language.tokens.conjunction.literal)}])?`, + 'g' + ); - // Call recursively - return tokenizer({ input: substring, output }, language); -} + return [ + ...input.matchAll(re)] + .map( + ({groups}) => Object.entries(groups) + .map(([key, value]) => ({ + type: key.startsWith('operator_group') ? 'operator_group' : key, + value}) + ) + ).flat(); +}; /** - * Check the + * Check if the input is valid * @param tokens * @returns */ -function validate(tokens: ITokens): boolean { +export function validate(input: string, options): boolean { // TODO: enhance the validation - return tokens.every( - ({ type }, index) => - type === ['field', 'operator_compare', 'value', 'conjunction'][index % 4], + + // API regular expression + // self.query_regex = re.compile( + // # A ( character. + // r"(\()?" + + // # Field name: name of the field to look on DB. + // r"([\w.]+)" + + // # Operator: looks for '=', '!=', '<', '>' or '~'. + // rf"([{''.join(self.query_operators.keys())}]{{1,2}})" + + // # Value: A string. + // r"((?:(?:\((?:\[[\[\]\w _\-.,:?\\/'\"=@%<>{}]*]|[\[\]\w _\-.:?\\/'\"=@%<>{}]*)\))*" + // r"(?:\[[\[\]\w _\-.,:?\\/'\"=@%<>{}]*]|[\[\]\w _\-.:?\\/'\"=@%<>{}]+)" + // r"(?:\((?:\[[\[\]\w _\-.,:?\\/'\"=@%<>{}]*]|[\[\]\w _\-.:?\\/'\"=@%<>{}]*)\))*)+)" + + // # A ) character. + // r"(\))?" + + // # Separator: looks for ';', ',' or nothing. + // rf"([{''.join(self.query_separators.keys())}])?" + // ) + + const re = new RegExp( + // A ( character. + '(\\()?' + + // Field name: name of the field to look on DB. + '([\\w.]+)?' + + // Operator: looks for '=', '!=', '<', '>' or '~'. + // This seems to be a bug because is not searching the literal valid operators. + // I guess the operator is validated after the regular expression matches + `([${Object.keys(language.tokens.operator_compare.literal)}]{1,2})?` + + // Value: A string. + '((?:(?:\\((?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|[\\[\\]\\w _\\-.:?\\/\'"=@%<>{}]*)\\))*' + + '(?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|[\\[\\]\\w _\\-.:?\\\\/\'"=@%<>{}]+)' + + '(?:\\((?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|[\\[\\]\\w _\\-.:?\\\\/\'"=@%<>{}]*)\\))*)+)?' + + // '([\\w]+)'+ + // A ) character. + '(\\))?' + + `([${Object.keys(language.tokens.conjunction.literal)}])?`, + 'g' ); + + [...input.matchAll(re)].reduce((accum, [_, operator_group_open, field, operator, value, operator_group_close, conjunction ]) => { + if(!accum){ + return accum; + }; + + return [operator_group_open, field, operator, value, operator_group_close, conjunction] + }, true); + + const errors = []; + + for (let [_, operator_group_open, field, operator, value, operator_group_close, conjunction ] in input.matchAll(re)) { + if(!options.fields.includes(field)){ + errors.push(`Field ${field} is not valid.`) + }; + } + return errors.length === 0; } type OptionSuggestionHandler = ( @@ -227,12 +211,30 @@ type optionsQL = { }; /** - * Get the last token by type + * Get the last token with value * @param tokens Tokens * @param tokenType token type to search * @returns */ -function getLastTokenByType( +function getLastTokenWithValue( + tokens: ITokens +): IToken | undefined { + // Reverse the tokens array and use the Array.protorype.find method + const shallowCopyArray = Array.from([...tokens]); + const shallowCopyArrayReversed = shallowCopyArray.reverse(); + const tokenFound = shallowCopyArrayReversed.find( + ({ value }) => value, + ); + return tokenFound; +} + +/** + * Get the last token with value by type + * @param tokens Tokens + * @param tokenType token type to search + * @returns + */ +function getLastTokenWithValueByType( tokens: ITokens, tokenType: ITokenType, ): IToken | undefined { @@ -241,7 +243,7 @@ function getLastTokenByType( const shallowCopyArray = Array.from([...tokens]); const shallowCopyArrayReversed = shallowCopyArray.reverse(); const tokenFound = shallowCopyArrayReversed.find( - ({ type }) => type === tokenType, + ({ type, value }) => type === tokenType && value, ); return tokenFound; } @@ -253,13 +255,18 @@ function getLastTokenByType( * @param options * @returns */ -export async function getSuggestions(tokens: ITokens, options: optionsQL) { +export async function getSuggestionsAPI(tokens: ITokens, options: optionsQL) { if (!tokens.length) { return []; } // Get last token - const lastToken = tokens[tokens.length - 1]; + const lastToken = getLastTokenWithValue(tokens); + + // If it can't get a token with value, then no return suggestions + if(!lastToken?.type){ + return []; + }; switch (lastToken.type) { case 'field': @@ -274,15 +281,15 @@ export async function getSuggestions(tokens: ITokens, options: optionsQL) { ({ label }) => label === lastToken.value, ) ? [ - ...Object.keys(language.tokens.operator_compare.literal).map( - operator => ({ - type: 'operator_compare', - label: operator, - description: - language.tokens.operator_compare.literal[operator], - }), - ), - ] + ...Object.keys(language.tokens.operator_compare.literal).map( + operator => ({ + type: 'operator_compare', + label: operator, + description: + language.tokens.operator_compare.literal[operator], + }), + ), + ] : []), ]; break; @@ -303,14 +310,14 @@ export async function getSuggestions(tokens: ITokens, options: optionsQL) { operator => operator === lastToken.value, ) ? [ - ...(await options.suggestions.value(undefined, { - previousField: getLastTokenByType(tokens, 'field')!.value, - previousOperatorCompare: getLastTokenByType( - tokens, - 'operator_compare', - )!.value, - })), - ] + ...(await options.suggestions.value(undefined, { + previousField: getLastTokenWithValueByType(tokens, 'field')!.value, + previousOperatorCompare: getLastTokenWithValueByType( + tokens, + 'operator_compare', + )!.value, + })), + ] : []), ]; break; @@ -318,27 +325,22 @@ export async function getSuggestions(tokens: ITokens, options: optionsQL) { return [ ...(lastToken.value ? [ - { - type: 'function_search', - label: 'Search', - description: 'Run the search query', - }, - ] + { + type: 'function_search', + label: 'Search', + description: 'Run the search query', + }, + ] : []), - { - type: 'value', - label: lastToken.value, - description: 'Current value', - }, ...(await options.suggestions.value(lastToken.value, { - previousField: getLastTokenByType(tokens, 'field')!.value, - previousOperatorCompare: getLastTokenByType( + previousField: getLastTokenWithValueByType(tokens, 'field')!.value, + previousOperatorCompare: getLastTokenWithValueByType( tokens, 'operator_compare', )!.value, })), ...Object.entries(language.tokens.conjunction.literal).map( - ([conjunction, description]) => ({ + ([ conjunction, description]) => ({ type: 'conjunction', label: conjunction, description, @@ -369,14 +371,14 @@ export async function getSuggestions(tokens: ITokens, options: optionsQL) { conjunction => conjunction === lastToken.value, ) ? [ - ...(await options.suggestions.field()).map( - ({ label, description }) => ({ - type: 'field', - label, - description, - }), - ), - ] + ...(await options.suggestions.field()).map( + ({ label, description }) => ({ + type: 'field', + label, + description, + }), + ), + ] : []), { type: 'operator_group', @@ -454,14 +456,14 @@ export const AQL = { }, async run(input, params) { // Get the tokens from the input - const tokens: ITokens = tokenizer({ input }, language); + const tokens: ITokens = tokenizerAPI(input); return { searchBarProps: { // Props that will be used by the EuiSuggest component // Suggestions suggestions: transformSuggestionsToUI( - await getSuggestions(tokens, params.queryLanguage.parameters), + await getSuggestionsAPI(tokens, params.queryLanguage.parameters), suggestionMappingLanguageTokenType, ), // Handler to manage when clicking in a suggestion item @@ -472,7 +474,7 @@ export const AQL = { params.onSearch(getOutput(input, params.queryLanguage.parameters)); } else { // When the clicked item has another iconType - const lastToken: IToken = tokens[tokens.length - 1]; + const lastToken: IToken = getLastTokenWithValue(tokens); // if the clicked suggestion is of same type of last token if ( suggestionMappingLanguageTokenType[lastToken.type].iconType === @@ -491,7 +493,12 @@ export const AQL = { } } // Change the input - params.setInput(tokens.map(({ value }) => value).join('')); + params.setInput(tokens + .filter(value => value) // Ensure the input is rebuilt using tokens with value. + // The input tokenization can contain tokens with no value due to the used + // regular expression. + .map(({ value }) => value) + .join('')); }, prepend: params.queryLanguage.parameters.implicitQuery ? ( ({ type: 'field', ...field })); + { label: 'group', description: 'Group' }, + { label: 'group_config_status', description: 'Synced configuration status' }, + { label: 'lastKeepAline', description: 'Date add' }, + { label: 'manager', description: 'Manager' }, + { label: 'mergedSum', description: 'Merged sum' }, + { label: 'name', description: 'Agent name' }, + { label: 'node_name', description: 'Node name' }, + { label: 'os.platform', description: 'Operating system platform' }, + { label: 'status', description: 'Status' }, + { label: 'version', description: 'Version' }, + ] + .map(field => ({ type: 'field', ...field })); }, value: async (currentValue, { previousField }) => { switch (previousField) { - case 'status': - try { - const results = await this.props.wzReq( - 'GET', - `/agents/stats/distinct`, - { - params: { - fields: 'status', - limit: 30, - }, - }, - ); - - return results.data.data.affected_items.map( - ({ status }) => ({ - type: 'value', - label: status, - }), - ); - } catch (error) { - console.log({ error }); - return []; - } + case 'configSum': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + case 'dateAdd': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + case 'id': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); break; case 'ip': - try { - const results = await this.props.wzReq( - 'GET', - `/agents/stats/distinct`, - { - params: { - fields: 'ip', - limit: 30, - }, - }, - ); - - console.log({ results }); - return results.data.data.affected_items.map( - ({ ip }) => ({ type: 'value', label: ip }), - ); - } catch (error) { - console.log({ error }); - return []; - } + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + case 'group': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + case 'group_config_status': + return [AGENT_SYNCED_STATUS.SYNCED, AGENT_SYNCED_STATUS.NOT_SYNCED].map( + (status) => ({ + type: 'value', + label: status, + }), + ); + break; + case 'lastKeepAline': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + case 'manager': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + case 'mergedSum': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + case 'name': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + case 'node_name': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + case 'os.platform': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + case 'status': + return UI_ORDER_AGENT_STATUS.map( + (status) => ({ + type: 'value', + label: status, + }), + ); + break; + case 'version': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); break; default: return []; @@ -879,3 +946,12 @@ AgentsTable.propTypes = { timeService: PropTypes.func, reload: PropTypes.func, }; + + +const getAgentFilterValuesMapToSearchBarSuggestion = async (key, value, params) => { + try{ + return (await getAgentFilterValues(key, value, params)).map(label => ({type: 'value', label})); + }catch(error){ + return []; + }; +}; From 0923569685105ba124bd05ce7037a8719ca774f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Thu, 2 Mar 2023 16:48:24 +0100 Subject: [PATCH 03/76] fix(search-bar): fixes a problem hidding the suggestion popover when using the Search suggestion in AQL - Fixes a problem hidding the suggestion popover when using the Search suggestion in AQL - Fixes a problem of input text with undefined value - Minor fixes - Remove `syntax` property of SearchBar component - Add disableFocusTrap property to the custom EuiSuggestInput component to be forwarded to the EuiInputPopover - Replace the inputRef by a reference instead of a state and pass as a parameter in the query language run function - Move the rebuiding of input text when using some suggestion that changes the input to be done when a related suggestion was clicked instead of any suggestion (exclude Search). --- .../components/eui-suggest/suggest_input.js | 2 ++ public/components/search-bar/index.tsx | 20 ++++++++--------- .../search-bar/query-language/aql.tsx | 22 ++++++++++++------- .../agent/components/agents-table.js | 1 - 4 files changed, 26 insertions(+), 19 deletions(-) diff --git a/public/components/eui-suggest/suggest_input.js b/public/components/eui-suggest/suggest_input.js index 55393ba820..f67149d489 100644 --- a/public/components/eui-suggest/suggest_input.js +++ b/public/components/eui-suggest/suggest_input.js @@ -53,6 +53,7 @@ export class EuiSuggestInput extends Component { onPopoverFocus, isPopoverOpen, onClosePopover, + disableFocusTrap, ...rest } = this.props; @@ -108,6 +109,7 @@ export class EuiSuggestInput extends Component { panelPaddingSize="none" fullWidth closePopover={onClosePopover} + disableFocusTrap={disableFocusTrap} >
{suggestions}
diff --git a/public/components/search-bar/index.tsx b/public/components/search-bar/index.tsx index 01a3764e14..25a11c0be4 100644 --- a/public/components/search-bar/index.tsx +++ b/public/components/search-bar/index.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { EuiButtonEmpty, EuiFormRow, @@ -51,14 +51,13 @@ export const SearchBar = ({ const [isOpenSuggestionPopover, setIsOpenSuggestionPopover] = useState(false); // Reference to the input - const [inputRef, setInputRef] = useState(); + const inputRef = useRef(); // Handler when searching const _onSearch = (output: any) => { // TODO: fix when searching - inputRef && inputRef.blur(); - setIsOpenSuggestionPopover(false); onSearch(output); + setIsOpenSuggestionPopover(false); }; // Handler on change the input field text @@ -75,7 +74,7 @@ export const SearchBar = ({ useEffect(() => { // React to external changes and set the internal input text. Use the `transformUnifiedQuery` of // the query language in use - setInput( + rest.input && setInput( searchBarQueryLanguages[queryLanguage.id]?.transformUnifiedQuery?.( rest.input, ), @@ -91,16 +90,17 @@ export const SearchBar = ({ setInput, closeSuggestionPopover: () => setIsOpenSuggestionPopover(false), openSuggestionPopover: () => setIsOpenSuggestionPopover(true), - queryLanguage: { - configuration: queryLanguage.configuration, - parameters: modes.find(({ id }) => id === queryLanguage.id), - }, setQueryLanguageConfiguration: (configuration: any) => setQueryLanguage(state => ({ ...state, configuration: configuration?.(state.configuration) || configuration, })), + inputRef, + queryLanguage: { + configuration: queryLanguage.configuration, + parameters: modes.find(({ id }) => id === queryLanguage.id), + }, }), ); })(); @@ -115,7 +115,7 @@ export const SearchBar = ({ return ( value) // Ensure the input is rebuilt using tokens with value. + // The input tokenization can contain tokens with no value due to the used + // regular expression. + .map(({ value }) => value) + .join('')); } - // Change the input - params.setInput(tokens - .filter(value => value) // Ensure the input is rebuilt using tokens with value. - // The input tokenization can contain tokens with no value due to the used - // regular expression. - .map(({ value }) => value) - .join('')); }, prepend: params.queryLanguage.parameters.implicitQuery ? ( This query is added to the input. ) : null, + // Disable the focus trap in the EuiInputPopover. + // This causes when using the Search suggestion, the suggestion popover can be closed. + // If this is disabled, then the suggestion popover is open after a short time for this + // use case. + disableFocusTrap: true }, output: getOutput(input, params.queryLanguage.parameters), }; diff --git a/public/controllers/agent/components/agents-table.js b/public/controllers/agent/components/agents-table.js index 5f3d01a2b5..7d01d14d7a 100644 --- a/public/controllers/agent/components/agents-table.js +++ b/public/controllers/agent/components/agents-table.js @@ -619,7 +619,6 @@ export const AgentsTable = withErrorBoundary( /> {/** Example implementation */} Date: Fri, 3 Mar 2023 15:45:53 +0100 Subject: [PATCH 04/76] feat(search-bar): add the ability to update the input of example implemenation - Add the ability to update the input of the search bar in the example implementation - Enhance the component documentation --- public/components/search-bar/README.md | 9 +++++- .../agent/components/agents-table.js | 28 +++++++++++++++++-- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/public/components/search-bar/README.md b/public/components/search-bar/README.md index 046157a9a0..7729aa8d70 100644 --- a/public/components/search-bar/README.md +++ b/public/components/search-bar/README.md @@ -22,7 +22,7 @@ Basic usage: ```tsx ``` diff --git a/public/controllers/agent/components/agents-table.js b/public/controllers/agent/components/agents-table.js index 7d01d14d7a..1fef9c9eb5 100644 --- a/public/controllers/agent/components/agents-table.js +++ b/public/controllers/agent/components/agents-table.js @@ -63,6 +63,7 @@ export const AgentsTable = withErrorBoundary( filters: sessionStorage.getItem('agents_preview_selected_options') ? JSON.parse(sessionStorage.getItem('agents_preview_selected_options')) : [], + query: '' }; this.suggestions = [ { @@ -210,7 +211,7 @@ export const AgentsTable = withErrorBoundary( this.props.filters && this.props.filters.length ) { - this.setState({ filters: this.props.filters, pageIndex: 0 }); + this.setState({ filters: this.props.filters, pageIndex: 0, query: this.props.filters.find(({field}) => field === 'q')?.value || '' }); this.props.removeFilters(); } } @@ -619,6 +620,7 @@ export const AgentsTable = withErrorBoundary( /> {/** Example implementation */} { + try{ + this.setState({isLoading: true}); + const response = await this.props.wzReq('GET', '/agents', { params: { + limit: this.state.pageSize, + offset: 0, + q: query, + sort: this.buildSortFilter() + }}); + + const formatedAgents = response?.data?.data?.affected_items?.map( + this.formatAgent.bind(this) + ); + + this._isMount && this.setState({ + agents: formatedAgents, + totalItems: response?.data?.data?.total_affected_items, + isLoading: false, + }); + }catch(error){ + this.setState({isLoading: false}); + }; + }} /> From 0b0b4519668483c5783d7a6b9503db820dc2c782 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Mon, 6 Mar 2023 10:21:24 +0100 Subject: [PATCH 05/76] feat(search-bar): add initial suggestions to AQL - (AQL) Add the fields and an open operator group when there is no input text --- .../components/search-bar/query-language/aql.tsx | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/public/components/search-bar/query-language/aql.tsx b/public/components/search-bar/query-language/aql.tsx index 0d240a0482..41902517e1 100644 --- a/public/components/search-bar/query-language/aql.tsx +++ b/public/components/search-bar/query-language/aql.tsx @@ -263,9 +263,17 @@ export async function getSuggestionsAPI(tokens: ITokens, options: optionsQL) { // Get last token const lastToken = getLastTokenWithValue(tokens); - // If it can't get a token with value, then no return suggestions + // If it can't get a token with value, then returns fields and open operator group if(!lastToken?.type){ - return []; + return [ + // fields + ...(await options.suggestions.field()), + { + type: 'operator_group', + label: '(', + description: language.tokens.operator_group.literal['('], + } + ]; }; switch (lastToken.type) { @@ -477,7 +485,7 @@ export const AQL = { const lastToken: IToken = getLastTokenWithValue(tokens); // if the clicked suggestion is of same type of last token if ( - suggestionMappingLanguageTokenType[lastToken.type].iconType === + lastToken && suggestionMappingLanguageTokenType[lastToken.type].iconType === item.type.iconType ) { // replace the value of last token From ab8a555c0342700acd55a4d1077a182312a86a20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Tue, 7 Mar 2023 16:39:50 +0100 Subject: [PATCH 06/76] feat(search-bar): add target and rel attributes to the documentation link of query language displayed in the popover --- public/components/search-bar/index.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/public/components/search-bar/index.tsx b/public/components/search-bar/index.tsx index 25a11c0be4..8b0ebcd1b7 100644 --- a/public/components/search-bar/index.tsx +++ b/public/components/search-bar/index.tsx @@ -148,13 +148,15 @@ export const SearchBar = ({ href={ searchBarQueryLanguages[queryLanguage.id].documentationLink } + target='__blank' + rel='noopener noreferrer' > Documentation )} - {modes?.length && modes.length > 1 && ( + {modes?.length > 1 && ( <> From 8d9f7461155478a3c284117ffb286399808f3582 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Tue, 7 Mar 2023 16:43:54 +0100 Subject: [PATCH 07/76] feat(search-bar): enhancements in AQL and search bar documentation - AQL enhancements: - documentation: - Enhance some descriptions - Enhance input processing - Remove intermetiate interface of EuiSuggestItem - Remove the intermediate interface of EuiSuggestItem. Now it is managed in the internal of query language instead of be built by the suggestion handler - Display suggestions when the input text is empty - Add the unifiedQuery field to the query language output - Adapt tests - Search Bar component: - Enhance documentation --- public/components/search-bar/README.md | 56 +++++++- .../search-bar/query-language/aql.md | 54 +++++--- .../search-bar/query-language/aql.test.tsx | 8 +- .../search-bar/query-language/aql.tsx | 130 +++++------------- 4 files changed, 128 insertions(+), 120 deletions(-) diff --git a/public/components/search-bar/README.md b/public/components/search-bar/README.md index 7729aa8d70..df007a4ea0 100644 --- a/public/components/search-bar/README.md +++ b/public/components/search-bar/README.md @@ -28,6 +28,10 @@ Basic usage: { id: 'aql', // specific query language parameters + // implicit query. Optional + // Set a implicit query that can't be changed by the user. + // Use the UQL (Unified Query Language) syntax. + // Each query language implementation must interpret implicitQuery: 'id!=000;', suggestions: { field(currentValue) { @@ -46,8 +50,7 @@ Basic usage: { label: 'os.platform', description: 'Operating system platform' }, { label: 'status', description: 'Status' }, { label: 'version', description: 'Version' }, - ] - .map(field => ({ type: 'field', ...field })); + ]; }, value: async (currentValue, { previousField }) => { switch (previousField) { @@ -165,7 +168,10 @@ Basic usage: onSearch={onSearch} // Used to define the internal input. Optional. // This could be used to change the input text from the external components. - input="" + // Use the UQL (Unified Query Language) syntax. + input="" + // Define the default mode. Optional. If not defined, it will use the first one mode. + defaultMode="" > ``` @@ -188,20 +194,37 @@ type SearchBarQueryLanguage = { id: string; label: string; getConfiguration?: () => any; - run: (input: string | undefined, params: any) => any; - transformUnifiedQuery: (unifiedQuery) => any; + run: (input: string | undefined, params: any) => Promise<{ + searchBarProps: any, + output: { + language: string, + unifiedQuery: string, + query: string + } + }>; + transformUnifiedQuery: (unifiedQuery: string) => string; }; ``` where: -- `description`: It is the description of the query language. This is displayed in a query language popover +- `description`: is the description of the query language. This is displayed in a query language popover on the right side of the search bar. Required. - `documentationLink`: URL to the documentation link. Optional. - `id`: identification of the query language. - `label`: name - `getConfiguration`: method that returns the configuration of the language. This allows custom behavior. -- `run`: method that returns the properties that will used by the base search bar component and the output used when searching +- `run`: method that returns: + - `searchBarProps`: properties to be passed to the search bar component. This allows the + customization the properties that will used by the base search bar component and the output used when searching + - `output`: + - `language`: query language ID + - `unifiedQuery`: query in unified query syntax + - `query`: current query in the specified language +- `transformUnifiedQuery`: method that transform the Unified Query Language to the specific query + language. This is used when receives a external input in the Unified Query Language, the returned + value is converted to the specific query language to set the new input text of the search bar + component. Create a new file located in `public/components/search-bar/query-language` and define the expected interface; @@ -233,3 +256,22 @@ export const searchBarQueryLanguages: { }; }, {}); ``` + +## Unified Query Language - UQL + +This is an unified syntax used by the search bar component that provides a way to communicate +with the different query language implementations. + +The input and output parameters of the search bar component must use this syntax. + +This is used in: +- input: + - `input` component property +- output: + - `onChange` component handler + - `onSearch` component handler + +Its syntax is equal to Wazuh API Query Language +https://wazuh.com/./user-manual/api/queries.html + +> The AQL query language is a implementation of this syntax. \ No newline at end of file diff --git a/public/components/search-bar/query-language/aql.md b/public/components/search-bar/query-language/aql.md index 637e8c9147..af0d2ccee9 100644 --- a/public/components/search-bar/query-language/aql.md +++ b/public/components/search-bar/query-language/aql.md @@ -12,14 +12,20 @@ The implementation is adapted to work with the search bar component defined - Suggestions for `fields` (configurable), `operators` and `values` (configurable) - Support implicit query +## Language syntax + +Documentation: https://wazuh.com/./user-manual/api/queries.html + ## Options -- `implicitQuery`: add an implicit query that is added to the user input. This can't be changed by -the user. If this is defined, will be displayed as a prepend of the search bar. +- `implicitQuery`: add an implicit query that is added to the user input. Optional. +Use UQL (Unified Query Language). +This can't be changed by the user. If this is defined, will be displayed as a prepend of the search bar. ```ts // language options -implicitQuery: 'id!=000;' // ID is not 000 and +// ID is not equal to 000 and . This is defined in UQL that is transformed internally to the specific query language. +implicitQuery: 'id!=000;' ``` - `suggestions`: define the suggestion handlers. This is required. @@ -44,8 +50,7 @@ implicitQuery: 'id!=000;' // ID is not 000 and { label: 'os.platform', description: 'Operating system platform' }, { label: 'status', description: 'Status' }, { label: 'version', description: 'Version' }, - ] - .map(field => ({ type: 'field', ...field })); + ]; } ``` @@ -161,18 +166,35 @@ implicitQuery: 'id!=000;' // ID is not 000 and } ``` -## How to get the suggestions +## Language workflow ```mermaid graph TD; - user_input[User input]-->tokenize; - subgraph tokenize - tokenize_regex - end - - tokenize-->suggestions[Get suggestions]; - subgraph suggestions[Get suggestions]; - get_last_token_with_value-->get_suggestions[Get suggestions] - end - suggestions-->EuiSuggestItem + user_input[User input]-->tokenizerAPI; + subgraph tokenizerAPI + tokenize_regex[Wazuh API `q` regular expression] + end + + tokenizerAPI-->tokens; + + tokens-->searchBarProps; + subgraph searchBarProps; + searchBarProps_suggestions-->searchBarProps_suggestions_get_last_token_with_value[Get last token with value] + searchBarProps_suggestions_get_last_token_with_value[Get last token with value]-->searchBarProps_suggestions__result[Suggestions] + searchBarProps_suggestions__result[Suggestions]-->EuiSuggestItem + searchBarProps_prepend[prepend]-->searchBarProps_prepend_implicitQuery{implicitQuery} + searchBarProps_prepend_implicitQuery{implicitQuery}-->searchBarProps_prepend_implicitQuery_yes[Yes]-->EuiButton + searchBarProps_prepend_implicitQuery{implicitQuery}-->searchBarProps_prepend_implicitQuery_no[No]-->null + searchBarProps_disableFocusTrap:true[disableFocusTrap = true] + searchBarProps_onItemClick[onItemClickSuggestion onclick handler]-->searchBarProps_onItemClick_suggestion_search[Search suggestion] + searchBarProps_onItemClick[onItemClickSuggestion onclick handler]-->searchBarProps_onItemClick_suggestion_edit_current_token[Edit current token]-->searchBarProps_onItemClick_build_input[Build input] + searchBarProps_onItemClick[onItemClickSuggestion onclick handler]-->searchBarProps_onItemClick_suggestion_add_new_token[Add new token]-->searchBarProps_onItemClick_build_input[Build input] + end + + tokens-->output; + subgraph output[output]; + output_result[implicitFilter + user input] + end + + output-->output_search_bar[Output] ``` \ No newline at end of file diff --git a/public/components/search-bar/query-language/aql.test.tsx b/public/components/search-bar/query-language/aql.test.tsx index 37c34904bc..a165ef5b75 100644 --- a/public/components/search-bar/query-language/aql.test.tsx +++ b/public/components/search-bar/query-language/aql.test.tsx @@ -35,17 +35,17 @@ describe('Query language - AQL', () => { // Get suggestions it.each` input | suggestions - ${''} | ${[]} + ${''} | ${[{ description: 'Field', label: 'field', type: 'field' }, { description: 'Field2', label: 'field2', type: 'field' }, { description: 'open group', label: '(', type: 'operator_group' }]} ${'w'} | ${[]} ${'f'} | ${[{ description: 'Field', label: 'field', type: 'field' }, { description: 'Field2', label: 'field2', type: 'field' }]} ${'field'} | ${[{ description: 'Field2', label: 'field2', type: 'field' }, { description: 'equality', label: '=', type: 'operator_compare' }, { description: 'not equality', label: '!=', type: 'operator_compare' }, { description: 'bigger', label: '>', type: 'operator_compare' }, { description: 'smaller', label: '<', type: 'operator_compare' }, { description: 'like as', label: '~', type: 'operator_compare' }]} ${'field='} | ${[{ label: 'value', type: 'value' }, { label: 'value2', type: 'value' }, { label: 'value3', type: 'value' }, { label: 'value4', type: 'value' }]} - ${'field=v'} | ${[{ description: 'Run the search query', label: 'Search', type: 'function_search' }, { description: 'Current value', label: 'v', type: 'value' }, { label: 'value', type: 'value' }, { label: 'value2', type: 'value' }, { label: 'value3', type: 'value' }, { label: 'value4', type: 'value' }, { description: 'and', label: ';', type: 'conjunction' }, { description: 'or', label: ',', type: 'conjunction' }, { description: 'close group', label: ')', type: 'operator_group' }]} - ${'field=value'} | ${[{ description: 'Run the search query', label: 'Search', type: 'function_search' }, { description: 'Current value', label: 'value', type: 'value' }, { label: 'value', type: 'value' }, { label: 'value2', type: 'value' }, { label: 'value3', type: 'value' }, { label: 'value4', type: 'value' }, { description: 'and', label: ';', type: 'conjunction' }, { description: 'or', label: ',', type: 'conjunction' }, { description: 'close group', label: ')', type: 'operator_group' }]} + ${'field=v'} | ${[{ description: 'Run the search query', label: 'Search', type: 'function_search' }, { label: 'value', type: 'value' }, { label: 'value2', type: 'value' }, { label: 'value3', type: 'value' }, { label: 'value4', type: 'value' }, { description: 'and', label: ';', type: 'conjunction' }, { description: 'or', label: ',', type: 'conjunction' }, { description: 'close group', label: ')', type: 'operator_group' }]} + ${'field=value'} | ${[{ description: 'Run the search query', label: 'Search', type: 'function_search' }, { label: 'value', type: 'value' }, { label: 'value2', type: 'value' }, { label: 'value3', type: 'value' }, { label: 'value4', type: 'value' }, { description: 'and', label: ';', type: 'conjunction' }, { description: 'or', label: ',', type: 'conjunction' }, { description: 'close group', label: ')', type: 'operator_group' }]} ${'field=value;'} | ${[{ description: 'Field', label: 'field', type: 'field' }, { description: 'Field2', label: 'field2', type: 'field' }, { description: 'open group', label: '(', type: 'operator_group' }]} ${'field=value;field2'} | ${[{ description: 'equality', label: '=', type: 'operator_compare' }, { description: 'not equality', label: '!=', type: 'operator_compare' }, { description: 'bigger', label: '>', type: 'operator_compare' }, { description: 'smaller', label: '<', type: 'operator_compare' }, { description: 'like as', label: '~', type: 'operator_compare' }]} ${'field=value;field2='} | ${[{ label: '127.0.0.1', type: 'value' }, { label: '127.0.0.2', type: 'value' }, { label: '190.0.0.1', type: 'value' }, { label: '190.0.0.2', type: 'value' }]} - ${'field=value;field2=127'} | ${[{ description: 'Run the search query', label: 'Search', type: 'function_search' }, { description: 'Current value', label: '127', type: 'value' }, { label: '127.0.0.1', type: 'value' }, { label: '127.0.0.2', type: 'value' }, { description: 'and', label: ';', type: 'conjunction' }, { description: 'or', label: ',', type: 'conjunction' }, { description: 'close group', label: ')', type: 'operator_group' }]} + ${'field=value;field2=127'} | ${[{ description: 'Run the search query', label: 'Search', type: 'function_search' }, { label: '127.0.0.1', type: 'value' }, { label: '127.0.0.2', type: 'value' }, { description: 'and', label: ';', type: 'conjunction' }, { description: 'or', label: ',', type: 'conjunction' }, { description: 'close group', label: ')', type: 'operator_group' }]} `('Get suggestion from the input: $input', async ({ input, suggestions }) => { expect( await getSuggestionsAPI(tokenizerAPI(input), { diff --git a/public/components/search-bar/query-language/aql.tsx b/public/components/search-bar/query-language/aql.tsx index 41902517e1..22d678844f 100644 --- a/public/components/search-bar/query-language/aql.tsx +++ b/public/components/search-bar/query-language/aql.tsx @@ -27,12 +27,9 @@ Implemented schema: */ // Language definition -const language = { +export const language = { // Tokens tokens: { - field: { - regex: /[\w.]/, - }, // eslint-disable-next-line camelcase operator_compare: { literal: { @@ -72,6 +69,23 @@ const suggestionMappingLanguageTokenType = { function_search: { iconType: 'search', color: 'tint5' }, }; +/** + * Creator of intermediate interfacte of EuiSuggestItem + * @param type + * @returns + */ +function mapSuggestionCreator(type: ITokenType ){ + return function({...params}){ + return { + type, + ...params + }; + }; +}; + +const mapSuggestionCreatorField = mapSuggestionCreator('field'); +const mapSuggestionCreatorValue = mapSuggestionCreator('value'); + /** * Tokenize the input string. Returns an array with the tokens. @@ -105,15 +119,15 @@ export function tokenizerAPI(input: string): ITokens{ // A ( character. '(?\\()?' + // Field name: name of the field to look on DB. - '(?[\\w.]+)?' + // Added a optional find + '(?[\\w.]+)?' + // Added an optional find // Operator: looks for '=', '!=', '<', '>' or '~'. // This seems to be a bug because is not searching the literal valid operators. // I guess the operator is validated after the regular expression matches - `(?[${Object.keys(language.tokens.operator_compare.literal)}]{1,2})?` + // Added a optional find + `(?[${Object.keys(language.tokens.operator_compare.literal)}]{1,2})?` + // Added an optional find // Value: A string. '(?(?:(?:\\((?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|[\\[\\]\\w _\\-.:?\\/\'"=@%<>{}]*)\\))*' + '(?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|[\\[\\]\\w _\\-.:?\\\\/\'"=@%<>{}]+)' + - '(?:\\((?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|[\\[\\]\\w _\\-.:?\\\\/\'"=@%<>{}]*)\\))*)+)?' + // Added a optional find + '(?:\\((?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|[\\[\\]\\w _\\-.:?\\\\/\'"=@%<>{}]*)\\))*)+)?' + // Added an optional find // A ) character. '(?\\))?' + `(?[${Object.keys(language.tokens.conjunction.literal)}])?`, @@ -124,84 +138,20 @@ export function tokenizerAPI(input: string): ITokens{ ...input.matchAll(re)] .map( ({groups}) => Object.entries(groups) - .map(([key, value]) => ({ - type: key.startsWith('operator_group') ? 'operator_group' : key, - value}) - ) + .map(([key, value]) => ({ + type: key.startsWith('operator_group') ? 'operator_group' : key, + value}) + ) ).flat(); }; -/** - * Check if the input is valid - * @param tokens - * @returns - */ -export function validate(input: string, options): boolean { - // TODO: enhance the validation - - // API regular expression - // self.query_regex = re.compile( - // # A ( character. - // r"(\()?" + - // # Field name: name of the field to look on DB. - // r"([\w.]+)" + - // # Operator: looks for '=', '!=', '<', '>' or '~'. - // rf"([{''.join(self.query_operators.keys())}]{{1,2}})" + - // # Value: A string. - // r"((?:(?:\((?:\[[\[\]\w _\-.,:?\\/'\"=@%<>{}]*]|[\[\]\w _\-.:?\\/'\"=@%<>{}]*)\))*" - // r"(?:\[[\[\]\w _\-.,:?\\/'\"=@%<>{}]*]|[\[\]\w _\-.:?\\/'\"=@%<>{}]+)" - // r"(?:\((?:\[[\[\]\w _\-.,:?\\/'\"=@%<>{}]*]|[\[\]\w _\-.:?\\/'\"=@%<>{}]*)\))*)+)" + - // # A ) character. - // r"(\))?" + - // # Separator: looks for ';', ',' or nothing. - // rf"([{''.join(self.query_separators.keys())}])?" - // ) - - const re = new RegExp( - // A ( character. - '(\\()?' + - // Field name: name of the field to look on DB. - '([\\w.]+)?' + - // Operator: looks for '=', '!=', '<', '>' or '~'. - // This seems to be a bug because is not searching the literal valid operators. - // I guess the operator is validated after the regular expression matches - `([${Object.keys(language.tokens.operator_compare.literal)}]{1,2})?` + - // Value: A string. - '((?:(?:\\((?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|[\\[\\]\\w _\\-.:?\\/\'"=@%<>{}]*)\\))*' + - '(?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|[\\[\\]\\w _\\-.:?\\\\/\'"=@%<>{}]+)' + - '(?:\\((?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|[\\[\\]\\w _\\-.:?\\\\/\'"=@%<>{}]*)\\))*)+)?' + - // '([\\w]+)'+ - // A ) character. - '(\\))?' + - `([${Object.keys(language.tokens.conjunction.literal)}])?`, - 'g' - ); - - [...input.matchAll(re)].reduce((accum, [_, operator_group_open, field, operator, value, operator_group_close, conjunction ]) => { - if(!accum){ - return accum; - }; - - return [operator_group_open, field, operator, value, operator_group_close, conjunction] - }, true); - - const errors = []; - - for (let [_, operator_group_open, field, operator, value, operator_group_close, conjunction ] in input.matchAll(re)) { - if(!options.fields.includes(field)){ - errors.push(`Field ${field} is not valid.`) - }; - } - return errors.length === 0; -} - type OptionSuggestionHandler = ( currentValue: string | undefined, { previousField, previousOperatorCompare, }: { previousField: string; previousOperatorCompare: string }, -) => Promise<{ description?: string; label: string; type: string }[]>; +) => Promise<{ description?: string; label: string }[]>; type optionsQL = { suggestions: { @@ -267,7 +217,7 @@ export async function getSuggestionsAPI(tokens: ITokens, options: optionsQL) { if(!lastToken?.type){ return [ // fields - ...(await options.suggestions.field()), + ...(await options.suggestions.field()).map(mapSuggestionCreatorField), { type: 'operator_group', label: '(', @@ -283,7 +233,7 @@ export async function getSuggestionsAPI(tokens: ITokens, options: optionsQL) { ...(await options.suggestions.field()).filter( ({ label }) => label.startsWith(lastToken.value) && label !== lastToken.value, - ), + ).map(mapSuggestionCreatorField), // operators if the input field is exact ...((await options.suggestions.field()).some( ({ label }) => label === lastToken.value, @@ -324,7 +274,7 @@ export async function getSuggestionsAPI(tokens: ITokens, options: optionsQL) { tokens, 'operator_compare', )!.value, - })), + })).map(mapSuggestionCreatorValue), ] : []), ]; @@ -346,7 +296,7 @@ export async function getSuggestionsAPI(tokens: ITokens, options: optionsQL) { tokens, 'operator_compare', )!.value, - })), + })).map(mapSuggestionCreatorValue), ...Object.entries(language.tokens.conjunction.literal).map( ([ conjunction, description]) => ({ type: 'conjunction', @@ -379,13 +329,7 @@ export async function getSuggestionsAPI(tokens: ITokens, options: optionsQL) { conjunction => conjunction === lastToken.value, ) ? [ - ...(await options.suggestions.field()).map( - ({ label, description }) => ({ - type: 'field', - label, - description, - }), - ), + ...(await options.suggestions.field()).map(mapSuggestionCreatorField), ] : []), { @@ -399,9 +343,7 @@ export async function getSuggestionsAPI(tokens: ITokens, options: optionsQL) { if (lastToken.value === '(') { return [ // fields - ...(await options.suggestions.field()).map( - ({ label, description }) => ({ type: 'field', label, description }), - ), + ...(await options.suggestions.field()).map(mapSuggestionCreatorField), ]; } else if (lastToken.value === ')') { return [ @@ -446,9 +388,11 @@ function transformSuggestionsToUI( * @returns */ function getOutput(input: string, options: {implicitQuery?: string} = {}) { + const unifiedQuery = `${options?.implicitQuery ?? ''}${input}`; return { language: AQL.id, - query: `${options?.implicitQuery ?? ''}${input}`, + query: unifiedQuery, + unifiedQuery }; }; @@ -502,7 +446,7 @@ export const AQL = { // Change the input params.setInput(tokens - .filter(value => value) // Ensure the input is rebuilt using tokens with value. + .filter(({ value }) => value) // Ensure the input is rebuilt using tokens with value. // The input tokenization can contain tokens with no value due to the used // regular expression. .map(({ value }) => value) @@ -553,7 +497,7 @@ export const AQL = { output: getOutput(input, params.queryLanguage.parameters), }; }, - transformUnifiedQuery(unifiedQuery) { + transformUnifiedQuery(unifiedQuery: string): string { return unifiedQuery; }, }; From 9c01c8e02c144e0c9e6632fd1ac1c5bdfc3d4312 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Tue, 7 Mar 2023 16:50:58 +0100 Subject: [PATCH 08/76] feat(search-bar): Add HAQL - Remove UIQL - Add HAQL query language that is a high-level implementation of AQL - Add the query language interface - Add tests for tokenizer, get suggestions and transformSpecificQLToUnifiedQL method - Add documentation about the language - Syntax - Options - Workflow --- .../search-bar/query-language/haql.md | 279 +++++++++ .../search-bar/query-language/haql.test.tsx | 139 +++++ .../search-bar/query-language/haql.tsx | 572 ++++++++++++++++++ .../search-bar/query-language/index.ts | 16 +- .../search-bar/query-language/uiql.tsx | 71 --- .../agent/components/agents-table.js | 136 ++++- 6 files changed, 1133 insertions(+), 80 deletions(-) create mode 100644 public/components/search-bar/query-language/haql.md create mode 100644 public/components/search-bar/query-language/haql.test.tsx create mode 100644 public/components/search-bar/query-language/haql.tsx delete mode 100644 public/components/search-bar/query-language/uiql.tsx diff --git a/public/components/search-bar/query-language/haql.md b/public/components/search-bar/query-language/haql.md new file mode 100644 index 0000000000..cf1d2b1166 --- /dev/null +++ b/public/components/search-bar/query-language/haql.md @@ -0,0 +1,279 @@ +# Query Language - HAQL + +HAQL (Human API Query Language) is a query language based in the `q` query parameters of the Wazuh API +endpoints. + +Documentation: https://wazuh.com/./user-manual/api/queries.html + +The implementation is adapted to work with the search bar component defined +`public/components/search-bar/index.tsx`. + +## Features +- Suggestions for `fields` (configurable), `operators` and `values` (configurable) +- Support implicit query + +## Language syntax + +### Schema + +``` +???????????? +``` + +### Fields + +Regular expression: /[\\w.]+/ + +Examples: + +``` +field +field.custom +``` + +### Operators + +#### Compare + +- `=` equal to +- `!=` not equal to +- `>` bigger +- `<` smaller +- `~` like + +#### Group + +- `(` open +- `)` close + +#### Logical + +- `and` +- `or` + +### Values + +- Value without spaces can be literal +- Value with spaces should be wrapped by `"`. The `"` can be escaped using `\"`. + +Examples: +``` +value +"custom value" +"custom \" value" +``` + +### Notes + +- The entities can be separated by whitespaces. + +### Examples + +- Simple query + +``` +id=001 +id = 001 +``` + +- Complex query +``` +status=active and os.platform~linux +status = active and os.platform ~ linux +``` + +``` +status!=never_connected and ip~240 or os.platform~linux +status != never_connected and ip ~ 240 or os.platform ~ linux +``` + +- Complex query with group operator +``` +(status!=never_connected and ip~240) or id=001 +( status != never_connected and ip ~ 240 ) or id = 001 +``` + +## Options + +- `implicitQuery`: add an implicit query that is added to the user input. Optional. +Use UQL (Unified Query Language). +This can't be changed by the user. If this is defined, will be displayed as a prepend of the search bar. + +```ts +// language options +// ID is not equal to 000 and . This is defined in UQL that is transformed internally to +// the specific query language. +implicitQuery: 'id!=000;' +``` + +- `suggestions`: define the suggestion handlers. This is required. + + - `field`: method that returns the suggestions for the fields + + ```ts + // language options + field(currentValue) { + return [ + { label: 'configSum', description: 'Config sum' }, + { label: 'dateAdd', description: 'Date add' }, + { label: 'id', description: 'ID' }, + { label: 'ip', description: 'IP address' }, + { label: 'group', description: 'Group' }, + { label: 'group_config_status', description: 'Synced configuration status' }, + { label: 'lastKeepAline', description: 'Date add' }, + { label: 'manager', description: 'Manager' }, + { label: 'mergedSum', description: 'Merged sum' }, + { label: 'name', description: 'Agent name' }, + { label: 'node_name', description: 'Node name' }, + { label: 'os.platform', description: 'Operating system platform' }, + { label: 'status', description: 'Status' }, + { label: 'version', description: 'Version' }, + ] + .map(field => ({ type: 'field', ...field })); + } + ``` + + - `value`: method that returns the suggestion for the values + ```ts + // language options + value: async (currentValue, { previousField }) => { + switch (previousField) { + case 'configSum': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + case 'dateAdd': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + case 'id': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + case 'ip': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + case 'group': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + case 'group_config_status': + return [AGENT_SYNCED_STATUS.SYNCED, AGENT_SYNCED_STATUS.NOT_SYNCED].map( + (status) => ({ + type: 'value', + label: status, + }), + ); + break; + case 'lastKeepAline': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + case 'manager': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + case 'mergedSum': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + case 'name': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + case 'node_name': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + case 'os.platform': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + case 'status': + return UI_ORDER_AGENT_STATUS.map( + (status) => ({ + type: 'value', + label: status, + }), + ); + break; + case 'version': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + default: + return []; + break; + } + } + ``` + +## Language workflow + +```mermaid +graph TD; + user_input[User input]-->tokenizer; + subgraph tokenizer + tokenize_regex[Query language regular expression] + end + + tokenizer-->tokens; + + tokens-->searchBarProps; + subgraph searchBarProps; + searchBarProps_suggestions-->searchBarProps_suggestions_get_last_token_with_value[Get last token with value] + searchBarProps_suggestions_get_last_token_with_value[Get last token with value]-->searchBarProps_suggestions__result[Suggestions] + searchBarProps_suggestions__result[Suggestions]-->EuiSuggestItem + searchBarProps_prepend[prepend]-->searchBarProps_prepend_implicitQuery{implicitQuery} + searchBarProps_prepend_implicitQuery{implicitQuery}-->searchBarProps_prepend_implicitQuery_yes[Yes]-->EuiButton + searchBarProps_prepend_implicitQuery{implicitQuery}-->searchBarProps_prepend_implicitQuery_no[No]-->null + searchBarProps_disableFocusTrap:true[disableFocusTrap = true] + searchBarProps_onItemClick[onItemClickSuggestion onclick handler]-->searchBarProps_onItemClick_suggestion_search[Search suggestion] + searchBarProps_onItemClick[onItemClickSuggestion onclick handler]-->searchBarProps_onItemClick_suggestion_edit_current_token[Edit current token]-->searchBarProps_onItemClick_build_input[Build input] + searchBarProps_onItemClick[onItemClickSuggestion onclick handler]-->searchBarProps_onItemClick_suggestion_add_new_token[Add new token]-->searchBarProps_onItemClick_build_input[Build input] + end + + tokens-->output; + subgraph output[output]; + output_result[implicitFilter + user input] + end + + output-->output_search_bar[Output] +``` \ No newline at end of file diff --git a/public/components/search-bar/query-language/haql.test.tsx b/public/components/search-bar/query-language/haql.test.tsx new file mode 100644 index 0000000000..7193bc8239 --- /dev/null +++ b/public/components/search-bar/query-language/haql.test.tsx @@ -0,0 +1,139 @@ +import { getSuggestions, tokenizer, transformSpecificQLToUnifiedQL } from './haql'; + +describe('Query language - HAQL', () => { + // Tokenize the input + function tokenCreator({type, value}){ + return {type, value}; + }; + + const t = { + opGroup: (value = undefined) => tokenCreator({type: 'operator_group', value}), + opCompare: (value = undefined) => tokenCreator({type: 'operator_compare', value}), + field: (value = undefined) => tokenCreator({type: 'field', value}), + value: (value = undefined) => tokenCreator({type: 'value', value}), + whitespace: (value = undefined) => tokenCreator({type: 'whitespace', value}), + conjunction: (value = undefined) => tokenCreator({type: 'conjunction', value}) + }; + + // Token undefined + const tu = { + opGroup: tokenCreator({type: 'operator_group', value: undefined}), + opCompare: tokenCreator({type: 'operator_compare', value: undefined}), + whitespace: tokenCreator({type: 'whitespace', value: undefined}), + field: tokenCreator({type: 'field', value: undefined}), + value: tokenCreator({type: 'value', value: undefined}), + conjunction: tokenCreator({type: 'conjunction', value: undefined}) + }; + + const tuBlankSerie = [ + tu.opGroup, + tu.whitespace, + tu.field, + tu.whitespace, + tu.opCompare, + tu.whitespace, + tu.value, + tu.whitespace, + tu.opGroup, + tu.whitespace, + tu.conjunction, + tu.whitespace + ]; + + + it.each` + input | tokens + ${''} | ${tuBlankSerie} + ${'f'} | ${[tu.opGroup, tu.whitespace, t.field('f'), tu.whitespace, tu.opCompare, tu.whitespace, tu.value, tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, tu.opCompare, tu.whitespace, tu.value, tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field='} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, tu.value, tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field=value'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field=value and'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.conjunction('and'), tu.whitespace, ...tuBlankSerie]} + ${'field=value and '} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.conjunction('and'), t.whitespace(' '), ...tuBlankSerie]} + ${'field=value and field2'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.conjunction('and'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.field('field2'), tu.whitespace, tu.opCompare, tu.whitespace, tu.value, tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field=value and field2!='} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.conjunction('and'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.field('field2'), tu.whitespace, t.opCompare('!='), tu.whitespace, tu.value, tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field=value and field2!="'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.conjunction('and'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.field('field2'), tu.whitespace, t.opCompare('!='), tu.whitespace, t.value('"'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field=value and field2!="value'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.conjunction('and'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.field('field2'), tu.whitespace, t.opCompare('!='), tu.whitespace, t.value('"value'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field=value and field2!="value"'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.conjunction('and'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.field('field2'), tu.whitespace, t.opCompare('!='), tu.whitespace, t.value('"value"'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field=value and field2!=value2 and'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.conjunction('and'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.field('field2'), tu.whitespace, t.opCompare('!='), tu.whitespace, t.value('value2'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.conjunction('and'), tu.whitespace, ...tuBlankSerie]} + `(`Tokenizer API input $input`, ({input, tokens}) => { + expect(tokenizer(input)).toEqual(tokens); + }); + + // Get suggestions + it.each` + input | suggestions + ${''} | ${[{ description: 'Field', label: 'field', type: 'field' }, { description: 'Field2', label: 'field2', type: 'field' }, { description: 'open group', label: '(', type: 'operator_group' }]} + ${'w'} | ${[]} + ${'f'} | ${[{ description: 'Field', label: 'field', type: 'field' }, { description: 'Field2', label: 'field2', type: 'field' }]} + ${'field'} | ${[{ description: 'Field2', label: 'field2', type: 'field' }, { description: 'equality', label: '=', type: 'operator_compare' }, { description: 'not equality', label: '!=', type: 'operator_compare' }, { description: 'bigger', label: '>', type: 'operator_compare' }, { description: 'smaller', label: '<', type: 'operator_compare' }, { description: 'like as', label: '~', type: 'operator_compare' }]} + ${'field='} | ${[{ label: 'value', type: 'value' }, { label: 'value2', type: 'value' }, { label: 'value3', type: 'value' }, { label: 'value4', type: 'value' }]} + ${'field=v'} | ${[{ description: 'Run the search query', label: 'Search', type: 'function_search' }, { label: 'value', type: 'value' }, { label: 'value2', type: 'value' }, { label: 'value3', type: 'value' }, { label: 'value4', type: 'value' }, { description: 'and', label: 'and', type: 'conjunction' }, { description: 'or', label: 'or', type: 'conjunction' }, { description: 'close group', label: ')', type: 'operator_group' }]} + ${'field=value'} | ${[{ description: 'Run the search query', label: 'Search', type: 'function_search' }, { label: 'value', type: 'value' }, { label: 'value2', type: 'value' }, { label: 'value3', type: 'value' }, { label: 'value4', type: 'value' }, { description: 'and', label: 'and', type: 'conjunction' }, { description: 'or', label: 'or', type: 'conjunction' }, { description: 'close group', label: ')', type: 'operator_group' }]} + ${'field=value and'} | ${[{ description: 'Field', label: 'field', type: 'field' }, { description: 'Field2', label: 'field2', type: 'field' }, { description: 'open group', label: '(', type: 'operator_group' }]} + `('Get suggestion from the input: $input', async ({ input, suggestions }) => { + expect( + await getSuggestions(tokenizer(input), { + id: 'aql', + suggestions: { + field(currentValue) { + return [ + { label: 'field', description: 'Field' }, + { label: 'field2', description: 'Field2' }, + ].map(({ label, description }) => ({ + type: 'field', + label, + description, + })); + }, + value(currentValue = '', { previousField }) { + switch (previousField) { + case 'field': + return ['value', 'value2', 'value3', 'value4'] + .filter(value => value.startsWith(currentValue)) + .map(value => ({ type: 'value', label: value })); + break; + case 'field2': + return ['127.0.0.1', '127.0.0.2', '190.0.0.1', '190.0.0.2'] + .filter(value => value.startsWith(currentValue)) + .map(value => ({ type: 'value', label: value })); + break; + default: + return []; + break; + } + }, + }, + }), + ).toEqual(suggestions); + }); + + it.each` + HAQL | UQL + ${'field'} | ${'field'} + ${'field='} | ${'field='} + ${'field=value'} | ${'field=value'} + ${'field=value()'} | ${'field=value()'} + ${'field="custom value"'} | ${'field=custom value'} + ${'field="custom \\"value"'} | ${'field=custom "value'} + ${'field="custom \\"value\\""'} | ${'field=custom "value"'} + ${'field=value and'} | ${'field=value;'} + ${'field="custom value" and'} | ${'field=custom value;'} + ${'(field=value'} | ${'(field=value'} + ${'(field=value)'} | ${'(field=value)'} + ${'(field=value) and'} | ${'(field=value);'} + ${'(field=value) and field2'} | ${'(field=value);field2'} + ${'(field=value) and field2>'} | ${'(field=value);field2>'} + ${'(field=value) and field2>"wrappedcommas"'} | ${'(field=value);field2>wrappedcommas'} + ${'(field=value) and field2>"value with spaces"'} | ${'(field=value);field2>value with spaces'} + ${'field ='} | ${'field='} + ${'field = value'} | ${'field=value'} + ${'field = value or'} | ${'field=value,'} + ${'field = value or field2'} | ${'field=value,field2'} + ${'field = value or field2 <'} | ${'field=value,field2<'} + ${'field = value or field2 < value2'} | ${'field=value,field2 "custom value" '} | ${'(field=value);field2>custom value'} + `('transformSpecificQLToUnifiedQL - HAQL $HAQL', ({HAQL, UQL}) => { + expect(transformSpecificQLToUnifiedQL(HAQL)).toEqual(UQL); + }) +}); diff --git a/public/components/search-bar/query-language/haql.tsx b/public/components/search-bar/query-language/haql.tsx new file mode 100644 index 0000000000..bf45cc9afc --- /dev/null +++ b/public/components/search-bar/query-language/haql.tsx @@ -0,0 +1,572 @@ +import React from 'react'; +import { EuiButtonEmpty, EuiPopover, EuiText, EuiCode } from '@elastic/eui'; +import { tokenizerAPI as tokenizerUQL } from './aql'; +import { pluginPlatform } from '../../../../package.json'; + +/* UI Query language +https://documentation.wazuh.com/current/user-manual/api/queries.html + +// Example of another query language definition +*/ + +type ITokenType = + | 'field' + | 'operator_compare' + | 'operator_group' + | 'value' + | 'conjunction' + | 'whitespace'; +type IToken = { type: ITokenType; value: string }; +type ITokens = IToken[]; + +/* API Query Language +Define the API Query Language to use in the search bar. +It is based in the language used by the q query parameter. +https://documentation.wazuh.com/current/user-manual/api/queries.html + +Use the regular expression of API with some modifications to allow the decomposition of +input in entities that doesn't compose a valid query. It allows get not-completed queries. + +API schema: +??? + +Implemented schema: +???????????? +*/ + +// Language definition +const language = { + // Tokens + tokens: { + field: { + regex: /[\w.]/, + }, + // eslint-disable-next-line camelcase + operator_compare: { + literal: { + '=': 'equality', + '!=': 'not equality', + '>': 'bigger', + '<': 'smaller', + '~': 'like as', + }, + }, + conjunction: { + literal: { + 'and': 'and', + 'or': 'or', + }, + }, + // eslint-disable-next-line camelcase + operator_group: { + literal: { + '(': 'open group', + ')': 'close group', + }, + }, + }, +}; + +// Suggestion mapper by language token type +const suggestionMappingLanguageTokenType = { + field: { iconType: 'kqlField', color: 'tint4' }, + // eslint-disable-next-line camelcase + operator_compare: { iconType: 'kqlOperand', color: 'tint1' }, + value: { iconType: 'kqlValue', color: 'tint0' }, + conjunction: { iconType: 'kqlSelector', color: 'tint3' }, + // eslint-disable-next-line camelcase + operator_group: { iconType: 'tokenDenseVector', color: 'tint3' }, + // eslint-disable-next-line camelcase + function_search: { iconType: 'search', color: 'tint5' }, +}; + + +/** + * Tokenize the input string. Returns an array with the tokens. + * @param input + * @returns + */ +export function tokenizer(input: string): ITokens{ + const re = new RegExp( + // A ( character. + '(?\\()?' + + // Whitespace + '(?\\s+)?' + + // Field name: name of the field to look on DB. + '(?[\\w.]+)?' + // Added an optional find + // Whitespace + '(?\\s+)?' + + // Operator: looks for '=', '!=', '<', '>' or '~'. + // This seems to be a bug because is not searching the literal valid operators. + // I guess the operator is validated after the regular expression matches + `(?[${Object.keys(language.tokens.operator_compare.literal)}]{1,2})?` + // Added an optional find + // Whitespace + '(?\\s+)?' + + // Value: A string. + // Simple value + // Quoted ", "value, "value", "escaped \"quote" + // Escape quoted string with escaping quotes: https://stackoverflow.com/questions/249791/regex-for-quoted-string-with-escaping-quotes + // '(?(?:(?:[^"\\s]+|(?:"(?:[^"\\\\]|\\\\")*"))))?' + + '(?(?:(?:[^"\\s]+|(?:"(?:[^"\\\\]|\\\\")*")|(?:"(?:[^"\\\\]|\\\\")*)|")))?' + + // Whitespace + '(?\\s+)?' + + // A ) character. + '(?\\))?' + + // Whitespace + '(?\\s+)?' + + `(?${Object.keys(language.tokens.conjunction.literal).join('|')})?` + + // Whitespace + '(?\\s+)?', + 'g' + ); + + return [ + ...input.matchAll(re)] + .map( + ({groups}) => Object.entries(groups) + .map(([key, value]) => ({ + type: key.startsWith('operator_group') // Transform operator_group group match + ? 'operator_group' + : (key.startsWith('whitespace') // Transform whitespace group match + ? 'whitespace' + : key), + value}) + ) + ).flat(); +}; + +type OptionSuggestionHandler = ( + currentValue: string | undefined, + { + previousField, + previousOperatorCompare, + }: { previousField: string; previousOperatorCompare: string }, +) => Promise<{ description?: string; label: string; type: string }[]>; + +type optionsQL = { + suggestions: { + field: OptionSuggestionHandler; + value: OptionSuggestionHandler; + }; +}; + +/** + * Get the last token with value + * @param tokens Tokens + * @param tokenType token type to search + * @returns + */ +function getLastTokenWithValue( + tokens: ITokens +): IToken | undefined { + // Reverse the tokens array and use the Array.protorype.find method + const shallowCopyArray = Array.from([...tokens]); + const shallowCopyArrayReversed = shallowCopyArray.reverse(); + const tokenFound = shallowCopyArrayReversed.find( + ({ type, value }) => type !== 'whitespace' && value, + ); + return tokenFound; +} + +/** + * Get the last token with value by type + * @param tokens Tokens + * @param tokenType token type to search + * @returns + */ +function getLastTokenWithValueByType( + tokens: ITokens, + tokenType: ITokenType, +): IToken | undefined { + // Find the last token by type + // Reverse the tokens array and use the Array.protorype.find method + const shallowCopyArray = Array.from([...tokens]); + const shallowCopyArrayReversed = shallowCopyArray.reverse(); + const tokenFound = shallowCopyArrayReversed.find( + ({ type, value }) => type === tokenType && value, + ); + return tokenFound; +} + +/** + * Get the suggestions from the tokens + * @param tokens + * @param language + * @param options + * @returns + */ +export async function getSuggestions(tokens: ITokens, options: optionsQL) { + if (!tokens.length) { + return []; + } + + // Get last token + const lastToken = getLastTokenWithValue(tokens); + + // If it can't get a token with value, then returns fields and open operator group + if(!lastToken?.type){ + return [ + // fields + ...(await options.suggestions.field()), + { + type: 'operator_group', + label: '(', + description: language.tokens.operator_group.literal['('], + } + ]; + }; + + switch (lastToken.type) { + case 'field': + return [ + // fields that starts with the input but is not equals + ...(await options.suggestions.field()).filter( + ({ label }) => + label.startsWith(lastToken.value) && label !== lastToken.value, + ), + // operators if the input field is exact + ...((await options.suggestions.field()).some( + ({ label }) => label === lastToken.value, + ) + ? [ + ...Object.keys(language.tokens.operator_compare.literal).map( + operator => ({ + type: 'operator_compare', + label: operator, + description: + language.tokens.operator_compare.literal[operator], + }), + ), + ] + : []), + ]; + break; + case 'operator_compare': + return [ + ...Object.keys(language.tokens.operator_compare.literal) + .filter( + operator => + operator.startsWith(lastToken.value) && + operator !== lastToken.value, + ) + .map(operator => ({ + type: 'operator_compare', + label: operator, + description: language.tokens.operator_compare.literal[operator], + })), + ...(Object.keys(language.tokens.operator_compare.literal).some( + operator => operator === lastToken.value, + ) + ? [ + ...(await options.suggestions.value(undefined, { + previousField: getLastTokenWithValueByType(tokens, 'field')!.value, + previousOperatorCompare: getLastTokenWithValueByType( + tokens, + 'operator_compare', + )!.value, + })), + ] + : []), + ]; + break; + case 'value': + return [ + ...(lastToken.value + ? [ + { + type: 'function_search', + label: 'Search', + description: 'Run the search query', + }, + ] + : []), + ...(await options.suggestions.value(lastToken.value, { + previousField: getLastTokenWithValueByType(tokens, 'field')!.value, + previousOperatorCompare: getLastTokenWithValueByType( + tokens, + 'operator_compare', + )!.value, + })), + ...Object.entries(language.tokens.conjunction.literal).map( + ([ conjunction, description]) => ({ + type: 'conjunction', + label: conjunction, + description, + }), + ), + { + type: 'operator_group', + label: ')', + description: language.tokens.operator_group.literal[')'], + }, + ]; + break; + case 'conjunction': + return [ + ...Object.keys(language.tokens.conjunction.literal) + .filter( + conjunction => + conjunction.startsWith(lastToken.value) && + conjunction !== lastToken.value, + ) + .map(conjunction => ({ + type: 'conjunction', + label: conjunction, + description: language.tokens.conjunction.literal[conjunction], + })), + // fields if the input field is exact + ...(Object.keys(language.tokens.conjunction.literal).some( + conjunction => conjunction === lastToken.value, + ) + ? [ + ...(await options.suggestions.field()).map( + ({ label, description }) => ({ + type: 'field', + label, + description, + }), + ), + ] + : []), + { + type: 'operator_group', + label: '(', + description: language.tokens.operator_group.literal['('], + }, + ]; + break; + case 'operator_group': + if (lastToken.value === '(') { + return [ + // fields + ...(await options.suggestions.field()).map( + ({ label, description }) => ({ type: 'field', label, description }), + ), + ]; + } else if (lastToken.value === ')') { + return [ + // conjunction + ...Object.keys(language.tokens.conjunction.literal).map( + conjunction => ({ + type: 'conjunction', + label: conjunction, + description: language.tokens.conjunction.literal[conjunction], + }), + ), + ]; + } + break; + default: + return []; + break; + } + + return []; +} + +/** + * Transform the suggestion object to the expected object by EuiSuggestItem + * @param suggestions + * @param options + * @returns + */ +function transformSuggestionsToUI( + suggestions: { type: string; label: string; description?: string }[], + mapSuggestionByLanguageToken: any, +) { + return suggestions.map(({ type, ...rest }) => ({ + type: { ...mapSuggestionByLanguageToken[type] }, + ...rest, + })); +}; + +/** + * Transform the UQL (Unified Query Language) to SpecificQueryLanguage + * @param input + * @returns + */ +export function transformUnifiedQueryToSpecificQueryLanguage(input: string){ + const tokens = tokenizerUQL(input); + return tokens + .filter(({value}) => value) + .map(({type, value}) => type === 'conjunction' + ? value === ';' + ? ' and ' + : ' or ' + : value + ).join(''); +}; + +/** + * Transform the input in SpecificQueryLanguage to UQL (Unified Query Language) + * @param input + * @returns + */ +export function transformSpecificQLToUnifiedQL(input: string){ + const tokens = tokenizer(input); + return tokens + .filter(({type, value}) => type !== 'whitespace' && value) + .map(({type, value}) => { + switch (type) { + case 'value':{ + // Value is wrapped with " + let [ _, extractedValue ] = value.match(/^"(.+)"$/) || [ null, null ]; + // Replace the escape commas (\") by comma (") + extractedValue && (extractedValue = extractedValue.replace(/\\"/g, '"')); + return extractedValue || value; + break; + } + case 'conjunction': + return value === 'and' + ? ';' + : ','; + break; + default: + return value; + break; + } + } + ).join(''); +}; + +/** + * Get the output from the input + * @param input + * @returns + */ +function getOutput(input: string, options: {implicitQuery?: string} = {}) { + const query = `${transformUnifiedQueryToSpecificQueryLanguage(options?.implicitQuery ?? '')}${input}`; + return { + language: HAQL.id, + unifiedQuery: transformSpecificQLToUnifiedQL(query), + query + }; +}; + +export const HAQL = { + id: 'haql', + label: 'HAQL', + description: 'HAQL allows to do queries.', + documentationLink: `https://github.com/wazuh/wazuh-kibana-app/blob/v${pluginPlatform.version.split('.').splice(0,2).join('.')}/public/components/search-bar/query-language/haql.md`, + getConfiguration() { + return { + isOpenPopoverImplicitFilter: false, + }; + }, + async run(input, params) { + // Get the tokens from the input + const tokens: ITokens = tokenizer(input); + + // + const implicitQueryAsSpecificQueryLanguage = params.queryLanguage.parameters.implicitQuery + ? transformUnifiedQueryToSpecificQueryLanguage(params.queryLanguage.parameters.implicitQuery) + : ''; + + return { + searchBarProps: { + // Props that will be used by the EuiSuggest component + // Suggestions + suggestions: transformSuggestionsToUI( + await getSuggestions(tokens, params.queryLanguage.parameters), + suggestionMappingLanguageTokenType, + ), + // Handler to manage when clicking in a suggestion item + onItemClick: item => { + // When the clicked item has the `search` iconType, run the `onSearch` function + if (item.type.iconType === 'search') { + // Execute the search action + params.onSearch(getOutput(input, params.queryLanguage.parameters)); + } else { + // When the clicked item has another iconType + const lastToken: IToken | undefined = getLastTokenWithValue(tokens); + // if the clicked suggestion is of same type of last token + if ( + lastToken && suggestionMappingLanguageTokenType[lastToken.type].iconType === + item.type.iconType + ) { + // replace the value of last token + lastToken.value = item.label; + } else { + // add a whitespace for conjunction + input.length + && !(/\s$/.test(input)) + && item.type.iconType === suggestionMappingLanguageTokenType.conjunction.iconType + && tokens.push({ + type: 'whitespace', + value: ' ' + }); + + // add a new token of the selected type and value + tokens.push({ + type: Object.entries(suggestionMappingLanguageTokenType).find( + ([, { iconType }]) => iconType === item.type.iconType, + )[0], + value: item.type.iconType === suggestionMappingLanguageTokenType.value.iconType + && /\s/.test(item.label) + ? `"${item.label}"` + : item.label, + }); + + // add a whitespace for conjunction + item.type.iconType === suggestionMappingLanguageTokenType.conjunction.iconType + && tokens.push({ + type: 'whitespace', + value: ' ' + }); + }; + + // Change the input + params.setInput(tokens + .filter(value => value) // Ensure the input is rebuilt using tokens with value. + // The input tokenization can contain tokens with no value due to the used + // regular expression. + .map(({ value }) => value) + .join('')); + } + }, + prepend: implicitQueryAsSpecificQueryLanguage ? ( + + params.setQueryLanguageConfiguration(state => ({ + ...state, + isOpenPopoverImplicitFilter: + !state.isOpenPopoverImplicitFilter, + })) + } + iconType='filter' + > + + {implicitQueryAsSpecificQueryLanguage} + + + } + isOpen={ + params.queryLanguage.configuration.isOpenPopoverImplicitFilter + } + closePopover={() => + params.setQueryLanguageConfiguration(state => ({ + ...state, + isOpenPopoverImplicitFilter: false, + })) + } + > + + Implicit query:{' '} + {implicitQueryAsSpecificQueryLanguage} + + This query is added to the input. + + ) : null, + // Disable the focus trap in the EuiInputPopover. + // This causes when using the Search suggestion, the suggestion popover can be closed. + // If this is disabled, then the suggestion popover is open after a short time for this + // use case. + disableFocusTrap: true + }, + output: getOutput(input, params.queryLanguage.parameters), + }; + }, + transformUnifiedQuery: transformUnifiedQueryToSpecificQueryLanguage, +}; diff --git a/public/components/search-bar/query-language/index.ts b/public/components/search-bar/query-language/index.ts index 1895aae4fb..22608fd9f5 100644 --- a/public/components/search-bar/query-language/index.ts +++ b/public/components/search-bar/query-language/index.ts @@ -1,5 +1,5 @@ import { AQL } from './aql'; -import { UIQL } from './uiql'; +import { HAQL } from './haql'; type SearchBarQueryLanguage = { description: string; @@ -7,14 +7,21 @@ type SearchBarQueryLanguage = { id: string; label: string; getConfiguration?: () => any; - run: (input: string | undefined, params: any) => any; - transformUnifiedQuery: (unifiedQuery) => any; + run: (input: string | undefined, params: any) => Promise<{ + searchBarProps: any, + output: { + language: string, + unifiedQuery: string, + query: string + } + }>; + transformUnifiedQuery: (unifiedQuery: string) => string; }; // Register the query languages export const searchBarQueryLanguages: { [key: string]: SearchBarQueryLanguage; -} = [AQL, UIQL].reduce((accum, item) => { +} = [AQL, HAQL].reduce((accum, item) => { if (accum[item.id]) { throw new Error(`Query language with id: ${item.id} already registered.`); } @@ -22,5 +29,4 @@ export const searchBarQueryLanguages: { ...accum, [item.id]: item, }; - ['hola']; }, {}); diff --git a/public/components/search-bar/query-language/uiql.tsx b/public/components/search-bar/query-language/uiql.tsx deleted file mode 100644 index 97b1927f54..0000000000 --- a/public/components/search-bar/query-language/uiql.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import React from 'react'; -import { EuiButtonEmpty, EuiPopover, EuiText, EuiCode } from '@elastic/eui'; - -/* UI Query language -https://documentation.wazuh.com/current/user-manual/api/queries.html - -// Example of another query language definition -*/ - -/** - * Get the output from the input - * @param input - * @returns - */ -function getOutput(input: string, options: {implicitQuery?: string} = {}) { - return { - language: UIQL.id, - query: `${options?.implicitQuery ?? ''}${input}`, - }; -}; - -export const UIQL = { - id: 'uiql', - label: 'UIQL', - description: 'UIQL allows to do queries.', - documentationLink: '', - getConfiguration() { - return { - anotherProp: false, - }; - }, - async run(input, params) { - // Get the tokens from the input - return { - searchBarProps: { - // Props that will be used by the EuiSuggest component - // Suggestions - suggestions: [], - // Handler to manage when clicking in a suggestion item - prepend: params.queryLanguage.parameters.implicitQuery ? ( - - params.setQueryLanguageConfiguration(state => ({ - ...state, - anotherProp: !state.anotherProp, - })) - } - iconType='filter' - > - } - isOpen={params.queryLanguage.configuration.anotherProp} - closePopover={() => - params.setQueryLanguageConfiguration(state => ({ - ...state, - anotherProp: false, - })) - } - > - - Implicit UIQL query:{' '} - {params.queryLanguage.parameters.implicitQuery} - - - ) : null, - }, - output: getOutput(input, params.queryLanguage.parameters), - }; - }, -}; diff --git a/public/controllers/agent/components/agents-table.js b/public/controllers/agent/components/agents-table.js index 1fef9c9eb5..0f9ff7640a 100644 --- a/public/controllers/agent/components/agents-table.js +++ b/public/controllers/agent/components/agents-table.js @@ -620,6 +620,7 @@ export const AgentsTable = withErrorBoundary( /> {/** Example implementation */} { + switch (previousField) { + case 'configSum': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + case 'dateAdd': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + case 'id': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + case 'ip': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + case 'group': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + case 'group_config_status': + return [AGENT_SYNCED_STATUS.SYNCED, AGENT_SYNCED_STATUS.NOT_SYNCED].map( + (status) => ({ + type: 'value', + label: status, + }), + ); + break; + case 'lastKeepAline': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + case 'manager': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + case 'mergedSum': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + case 'name': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + case 'node_name': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + case 'os.platform': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + case 'status': + return UI_ORDER_AGENT_STATUS.map( + (status) => ({ + type: 'value', + label: status, + }), + ); + break; + case 'version': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + default: + return []; + break; + } + }, + }, }, ]} onChange={console.log} - onSearch={async ({language, query}) => { + onSearch={async ({language, unifiedQuery}) => { try{ this.setState({isLoading: true}); const response = await this.props.wzReq('GET', '/agents', { params: { limit: this.state.pageSize, offset: 0, - q: query, + q: unifiedQuery, sort: this.buildSortFilter() }}); @@ -973,7 +1101,7 @@ AgentsTable.propTypes = { const getAgentFilterValuesMapToSearchBarSuggestion = async (key, value, params) => { try{ - return (await getAgentFilterValues(key, value, params)).map(label => ({type: 'value', label})); + return (await getAgentFilterValues(key, value, params)).map(label => ({label})); }catch(error){ return []; }; From b6f0d6d76963555704f0e3c5db46f07cc32d34af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Tue, 14 Mar 2023 14:32:49 +0100 Subject: [PATCH 09/76] feat(search-bar): add test to HAQL and AQL query languages - Add tests to HAQL and AQL query languages - Fix suggestions for HAQL when typing as first element a value entity. Now there are no suggestions because the field and operator_compare are missing. - Enhance documentation of HAQL and AQL - Removed unnecesary returns of suggestion handler in the example implementation of search bar on Agents section --- .../__snapshots__/index.test.tsx.snap | 59 ++++++ public/components/search-bar/index.test.tsx | 54 +++++ public/components/search-bar/index.tsx | 6 +- .../__snapshots__/aql.test.tsx.snap | 99 +++++++++ .../__snapshots__/haql.test.tsx.snap | 99 +++++++++ .../search-bar/query-language/aql.md | 6 +- .../search-bar/query-language/aql.test.tsx | 126 +++++++++++- .../search-bar/query-language/aql.tsx | 62 ++++-- .../search-bar/query-language/haql.md | 3 +- .../search-bar/query-language/haql.test.tsx | 127 +++++++++++- .../search-bar/query-language/haql.tsx | 193 ++++++++++++------ .../agent/components/agents-table.js | 7 +- 12 files changed, 745 insertions(+), 96 deletions(-) create mode 100644 public/components/search-bar/__snapshots__/index.test.tsx.snap create mode 100644 public/components/search-bar/index.test.tsx create mode 100644 public/components/search-bar/query-language/__snapshots__/aql.test.tsx.snap create mode 100644 public/components/search-bar/query-language/__snapshots__/haql.test.tsx.snap diff --git a/public/components/search-bar/__snapshots__/index.test.tsx.snap b/public/components/search-bar/__snapshots__/index.test.tsx.snap new file mode 100644 index 0000000000..007974639e --- /dev/null +++ b/public/components/search-bar/__snapshots__/index.test.tsx.snap @@ -0,0 +1,59 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SearchBar component Renders correctly the initial render 1`] = ` +
+
+
+
+
+
+
+
+ +
+
+
+ +
+
+
+
+
+
+
+
+
+`; diff --git a/public/components/search-bar/index.test.tsx b/public/components/search-bar/index.test.tsx new file mode 100644 index 0000000000..b77cb2baa8 --- /dev/null +++ b/public/components/search-bar/index.test.tsx @@ -0,0 +1,54 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import { SearchBar } from './index'; + +describe('SearchBar component', () => { + const componentProps = { + defaultMode: 'haql', + input: '', + modes: [ + { + id: 'aql', + implicitQuery: 'id!=000;', + suggestions: { + field(currentValue) { + return []; + }, + value(currentValue, { previousField }){ + return []; + }, + }, + }, + { + id: 'haql', + implicitQuery: 'id!=000;', + suggestions: { + field(currentValue) { + return []; + }, + value(currentValue, { previousField }){ + return []; + }, + }, + }, + ], + /* eslint-disable @typescript-eslint/no-empty-function */ + onChange: () => {}, + onSearch: () => {} + /* eslint-enable @typescript-eslint/no-empty-function */ + }; + + it('Renders correctly the initial render', async () => { + const wrapper = render( + + ); + + /* This test causes a warning about act. This is intentional, because the test pretends to get + the first rendering of the component that doesn't have the component properties coming of the + selected query language */ + expect(wrapper.container).toMatchSnapshot(); + }); +}); \ No newline at end of file diff --git a/public/components/search-bar/index.tsx b/public/components/search-bar/index.tsx index 8b0ebcd1b7..63b1d5a49a 100644 --- a/public/components/search-bar/index.tsx +++ b/public/components/search-bar/index.tsx @@ -119,7 +119,11 @@ export const SearchBar = ({ value={input} onChange={onChangeInput} onKeyPress={onKeyPressHandler} - onInputChange={() => {}} + // eslint-disable-next-line @typescript-eslint/no-empty-function + onInputChange={() => {}} /* This method is run by EuiSuggest when there is a change in + a div wrapper of the input and should be defined. Defining this + property prevents an error. */ + suggestions={[]} isPopoverOpen={ queryLanguageOutputRun.searchBarProps.suggestions.length > 0 && isOpenSuggestionPopover diff --git a/public/components/search-bar/query-language/__snapshots__/aql.test.tsx.snap b/public/components/search-bar/query-language/__snapshots__/aql.test.tsx.snap new file mode 100644 index 0000000000..0ef68d2e9e --- /dev/null +++ b/public/components/search-bar/query-language/__snapshots__/aql.test.tsx.snap @@ -0,0 +1,99 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SearchBar component Renders correctly to match the snapshot of query language 1`] = ` +
+
+
+
+
+
+
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+
+
+
+
+
+
+`; diff --git a/public/components/search-bar/query-language/__snapshots__/haql.test.tsx.snap b/public/components/search-bar/query-language/__snapshots__/haql.test.tsx.snap new file mode 100644 index 0000000000..8636885205 --- /dev/null +++ b/public/components/search-bar/query-language/__snapshots__/haql.test.tsx.snap @@ -0,0 +1,99 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SearchBar component Renders correctly to match the snapshot of query language 1`] = ` +
+
+
+
+
+
+
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+
+
+
+
+
+
+`; diff --git a/public/components/search-bar/query-language/aql.md b/public/components/search-bar/query-language/aql.md index af0d2ccee9..9dcb5bb0e0 100644 --- a/public/components/search-bar/query-language/aql.md +++ b/public/components/search-bar/query-language/aql.md @@ -170,12 +170,12 @@ implicitQuery: 'id!=000;' ```mermaid graph TD; - user_input[User input]-->tokenizerAPI; - subgraph tokenizerAPI + user_input[User input]-->tokenizer; + subgraph tokenizer tokenize_regex[Wazuh API `q` regular expression] end - tokenizerAPI-->tokens; + tokenizer-->tokens; tokens-->searchBarProps; subgraph searchBarProps; diff --git a/public/components/search-bar/query-language/aql.test.tsx b/public/components/search-bar/query-language/aql.test.tsx index a165ef5b75..597d31188a 100644 --- a/public/components/search-bar/query-language/aql.test.tsx +++ b/public/components/search-bar/query-language/aql.test.tsx @@ -1,4 +1,46 @@ -import { getSuggestionsAPI, tokenizerAPI, validate } from './aql'; +import { AQL, getSuggestions, tokenizer } from './aql'; +import React from 'react'; +import { render, waitFor } from '@testing-library/react'; +import { SearchBar } from '../index'; + +describe('SearchBar component', () => { + const componentProps = { + defaultMode: AQL.id, + input: '', + modes: [ + { + id: AQL.id, + implicitQuery: 'id!=000;', + suggestions: { + field(currentValue) { + return []; + }, + value(currentValue, { previousField }){ + return []; + }, + }, + } + ], + /* eslint-disable @typescript-eslint/no-empty-function */ + onChange: () => {}, + onSearch: () => {} + /* eslint-enable @typescript-eslint/no-empty-function */ + }; + + it('Renders correctly to match the snapshot of query language', async () => { + const wrapper = render( + + ); + + await waitFor(() => { + const elementImplicitQuery = wrapper.container.querySelector('.euiCodeBlock__code'); + expect(elementImplicitQuery?.innerHTML).toEqual('id!=000;'); + expect(wrapper.container).toMatchSnapshot(); + }); + }); +}); describe('Query language - AQL', () => { // Tokenize the input @@ -29,7 +71,7 @@ describe('Query language - AQL', () => { ${'(field>2;field2=value2)'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: '2' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: 'field2' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value2' }, { type: 'operator_group', value: ')' }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} ${'(field>2;field2=custom value())'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: '2' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: 'field2' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'custom value()' }, { type: 'operator_group', value: ')' }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} `(`Tokenizer API input $input`, ({input, tokens}) => { - expect(tokenizerAPI(input)).toEqual(tokens); + expect(tokenizer(input)).toEqual(tokens); }); // Get suggestions @@ -48,7 +90,7 @@ describe('Query language - AQL', () => { ${'field=value;field2=127'} | ${[{ description: 'Run the search query', label: 'Search', type: 'function_search' }, { label: '127.0.0.1', type: 'value' }, { label: '127.0.0.2', type: 'value' }, { description: 'and', label: ';', type: 'conjunction' }, { description: 'or', label: ',', type: 'conjunction' }, { description: 'close group', label: ')', type: 'operator_group' }]} `('Get suggestion from the input: $input', async ({ input, suggestions }) => { expect( - await getSuggestionsAPI(tokenizerAPI(input), { + await getSuggestions(tokenizer(input), { id: 'aql', suggestions: { field(currentValue) { @@ -82,4 +124,82 @@ describe('Query language - AQL', () => { }), ).toEqual(suggestions); }); + + // When a suggestion is clicked, change the input text + it.each` + AQL | clikedSuggestion | changedInput + ${''} | ${{type: { iconType: 'kqlField', color: 'tint4' }, label: 'field'}} | ${'field'} + ${'field'} | ${{type: { iconType: 'kqlField', color: 'tint4' }, label: 'field2'}} | ${'field2'} + ${'field'} | ${{type: { iconType: 'kqlOperand', color: 'tint1' }, label: '='}} | ${'field='} + ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value'}} | ${'field=value'} + ${'field='} | ${{type: { iconType: 'kqlOperand', color: 'tint1' }, label: '!='}} | ${'field!='} + ${'field=value'} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value2'}} | ${'field=value2'} + ${'field=value'} | ${{type: { iconType: 'kqlSelector', color: 'tint3' }, label: ';'}} | ${'field=value;'} + ${'field=value;'} | ${{type: { iconType: 'kqlField', color: 'tint4' }, label: 'field2'}} | ${'field=value;field2'} + ${'field=value;field2'} | ${{type: { iconType: 'kqlOperand', color: 'tint1' }, label: '>'}} | ${'field=value;field2>'} + ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with spaces'}} | ${'field=with spaces'} + ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with "spaces'}} | ${'field=with "spaces'} + ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: '"value'}} | ${'field="value'} + ${''} | ${{type: { iconType: 'tokenDenseVector', color: 'tint3' }, label: '('}} | ${'('} + ${'('} | ${{type: { iconType: 'kqlField', color: 'tint4' }, label: 'field'}} | ${'(field'} + ${'(field'} | ${{type: { iconType: 'kqlField', color: 'tint4' }, label: 'field2'}} | ${'(field2'} + ${'(field'} | ${{type: { iconType: 'kqlOperand', color: 'tint1' }, label: '='}} | ${'(field='} + ${'(field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value'}} | ${'(field=value'} + ${'(field=value'} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value2'}} | ${'(field=value2'} + ${'(field=value'} | ${{type: { iconType: 'kqlSelector', color: 'tint3' }, label: ','}} | ${'(field=value,'} + ${'(field=value,'} | ${{type: { iconType: 'kqlField', color: 'tint4' }, label: 'field2'}} | ${'(field=value,field2'} + ${'(field=value,field2'} | ${{type: { iconType: 'kqlOperand', color: 'tint1' }, label: '>'}} | ${'(field=value,field2>'} + ${'(field=value,field2>'} | ${{type: { iconType: 'kqlOperand', color: 'tint1' }, label: '>'}} | ${'(field=value,field2>'} + ${'(field=value,field2>'} | ${{type: { iconType: 'kqlOperand', color: 'tint1' }, label: '~'}} | ${'(field=value,field2~'} + ${'(field=value,field2>'} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value2'}} | ${'(field=value,field2>value2'} + ${'(field=value,field2>value2'} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value3'}} | ${'(field=value,field2>value3'} + ${'(field=value,field2>value2'} | ${{type: { iconType: 'tokenDenseVector', color: 'tint3' }, label: ')'}} | ${'(field=value,field2>value2)'} + `('click suggestion - AQL $AQL => $changedInput', async ({AQL: currentInput, clikedSuggestion, changedInput}) => { + // Mock input + let input = currentInput; + + const qlOutput = await AQL.run(input, { + setInput: (value: string): void => { input = value; }, + queryLanguage: { + parameters: { + implicitQuery: '', + suggestions: { + field: () => ([]), + value: () => ([]) + } + } + } + }); + qlOutput.searchBarProps.onItemClick(clikedSuggestion); + expect(input).toEqual(changedInput); + }); + + // Transform the external input in UQL (Unified Query Language) to QL + it.each` + UQL | AQL + ${''} | ${''} + ${'field'} | ${'field'} + ${'field='} | ${'field='} + ${'field!='} | ${'field!='} + ${'field>'} | ${'field>'} + ${'field<'} | ${'field<'} + ${'field~'} | ${'field~'} + ${'field=value'} | ${'field=value'} + ${'field=value;'} | ${'field=value;'} + ${'field=value;field2'} | ${'field=value;field2'} + ${'field="'} | ${'field="'} + ${'field=with spaces'} | ${'field=with spaces'} + ${'field=with "spaces'} | ${'field=with "spaces'} + ${'('} | ${'('} + ${'(field'} | ${'(field'} + ${'(field='} | ${'(field='} + ${'(field=value'} | ${'(field=value'} + ${'(field=value,'} | ${'(field=value,'} + ${'(field=value,field2'} | ${'(field=value,field2'} + ${'(field=value,field2>'} | ${'(field=value,field2>'} + ${'(field=value,field2>value2'} | ${'(field=value,field2>value2'} + ${'(field=value,field2>value2)'} | ${'(field=value,field2>value2)'} + `('Transform the external input UQL to QL - UQL $UQL => $AQL', async ({UQL, AQL: changedInput}) => { + expect(AQL.transformUnifiedQuery(UQL)).toEqual(changedInput); + }); }); diff --git a/public/components/search-bar/query-language/aql.tsx b/public/components/search-bar/query-language/aql.tsx index 22d678844f..a93f33df19 100644 --- a/public/components/search-bar/query-language/aql.tsx +++ b/public/components/search-bar/query-language/aql.tsx @@ -70,7 +70,7 @@ const suggestionMappingLanguageTokenType = { }; /** - * Creator of intermediate interfacte of EuiSuggestItem + * Creator of intermediate interface of EuiSuggestItem * @param type * @returns */ @@ -92,7 +92,7 @@ const mapSuggestionCreatorValue = mapSuggestionCreator('value'); * @param input * @returns */ -export function tokenizerAPI(input: string): ITokens{ +export function tokenizer(input: string): ITokens{ // API regular expression // https://github.com/wazuh/wazuh/blob/v4.4.0-rc1/framework/wazuh/core/utils.py#L1242-L1257 // self.query_regex = re.compile( @@ -145,18 +145,31 @@ export function tokenizerAPI(input: string): ITokens{ ).flat(); }; -type OptionSuggestionHandler = ( +type QLOptionSuggestionEntityItem = { + description?: string + label: string +}; + +type QLOptionSuggestionEntityItemTyped = + QLOptionSuggestionEntityItem + & { type: 'operator_group'|'field'|'operator_compare'|'value'|'conjunction' }; + +type SuggestItem = QLOptionSuggestionEntityItem & { + type: { iconType: string, color: string } +}; + +type QLOptionSuggestionHandler = ( currentValue: string | undefined, { previousField, previousOperatorCompare, }: { previousField: string; previousOperatorCompare: string }, -) => Promise<{ description?: string; label: string }[]>; +) => Promise; type optionsQL = { suggestions: { - field: OptionSuggestionHandler; - value: OptionSuggestionHandler; + field: QLOptionSuggestionHandler; + value: QLOptionSuggestionHandler; }; }; @@ -205,7 +218,7 @@ function getLastTokenWithValueByType( * @param options * @returns */ -export async function getSuggestionsAPI(tokens: ITokens, options: optionsQL) { +export async function getSuggestions(tokens: ITokens, options: optionsQL): Promise { if (!tokens.length) { return []; } @@ -366,21 +379,29 @@ export async function getSuggestionsAPI(tokens: ITokens, options: optionsQL) { return []; } +/** + * Transform the suggestion object to the expected object by EuiSuggestItem + * @param param0 + * @returns + */ +export function transformSuggestionToEuiSuggestItem(suggestion: QLOptionSuggestionEntityItemTyped): SuggestItem{ + const { type, ...rest} = suggestion; + return { + type: { ...suggestionMappingLanguageTokenType[type] }, + ...rest + }; +}; + /** * Transform the suggestion object to the expected object by EuiSuggestItem * @param suggestions - * @param options * @returns */ -function transformSuggestionsToUI( - suggestions: { type: string; label: string; description?: string }[], - mapSuggestionByLanguageToken: any, -) { - return suggestions.map(({ type, ...rest }) => ({ - type: { ...mapSuggestionByLanguageToken[type] }, - ...rest, - })); -} +function transformSuggestionsToEuiSuggestItem( + suggestions: QLOptionSuggestionEntityItemTyped[] +): SuggestItem[] { + return suggestions.map(transformSuggestionToEuiSuggestItem); +}; /** * Get the output from the input @@ -408,15 +429,14 @@ export const AQL = { }, async run(input, params) { // Get the tokens from the input - const tokens: ITokens = tokenizerAPI(input); + const tokens: ITokens = tokenizer(input); return { searchBarProps: { // Props that will be used by the EuiSuggest component // Suggestions - suggestions: transformSuggestionsToUI( - await getSuggestionsAPI(tokens, params.queryLanguage.parameters), - suggestionMappingLanguageTokenType, + suggestions: transformSuggestionsToEuiSuggestItem( + await getSuggestions(tokens, params.queryLanguage.parameters) ), // Handler to manage when clicking in a suggestion item onItemClick: item => { diff --git a/public/components/search-bar/query-language/haql.md b/public/components/search-bar/query-language/haql.md index cf1d2b1166..8f5253eda0 100644 --- a/public/components/search-bar/query-language/haql.md +++ b/public/components/search-bar/query-language/haql.md @@ -128,8 +128,7 @@ implicitQuery: 'id!=000;' { label: 'os.platform', description: 'Operating system platform' }, { label: 'status', description: 'Status' }, { label: 'version', description: 'Version' }, - ] - .map(field => ({ type: 'field', ...field })); + ]; } ``` diff --git a/public/components/search-bar/query-language/haql.test.tsx b/public/components/search-bar/query-language/haql.test.tsx index 7193bc8239..eeef8eb91a 100644 --- a/public/components/search-bar/query-language/haql.test.tsx +++ b/public/components/search-bar/query-language/haql.test.tsx @@ -1,5 +1,48 @@ -import { getSuggestions, tokenizer, transformSpecificQLToUnifiedQL } from './haql'; +import { getSuggestions, tokenizer, transformSpecificQLToUnifiedQL, HAQL } from './haql'; +import React from 'react'; +import { render, waitFor } from '@testing-library/react'; +import { SearchBar } from '../index'; +describe('SearchBar component', () => { + const componentProps = { + defaultMode: HAQL.id, + input: '', + modes: [ + { + id: HAQL.id, + implicitQuery: 'id!=000;', + suggestions: { + field(currentValue) { + return []; + }, + value(currentValue, { previousField }){ + return []; + }, + }, + } + ], + /* eslint-disable @typescript-eslint/no-empty-function */ + onChange: () => {}, + onSearch: () => {} + /* eslint-enable @typescript-eslint/no-empty-function */ + }; + + it('Renders correctly to match the snapshot of query language', async () => { + const wrapper = render( + + ); + + await waitFor(() => { + const elementImplicitQuery = wrapper.container.querySelector('.euiCodeBlock__code'); + expect(elementImplicitQuery?.innerHTML).toEqual('id!=000 and '); + expect(wrapper.container).toMatchSnapshot(); + }); + }); +}); + +/* eslint-disable max-len */ describe('Query language - HAQL', () => { // Tokenize the input function tokenCreator({type, value}){ @@ -108,6 +151,7 @@ describe('Query language - HAQL', () => { ).toEqual(suggestions); }); + // Transform specific query language to UQL (Unified Query Language) it.each` HAQL | UQL ${'field'} | ${'field'} @@ -135,5 +179,84 @@ describe('Query language - HAQL', () => { ${'( field = value ) and field2 > "custom value" '} | ${'(field=value);field2>custom value'} `('transformSpecificQLToUnifiedQL - HAQL $HAQL', ({HAQL, UQL}) => { expect(transformSpecificQLToUnifiedQL(HAQL)).toEqual(UQL); - }) + }); + + // When a suggestion is clicked, change the input text + it.each` + HAQL | clikedSuggestion | changedInput + ${''} | ${{type: { iconType: 'kqlField', color: 'tint4' }, label: 'field'}} | ${'field'} + ${'field'} | ${{type: { iconType: 'kqlField', color: 'tint4' }, label: 'field2'}} | ${'field2'} + ${'field'} | ${{type: { iconType: 'kqlOperand', color: 'tint1' }, label: '='}} | ${'field='} + ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value'}} | ${'field=value'} + ${'field='} | ${{type: { iconType: 'kqlOperand', color: 'tint1' }, label: '!='}} | ${'field!='} + ${'field=value'} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value2'}} | ${'field=value2'} + ${'field=value'} | ${{type: { iconType: 'kqlSelector', color: 'tint3' }, label: 'and'}} | ${'field=value and '} + ${'field=value and '} | ${{type: { iconType: 'kqlField', color: 'tint4' }, label: 'field2'}} | ${'field=value and field2'} + ${'field=value and field2'} | ${{type: { iconType: 'kqlOperand', color: 'tint1' }, label: '>'}} | ${'field=value and field2>'} + ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with spaces'}} | ${'field="with spaces"'} + ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with "spaces'}} | ${'field="with \\"spaces"'} + ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: '"value'}} | ${'field="\\"value"'} + ${''} | ${{type: { iconType: 'tokenDenseVector', color: 'tint3' }, label: '('}} | ${'('} + ${'('} | ${{type: { iconType: 'kqlField', color: 'tint4' }, label: 'field'}} | ${'(field'} + ${'(field'} | ${{type: { iconType: 'kqlField', color: 'tint4' }, label: 'field2'}} | ${'(field2'} + ${'(field'} | ${{type: { iconType: 'kqlOperand', color: 'tint1' }, label: '='}} | ${'(field='} + ${'(field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value'}} | ${'(field=value'} + ${'(field=value'} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value2'}} | ${'(field=value2'} + ${'(field=value'} | ${{type: { iconType: 'kqlSelector', color: 'tint3' }, label: 'or'}} | ${'(field=value or '} + ${'(field=value or '} | ${{type: { iconType: 'kqlField', color: 'tint4' }, label: 'field2'}} | ${'(field=value or field2'} + ${'(field=value or field2'} | ${{type: { iconType: 'kqlOperand', color: 'tint1' }, label: '>'}} | ${'(field=value or field2>'} + ${'(field=value or field2>'} | ${{type: { iconType: 'kqlOperand', color: 'tint1' }, label: '>'}} | ${'(field=value or field2>'} + ${'(field=value or field2>'} | ${{type: { iconType: 'kqlOperand', color: 'tint1' }, label: '~'}} | ${'(field=value or field2~'} + ${'(field=value or field2>'} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value2'}} | ${'(field=value or field2>value2'} + ${'(field=value or field2>value2'}| ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value3'}} | ${'(field=value or field2>value3'} + ${'(field=value or field2>value2'}| ${{type: { iconType: 'tokenDenseVector', color: 'tint3' }, label: ')'}} | ${'(field=value or field2>value2)'} + `('click suggestion - HAQL $HAQL => $changedInput', async ({HAQL: currentInput, clikedSuggestion, changedInput}) => { + // Mock input + let input = currentInput; + + const qlOutput = await HAQL.run(input, { + setInput: (value: string): void => { input = value; }, + queryLanguage: { + parameters: { + implicitQuery: '', + suggestions: { + field: () => ([]), + value: () => ([]) + } + } + } + }); + qlOutput.searchBarProps.onItemClick(clikedSuggestion); + expect(input).toEqual(changedInput); + }); + + // Transform the external input in UQL (Unified Query Language) to QL + it.each` + UQL | HAQL + ${''} | ${''} + ${'field'} | ${'field'} + ${'field='} | ${'field='} + ${'field!='} | ${'field!='} + ${'field>'} | ${'field>'} + ${'field<'} | ${'field<'} + ${'field~'} | ${'field~'} + ${'field=value'} | ${'field=value'} + ${'field=value;'} | ${'field=value and '} + ${'field=value;field2'} | ${'field=value and field2'} + ${'field="'} | ${'field="\\""'} + ${'field=with spaces'} | ${'field="with spaces"'} + ${'field=with "spaces'} | ${'field="with \\"spaces"'} + ${'('} | ${'('} + ${'(field'} | ${'(field'} + ${'(field='} | ${'(field='} + ${'(field=value'} | ${'(field=value'} + ${'(field=value,'} | ${'(field=value or '} + ${'(field=value,field2'} | ${'(field=value or field2'} + ${'(field=value,field2>'} | ${'(field=value or field2>'} + ${'(field=value,field2>value2'} | ${'(field=value or field2>value2'} + ${'(field=value,field2>value2)'} | ${'(field=value or field2>value2)'} + `('Transform the external input UQL to QL - UQL $UQL => $HAQL', async ({UQL, HAQL: changedInput}) => { + expect(HAQL.transformUnifiedQuery(UQL)).toEqual(changedInput); + }); + }); diff --git a/public/components/search-bar/query-language/haql.tsx b/public/components/search-bar/query-language/haql.tsx index bf45cc9afc..cda8a12dfc 100644 --- a/public/components/search-bar/query-language/haql.tsx +++ b/public/components/search-bar/query-language/haql.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { EuiButtonEmpty, EuiPopover, EuiText, EuiCode } from '@elastic/eui'; -import { tokenizerAPI as tokenizerUQL } from './aql'; +import { tokenizer as tokenizerUQL } from './aql'; import { pluginPlatform } from '../../../../package.json'; /* UI Query language @@ -38,9 +38,6 @@ Implemented schema: const language = { // Tokens tokens: { - field: { - regex: /[\w.]/, - }, // eslint-disable-next-line camelcase operator_compare: { literal: { @@ -80,6 +77,48 @@ const suggestionMappingLanguageTokenType = { function_search: { iconType: 'search', color: 'tint5' }, }; +/** + * Creator of intermediate interface of EuiSuggestItem + * @param type + * @returns + */ +function mapSuggestionCreator(type: ITokenType ){ + return function({...params}){ + return { + type, + ...params + }; + }; +}; + +const mapSuggestionCreatorField = mapSuggestionCreator('field'); +const mapSuggestionCreatorValue = mapSuggestionCreator('value'); + +/** + * Transform the conjunction to the query language syntax + * @param conjunction + * @returns + */ +function transformQLConjunction(conjunction: string): string{ + // If the value has a whitespace or comma, then + return conjunction === ';' + ? ' and ' + : ' or '; +}; + +/** + * Transform the value to the query language syntax + * @param value + * @returns + */ +function transformQLValue(value: string): string{ + // If the value has a whitespace or comma, then + return /[\s|"]/.test(value) + // Escape the commas (") => (\") and wraps the string with commas ("") + ? `"${value.replace(/"/, '\\"')}"` + // Raw value + : value; +}; /** * Tokenize the input string. Returns an array with the tokens. @@ -135,18 +174,31 @@ export function tokenizer(input: string): ITokens{ ).flat(); }; -type OptionSuggestionHandler = ( +type QLOptionSuggestionEntityItem = { + description?: string + label: string +}; + +type QLOptionSuggestionEntityItemTyped = + QLOptionSuggestionEntityItem + & { type: 'operator_group'|'field'|'operator_compare'|'value'|'conjunction' }; + +type SuggestItem = QLOptionSuggestionEntityItem & { + type: { iconType: string, color: string } +}; + +type QLOptionSuggestionHandler = ( currentValue: string | undefined, { previousField, previousOperatorCompare, }: { previousField: string; previousOperatorCompare: string }, -) => Promise<{ description?: string; label: string; type: string }[]>; +) => Promise; type optionsQL = { suggestions: { - field: OptionSuggestionHandler; - value: OptionSuggestionHandler; + field: QLOptionSuggestionHandler; + value: QLOptionSuggestionHandler; }; }; @@ -195,7 +247,7 @@ function getLastTokenWithValueByType( * @param options * @returns */ -export async function getSuggestions(tokens: ITokens, options: optionsQL) { +export async function getSuggestions(tokens: ITokens, options: optionsQL): Promise { if (!tokens.length) { return []; } @@ -207,7 +259,7 @@ export async function getSuggestions(tokens: ITokens, options: optionsQL) { if(!lastToken?.type){ return [ // fields - ...(await options.suggestions.field()), + ...(await options.suggestions.field()).map(mapSuggestionCreatorField), { type: 'operator_group', label: '(', @@ -223,7 +275,7 @@ export async function getSuggestions(tokens: ITokens, options: optionsQL) { ...(await options.suggestions.field()).filter( ({ label }) => label.startsWith(lastToken.value) && label !== lastToken.value, - ), + ).map(mapSuggestionCreatorField), // operators if the input field is exact ...((await options.suggestions.field()).some( ({ label }) => label === lastToken.value, @@ -241,7 +293,19 @@ export async function getSuggestions(tokens: ITokens, options: optionsQL) { : []), ]; break; - case 'operator_compare': + case 'operator_compare':{ + const previousField = getLastTokenWithValueByType(tokens, 'field')?.value; + const previousOperatorCompare = getLastTokenWithValueByType( + tokens, + 'operator_compare', + )?.value; + + // If there is no a previous field, then no return suggestions because it would be an syntax + // error + if(!previousField){ + return []; + }; + return [ ...Object.keys(language.tokens.operator_compare.literal) .filter( @@ -259,17 +323,27 @@ export async function getSuggestions(tokens: ITokens, options: optionsQL) { ) ? [ ...(await options.suggestions.value(undefined, { - previousField: getLastTokenWithValueByType(tokens, 'field')!.value, - previousOperatorCompare: getLastTokenWithValueByType( - tokens, - 'operator_compare', - )!.value, - })), + previousField, + previousOperatorCompare, + })).map(mapSuggestionCreatorValue), ] : []), ]; break; - case 'value': + } + case 'value':{ + const previousField = getLastTokenWithValueByType(tokens, 'field')?.value; + const previousOperatorCompare = getLastTokenWithValueByType( + tokens, + 'operator_compare', + )?.value; + + // If there is no a previous field or operator_compar, then no return suggestions because + //it would be an syntax error + if(!previousField || !previousOperatorCompare){ + return []; + }; + return [ ...(lastToken.value ? [ @@ -281,12 +355,9 @@ export async function getSuggestions(tokens: ITokens, options: optionsQL) { ] : []), ...(await options.suggestions.value(lastToken.value, { - previousField: getLastTokenWithValueByType(tokens, 'field')!.value, - previousOperatorCompare: getLastTokenWithValueByType( - tokens, - 'operator_compare', - )!.value, - })), + previousField, + previousOperatorCompare, + })).map(mapSuggestionCreatorValue), ...Object.entries(language.tokens.conjunction.literal).map( ([ conjunction, description]) => ({ type: 'conjunction', @@ -301,6 +372,7 @@ export async function getSuggestions(tokens: ITokens, options: optionsQL) { }, ]; break; + } case 'conjunction': return [ ...Object.keys(language.tokens.conjunction.literal) @@ -319,13 +391,7 @@ export async function getSuggestions(tokens: ITokens, options: optionsQL) { conjunction => conjunction === lastToken.value, ) ? [ - ...(await options.suggestions.field()).map( - ({ label, description }) => ({ - type: 'field', - label, - description, - }), - ), + ...(await options.suggestions.field()).map(mapSuggestionCreatorField), ] : []), { @@ -339,9 +405,7 @@ export async function getSuggestions(tokens: ITokens, options: optionsQL) { if (lastToken.value === '(') { return [ // fields - ...(await options.suggestions.field()).map( - ({ label, description }) => ({ type: 'field', label, description }), - ), + ...(await options.suggestions.field()).map(mapSuggestionCreatorField), ]; } else if (lastToken.value === ')') { return [ @@ -364,20 +428,28 @@ export async function getSuggestions(tokens: ITokens, options: optionsQL) { return []; } +/** + * Transform the suggestion object to the expected object by EuiSuggestItem + * @param param0 + * @returns + */ +export function transformSuggestionToEuiSuggestItem(suggestion: QLOptionSuggestionEntityItemTyped): SuggestItem{ + const { type, ...rest} = suggestion; + return { + type: { ...suggestionMappingLanguageTokenType[type] }, + ...rest + }; +}; + /** * Transform the suggestion object to the expected object by EuiSuggestItem * @param suggestions - * @param options * @returns */ -function transformSuggestionsToUI( - suggestions: { type: string; label: string; description?: string }[], - mapSuggestionByLanguageToken: any, -) { - return suggestions.map(({ type, ...rest }) => ({ - type: { ...mapSuggestionByLanguageToken[type] }, - ...rest, - })); +function transformSuggestionsToEuiSuggestItem( + suggestions: QLOptionSuggestionEntityItemTyped[] +): SuggestItem[] { + return suggestions.map(transformSuggestionToEuiSuggestItem); }; /** @@ -389,11 +461,19 @@ export function transformUnifiedQueryToSpecificQueryLanguage(input: string){ const tokens = tokenizerUQL(input); return tokens .filter(({value}) => value) - .map(({type, value}) => type === 'conjunction' - ? value === ';' - ? ' and ' - : ' or ' - : value + .map(({type, value}) => { + switch (type) { + case 'conjunction': + return transformQLConjunction(value); + break; + case 'value': + return transformQLValue(value); + break; + default: + return value; + break; + } + } ).join(''); }; @@ -466,9 +546,8 @@ export const HAQL = { searchBarProps: { // Props that will be used by the EuiSuggest component // Suggestions - suggestions: transformSuggestionsToUI( - await getSuggestions(tokens, params.queryLanguage.parameters), - suggestionMappingLanguageTokenType, + suggestions: transformSuggestionsToEuiSuggestItem( + await getSuggestions(tokens, params.queryLanguage.parameters) ), // Handler to manage when clicking in a suggestion item onItemClick: item => { @@ -487,9 +566,8 @@ export const HAQL = { // replace the value of last token lastToken.value = item.label; } else { - // add a whitespace for conjunction - input.length - && !(/\s$/.test(input)) + // add a whitespace for conjunction + !(/\s$/.test(input)) && item.type.iconType === suggestionMappingLanguageTokenType.conjunction.iconType && tokens.push({ type: 'whitespace', @@ -502,12 +580,11 @@ export const HAQL = { ([, { iconType }]) => iconType === item.type.iconType, )[0], value: item.type.iconType === suggestionMappingLanguageTokenType.value.iconType - && /\s/.test(item.label) - ? `"${item.label}"` + ? transformQLValue(item.label) : item.label, }); - // add a whitespace for conjunction + // add a whitespace for conjunction item.type.iconType === suggestionMappingLanguageTokenType.conjunction.iconType && tokens.push({ type: 'whitespace', diff --git a/public/controllers/agent/components/agents-table.js b/public/controllers/agent/components/agents-table.js index 0f9ff7640a..3de71e4a32 100644 --- a/public/controllers/agent/components/agents-table.js +++ b/public/controllers/agent/components/agents-table.js @@ -643,8 +643,7 @@ export const AgentsTable = withErrorBoundary( { label: 'os.platform', description: 'Operating system platform' }, { label: 'status', description: 'Status' }, { label: 'version', description: 'Version' }, - ] - .map(field => ({ type: 'field', ...field })); + ]; }, value: async (currentValue, { previousField }) => { switch (previousField) { @@ -686,7 +685,6 @@ export const AgentsTable = withErrorBoundary( case 'group_config_status': return [AGENT_SYNCED_STATUS.SYNCED, AGENT_SYNCED_STATUS.NOT_SYNCED].map( (status) => ({ - type: 'value', label: status, }), ); @@ -736,7 +734,6 @@ export const AgentsTable = withErrorBoundary( case 'status': return UI_ORDER_AGENT_STATUS.map( (status) => ({ - type: 'value', label: status, }), ); @@ -817,7 +814,6 @@ export const AgentsTable = withErrorBoundary( case 'group_config_status': return [AGENT_SYNCED_STATUS.SYNCED, AGENT_SYNCED_STATUS.NOT_SYNCED].map( (status) => ({ - type: 'value', label: status, }), ); @@ -867,7 +863,6 @@ export const AgentsTable = withErrorBoundary( case 'status': return UI_ORDER_AGENT_STATUS.map( (status) => ({ - type: 'value', label: status, }), ); From 22676d4ddc7e8ef6412d0e15f86a31836be6c0ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Wed, 15 Mar 2023 12:48:24 +0100 Subject: [PATCH 10/76] feat(search-bar): Rename HAQL query language to WQL - Rename query language HAQL to WQL - Update tests - Remove AQL usage from the implementation in the agents section --- .../__snapshots__/index.test.tsx.snap | 2 +- public/components/search-bar/index.test.tsx | 6 +- .../{haql.test.tsx.snap => wql.test.tsx.snap} | 2 +- .../search-bar/query-language/index.ts | 4 +- .../query-language/{haql.md => wql.md} | 4 +- .../{haql.test.tsx => wql.test.tsx} | 26 ++-- .../query-language/{haql.tsx => wql.tsx} | 12 +- .../agent/components/agents-table.js | 133 +----------------- 8 files changed, 30 insertions(+), 159 deletions(-) rename public/components/search-bar/query-language/__snapshots__/{haql.test.tsx.snap => wql.test.tsx.snap} (99%) rename public/components/search-bar/query-language/{haql.md => wql.md} (98%) rename public/components/search-bar/query-language/{haql.test.tsx => wql.test.tsx} (95%) rename public/components/search-bar/query-language/{haql.tsx => wql.tsx} (99%) diff --git a/public/components/search-bar/__snapshots__/index.test.tsx.snap b/public/components/search-bar/__snapshots__/index.test.tsx.snap index 007974639e..5602512bd0 100644 --- a/public/components/search-bar/__snapshots__/index.test.tsx.snap +++ b/public/components/search-bar/__snapshots__/index.test.tsx.snap @@ -43,7 +43,7 @@ exports[`SearchBar component Renders correctly the initial render 1`] = ` - HAQL + WQL diff --git a/public/components/search-bar/index.test.tsx b/public/components/search-bar/index.test.tsx index b77cb2baa8..be6d332b6b 100644 --- a/public/components/search-bar/index.test.tsx +++ b/public/components/search-bar/index.test.tsx @@ -4,7 +4,7 @@ import { SearchBar } from './index'; describe('SearchBar component', () => { const componentProps = { - defaultMode: 'haql', + defaultMode: 'wql', input: '', modes: [ { @@ -20,7 +20,7 @@ describe('SearchBar component', () => { }, }, { - id: 'haql', + id: 'wql', implicitQuery: 'id!=000;', suggestions: { field(currentValue) { @@ -42,7 +42,7 @@ describe('SearchBar component', () => { const wrapper = render( ); diff --git a/public/components/search-bar/query-language/__snapshots__/haql.test.tsx.snap b/public/components/search-bar/query-language/__snapshots__/wql.test.tsx.snap similarity index 99% rename from public/components/search-bar/query-language/__snapshots__/haql.test.tsx.snap rename to public/components/search-bar/query-language/__snapshots__/wql.test.tsx.snap index 8636885205..f1bad4e5d4 100644 --- a/public/components/search-bar/query-language/__snapshots__/haql.test.tsx.snap +++ b/public/components/search-bar/query-language/__snapshots__/wql.test.tsx.snap @@ -83,7 +83,7 @@ exports[`SearchBar component Renders correctly to match the snapshot of query la - HAQL + WQL diff --git a/public/components/search-bar/query-language/index.ts b/public/components/search-bar/query-language/index.ts index 22608fd9f5..9c0d6e2b9b 100644 --- a/public/components/search-bar/query-language/index.ts +++ b/public/components/search-bar/query-language/index.ts @@ -1,5 +1,5 @@ import { AQL } from './aql'; -import { HAQL } from './haql'; +import { WQL } from './wql'; type SearchBarQueryLanguage = { description: string; @@ -21,7 +21,7 @@ type SearchBarQueryLanguage = { // Register the query languages export const searchBarQueryLanguages: { [key: string]: SearchBarQueryLanguage; -} = [AQL, HAQL].reduce((accum, item) => { +} = [AQL, WQL].reduce((accum, item) => { if (accum[item.id]) { throw new Error(`Query language with id: ${item.id} already registered.`); } diff --git a/public/components/search-bar/query-language/haql.md b/public/components/search-bar/query-language/wql.md similarity index 98% rename from public/components/search-bar/query-language/haql.md rename to public/components/search-bar/query-language/wql.md index 8f5253eda0..7ff72ddc1f 100644 --- a/public/components/search-bar/query-language/haql.md +++ b/public/components/search-bar/query-language/wql.md @@ -1,6 +1,6 @@ -# Query Language - HAQL +# Query Language - WQL -HAQL (Human API Query Language) is a query language based in the `q` query parameters of the Wazuh API +WQL (Wazuh Query Language) is a query language based in the `q` query parameters of the Wazuh API endpoints. Documentation: https://wazuh.com/./user-manual/api/queries.html diff --git a/public/components/search-bar/query-language/haql.test.tsx b/public/components/search-bar/query-language/wql.test.tsx similarity index 95% rename from public/components/search-bar/query-language/haql.test.tsx rename to public/components/search-bar/query-language/wql.test.tsx index eeef8eb91a..be2e42a707 100644 --- a/public/components/search-bar/query-language/haql.test.tsx +++ b/public/components/search-bar/query-language/wql.test.tsx @@ -1,15 +1,15 @@ -import { getSuggestions, tokenizer, transformSpecificQLToUnifiedQL, HAQL } from './haql'; +import { getSuggestions, tokenizer, transformSpecificQLToUnifiedQL, WQL } from './wql'; import React from 'react'; import { render, waitFor } from '@testing-library/react'; import { SearchBar } from '../index'; describe('SearchBar component', () => { const componentProps = { - defaultMode: HAQL.id, + defaultMode: WQL.id, input: '', modes: [ { - id: HAQL.id, + id: WQL.id, implicitQuery: 'id!=000;', suggestions: { field(currentValue) { @@ -43,7 +43,7 @@ describe('SearchBar component', () => { }); /* eslint-disable max-len */ -describe('Query language - HAQL', () => { +describe('Query language - WQL', () => { // Tokenize the input function tokenCreator({type, value}){ return {type, value}; @@ -153,7 +153,7 @@ describe('Query language - HAQL', () => { // Transform specific query language to UQL (Unified Query Language) it.each` - HAQL | UQL + WQL | UQL ${'field'} | ${'field'} ${'field='} | ${'field='} ${'field=value'} | ${'field=value'} @@ -177,13 +177,13 @@ describe('Query language - HAQL', () => { ${'field = value or field2 <'} | ${'field=value,field2<'} ${'field = value or field2 < value2'} | ${'field=value,field2 "custom value" '} | ${'(field=value);field2>custom value'} - `('transformSpecificQLToUnifiedQL - HAQL $HAQL', ({HAQL, UQL}) => { - expect(transformSpecificQLToUnifiedQL(HAQL)).toEqual(UQL); + `('transformSpecificQLToUnifiedQL - WQL $WQL', ({WQL, UQL}) => { + expect(transformSpecificQLToUnifiedQL(WQL)).toEqual(UQL); }); // When a suggestion is clicked, change the input text it.each` - HAQL | clikedSuggestion | changedInput + WQL | clikedSuggestion | changedInput ${''} | ${{type: { iconType: 'kqlField', color: 'tint4' }, label: 'field'}} | ${'field'} ${'field'} | ${{type: { iconType: 'kqlField', color: 'tint4' }, label: 'field2'}} | ${'field2'} ${'field'} | ${{type: { iconType: 'kqlOperand', color: 'tint1' }, label: '='}} | ${'field='} @@ -210,11 +210,11 @@ describe('Query language - HAQL', () => { ${'(field=value or field2>'} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value2'}} | ${'(field=value or field2>value2'} ${'(field=value or field2>value2'}| ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value3'}} | ${'(field=value or field2>value3'} ${'(field=value or field2>value2'}| ${{type: { iconType: 'tokenDenseVector', color: 'tint3' }, label: ')'}} | ${'(field=value or field2>value2)'} - `('click suggestion - HAQL $HAQL => $changedInput', async ({HAQL: currentInput, clikedSuggestion, changedInput}) => { + `('click suggestion - WQL $WQL => $changedInput', async ({WQL: currentInput, clikedSuggestion, changedInput}) => { // Mock input let input = currentInput; - const qlOutput = await HAQL.run(input, { + const qlOutput = await WQL.run(input, { setInput: (value: string): void => { input = value; }, queryLanguage: { parameters: { @@ -232,7 +232,7 @@ describe('Query language - HAQL', () => { // Transform the external input in UQL (Unified Query Language) to QL it.each` - UQL | HAQL + UQL | WQL ${''} | ${''} ${'field'} | ${'field'} ${'field='} | ${'field='} @@ -255,8 +255,8 @@ describe('Query language - HAQL', () => { ${'(field=value,field2>'} | ${'(field=value or field2>'} ${'(field=value,field2>value2'} | ${'(field=value or field2>value2'} ${'(field=value,field2>value2)'} | ${'(field=value or field2>value2)'} - `('Transform the external input UQL to QL - UQL $UQL => $HAQL', async ({UQL, HAQL: changedInput}) => { - expect(HAQL.transformUnifiedQuery(UQL)).toEqual(changedInput); + `('Transform the external input UQL to QL - UQL $UQL => $WQL', async ({UQL, WQL: changedInput}) => { + expect(WQL.transformUnifiedQuery(UQL)).toEqual(changedInput); }); }); diff --git a/public/components/search-bar/query-language/haql.tsx b/public/components/search-bar/query-language/wql.tsx similarity index 99% rename from public/components/search-bar/query-language/haql.tsx rename to public/components/search-bar/query-language/wql.tsx index cda8a12dfc..970d25020c 100644 --- a/public/components/search-bar/query-language/haql.tsx +++ b/public/components/search-bar/query-language/wql.tsx @@ -517,17 +517,17 @@ export function transformSpecificQLToUnifiedQL(input: string){ function getOutput(input: string, options: {implicitQuery?: string} = {}) { const query = `${transformUnifiedQueryToSpecificQueryLanguage(options?.implicitQuery ?? '')}${input}`; return { - language: HAQL.id, + language: WQL.id, unifiedQuery: transformSpecificQLToUnifiedQL(query), query }; }; -export const HAQL = { - id: 'haql', - label: 'HAQL', - description: 'HAQL allows to do queries.', - documentationLink: `https://github.com/wazuh/wazuh-kibana-app/blob/v${pluginPlatform.version.split('.').splice(0,2).join('.')}/public/components/search-bar/query-language/haql.md`, +export const WQL = { + id: 'wql', + label: 'WQL', + description: 'WQL (Wazuh Query language) allows to do queries.', + documentationLink: `https://github.com/wazuh/wazuh-kibana-app/blob/v${pluginPlatform.version.split('.').splice(0,2).join('.')}/public/components/search-bar/query-language/wql.md`, getConfiguration() { return { isOpenPopoverImplicitFilter: false, diff --git a/public/controllers/agent/components/agents-table.js b/public/controllers/agent/components/agents-table.js index 3de71e4a32..272d8b9c0f 100644 --- a/public/controllers/agent/components/agents-table.js +++ b/public/controllers/agent/components/agents-table.js @@ -620,140 +620,11 @@ export const AgentsTable = withErrorBoundary( /> {/** Example implementation */} { - switch (previousField) { - case 'configSum': - return await getAgentFilterValuesMapToSearchBarSuggestion( - previousField, - currentValue, - {q: 'id!=000'} - ); - break; - case 'dateAdd': - return await getAgentFilterValuesMapToSearchBarSuggestion( - previousField, - currentValue, - {q: 'id!=000'} - ); - break; - case 'id': - return await getAgentFilterValuesMapToSearchBarSuggestion( - previousField, - currentValue, - {q: 'id!=000'} - ); - break; - case 'ip': - return await getAgentFilterValuesMapToSearchBarSuggestion( - previousField, - currentValue, - {q: 'id!=000'} - ); - break; - case 'group': - return await getAgentFilterValuesMapToSearchBarSuggestion( - previousField, - currentValue, - {q: 'id!=000'} - ); - break; - case 'group_config_status': - return [AGENT_SYNCED_STATUS.SYNCED, AGENT_SYNCED_STATUS.NOT_SYNCED].map( - (status) => ({ - label: status, - }), - ); - break; - case 'lastKeepAline': - return await getAgentFilterValuesMapToSearchBarSuggestion( - previousField, - currentValue, - {q: 'id!=000'} - ); - break; - case 'manager': - return await getAgentFilterValuesMapToSearchBarSuggestion( - previousField, - currentValue, - {q: 'id!=000'} - ); - break; - case 'mergedSum': - return await getAgentFilterValuesMapToSearchBarSuggestion( - previousField, - currentValue, - {q: 'id!=000'} - ); - break; - case 'name': - return await getAgentFilterValuesMapToSearchBarSuggestion( - previousField, - currentValue, - {q: 'id!=000'} - ); - break; - case 'node_name': - return await getAgentFilterValuesMapToSearchBarSuggestion( - previousField, - currentValue, - {q: 'id!=000'} - ); - break; - case 'os.platform': - return await getAgentFilterValuesMapToSearchBarSuggestion( - previousField, - currentValue, - {q: 'id!=000'} - ); - break; - case 'status': - return UI_ORDER_AGENT_STATUS.map( - (status) => ({ - label: status, - }), - ); - break; - case 'version': - return await getAgentFilterValuesMapToSearchBarSuggestion( - previousField, - currentValue, - {q: 'id!=000'} - ); - break; - default: - return []; - break; - } - }, - }, - }, - { - id: 'haql', + id: 'wql', implicitQuery: 'id!=000;', suggestions: { field(currentValue) { From 459a9329a8910b32a802ba4d1b20bced340ec308 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Wed, 15 Mar 2023 14:34:13 +0100 Subject: [PATCH 11/76] feat(search-bar): Add more use cases to the tests of WQL query language - Add more use cases to the test of WQL query language - Replace some literals by constants in the WQL query language implementation --- .../search-bar/query-language/wql.test.tsx | 93 ++++++++++++++++++- .../search-bar/query-language/wql.tsx | 21 +++-- 2 files changed, 105 insertions(+), 9 deletions(-) diff --git a/public/components/search-bar/query-language/wql.test.tsx b/public/components/search-bar/query-language/wql.test.tsx index be2e42a707..e30402390d 100644 --- a/public/components/search-bar/query-language/wql.test.tsx +++ b/public/components/search-bar/query-language/wql.test.tsx @@ -91,6 +91,24 @@ describe('Query language - WQL', () => { ${'field'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, tu.opCompare, tu.whitespace, tu.value, tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} ${'field='} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, tu.value, tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} ${'field=value'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field=and'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('and'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field=or'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('or'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field=valueand'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('valueand'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field=valueor'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('valueor'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field=value='} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value='), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field=value!='} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value!='), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field=value>'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value>'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field=value<'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value<'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field=value~'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value~'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie] /** ~ character is not supported as value in the q query parameter */} + ${'field="'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('"'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field="value and'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('"value and'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field="value and value2"'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('"value and value2"'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field="value or value2"'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('"value or value2"'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field="value = value2"'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('"value = value2"'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field="value != value2"'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('"value != value2"'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field="value > value2"'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('"value > value2"'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field="value < value2"'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('"value < value2"'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field="value ~ value2"'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('"value ~ value2"'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie] /** ~ character is not supported as value in the q query parameter */} ${'field=value and'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.conjunction('and'), tu.whitespace, ...tuBlankSerie]} ${'field=value and '} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.conjunction('and'), t.whitespace(' '), ...tuBlankSerie]} ${'field=value and field2'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.conjunction('and'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.field('field2'), tu.whitespace, tu.opCompare, tu.whitespace, tu.value, tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} @@ -153,12 +171,27 @@ describe('Query language - WQL', () => { // Transform specific query language to UQL (Unified Query Language) it.each` - WQL | UQL + WQL | UQL ${'field'} | ${'field'} ${'field='} | ${'field='} ${'field=value'} | ${'field=value'} ${'field=value()'} | ${'field=value()'} + ${'field=valueand'} | ${'field=valueand'} + ${'field=valueor'} | ${'field=valueor'} + ${'field=value='} | ${'field=value='} + ${'field=value!='} | ${'field=value!='} + ${'field=value>'} | ${'field=value>'} + ${'field=value<'} | ${'field=value<'} + ${'field=value~'} | ${'field=value~' /** ~ character is not supported as value in the q query parameter */} ${'field="custom value"'} | ${'field=custom value'} + ${'field="custom value()"'} | ${'field=custom value()'} + ${'field="value and value2"'} | ${'field=value and value2'} + ${'field="value or value2"'} | ${'field=value or value2'} + ${'field="value = value2"'} | ${'field=value = value2'} + ${'field="value != value2"'} | ${'field=value != value2'} + ${'field="value > value2"'} | ${'field=value > value2'} + ${'field="value < value2"'} | ${'field=value < value2'} + ${'field="value ~ value2"'} | ${'field=value ~ value2' /** ~ character is not supported as value in the q query parameter */} ${'field="custom \\"value"'} | ${'field=custom "value'} ${'field="custom \\"value\\""'} | ${'field=custom "value"'} ${'field=value and'} | ${'field=value;'} @@ -172,22 +205,47 @@ describe('Query language - WQL', () => { ${'(field=value) and field2>"value with spaces"'} | ${'(field=value);field2>value with spaces'} ${'field ='} | ${'field='} ${'field = value'} | ${'field=value'} + ${'field = value()'} | ${'field=value()'} + ${'field = valueand'} | ${'field=valueand'} + ${'field = valueor'} | ${'field=valueor'} + ${'field = value='} | ${'field=value='} + ${'field = value!='} | ${'field=value!='} + ${'field = value>'} | ${'field=value>'} + ${'field = value<'} | ${'field=value<'} + ${'field = value~'} | ${'field=value~' /** ~ character is not supported as value in the q query parameter */} + ${'field = "custom value"'} | ${'field=custom value'} + ${'field = "custom value()"'} | ${'field=custom value()'} + ${'field = "value and value2"'} | ${'field=value and value2'} + ${'field = "value or value2"'} | ${'field=value or value2'} + ${'field = "value = value2"'} | ${'field=value = value2'} + ${'field = "value != value2"'} | ${'field=value != value2'} + ${'field = "value > value2"'} | ${'field=value > value2'} + ${'field = "value < value2"'} | ${'field=value < value2'} + ${'field = "value ~ value2"'} | ${'field=value ~ value2' /** ~ character is not supported as value in the q query parameter */} ${'field = value or'} | ${'field=value,'} ${'field = value or field2'} | ${'field=value,field2'} ${'field = value or field2 <'} | ${'field=value,field2<'} ${'field = value or field2 < value2'} | ${'field=value,field2 "custom value" '} | ${'(field=value);field2>custom value'} - `('transformSpecificQLToUnifiedQL - WQL $WQL', ({WQL, UQL}) => { + `('transformSpecificQLToUnifiedQL - WQL $WQL TO UQL $UQL', ({WQL, UQL}) => { expect(transformSpecificQLToUnifiedQL(WQL)).toEqual(UQL); }); // When a suggestion is clicked, change the input text it.each` - WQL | clikedSuggestion | changedInput + WQL | clikedSuggestion | changedInput ${''} | ${{type: { iconType: 'kqlField', color: 'tint4' }, label: 'field'}} | ${'field'} ${'field'} | ${{type: { iconType: 'kqlField', color: 'tint4' }, label: 'field2'}} | ${'field2'} ${'field'} | ${{type: { iconType: 'kqlOperand', color: 'tint1' }, label: '='}} | ${'field='} ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value'}} | ${'field=value'} + ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value()'}} | ${'field=value()'} + ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'valueand'}} | ${'field=valueand'} + ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'valueor'}} | ${'field=valueor'} + ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value='}} | ${'field=value='} + ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value!='}} | ${'field=value!='} + ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value>'}} | ${'field=value>'} + ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value<'}} | ${'field=value<'} + ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value~'}} | ${'field=value~' /** ~ character is not supported as value in the q query parameter */} ${'field='} | ${{type: { iconType: 'kqlOperand', color: 'tint1' }, label: '!='}} | ${'field!='} ${'field=value'} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value2'}} | ${'field=value2'} ${'field=value'} | ${{type: { iconType: 'kqlSelector', color: 'tint3' }, label: 'and'}} | ${'field=value and '} @@ -195,6 +253,14 @@ describe('Query language - WQL', () => { ${'field=value and field2'} | ${{type: { iconType: 'kqlOperand', color: 'tint1' }, label: '>'}} | ${'field=value and field2>'} ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with spaces'}} | ${'field="with spaces"'} ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with "spaces'}} | ${'field="with \\"spaces"'} + ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with value()'}} | ${'field="with value()"'} + ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with and value'}}| ${'field="with and value"'} + ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with or value'}} | ${'field="with or value"'} + ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with = value'}} | ${'field="with = value"'} + ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with != value'}} | ${'field="with != value"'} + ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with > value'}} | ${'field="with > value"'} + ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with < value'}} | ${'field="with < value"'} + ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with ~ value'}} | ${'field="with ~ value"' /** ~ character is not supported as value in the q query parameter */} ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: '"value'}} | ${'field="\\"value"'} ${''} | ${{type: { iconType: 'tokenDenseVector', color: 'tint3' }, label: '('}} | ${'('} ${'('} | ${{type: { iconType: 'kqlField', color: 'tint4' }, label: 'field'}} | ${'(field'} @@ -236,6 +302,14 @@ describe('Query language - WQL', () => { ${''} | ${''} ${'field'} | ${'field'} ${'field='} | ${'field='} + ${'field=()'} | ${'field=()'} + ${'field=valueand'} | ${'field=valueand'} + ${'field=valueor'} | ${'field=valueor'} + ${'field=value='} | ${'field=value='} + ${'field=value!='} | ${'field=value!='} + ${'field=value>'} | ${'field=value>'} + ${'field=value<'} | ${'field=value<'} + ${'field=value~'} | ${'field=value~'} ${'field!='} | ${'field!='} ${'field>'} | ${'field>'} ${'field<'} | ${'field<'} @@ -246,6 +320,12 @@ describe('Query language - WQL', () => { ${'field="'} | ${'field="\\""'} ${'field=with spaces'} | ${'field="with spaces"'} ${'field=with "spaces'} | ${'field="with \\"spaces"'} + ${'field=value ()'} | ${'field="value ()"'} + ${'field=with and value'} | ${'field="with and value"'} + ${'field=with or value'} | ${'field="with or value"'} + ${'field=with = value'} | ${'field="with = value"'} + ${'field=with > value'} | ${'field="with > value"'} + ${'field=with < value'} | ${'field="with < value"'} ${'('} | ${'('} ${'(field'} | ${'(field'} ${'(field='} | ${'(field='} @@ -259,4 +339,11 @@ describe('Query language - WQL', () => { expect(WQL.transformUnifiedQuery(UQL)).toEqual(changedInput); }); + /* The ! and ~ characters can't be part of a value that contains examples. The tests doesn't + include these cases. + + Value examples: + - with != value + - with ~ value + */ }); diff --git a/public/components/search-bar/query-language/wql.tsx b/public/components/search-bar/query-language/wql.tsx index 970d25020c..1202a7cc81 100644 --- a/public/components/search-bar/query-language/wql.tsx +++ b/public/components/search-bar/query-language/wql.tsx @@ -62,6 +62,14 @@ const language = { }, }, }, + equivalencesToUQL:{ + conjunction:{ + literal:{ + 'and': ';', + 'or': ',', + } + } + } }; // Suggestion mapper by language token type @@ -101,9 +109,9 @@ const mapSuggestionCreatorValue = mapSuggestionCreator('value'); */ function transformQLConjunction(conjunction: string): string{ // If the value has a whitespace or comma, then - return conjunction === ';' - ? ' and ' - : ' or '; + return conjunction === language.equivalencesToUQL.conjunction.literal['and'] + ? ` ${language.tokens.conjunction.literal['and']} ` + : ` ${language.tokens.conjunction.literal['or']} `; }; /** @@ -491,15 +499,16 @@ export function transformSpecificQLToUnifiedQL(input: string){ case 'value':{ // Value is wrapped with " let [ _, extractedValue ] = value.match(/^"(.+)"$/) || [ null, null ]; - // Replace the escape commas (\") by comma (") + // Replace the escaped comma (\") by comma (") + // WARN: This could cause a problem with value that contains this sequence \" extractedValue && (extractedValue = extractedValue.replace(/\\"/g, '"')); return extractedValue || value; break; } case 'conjunction': return value === 'and' - ? ';' - : ','; + ? language.equivalencesToUQL.conjunction.literal['and'] + : language.equivalencesToUQL.conjunction.literal['or']; break; default: return value; From 315932a9900d929911d2696a9193c2f2746f2dd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Wed, 15 Mar 2023 14:39:38 +0100 Subject: [PATCH 12/76] feat(search-bar): enhance the documenation of query languages --- .../search-bar/query-language/aql.md | 4 ++- .../search-bar/query-language/wql.md | 30 ++++++++++++------- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/public/components/search-bar/query-language/aql.md b/public/components/search-bar/query-language/aql.md index 9dcb5bb0e0..293d2a049e 100644 --- a/public/components/search-bar/query-language/aql.md +++ b/public/components/search-bar/query-language/aql.md @@ -12,10 +12,12 @@ The implementation is adapted to work with the search bar component defined - Suggestions for `fields` (configurable), `operators` and `values` (configurable) - Support implicit query -## Language syntax +# Language syntax Documentation: https://wazuh.com/./user-manual/api/queries.html +# Developer notes + ## Options - `implicitQuery`: add an implicit query that is added to the user input. Optional. diff --git a/public/components/search-bar/query-language/wql.md b/public/components/search-bar/query-language/wql.md index 7ff72ddc1f..4c19b731ca 100644 --- a/public/components/search-bar/query-language/wql.md +++ b/public/components/search-bar/query-language/wql.md @@ -12,15 +12,15 @@ The implementation is adapted to work with the search bar component defined - Suggestions for `fields` (configurable), `operators` and `values` (configurable) - Support implicit query -## Language syntax +# Language syntax -### Schema +## Schema ``` ???????????? ``` -### Fields +## Fields Regular expression: /[\\w.]+/ @@ -31,9 +31,9 @@ field field.custom ``` -### Operators +## Operators -#### Compare +### Compare - `=` equal to - `!=` not equal to @@ -41,15 +41,15 @@ field.custom - `<` smaller - `~` like -#### Group +### Group - `(` open - `)` close -#### Logical +### Conjunction (logical) -- `and` -- `or` +- `and` intersection +- `or` union ### Values @@ -93,6 +93,8 @@ status != never_connected and ip ~ 240 or os.platform ~ linux ( status != never_connected and ip ~ 240 ) or id = 001 ``` +## Developer notes + ## Options - `implicitQuery`: add an implicit query that is added to the user input. Optional. @@ -275,4 +277,12 @@ graph TD; end output-->output_search_bar[Output] -``` \ No newline at end of file +``` + +## Notes + +- The value that contains the following characters: `!`, `~` are not supported by the AQL and this +could cause problems when do the request to the API. +- The value with spaces are wrapped with `"`. If the value contains the `\"` sequence this is +replaced by `"`. This could cause a problem with values that are intended to have the mentioned +sequence. \ No newline at end of file From 4303a1af4b0bf72d790c4aa4b47750c2653db914 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Wed, 15 Mar 2023 14:50:05 +0100 Subject: [PATCH 13/76] feat(search-bar): Add a popover title to replicate similar UI to the platform search bar --- public/components/search-bar/index.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/public/components/search-bar/index.tsx b/public/components/search-bar/index.tsx index 63b1d5a49a..fd1be3cdab 100644 --- a/public/components/search-bar/index.tsx +++ b/public/components/search-bar/index.tsx @@ -4,6 +4,7 @@ import { EuiFormRow, EuiLink, EuiPopover, + EuiPopoverTitle, EuiSpacer, EuiSelect, EuiText, @@ -141,6 +142,7 @@ export const SearchBar = ({ isOpen={isOpenPopoverQueryLanguage} closePopover={onQueryLanguagePopoverSwitch} > + SYNTAX OPTIONS {searchBarQueryLanguages[queryLanguage.id].description} From 5e1485c330e1204769aca21bc9ab2cfabbaa4473 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Wed, 15 Mar 2023 14:51:46 +0100 Subject: [PATCH 14/76] feat(search-bar): wrap the user input with group operators when there is an implicit query --- public/components/search-bar/query-language/aql.tsx | 2 +- public/components/search-bar/query-language/wql.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/components/search-bar/query-language/aql.tsx b/public/components/search-bar/query-language/aql.tsx index a93f33df19..9cb38211d5 100644 --- a/public/components/search-bar/query-language/aql.tsx +++ b/public/components/search-bar/query-language/aql.tsx @@ -409,7 +409,7 @@ function transformSuggestionsToEuiSuggestItem( * @returns */ function getOutput(input: string, options: {implicitQuery?: string} = {}) { - const unifiedQuery = `${options?.implicitQuery ?? ''}${input}`; + const unifiedQuery = `${options?.implicitQuery ?? ''}${options?.implicitQuery ? `(${input})` : input}`; return { language: AQL.id, query: unifiedQuery, diff --git a/public/components/search-bar/query-language/wql.tsx b/public/components/search-bar/query-language/wql.tsx index 1202a7cc81..346569c5ec 100644 --- a/public/components/search-bar/query-language/wql.tsx +++ b/public/components/search-bar/query-language/wql.tsx @@ -524,7 +524,7 @@ export function transformSpecificQLToUnifiedQL(input: string){ * @returns */ function getOutput(input: string, options: {implicitQuery?: string} = {}) { - const query = `${transformUnifiedQueryToSpecificQueryLanguage(options?.implicitQuery ?? '')}${input}`; + const query = `${transformUnifiedQueryToSpecificQueryLanguage(options?.implicitQuery ?? '')}${options?.implicitQuery ? `(${input})` : input}`; return { language: WQL.id, unifiedQuery: transformSpecificQLToUnifiedQL(query), From 9983153383cd94165d62929173a434aaf6fc9253 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Thu, 16 Mar 2023 16:42:30 +0100 Subject: [PATCH 15/76] feat(search-bar): add implicit query mode to WQL - WQL - add implicit query mode to WQL - enhance query language documentation - renamed transformUnifiedQuery to transformUQLToQL - now wraps the user input if this is defined and there a implicit query string - fix a problem with the value suggestions if there is a previous conjunction - add tests cases - update tests - AQL - enhance query language documentation - renamed transformUnifiedQuery to transformUQLToQL - add warning about the query language implementation is not updated to the last changes in the search bar component - update tests - Search Bar - renamed transformUnifiedQuery to transformUQLToQL --- public/components/search-bar/README.md | 12 +- public/components/search-bar/index.test.tsx | 5 +- public/components/search-bar/index.tsx | 8 +- .../search-bar/query-language/aql.md | 2 + .../search-bar/query-language/aql.test.tsx | 2 +- .../search-bar/query-language/aql.tsx | 2 +- .../search-bar/query-language/index.ts | 2 +- .../search-bar/query-language/wql.md | 85 +++++++++++-- .../search-bar/query-language/wql.test.tsx | 13 +- .../search-bar/query-language/wql.tsx | 119 +++++++++++++----- .../agent/components/agents-table.js | 20 ++- 11 files changed, 210 insertions(+), 60 deletions(-) diff --git a/public/components/search-bar/README.md b/public/components/search-bar/README.md index df007a4ea0..58e557a26c 100644 --- a/public/components/search-bar/README.md +++ b/public/components/search-bar/README.md @@ -26,13 +26,15 @@ Basic usage: // The ID of them should be registered previously. See How to add a new query language documentation. modes={[ { - id: 'aql', + id: 'wql', // specific query language parameters // implicit query. Optional // Set a implicit query that can't be changed by the user. // Use the UQL (Unified Query Language) syntax. - // Each query language implementation must interpret - implicitQuery: 'id!=000;', + implicitQuery: { + query: 'id!=000', + conjunction: ';' + }, suggestions: { field(currentValue) { return [ @@ -202,7 +204,7 @@ type SearchBarQueryLanguage = { query: string } }>; - transformUnifiedQuery: (unifiedQuery: string) => string; + transformUQLToQL: (unifiedQuery: string) => string; }; ``` @@ -221,7 +223,7 @@ where: - `language`: query language ID - `unifiedQuery`: query in unified query syntax - `query`: current query in the specified language -- `transformUnifiedQuery`: method that transform the Unified Query Language to the specific query +- `transformUQLToQL`: method that transforms the UQL (Unified Query Language) to the specific query language. This is used when receives a external input in the Unified Query Language, the returned value is converted to the specific query language to set the new input text of the search bar component. diff --git a/public/components/search-bar/index.test.tsx b/public/components/search-bar/index.test.tsx index be6d332b6b..2f8f2366a4 100644 --- a/public/components/search-bar/index.test.tsx +++ b/public/components/search-bar/index.test.tsx @@ -21,7 +21,10 @@ describe('SearchBar component', () => { }, { id: 'wql', - implicitQuery: 'id!=000;', + implicitQuery: { + query: 'id!=000', + conjunction: ';' + }, suggestions: { field(currentValue) { return []; diff --git a/public/components/search-bar/index.tsx b/public/components/search-bar/index.tsx index fd1be3cdab..e902f553f4 100644 --- a/public/components/search-bar/index.tsx +++ b/public/components/search-bar/index.tsx @@ -12,7 +12,7 @@ import { import { EuiSuggest } from '../eui-suggest'; import { searchBarQueryLanguages } from './query-language'; -type Props = { +type SearchBarProps = { defaultMode?: string; modes: { id: string; [key: string]: any }[]; onChange?: (params: any) => void; @@ -26,7 +26,7 @@ export const SearchBar = ({ onChange, onSearch, ...rest -}: Props) => { +}: SearchBarProps) => { // Query language ID and configuration const [queryLanguage, setQueryLanguage] = useState<{ id: string; @@ -73,10 +73,10 @@ export const SearchBar = ({ }; useEffect(() => { - // React to external changes and set the internal input text. Use the `transformUnifiedQuery` of + // React to external changes and set the internal input text. Use the `transformUQLToQL` of // the query language in use rest.input && setInput( - searchBarQueryLanguages[queryLanguage.id]?.transformUnifiedQuery?.( + searchBarQueryLanguages[queryLanguage.id]?.transformUQLToQL?.( rest.input, ), ); diff --git a/public/components/search-bar/query-language/aql.md b/public/components/search-bar/query-language/aql.md index 293d2a049e..9d144e3b15 100644 --- a/public/components/search-bar/query-language/aql.md +++ b/public/components/search-bar/query-language/aql.md @@ -1,3 +1,5 @@ +**WARNING: The search bar was changed and this language needs some adaptations to work.** + # Query Language - AQL AQL (API Query Language) is a query language based in the `q` query parameters of the Wazuh API diff --git a/public/components/search-bar/query-language/aql.test.tsx b/public/components/search-bar/query-language/aql.test.tsx index 597d31188a..86fc9de5c9 100644 --- a/public/components/search-bar/query-language/aql.test.tsx +++ b/public/components/search-bar/query-language/aql.test.tsx @@ -200,6 +200,6 @@ describe('Query language - AQL', () => { ${'(field=value,field2>value2'} | ${'(field=value,field2>value2'} ${'(field=value,field2>value2)'} | ${'(field=value,field2>value2)'} `('Transform the external input UQL to QL - UQL $UQL => $AQL', async ({UQL, AQL: changedInput}) => { - expect(AQL.transformUnifiedQuery(UQL)).toEqual(changedInput); + expect(AQL.transformUQLToQL(UQL)).toEqual(changedInput); }); }); diff --git a/public/components/search-bar/query-language/aql.tsx b/public/components/search-bar/query-language/aql.tsx index 9cb38211d5..4256cd4540 100644 --- a/public/components/search-bar/query-language/aql.tsx +++ b/public/components/search-bar/query-language/aql.tsx @@ -517,7 +517,7 @@ export const AQL = { output: getOutput(input, params.queryLanguage.parameters), }; }, - transformUnifiedQuery(unifiedQuery: string): string { + transformUQLToQL(unifiedQuery: string): string { return unifiedQuery; }, }; diff --git a/public/components/search-bar/query-language/index.ts b/public/components/search-bar/query-language/index.ts index 9c0d6e2b9b..fc95717fca 100644 --- a/public/components/search-bar/query-language/index.ts +++ b/public/components/search-bar/query-language/index.ts @@ -15,7 +15,7 @@ type SearchBarQueryLanguage = { query: string } }>; - transformUnifiedQuery: (unifiedQuery: string) => string; + transformUQLToQL: (unifiedQuery: string) => string; }; // Register the query languages diff --git a/public/components/search-bar/query-language/wql.md b/public/components/search-bar/query-language/wql.md index 4c19b731ca..b99538b9c7 100644 --- a/public/components/search-bar/query-language/wql.md +++ b/public/components/search-bar/query-language/wql.md @@ -8,19 +8,24 @@ Documentation: https://wazuh.com/./user-manual/api The implementation is adapted to work with the search bar component defined `public/components/search-bar/index.tsx`. -## Features -- Suggestions for `fields` (configurable), `operators` and `values` (configurable) -- Support implicit query - # Language syntax -## Schema +It supports 2 modes: + +- `explicit`: define the field, operator and value +- `implicit`: use a term to search in the available fields + +Theses modes can not be combined. + +## Mode: explicit + +### Schema ``` ???????????? ``` -## Fields +### Fields Regular expression: /[\\w.]+/ @@ -31,9 +36,9 @@ field field.custom ``` -## Operators +### Operators -### Compare +#### Compare - `=` equal to - `!=` not equal to @@ -41,17 +46,17 @@ field.custom - `<` smaller - `~` like -### Group +#### Group - `(` open - `)` close -### Conjunction (logical) +#### Conjunction (logical) - `and` intersection - `or` union -### Values +#### Values - Value without spaces can be literal - Value with spaces should be wrapped by `"`. The `"` can be escaped using `\"`. @@ -93,19 +98,73 @@ status != never_connected and ip ~ 240 or os.platform ~ linux ( status != never_connected and ip ~ 240 ) or id = 001 ``` +## Mode: implicit + +Search the term in the available fields. + +This mode is used when there is no a `field` and `operator` attending to the regular expression +of the **explicit** mode. + +### Examples: + +``` +linux +``` + +If the available fields are `id` and `ip`, then the input will be translated under the hood to the +following UQL syntax: + +``` +id~linux,ip~linux +``` + ## Developer notes +## Features +- Support suggestions for each token entity. `fields` and `values` are customizable. +- Support implicit query. +- Support for search term mode. It enables to search a term in multiple fields. + The query is built under the hoods. This mode requires there are `field` and `operator_compare`. + +### Implicit query + +This a query that can't be added, edited or removed by the user. It is added to the user input. + +### Search term mode + +This mode enables to search in multiple fields. The fields to use must be defined. + +Use an union expression of each field with the like as operation `~`. + +The user input is transformed to something as: +``` +field1~user_input,field2~user_input,field3~user_input +``` + ## Options - `implicitQuery`: add an implicit query that is added to the user input. Optional. + This can't be changed by the user. If this is defined, will be displayed as a prepend of the search bar. + - `query`: query string in UQL (Unified Query Language) Use UQL (Unified Query Language). -This can't be changed by the user. If this is defined, will be displayed as a prepend of the search bar. + - `conjunction`: query string of the conjunction in UQL (Unified Query Language) + ```ts // language options // ID is not equal to 000 and . This is defined in UQL that is transformed internally to // the specific query language. -implicitQuery: 'id!=000;' +implicitQuery: { + query: 'id!=000', + conjunction: ';' +} +``` + +- `searchTermFields`: define the fields used to build the query for the search term mode + +```ts +// language options +searchTermFields: ['id', 'ip'] ``` - `suggestions`: define the suggestion handlers. This is required. diff --git a/public/components/search-bar/query-language/wql.test.tsx b/public/components/search-bar/query-language/wql.test.tsx index e30402390d..89ac5c50ae 100644 --- a/public/components/search-bar/query-language/wql.test.tsx +++ b/public/components/search-bar/query-language/wql.test.tsx @@ -10,7 +10,10 @@ describe('SearchBar component', () => { modes: [ { id: WQL.id, - implicitQuery: 'id!=000;', + implicitQuery: { + query: 'id!=000', + conjunction: ';' + }, suggestions: { field(currentValue) { return []; @@ -249,6 +252,9 @@ describe('Query language - WQL', () => { ${'field='} | ${{type: { iconType: 'kqlOperand', color: 'tint1' }, label: '!='}} | ${'field!='} ${'field=value'} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value2'}} | ${'field=value2'} ${'field=value'} | ${{type: { iconType: 'kqlSelector', color: 'tint3' }, label: 'and'}} | ${'field=value and '} + ${'field=value and'} | ${{type: { iconType: 'kqlSelector', color: 'tint3' }, label: 'or'}} | ${'field=value or'} + ${'field=value and'} | ${{type: { iconType: 'kqlField', color: 'tint4' }, label: 'field2'}} | ${'field=value and field2'} + ${'field=value and '} | ${{type: { iconType: 'kqlSelector', color: 'tint3' }, label: 'or'}} | ${'field=value or '} ${'field=value and '} | ${{type: { iconType: 'kqlField', color: 'tint4' }, label: 'field2'}} | ${'field=value and field2'} ${'field=value and field2'} | ${{type: { iconType: 'kqlOperand', color: 'tint1' }, label: '>'}} | ${'field=value and field2>'} ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with spaces'}} | ${'field="with spaces"'} @@ -269,6 +275,9 @@ describe('Query language - WQL', () => { ${'(field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value'}} | ${'(field=value'} ${'(field=value'} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value2'}} | ${'(field=value2'} ${'(field=value'} | ${{type: { iconType: 'kqlSelector', color: 'tint3' }, label: 'or'}} | ${'(field=value or '} + ${'(field=value or'} | ${{type: { iconType: 'kqlSelector', color: 'tint3' }, label: 'and'}} | ${'(field=value and'} + ${'(field=value or'} | ${{type: { iconType: 'kqlField', color: 'tint4' }, label: 'field2'}} | ${'(field=value or field2'} + ${'(field=value or '} | ${{type: { iconType: 'kqlSelector', color: 'tint3' }, label: 'and'}} | ${'(field=value and '} ${'(field=value or '} | ${{type: { iconType: 'kqlField', color: 'tint4' }, label: 'field2'}} | ${'(field=value or field2'} ${'(field=value or field2'} | ${{type: { iconType: 'kqlOperand', color: 'tint1' }, label: '>'}} | ${'(field=value or field2>'} ${'(field=value or field2>'} | ${{type: { iconType: 'kqlOperand', color: 'tint1' }, label: '>'}} | ${'(field=value or field2>'} @@ -336,7 +345,7 @@ describe('Query language - WQL', () => { ${'(field=value,field2>value2'} | ${'(field=value or field2>value2'} ${'(field=value,field2>value2)'} | ${'(field=value or field2>value2)'} `('Transform the external input UQL to QL - UQL $UQL => $WQL', async ({UQL, WQL: changedInput}) => { - expect(WQL.transformUnifiedQuery(UQL)).toEqual(changedInput); + expect(WQL.transformUQLToQL(UQL)).toEqual(changedInput); }); /* The ! and ~ characters can't be part of a value that contains examples. The tests doesn't diff --git a/public/components/search-bar/query-language/wql.tsx b/public/components/search-bar/query-language/wql.tsx index 346569c5ec..795417e7d1 100644 --- a/public/components/search-bar/query-language/wql.tsx +++ b/public/components/search-bar/query-language/wql.tsx @@ -203,7 +203,13 @@ type QLOptionSuggestionHandler = ( }: { previousField: string; previousOperatorCompare: string }, ) => Promise; -type optionsQL = { +type OptionsQLImplicitQuery = { + query: string + conjunction: string +} +type OptionsQL = { + implicitQuery?: OptionsQLImplicitQuery + searchTermFields?: string[] suggestions: { field: QLOptionSuggestionHandler; value: QLOptionSuggestionHandler; @@ -216,7 +222,7 @@ type optionsQL = { * @param tokenType token type to search * @returns */ -function getLastTokenWithValue( +function getLastTokenDefined( tokens: ITokens ): IToken | undefined { // Reverse the tokens array and use the Array.protorype.find method @@ -234,7 +240,7 @@ function getLastTokenWithValue( * @param tokenType token type to search * @returns */ -function getLastTokenWithValueByType( +function getLastTokenDefinedByType( tokens: ITokens, tokenType: ITokenType, ): IToken | undefined { @@ -255,13 +261,13 @@ function getLastTokenWithValueByType( * @param options * @returns */ -export async function getSuggestions(tokens: ITokens, options: optionsQL): Promise { +export async function getSuggestions(tokens: ITokens, options: OptionsQL): Promise { if (!tokens.length) { return []; } // Get last token - const lastToken = getLastTokenWithValue(tokens); + const lastToken = getLastTokenDefined(tokens); // If it can't get a token with value, then returns fields and open operator group if(!lastToken?.type){ @@ -302,8 +308,8 @@ export async function getSuggestions(tokens: ITokens, options: optionsQL): Promi ]; break; case 'operator_compare':{ - const previousField = getLastTokenWithValueByType(tokens, 'field')?.value; - const previousOperatorCompare = getLastTokenWithValueByType( + const previousField = getLastTokenDefinedByType(tokens, 'field')?.value; + const previousOperatorCompare = getLastTokenDefinedByType( tokens, 'operator_compare', )?.value; @@ -340,14 +346,14 @@ export async function getSuggestions(tokens: ITokens, options: optionsQL): Promi break; } case 'value':{ - const previousField = getLastTokenWithValueByType(tokens, 'field')?.value; - const previousOperatorCompare = getLastTokenWithValueByType( + const previousField = getLastTokenDefinedByType(tokens, 'field')?.value; + const previousOperatorCompare = getLastTokenDefinedByType( tokens, 'operator_compare', )?.value; - // If there is no a previous field or operator_compar, then no return suggestions because - //it would be an syntax error + /* If there is no a previous field or operator_compare, then no return suggestions because + it would be an syntax error */ if(!previousField || !previousOperatorCompare){ return []; }; @@ -461,11 +467,11 @@ function transformSuggestionsToEuiSuggestItem( }; /** - * Transform the UQL (Unified Query Language) to SpecificQueryLanguage + * Transform the UQL (Unified Query Language) to QL * @param input * @returns */ -export function transformUnifiedQueryToSpecificQueryLanguage(input: string){ +export function transformUQLToQL(input: string){ const tokens = tokenizerUQL(input); return tokens .filter(({value}) => value) @@ -485,13 +491,29 @@ export function transformUnifiedQueryToSpecificQueryLanguage(input: string){ ).join(''); }; +export function shouldUseSearchTerm(tokens: ITokens): boolean{ + return !( + tokens.some(({type, value}) => type === 'operator_compare' && value ) + && tokens.some(({type, value}) => type === 'field' && value ) + ); +}; + +export function transformToSearchTerm(searchTermFields: string[], input: string): string{ + return searchTermFields.map(searchTermField => `${searchTermField}~${input}`).join(','); +}; + /** - * Transform the input in SpecificQueryLanguage to UQL (Unified Query Language) + * Transform the input in QL to UQL (Unified Query Language) * @param input * @returns */ -export function transformSpecificQLToUnifiedQL(input: string){ +export function transformSpecificQLToUnifiedQL(input: string, searchTermFields: string[]){ const tokens = tokenizer(input); + + if(input && searchTermFields && shouldUseSearchTerm(tokens)){ + return transformToSearchTerm(searchTermFields, input); + }; + return tokens .filter(({type, value}) => type !== 'whitespace' && value) .map(({type, value}) => { @@ -523,19 +545,45 @@ export function transformSpecificQLToUnifiedQL(input: string){ * @param input * @returns */ -function getOutput(input: string, options: {implicitQuery?: string} = {}) { - const query = `${transformUnifiedQueryToSpecificQueryLanguage(options?.implicitQuery ?? '')}${options?.implicitQuery ? `(${input})` : input}`; +function getOutput(input: string, options: OptionsQL) { + // Implicit query + const implicitQueryAsUQL = options?.implicitQuery?.query ?? ''; + const implicitQueryAsQL = transformUQLToQL( + implicitQueryAsUQL + ); + + // Implicit query conjunction + const implicitQueryConjunctionAsUQL = options?.implicitQuery?.conjunction ?? ''; + const implicitQueryConjunctionAsQL = transformUQLToQL( + implicitQueryConjunctionAsUQL + ); + + // User input query + const inputQueryAsQL = input; + const inputQueryAsUQL = transformSpecificQLToUnifiedQL( + inputQueryAsQL, + options?.searchTermFields ?? [] + ); + return { language: WQL.id, - unifiedQuery: transformSpecificQLToUnifiedQL(query), - query + unifiedQuery: [ + implicitQueryAsUQL, + implicitQueryAsUQL && inputQueryAsUQL ? implicitQueryConjunctionAsUQL : '', + implicitQueryAsUQL && inputQueryAsUQL ? `(${inputQueryAsUQL})`: inputQueryAsUQL + ].join(''), + query: [ + implicitQueryAsQL, + implicitQueryAsQL && inputQueryAsQL ? implicitQueryConjunctionAsQL : '', + implicitQueryAsQL && inputQueryAsQL ? `(${inputQueryAsQL})`: inputQueryAsQL + ].join('') }; }; export const WQL = { id: 'wql', label: 'WQL', - description: 'WQL (Wazuh Query language) allows to do queries.', + description: 'WQL (Wazuh Query Language) allows to do queries.', documentationLink: `https://github.com/wazuh/wazuh-kibana-app/blob/v${pluginPlatform.version.split('.').splice(0,2).join('.')}/public/components/search-bar/query-language/wql.md`, getConfiguration() { return { @@ -546,11 +594,17 @@ export const WQL = { // Get the tokens from the input const tokens: ITokens = tokenizer(input); - // - const implicitQueryAsSpecificQueryLanguage = params.queryLanguage.parameters.implicitQuery - ? transformUnifiedQueryToSpecificQueryLanguage(params.queryLanguage.parameters.implicitQuery) + // Get the implicit query as query language syntax + const implicitQueryAsQL = params.queryLanguage.parameters.implicitQuery + ? transformUQLToQL( + params.queryLanguage.parameters.implicitQuery.query + + params.queryLanguage.parameters.implicitQuery.conjunction + ) : ''; + // Get the output of query language + const output = getOutput(input, params.queryLanguage.parameters); + return { searchBarProps: { // Props that will be used by the EuiSuggest component @@ -563,10 +617,10 @@ export const WQL = { // When the clicked item has the `search` iconType, run the `onSearch` function if (item.type.iconType === 'search') { // Execute the search action - params.onSearch(getOutput(input, params.queryLanguage.parameters)); + params.onSearch(output); } else { // When the clicked item has another iconType - const lastToken: IToken | undefined = getLastTokenWithValue(tokens); + const lastToken: IToken | undefined = getLastTokenDefined(tokens); // if the clicked suggestion is of same type of last token if ( lastToken && suggestionMappingLanguageTokenType[lastToken.type].iconType === @@ -577,7 +631,10 @@ export const WQL = { } else { // add a whitespace for conjunction !(/\s$/.test(input)) - && item.type.iconType === suggestionMappingLanguageTokenType.conjunction.iconType + && ( + item.type.iconType === suggestionMappingLanguageTokenType.conjunction.iconType + || lastToken?.type === 'conjunction' + ) && tokens.push({ type: 'whitespace', value: ' ' @@ -610,7 +667,7 @@ export const WQL = { .join('')); } }, - prepend: implicitQueryAsSpecificQueryLanguage ? ( + prepend: implicitQueryAsQL ? ( - {implicitQueryAsSpecificQueryLanguage} + {implicitQueryAsQL} } @@ -640,7 +697,7 @@ export const WQL = { > Implicit query:{' '} - {implicitQueryAsSpecificQueryLanguage} + {implicitQueryAsQL} This query is added to the input. @@ -651,8 +708,8 @@ export const WQL = { // use case. disableFocusTrap: true }, - output: getOutput(input, params.queryLanguage.parameters), + output, }; }, - transformUnifiedQuery: transformUnifiedQueryToSpecificQueryLanguage, + transformUQLToQL: transformUQLToQL, }; diff --git a/public/controllers/agent/components/agents-table.js b/public/controllers/agent/components/agents-table.js index 272d8b9c0f..82ce73d88d 100644 --- a/public/controllers/agent/components/agents-table.js +++ b/public/controllers/agent/components/agents-table.js @@ -625,7 +625,25 @@ export const AgentsTable = withErrorBoundary( modes={[ { id: 'wql', - implicitQuery: 'id!=000;', + implicitQuery: { + query: 'id!=000', + conjunction: ';' + }, + searchTermFields: [ + 'configSum', + 'dateAdd', + 'id', + 'group', + 'group_config_status', + 'lastKeepAline', + 'manager', + 'mergedSum', + 'name', + 'node_name', + 'os.platform', + 'status', + 'version' + ], suggestions: { field(currentValue) { return [ From 1eaf94400c83181550a71d6dea07c8d2e5e69937 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Thu, 23 Mar 2023 09:12:07 +0100 Subject: [PATCH 16/76] feat(search-bar): set the width of the syntax options popover --- public/components/search-bar/index.tsx | 94 +++++++++++++------------- 1 file changed, 48 insertions(+), 46 deletions(-) diff --git a/public/components/search-bar/index.tsx b/public/components/search-bar/index.tsx index e902f553f4..2ecf57f858 100644 --- a/public/components/search-bar/index.tsx +++ b/public/components/search-bar/index.tsx @@ -143,52 +143,54 @@ export const SearchBar = ({ closePopover={onQueryLanguagePopoverSwitch} > SYNTAX OPTIONS - - {searchBarQueryLanguages[queryLanguage.id].description} - - {searchBarQueryLanguages[queryLanguage.id].documentationLink && ( - <> - -
- - Documentation - -
- - )} - {modes?.length > 1 && ( - <> - - - ({ - value: id, - text: searchBarQueryLanguages[id].label, - }))} - value={queryLanguage.id} - onChange={(event: React.ChangeEvent) => { - const queryLanguageID: string = event.target.value; - setQueryLanguage({ - id: queryLanguageID, - configuration: - searchBarQueryLanguages[ - queryLanguageID - ]?.getConfiguration?.() || {}, - }); - setInput(''); - }} - aria-label='query-language-selector' - /> - - - )} +
+ + {searchBarQueryLanguages[queryLanguage.id].description} + + {searchBarQueryLanguages[queryLanguage.id].documentationLink && ( + <> + +
+ + Documentation + +
+ + )} + {modes?.length > 1 && ( + <> + + + ({ + value: id, + text: searchBarQueryLanguages[id].label, + }))} + value={queryLanguage.id} + onChange={(event: React.ChangeEvent) => { + const queryLanguageID: string = event.target.value; + setQueryLanguage({ + id: queryLanguageID, + configuration: + searchBarQueryLanguages[ + queryLanguageID + ]?.getConfiguration?.() || {}, + }); + setInput(''); + }} + aria-label='query-language-selector' + /> + + + )} +
} {...queryLanguageOutputRun.searchBarProps} From d9abea0d47bb9707f9e5c2de9187617f532d1edc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Thu, 23 Mar 2023 10:47:11 +0100 Subject: [PATCH 17/76] feat(search-bar): unify suggestion descriptions in WQL - Set a width for the syntax options popover - Unify the description in the suggestions of WQL example implementation - Update tests - Fix minor bugs in the WQL example implementation in Agents --- .../search-bar/query-language/aql.test.tsx | 6 ++-- .../search-bar/query-language/aql.tsx | 2 +- .../search-bar/query-language/wql.test.tsx | 4 +-- .../search-bar/query-language/wql.tsx | 4 +-- .../agent/components/agents-table.js | 31 ++++++++++--------- 5 files changed, 24 insertions(+), 23 deletions(-) diff --git a/public/components/search-bar/query-language/aql.test.tsx b/public/components/search-bar/query-language/aql.test.tsx index 86fc9de5c9..a5f7c7d36c 100644 --- a/public/components/search-bar/query-language/aql.test.tsx +++ b/public/components/search-bar/query-language/aql.test.tsx @@ -82,12 +82,12 @@ describe('Query language - AQL', () => { ${'f'} | ${[{ description: 'Field', label: 'field', type: 'field' }, { description: 'Field2', label: 'field2', type: 'field' }]} ${'field'} | ${[{ description: 'Field2', label: 'field2', type: 'field' }, { description: 'equality', label: '=', type: 'operator_compare' }, { description: 'not equality', label: '!=', type: 'operator_compare' }, { description: 'bigger', label: '>', type: 'operator_compare' }, { description: 'smaller', label: '<', type: 'operator_compare' }, { description: 'like as', label: '~', type: 'operator_compare' }]} ${'field='} | ${[{ label: 'value', type: 'value' }, { label: 'value2', type: 'value' }, { label: 'value3', type: 'value' }, { label: 'value4', type: 'value' }]} - ${'field=v'} | ${[{ description: 'Run the search query', label: 'Search', type: 'function_search' }, { label: 'value', type: 'value' }, { label: 'value2', type: 'value' }, { label: 'value3', type: 'value' }, { label: 'value4', type: 'value' }, { description: 'and', label: ';', type: 'conjunction' }, { description: 'or', label: ',', type: 'conjunction' }, { description: 'close group', label: ')', type: 'operator_group' }]} - ${'field=value'} | ${[{ description: 'Run the search query', label: 'Search', type: 'function_search' }, { label: 'value', type: 'value' }, { label: 'value2', type: 'value' }, { label: 'value3', type: 'value' }, { label: 'value4', type: 'value' }, { description: 'and', label: ';', type: 'conjunction' }, { description: 'or', label: ',', type: 'conjunction' }, { description: 'close group', label: ')', type: 'operator_group' }]} + ${'field=v'} | ${[{ description: 'run the search query', label: 'Search', type: 'function_search' }, { label: 'value', type: 'value' }, { label: 'value2', type: 'value' }, { label: 'value3', type: 'value' }, { label: 'value4', type: 'value' }, { description: 'and', label: ';', type: 'conjunction' }, { description: 'or', label: ',', type: 'conjunction' }, { description: 'close group', label: ')', type: 'operator_group' }]} + ${'field=value'} | ${[{ description: 'run the search query', label: 'Search', type: 'function_search' }, { label: 'value', type: 'value' }, { label: 'value2', type: 'value' }, { label: 'value3', type: 'value' }, { label: 'value4', type: 'value' }, { description: 'and', label: ';', type: 'conjunction' }, { description: 'or', label: ',', type: 'conjunction' }, { description: 'close group', label: ')', type: 'operator_group' }]} ${'field=value;'} | ${[{ description: 'Field', label: 'field', type: 'field' }, { description: 'Field2', label: 'field2', type: 'field' }, { description: 'open group', label: '(', type: 'operator_group' }]} ${'field=value;field2'} | ${[{ description: 'equality', label: '=', type: 'operator_compare' }, { description: 'not equality', label: '!=', type: 'operator_compare' }, { description: 'bigger', label: '>', type: 'operator_compare' }, { description: 'smaller', label: '<', type: 'operator_compare' }, { description: 'like as', label: '~', type: 'operator_compare' }]} ${'field=value;field2='} | ${[{ label: '127.0.0.1', type: 'value' }, { label: '127.0.0.2', type: 'value' }, { label: '190.0.0.1', type: 'value' }, { label: '190.0.0.2', type: 'value' }]} - ${'field=value;field2=127'} | ${[{ description: 'Run the search query', label: 'Search', type: 'function_search' }, { label: '127.0.0.1', type: 'value' }, { label: '127.0.0.2', type: 'value' }, { description: 'and', label: ';', type: 'conjunction' }, { description: 'or', label: ',', type: 'conjunction' }, { description: 'close group', label: ')', type: 'operator_group' }]} + ${'field=value;field2=127'} | ${[{ description: 'run the search query', label: 'Search', type: 'function_search' }, { label: '127.0.0.1', type: 'value' }, { label: '127.0.0.2', type: 'value' }, { description: 'and', label: ';', type: 'conjunction' }, { description: 'or', label: ',', type: 'conjunction' }, { description: 'close group', label: ')', type: 'operator_group' }]} `('Get suggestion from the input: $input', async ({ input, suggestions }) => { expect( await getSuggestions(tokenizer(input), { diff --git a/public/components/search-bar/query-language/aql.tsx b/public/components/search-bar/query-language/aql.tsx index 4256cd4540..8c898af3e2 100644 --- a/public/components/search-bar/query-language/aql.tsx +++ b/public/components/search-bar/query-language/aql.tsx @@ -299,7 +299,7 @@ export async function getSuggestions(tokens: ITokens, options: optionsQL): Promi { type: 'function_search', label: 'Search', - description: 'Run the search query', + description: 'run the search query', }, ] : []), diff --git a/public/components/search-bar/query-language/wql.test.tsx b/public/components/search-bar/query-language/wql.test.tsx index 89ac5c50ae..752b9b8451 100644 --- a/public/components/search-bar/query-language/wql.test.tsx +++ b/public/components/search-bar/query-language/wql.test.tsx @@ -132,8 +132,8 @@ describe('Query language - WQL', () => { ${'f'} | ${[{ description: 'Field', label: 'field', type: 'field' }, { description: 'Field2', label: 'field2', type: 'field' }]} ${'field'} | ${[{ description: 'Field2', label: 'field2', type: 'field' }, { description: 'equality', label: '=', type: 'operator_compare' }, { description: 'not equality', label: '!=', type: 'operator_compare' }, { description: 'bigger', label: '>', type: 'operator_compare' }, { description: 'smaller', label: '<', type: 'operator_compare' }, { description: 'like as', label: '~', type: 'operator_compare' }]} ${'field='} | ${[{ label: 'value', type: 'value' }, { label: 'value2', type: 'value' }, { label: 'value3', type: 'value' }, { label: 'value4', type: 'value' }]} - ${'field=v'} | ${[{ description: 'Run the search query', label: 'Search', type: 'function_search' }, { label: 'value', type: 'value' }, { label: 'value2', type: 'value' }, { label: 'value3', type: 'value' }, { label: 'value4', type: 'value' }, { description: 'and', label: 'and', type: 'conjunction' }, { description: 'or', label: 'or', type: 'conjunction' }, { description: 'close group', label: ')', type: 'operator_group' }]} - ${'field=value'} | ${[{ description: 'Run the search query', label: 'Search', type: 'function_search' }, { label: 'value', type: 'value' }, { label: 'value2', type: 'value' }, { label: 'value3', type: 'value' }, { label: 'value4', type: 'value' }, { description: 'and', label: 'and', type: 'conjunction' }, { description: 'or', label: 'or', type: 'conjunction' }, { description: 'close group', label: ')', type: 'operator_group' }]} + ${'field=v'} | ${[{ description: 'run the search query', label: 'Search', type: 'function_search' }, { label: 'value', type: 'value' }, { label: 'value2', type: 'value' }, { label: 'value3', type: 'value' }, { label: 'value4', type: 'value' }, { description: 'and', label: 'and', type: 'conjunction' }, { description: 'or', label: 'or', type: 'conjunction' }, { description: 'close group', label: ')', type: 'operator_group' }]} + ${'field=value'} | ${[{ description: 'run the search query', label: 'Search', type: 'function_search' }, { label: 'value', type: 'value' }, { label: 'value2', type: 'value' }, { label: 'value3', type: 'value' }, { label: 'value4', type: 'value' }, { description: 'and', label: 'and', type: 'conjunction' }, { description: 'or', label: 'or', type: 'conjunction' }, { description: 'close group', label: ')', type: 'operator_group' }]} ${'field=value and'} | ${[{ description: 'Field', label: 'field', type: 'field' }, { description: 'Field2', label: 'field2', type: 'field' }, { description: 'open group', label: '(', type: 'operator_group' }]} `('Get suggestion from the input: $input', async ({ input, suggestions }) => { expect( diff --git a/public/components/search-bar/query-language/wql.tsx b/public/components/search-bar/query-language/wql.tsx index 795417e7d1..84758b3ec3 100644 --- a/public/components/search-bar/query-language/wql.tsx +++ b/public/components/search-bar/query-language/wql.tsx @@ -364,7 +364,7 @@ export async function getSuggestions(tokens: ITokens, options: OptionsQL): Promi { type: 'function_search', label: 'Search', - description: 'Run the search query', + description: 'run the search query', }, ] : []), @@ -583,7 +583,7 @@ function getOutput(input: string, options: OptionsQL) { export const WQL = { id: 'wql', label: 'WQL', - description: 'WQL (Wazuh Query Language) allows to do queries.', + description: 'WQL (Wazuh Query Language) offers a human query syntax based on the Wazuh API query language.', documentationLink: `https://github.com/wazuh/wazuh-kibana-app/blob/v${pluginPlatform.version.split('.').splice(0,2).join('.')}/public/components/search-bar/query-language/wql.md`, getConfiguration() { return { diff --git a/public/controllers/agent/components/agents-table.js b/public/controllers/agent/components/agents-table.js index 82ce73d88d..84ac881f60 100644 --- a/public/controllers/agent/components/agents-table.js +++ b/public/controllers/agent/components/agents-table.js @@ -633,9 +633,10 @@ export const AgentsTable = withErrorBoundary( 'configSum', 'dateAdd', 'id', + 'ip', 'group', 'group_config_status', - 'lastKeepAline', + 'lastKeepAlive', 'manager', 'mergedSum', 'name', @@ -647,20 +648,20 @@ export const AgentsTable = withErrorBoundary( suggestions: { field(currentValue) { return [ - { label: 'configSum', description: 'Config sum' }, - { label: 'dateAdd', description: 'Date add' }, - { label: 'id', description: 'ID' }, - { label: 'ip', description: 'IP address' }, - { label: 'group', description: 'Group' }, - { label: 'group_config_status', description: 'Synced configuration status' }, - { label: 'lastKeepAline', description: 'Date add' }, - { label: 'manager', description: 'Manager' }, - { label: 'mergedSum', description: 'Merged sum' }, - { label: 'name', description: 'Agent name' }, - { label: 'node_name', description: 'Node name' }, - { label: 'os.platform', description: 'Operating system platform' }, - { label: 'status', description: 'Status' }, - { label: 'version', description: 'Version' }, + { label: 'configSum', description: 'filter by config sum' }, + { label: 'dateAdd', description: 'filter by date add' }, + { label: 'id', description: 'filter by ID' }, + { label: 'ip', description: 'filter by IP address' }, + { label: 'group', description: 'filter by Group' }, + { label: 'group_config_status', description: 'filter by Synced configuration status' }, + { label: 'lastKeepAlive', description: 'filter by date add' }, + { label: 'manager', description: 'filter by manager' }, + { label: 'mergedSum', description: 'filter by merged sum' }, + { label: 'name', description: 'filter by name' }, + { label: 'node_name', description: 'filter by manager node name' }, + { label: 'os.platform', description: 'filter by operating system platform' }, + { label: 'status', description: 'filter by status' }, + { label: 'version', description: 'filter by version' }, ]; }, value: async (currentValue, { previousField }) => { From c221f97b3f7044282f83a0daf84fdf8b9a343743 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Mon, 10 Apr 2023 16:56:36 +0200 Subject: [PATCH 18/76] feat(search-bar): add enhancements to WQL - WQL - Enhance documentation - Add partial and "expanded" input validation - Add tests --- .../components/eui-suggest/suggest_input.js | 2 +- public/components/search-bar/index.tsx | 3 +- .../search-bar/query-language/wql.md | 171 +++----------- .../search-bar/query-language/wql.test.tsx | 52 +++++ .../search-bar/query-language/wql.tsx | 211 +++++++++++++++++- .../agent/components/agents-table.js | 11 +- 6 files changed, 300 insertions(+), 150 deletions(-) diff --git a/public/components/eui-suggest/suggest_input.js b/public/components/eui-suggest/suggest_input.js index f67149d489..7a4f5df6f2 100644 --- a/public/components/eui-suggest/suggest_input.js +++ b/public/components/eui-suggest/suggest_input.js @@ -53,7 +53,7 @@ export class EuiSuggestInput extends Component { onPopoverFocus, isPopoverOpen, onClosePopover, - disableFocusTrap, + disableFocusTrap = false, ...rest } = this.props; diff --git a/public/components/search-bar/index.tsx b/public/components/search-bar/index.tsx index 2ecf57f858..f4e7fc4038 100644 --- a/public/components/search-bar/index.tsx +++ b/public/components/search-bar/index.tsx @@ -97,6 +97,7 @@ export const SearchBar = ({ configuration: configuration?.(state.configuration) || configuration, })), + setQueryLanguageOutput: setQueryLanguageOutputRun, inputRef, queryLanguage: { configuration: queryLanguage.configuration, @@ -126,7 +127,7 @@ export const SearchBar = ({ property prevents an error. */ suggestions={[]} isPopoverOpen={ - queryLanguageOutputRun.searchBarProps.suggestions.length > 0 && + queryLanguageOutputRun?.searchBarProps?.suggestions?.length > 0 && isOpenSuggestionPopover } onClosePopover={() => setIsOpenSuggestionPopover(false)} diff --git a/public/components/search-bar/query-language/wql.md b/public/components/search-bar/query-language/wql.md index b99538b9c7..2f8c4bb9f6 100644 --- a/public/components/search-bar/query-language/wql.md +++ b/public/components/search-bar/query-language/wql.md @@ -1,6 +1,6 @@ # Query Language - WQL -WQL (Wazuh Query Language) is a query language based in the `q` query parameters of the Wazuh API +WQL (Wazuh Query Language) is a query language based in the `q` query parameter of the Wazuh API endpoints. Documentation: https://wazuh.com/./user-manual/api/queries.html @@ -13,10 +13,12 @@ The implementation is adapted to work with the search bar component defined It supports 2 modes: - `explicit`: define the field, operator and value -- `implicit`: use a term to search in the available fields +- `search term`: use a term to search in the available fields Theses modes can not be combined. +`explicit` mode is enabled when it finds a field and operator tokens. + ## Mode: explicit ### Schema @@ -63,14 +65,14 @@ field.custom Examples: ``` -value -"custom value" -"custom \" value" +value_without_whitespace +"value with whitespaces" +"value with whitespaces and escaped \"quotes\"" ``` ### Notes -- The entities can be separated by whitespaces. +- The tokens can be separated by whitespaces. ### Examples @@ -81,7 +83,7 @@ id=001 id = 001 ``` -- Complex query +- Complex query (logical operator) ``` status=active and os.platform~linux status = active and os.platform ~ linux @@ -92,17 +94,17 @@ status!=never_connected and ip~240 or os.platform~linux status != never_connected and ip ~ 240 or os.platform ~ linux ``` -- Complex query with group operator +- Complex query (logical operators and group operator) ``` (status!=never_connected and ip~240) or id=001 ( status != never_connected and ip ~ 240 ) or id = 001 ``` -## Mode: implicit +## Mode: search term Search the term in the available fields. -This mode is used when there is no a `field` and `operator` attending to the regular expression +This mode is used when there is no a `field` and `operator` according to the regular expression of the **explicit** mode. ### Examples: @@ -132,7 +134,7 @@ This a query that can't be added, edited or removed by the user. It is added to ### Search term mode -This mode enables to search in multiple fields. The fields to use must be defined. +This mode enables to search in multiple fields using a search term. The fields to use must be defined. Use an union expression of each field with the like as operation `~`. @@ -174,21 +176,10 @@ searchTermFields: ['id', 'ip'] ```ts // language options field(currentValue) { + // static or async fetching is allowed return [ - { label: 'configSum', description: 'Config sum' }, - { label: 'dateAdd', description: 'Date add' }, - { label: 'id', description: 'ID' }, - { label: 'ip', description: 'IP address' }, - { label: 'group', description: 'Group' }, - { label: 'group_config_status', description: 'Synced configuration status' }, - { label: 'lastKeepAline', description: 'Date add' }, - { label: 'manager', description: 'Manager' }, - { label: 'mergedSum', description: 'Merged sum' }, - { label: 'name', description: 'Agent name' }, - { label: 'node_name', description: 'Node name' }, - { label: 'os.platform', description: 'Operating system platform' }, - { label: 'status', description: 'Status' }, - { label: 'version', description: 'Version' }, + { label: 'field1', description: 'Description' }, + { label: 'field2', description: 'Description' } ]; } ``` @@ -197,111 +188,13 @@ searchTermFields: ['id', 'ip'] ```ts // language options value: async (currentValue, { previousField }) => { - switch (previousField) { - case 'configSum': - return await getAgentFilterValuesMapToSearchBarSuggestion( - previousField, - currentValue, - {q: 'id!=000'} - ); - break; - case 'dateAdd': - return await getAgentFilterValuesMapToSearchBarSuggestion( - previousField, - currentValue, - {q: 'id!=000'} - ); - break; - case 'id': - return await getAgentFilterValuesMapToSearchBarSuggestion( - previousField, - currentValue, - {q: 'id!=000'} - ); - break; - case 'ip': - return await getAgentFilterValuesMapToSearchBarSuggestion( - previousField, - currentValue, - {q: 'id!=000'} - ); - break; - case 'group': - return await getAgentFilterValuesMapToSearchBarSuggestion( - previousField, - currentValue, - {q: 'id!=000'} - ); - break; - case 'group_config_status': - return [AGENT_SYNCED_STATUS.SYNCED, AGENT_SYNCED_STATUS.NOT_SYNCED].map( - (status) => ({ - type: 'value', - label: status, - }), - ); - break; - case 'lastKeepAline': - return await getAgentFilterValuesMapToSearchBarSuggestion( - previousField, - currentValue, - {q: 'id!=000'} - ); - break; - case 'manager': - return await getAgentFilterValuesMapToSearchBarSuggestion( - previousField, - currentValue, - {q: 'id!=000'} - ); - break; - case 'mergedSum': - return await getAgentFilterValuesMapToSearchBarSuggestion( - previousField, - currentValue, - {q: 'id!=000'} - ); - break; - case 'name': - return await getAgentFilterValuesMapToSearchBarSuggestion( - previousField, - currentValue, - {q: 'id!=000'} - ); - break; - case 'node_name': - return await getAgentFilterValuesMapToSearchBarSuggestion( - previousField, - currentValue, - {q: 'id!=000'} - ); - break; - case 'os.platform': - return await getAgentFilterValuesMapToSearchBarSuggestion( - previousField, - currentValue, - {q: 'id!=000'} - ); - break; - case 'status': - return UI_ORDER_AGENT_STATUS.map( - (status) => ({ - type: 'value', - label: status, - }), - ); - break; - case 'version': - return await getAgentFilterValuesMapToSearchBarSuggestion( - previousField, - currentValue, - {q: 'id!=000'} - ); - break; - default: - return []; - break; - } + // static or async fetching is allowed + // async fetching data + // const response = await fetchData(); + return [ + { label: 'value1' }, + { label: 'value2' } + ] } ``` @@ -315,19 +208,27 @@ graph TD; end tokenizer-->tokens; + tokens-->validate; tokens-->searchBarProps; subgraph searchBarProps; - searchBarProps_suggestions-->searchBarProps_suggestions_get_last_token_with_value[Get last token with value] - searchBarProps_suggestions_get_last_token_with_value[Get last token with value]-->searchBarProps_suggestions__result[Suggestions] + searchBarProps_suggestions[suggestions]-->searchBarProps_suggestions_input_isvalid{Input is valid} + searchBarProps_suggestions_input_isvalid{Is input valid?}-->searchBarProps_suggestions_input_isvalid_success[Yes] + searchBarProps_suggestions_input_isvalid{Is input valid?}-->searchBarProps_suggestions_input_isvalid_fail[No] + searchBarProps_suggestions_input_isvalid_success[Yes]--->searchBarProps_suggestions_get_last_token_with_value[Get last token with value]-->searchBarProps_suggestions__result[Suggestions] searchBarProps_suggestions__result[Suggestions]-->EuiSuggestItem + searchBarProps_suggestions_input_isvalid_fail[No]-->searchBarProps_suggestions_invalid[Invalid with error message] + searchBarProps_suggestions_invalid[Invalid with error message]-->EuiSuggestItem searchBarProps_prepend[prepend]-->searchBarProps_prepend_implicitQuery{implicitQuery} searchBarProps_prepend_implicitQuery{implicitQuery}-->searchBarProps_prepend_implicitQuery_yes[Yes]-->EuiButton searchBarProps_prepend_implicitQuery{implicitQuery}-->searchBarProps_prepend_implicitQuery_no[No]-->null searchBarProps_disableFocusTrap:true[disableFocusTrap = true] - searchBarProps_onItemClick[onItemClickSuggestion onclick handler]-->searchBarProps_onItemClick_suggestion_search[Search suggestion] - searchBarProps_onItemClick[onItemClickSuggestion onclick handler]-->searchBarProps_onItemClick_suggestion_edit_current_token[Edit current token]-->searchBarProps_onItemClick_build_input[Build input] - searchBarProps_onItemClick[onItemClickSuggestion onclick handler]-->searchBarProps_onItemClick_suggestion_add_new_token[Add new token]-->searchBarProps_onItemClick_build_input[Build input] + searchBarProps_onItemClick[onItemClickSuggestion]-->searchBarProps_onItemClick_suggestion_search[Search suggestion] + searchBarProps_onItemClick_suggestion_search[Search suggestion]-->searchBarProps_onItemClick_suggestion_search_run[Run search] + searchBarProps_onItemClick[onItemClickSuggestion]-->searchBarProps_onItemClick_suggestion_edit_current_token[Edit current token]-->searchBarProps_onItemClick_build_input[Build input] + searchBarProps_onItemClick[onItemClickSuggestion]-->searchBarProps_onItemClick_suggestion_add_new_token[Add new token]-->searchBarProps_onItemClick_build_input[Build input] + searchBarProps_onItemClick[onItemClickSuggestion]-->searchBarProps_onItemClick_suggestion_error[Error] + searchBarProps_isInvalid[isInvalid] end tokens-->output; diff --git a/public/components/search-bar/query-language/wql.test.tsx b/public/components/search-bar/query-language/wql.test.tsx index 752b9b8451..03095d54c3 100644 --- a/public/components/search-bar/query-language/wql.test.tsx +++ b/public/components/search-bar/query-language/wql.test.tsx @@ -355,4 +355,56 @@ describe('Query language - WQL', () => { - with != value - with ~ value */ + + // Validate the tokens + it.only.each` + WQL | validationError + ${''} | ${undefined} + ${'field1'} | ${undefined} + ${'field2'} | ${undefined} + ${'field1='} | ${['The value for field "field1" is missing.']} + ${'field2='} | ${['The value for field "field2" is missing.']} + ${'field='} | ${['"field" is not a valid field.']} + ${'custom='} | ${['"custom" is not a valid field.']} + ${'field1=value'} | ${undefined} + ${'field2=value'} | ${undefined} + ${'field=value'} | ${['"field" is not a valid field.']} + ${'custom=value'} | ${['"custom" is not a valid field.']} + ${'field1=value and'} | ${['There is no sentence after conjunction "and".']} + ${'field2=value and'} | ${['There is no sentence after conjunction "and".']} + ${'field=value and'} | ${['"field" is not a valid field.', 'There is no sentence after conjunction "and".']} + ${'custom=value and'} | ${['"custom" is not a valid field.', 'There is no sentence after conjunction "and".']} + ${'field1=value and '} | ${['There is no sentence after conjunction "and".']} + ${'field2=value and '} | ${['There is no sentence after conjunction "and".']} + ${'field=value and '} | ${['"field" is not a valid field.', 'There is no sentence after conjunction "and".']} + ${'custom=value and '} | ${['"custom" is not a valid field.', 'There is no sentence after conjunction "and".']} + ${'field1=value and field2'} | ${['The operator for field \"field2\" is missing.']} + ${'field2=value and field1'} | ${['The operator for field \"field1\" is missing.']} + ${'field1=value and field'} | ${['"field" is not a valid field.']} + ${'field2=value and field'} | ${['"field" is not a valid field.']} + ${'field=value and custom'} | ${['"field" is not a valid field.', '"custom" is not a valid field.']} + ${'('} | ${undefined} + ${'(field'} | ${undefined} + ${'(field='} | ${['"field" is not a valid field.']} + ${'(field=value'} | ${['"field" is not a valid field.']} + ${'(field=value or'} | ${['"field" is not a valid field.', 'There is no sentence after conjunction or.']} + ${'(field=value or '} | ${['"field" is not a valid field.', 'There is no sentence after conjunction or.']} + ${'(field=value or field2'} | ${['"field" is not a valid field.', 'The operator for field \"field2\" is missing.']} + ${'(field=value or field2>'} | ${['"field" is not a valid field.', 'The value for field "field2" is missing.']} + ${'(field=value or field2>value2'}| ${['"field" is not a valid field.']} + `('validate the tokens - WQL $WQL => $validationError', async ({WQL: currentInput, validationError}) => { + + const qlOutput = await WQL.run(currentInput, { + queryLanguage: { + parameters: { + implicitQuery: '', + suggestions: { + field: () => (['field1', 'field2'].map(label => ({label}))), + value: () => ([]) + } + } + } + }); + expect(qlOutput.output.error).toEqual(validationError); + }); }); diff --git a/public/components/search-bar/query-language/wql.tsx b/public/components/search-bar/query-language/wql.tsx index 84758b3ec3..2c59ad2f66 100644 --- a/public/components/search-bar/query-language/wql.tsx +++ b/public/components/search-bar/query-language/wql.tsx @@ -83,6 +83,8 @@ const suggestionMappingLanguageTokenType = { operator_group: { iconType: 'tokenDenseVector', color: 'tint3' }, // eslint-disable-next-line camelcase function_search: { iconType: 'search', color: 'tint5' }, + // eslint-disable-next-line camelcase + validation_error: { iconType: 'alert', color: 'tint2' } }; /** @@ -153,7 +155,6 @@ export function tokenizer(input: string): ITokens{ // Simple value // Quoted ", "value, "value", "escaped \"quote" // Escape quoted string with escaping quotes: https://stackoverflow.com/questions/249791/regex-for-quoted-string-with-escaping-quotes - // '(?(?:(?:[^"\\s]+|(?:"(?:[^"\\\\]|\\\\")*"))))?' + '(?(?:(?:[^"\\s]+|(?:"(?:[^"\\\\]|\\\\")*")|(?:"(?:[^"\\\\]|\\\\")*)|")))?' + // Whitespace '(?\\s+)?' + @@ -168,8 +169,8 @@ export function tokenizer(input: string): ITokens{ ); return [ - ...input.matchAll(re)] - .map( + ...input.matchAll(re) + ].map( ({groups}) => Object.entries(groups) .map(([key, value]) => ({ type: key.startsWith('operator_group') // Transform operator_group group match @@ -252,7 +253,32 @@ function getLastTokenDefinedByType( ({ type, value }) => type === tokenType && value, ); return tokenFound; -} +}; + +/** + * Get the token that is near to a token position of the token type. + * @param tokens + * @param tokenReferencePosition + * @param tokenType + * @param mode + * @returns + */ +function getTokenNearTo( + tokens: ITokens, + tokenType: ITokenType, + mode : 'previous' | 'next' = 'previous', + options : {tokenReferencePosition?: number, tokenFoundShouldHaveValue?: boolean} = {} +): IToken | undefined { + const shallowCopyTokens = Array.from([...tokens]); + const computedShallowCopyTokens = mode === 'previous' + ? shallowCopyTokens.slice(0, options?.tokenReferencePosition || tokens.length).reverse() + : shallowCopyTokens.slice(options?.tokenReferencePosition || 0); + return computedShallowCopyTokens + .find(({type, value}) => + type === tokenType + && (options?.tokenFoundShouldHaveValue ? value : true) + ); +}; /** * Get the suggestions from the tokens @@ -580,6 +606,128 @@ function getOutput(input: string, options: OptionsQL) { }; }; +/** + * Validate the tokens while the user is building the query + * @param tokens + * @param options + * @returns + */ +function validatePartial(tokens: ITokens, options: {field: string[]}): undefined | string{ + // Ensure is not in search term mode + if (!shouldUseSearchTerm(tokens)){ + return tokens.map((token: IToken, index) => { + if(token.value){ + if(token.type === 'field'){ + // Ensure there is a operator next to field to check if the fields is valid or not. + // This allows the user can type the field token and get the suggestions for the field. + const tokenOperatorNearToField = getTokenNearTo( + tokens, + 'operator_compare', + 'next', + { tokenReferencePosition: index, tokenFoundShouldHaveValue: true } + ); + return tokenOperatorNearToField + && !options.field.includes(token.value) + ? `"${token.value}" is not a valid field.` + : undefined; + }; + // Check if the value is allowed + if(token.type === 'value'){ + // Usage of API regular expression + const re = new RegExp( + // Value: A string. + '^(?(?:(?:\\((?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|[\\[\\]\\w _\\-.:?\\/\'"=@%<>{}]*)\\))*' + + '(?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|[\\[\\]\\w _\\-.:?\\\\/\'"=@%<>{}]+)' + + '(?:\\((?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|[\\[\\]\\w _\\-.:?\\\\/\'"=@%<>{}]*)\\))*)+)$' + ); + + const match = token.value.match(re); + return match?.groups?.value === token.value + ? undefined + : `"${token.value}" is not a valid value222.`; + } + }; + }) + .filter(t => typeof t !== 'undefined') + .join('\n') || undefined; + } +}; + +/** + * Validate the tokens if they are a valid syntax + * @param tokens + * @param options + * @returns + */ +function validate(tokens: ITokens, options: {field: string[]}): undefined | string[]{ + if (!shouldUseSearchTerm(tokens)){ + const errors = tokens.map((token: IToken, index) => { + const errors = []; + if(token.value){ + if(token.type === 'field'){ + const tokenOperatorNearToField = getTokenNearTo( + tokens, + 'operator_compare', + 'next', + { tokenReferencePosition: index, tokenFoundShouldHaveValue: true } + ); + const tokenValueNearToField = getTokenNearTo( + tokens, + 'value', + 'next', + { tokenReferencePosition: index, tokenFoundShouldHaveValue: true } + ); + if(!options.field.includes(token.value)){ + errors.push(`"${token.value}" is not a valid field.`); + }else if(!tokenOperatorNearToField){ + errors.push(`The operator for field "${token.value}" is missing.`); + }else if(!tokenValueNearToField){ + errors.push(`The value for field "${token.value}" is missing.`); + } + }; + // Check if the value is allowed + if(token.type === 'value'){ + // Usage of API regular expression + const re = new RegExp( + // Value: A string. + '^(?(?:(?:\\((?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|[\\[\\]\\w _\\-.:?\\/\'"=@%<>{}]*)\\))*' + + '(?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|[\\[\\]\\w _\\-.:?\\\\/\'"=@%<>{}]+)' + + '(?:\\((?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|[\\[\\]\\w _\\-.:?\\\\/\'"=@%<>{}]*)\\))*)+)$' + ); + + const match = token.value.match(re); + match?.groups?.value !== token.value && errors.push(`"${token.value}" is not a valid value.`); + }; + + // Check if the value is allowed + if(token.type === 'conjunction'){ + + const tokenWhitespaceNearToFieldNext = getTokenNearTo( + tokens, + 'whitespace', + 'next', + { tokenReferencePosition: index } + ); + const tokenFieldNearToFieldNext = getTokenNearTo( + tokens, + 'field', + 'next', + { tokenReferencePosition: index, tokenFoundShouldHaveValue: true } + ); + !tokenWhitespaceNearToFieldNext?.value?.length + && errors.push(`There is no whitespace after conjunction "${token.value}".`); + !tokenFieldNearToFieldNext?.value?.length + && errors.push(`There is no sentence after conjunction "${token.value}".`); + }; + }; + return errors.length ? errors : undefined; + }).filter(errors => errors) + .flat() + return errors.length ? errors : undefined; + }; + return undefined; +}; + export const WQL = { id: 'wql', label: 'WQL', @@ -602,22 +750,59 @@ export const WQL = { ) : ''; + // Validate the user input + const fieldsSuggestion: string[] = await params.queryLanguage.parameters.suggestions.field() + .map(({label}) => label); + + const validationPartial = validatePartial(tokens, { + field: fieldsSuggestion + }); + + const validationStrict = validate(tokens, { + field: fieldsSuggestion + }); + // Get the output of query language - const output = getOutput(input, params.queryLanguage.parameters); + const output = { + ...getOutput(input, params.queryLanguage.parameters), + error: validationStrict + }; + + const onSearch = () => { + if(output?.error){ + params.setQueryLanguageOutput((state) => ({ + ...state, + searchBarProps: { + ...state.searchBarProps, + suggestions: transformSuggestionsToEuiSuggestItem( + output.error.map(error => ({type: 'validation_error', label: 'Invalid', description: error})) + ) + } + })); + }else{ + params.onSearch(output); + }; + }; return { searchBarProps: { // Props that will be used by the EuiSuggest component // Suggestions suggestions: transformSuggestionsToEuiSuggestItem( - await getSuggestions(tokens, params.queryLanguage.parameters) + validationPartial + ? [{ type: 'validation_error', label: 'Invalid', description: validationPartial}] + : await getSuggestions(tokens, params.queryLanguage.parameters) ), // Handler to manage when clicking in a suggestion item onItemClick: item => { + // There is an error, clicking on the item does nothing + if (item.type.iconType === 'alert'){ + return; + }; // When the clicked item has the `search` iconType, run the `onSearch` function if (item.type.iconType === 'search') { // Execute the search action - params.onSearch(output); + onSearch(); } else { // When the clicked item has another iconType const lastToken: IToken | undefined = getLastTokenDefined(tokens); @@ -706,9 +891,17 @@ export const WQL = { // This causes when using the Search suggestion, the suggestion popover can be closed. // If this is disabled, then the suggestion popover is open after a short time for this // use case. - disableFocusTrap: true + disableFocusTrap: true, + // Show the input is invalid + isInvalid: Boolean(validationStrict), + // Define the handler when the a key is pressed while the input is focused + onKeyPress: (event) => { + if (event.key === 'Enter') { + onSearch(); + }; + } }, - output, + output }; }, transformUQLToQL: transformUQLToQL, diff --git a/public/controllers/agent/components/agents-table.js b/public/controllers/agent/components/agents-table.js index 84ac881f60..bee79d1cc0 100644 --- a/public/controllers/agent/components/agents-table.js +++ b/public/controllers/agent/components/agents-table.js @@ -652,8 +652,8 @@ export const AgentsTable = withErrorBoundary( { label: 'dateAdd', description: 'filter by date add' }, { label: 'id', description: 'filter by ID' }, { label: 'ip', description: 'filter by IP address' }, - { label: 'group', description: 'filter by Group' }, - { label: 'group_config_status', description: 'filter by Synced configuration status' }, + { label: 'group', description: 'filter by group' }, + { label: 'group_config_status', description: 'filter by synced configuration status' }, { label: 'lastKeepAlive', description: 'filter by date add' }, { label: 'manager', description: 'filter by manager' }, { label: 'mergedSum', description: 'filter by merged sum' }, @@ -708,7 +708,7 @@ export const AgentsTable = withErrorBoundary( }), ); break; - case 'lastKeepAline': + case 'lastKeepAlive': return await getAgentFilterValuesMapToSearchBarSuggestion( previousField, currentValue, @@ -773,7 +773,10 @@ export const AgentsTable = withErrorBoundary( }, ]} onChange={console.log} - onSearch={async ({language, unifiedQuery}) => { + onSearch={async ({language, unifiedQuery, error}) => { + if(error){ + return; + } try{ this.setState({isLoading: true}); const response = await this.props.wzReq('GET', '/agents', { params: { From cdbbb6d4a3206bc8832278bc3070a91051cfec2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Mon, 10 Apr 2023 17:28:00 +0200 Subject: [PATCH 19/76] feat(search-bar): rename previousField and previousOperatorCompare in WQL --- public/components/search-bar/README.md | 101 ++---------------- public/components/search-bar/index.test.tsx | 4 +- .../search-bar/query-language/wql.md | 2 +- .../search-bar/query-language/wql.test.tsx | 18 ++-- .../search-bar/query-language/wql.tsx | 26 ++--- .../agent/components/agents-table.js | 28 ++--- 6 files changed, 49 insertions(+), 130 deletions(-) diff --git a/public/components/search-bar/README.md b/public/components/search-bar/README.md index 58e557a26c..b017a5871d 100644 --- a/public/components/search-bar/README.md +++ b/public/components/search-bar/README.md @@ -54,108 +54,27 @@ Basic usage: { label: 'version', description: 'Version' }, ]; }, - value: async (currentValue, { previousField }) => { - switch (previousField) { + value: async (currentValue, { field }) => { + switch (field) { case 'configSum': - return await getAgentFilterValuesMapToSearchBarSuggestion( - previousField, - currentValue, - {q: 'id!=000'} - ); + return [ + { label: 'configSum1' }, + { label: 'configSum2' }, + ]; break; case 'dateAdd': - return await getAgentFilterValuesMapToSearchBarSuggestion( - previousField, - currentValue, - {q: 'id!=000'} - ); - break; - case 'id': - return await getAgentFilterValuesMapToSearchBarSuggestion( - previousField, - currentValue, - {q: 'id!=000'} - ); - break; - case 'ip': - return await getAgentFilterValuesMapToSearchBarSuggestion( - previousField, - currentValue, - {q: 'id!=000'} - ); - break; - case 'group': - return await getAgentFilterValuesMapToSearchBarSuggestion( - previousField, - currentValue, - {q: 'id!=000'} - ); - break; - case 'group_config_status': - return [AGENT_SYNCED_STATUS.SYNCED, AGENT_SYNCED_STATUS.NOT_SYNCED].map( - (status) => ({ - type: 'value', - label: status, - }), - ); - break; - case 'lastKeepAline': - return await getAgentFilterValuesMapToSearchBarSuggestion( - previousField, - currentValue, - {q: 'id!=000'} - ); - break; - case 'manager': - return await getAgentFilterValuesMapToSearchBarSuggestion( - previousField, - currentValue, - {q: 'id!=000'} - ); - break; - case 'mergedSum': - return await getAgentFilterValuesMapToSearchBarSuggestion( - previousField, - currentValue, - {q: 'id!=000'} - ); - break; - case 'name': - return await getAgentFilterValuesMapToSearchBarSuggestion( - previousField, - currentValue, - {q: 'id!=000'} - ); - break; - case 'node_name': - return await getAgentFilterValuesMapToSearchBarSuggestion( - previousField, - currentValue, - {q: 'id!=000'} - ); - break; - case 'os.platform': - return await getAgentFilterValuesMapToSearchBarSuggestion( - previousField, - currentValue, - {q: 'id!=000'} - ); + return [ + { label: 'dateAdd1' }, + { label: 'dateAdd2' }, + ]; break; case 'status': return UI_ORDER_AGENT_STATUS.map( (status) => ({ - type: 'value', label: status, }), ); break; - case 'version': - return await getAgentFilterValuesMapToSearchBarSuggestion( - previousField, - currentValue, - {q: 'id!=000'} - ); - break; default: return []; break; diff --git a/public/components/search-bar/index.test.tsx b/public/components/search-bar/index.test.tsx index 2f8f2366a4..31f18f6dda 100644 --- a/public/components/search-bar/index.test.tsx +++ b/public/components/search-bar/index.test.tsx @@ -14,7 +14,7 @@ describe('SearchBar component', () => { field(currentValue) { return []; }, - value(currentValue, { previousField }){ + value(currentValue, { field }){ return []; }, }, @@ -29,7 +29,7 @@ describe('SearchBar component', () => { field(currentValue) { return []; }, - value(currentValue, { previousField }){ + value(currentValue, { field }){ return []; }, }, diff --git a/public/components/search-bar/query-language/wql.md b/public/components/search-bar/query-language/wql.md index 2f8c4bb9f6..ce70e7e6c6 100644 --- a/public/components/search-bar/query-language/wql.md +++ b/public/components/search-bar/query-language/wql.md @@ -187,7 +187,7 @@ searchTermFields: ['id', 'ip'] - `value`: method that returns the suggestion for the values ```ts // language options - value: async (currentValue, { previousField }) => { + value: async (currentValue, { field }) => { // static or async fetching is allowed // async fetching data // const response = await fetchData(); diff --git a/public/components/search-bar/query-language/wql.test.tsx b/public/components/search-bar/query-language/wql.test.tsx index 03095d54c3..4c645a8939 100644 --- a/public/components/search-bar/query-language/wql.test.tsx +++ b/public/components/search-bar/query-language/wql.test.tsx @@ -18,7 +18,7 @@ describe('SearchBar component', () => { field(currentValue) { return []; }, - value(currentValue, { previousField }){ + value(currentValue, { field }){ return []; }, }, @@ -150,8 +150,8 @@ describe('Query language - WQL', () => { description, })); }, - value(currentValue = '', { previousField }) { - switch (previousField) { + value(currentValue = '', { field }) { + switch (field) { case 'field': return ['value', 'value2', 'value3', 'value4'] .filter(value => value.startsWith(currentValue)) @@ -370,10 +370,10 @@ describe('Query language - WQL', () => { ${'field2=value'} | ${undefined} ${'field=value'} | ${['"field" is not a valid field.']} ${'custom=value'} | ${['"custom" is not a valid field.']} - ${'field1=value and'} | ${['There is no sentence after conjunction "and".']} - ${'field2=value and'} | ${['There is no sentence after conjunction "and".']} - ${'field=value and'} | ${['"field" is not a valid field.', 'There is no sentence after conjunction "and".']} - ${'custom=value and'} | ${['"custom" is not a valid field.', 'There is no sentence after conjunction "and".']} + ${'field1=value and'} | ${['There is no whitespace after conjunction "and".', 'There is no sentence after conjunction "and".']} + ${'field2=value and'} | ${['There is no whitespace after conjunction "and".', 'There is no sentence after conjunction "and".']} + ${'field=value and'} | ${['"field" is not a valid field.', 'There is no whitespace after conjunction "and".', 'There is no sentence after conjunction "and".']} + ${'custom=value and'} | ${['"custom" is not a valid field.', 'There is no whitespace after conjunction "and".', 'There is no sentence after conjunction "and".']} ${'field1=value and '} | ${['There is no sentence after conjunction "and".']} ${'field2=value and '} | ${['There is no sentence after conjunction "and".']} ${'field=value and '} | ${['"field" is not a valid field.', 'There is no sentence after conjunction "and".']} @@ -387,8 +387,8 @@ describe('Query language - WQL', () => { ${'(field'} | ${undefined} ${'(field='} | ${['"field" is not a valid field.']} ${'(field=value'} | ${['"field" is not a valid field.']} - ${'(field=value or'} | ${['"field" is not a valid field.', 'There is no sentence after conjunction or.']} - ${'(field=value or '} | ${['"field" is not a valid field.', 'There is no sentence after conjunction or.']} + ${'(field=value or'} | ${['"field" is not a valid field.', 'There is no whitespace after conjunction "or".', 'There is no sentence after conjunction "or".']} + ${'(field=value or '} | ${['"field" is not a valid field.', 'There is no sentence after conjunction "or".']} ${'(field=value or field2'} | ${['"field" is not a valid field.', 'The operator for field \"field2\" is missing.']} ${'(field=value or field2>'} | ${['"field" is not a valid field.', 'The value for field "field2" is missing.']} ${'(field=value or field2>value2'}| ${['"field" is not a valid field.']} diff --git a/public/components/search-bar/query-language/wql.tsx b/public/components/search-bar/query-language/wql.tsx index 2c59ad2f66..984ece8c01 100644 --- a/public/components/search-bar/query-language/wql.tsx +++ b/public/components/search-bar/query-language/wql.tsx @@ -199,9 +199,9 @@ type SuggestItem = QLOptionSuggestionEntityItem & { type QLOptionSuggestionHandler = ( currentValue: string | undefined, { - previousField, - previousOperatorCompare, - }: { previousField: string; previousOperatorCompare: string }, + field, + operatorCompare, + }: { field: string; operatorCompare: string }, ) => Promise; type OptionsQLImplicitQuery = { @@ -334,15 +334,15 @@ export async function getSuggestions(tokens: ITokens, options: OptionsQL): Promi ]; break; case 'operator_compare':{ - const previousField = getLastTokenDefinedByType(tokens, 'field')?.value; - const previousOperatorCompare = getLastTokenDefinedByType( + const field = getLastTokenDefinedByType(tokens, 'field')?.value; + const operatorCompare = getLastTokenDefinedByType( tokens, 'operator_compare', )?.value; // If there is no a previous field, then no return suggestions because it would be an syntax // error - if(!previousField){ + if(!field){ return []; }; @@ -363,8 +363,8 @@ export async function getSuggestions(tokens: ITokens, options: OptionsQL): Promi ) ? [ ...(await options.suggestions.value(undefined, { - previousField, - previousOperatorCompare, + field, + operatorCompare, })).map(mapSuggestionCreatorValue), ] : []), @@ -372,15 +372,15 @@ export async function getSuggestions(tokens: ITokens, options: OptionsQL): Promi break; } case 'value':{ - const previousField = getLastTokenDefinedByType(tokens, 'field')?.value; - const previousOperatorCompare = getLastTokenDefinedByType( + const field = getLastTokenDefinedByType(tokens, 'field')?.value; + const operatorCompare = getLastTokenDefinedByType( tokens, 'operator_compare', )?.value; /* If there is no a previous field or operator_compare, then no return suggestions because it would be an syntax error */ - if(!previousField || !previousOperatorCompare){ + if(!field || !operatorCompare){ return []; }; @@ -395,8 +395,8 @@ export async function getSuggestions(tokens: ITokens, options: OptionsQL): Promi ] : []), ...(await options.suggestions.value(lastToken.value, { - previousField, - previousOperatorCompare, + field, + operatorCompare, })).map(mapSuggestionCreatorValue), ...Object.entries(language.tokens.conjunction.literal).map( ([ conjunction, description]) => ({ diff --git a/public/controllers/agent/components/agents-table.js b/public/controllers/agent/components/agents-table.js index 374d357bef..d71608441a 100644 --- a/public/controllers/agent/components/agents-table.js +++ b/public/controllers/agent/components/agents-table.js @@ -655,39 +655,39 @@ export const AgentsTable = withErrorBoundary( { label: 'version', description: 'filter by version' }, ]; }, - value: async (currentValue, { previousField }) => { - switch (previousField) { + value: async (currentValue, { field }) => { + switch (field) { case 'configSum': return await getAgentFilterValuesMapToSearchBarSuggestion( - previousField, + field, currentValue, {q: 'id!=000'} ); break; case 'dateAdd': return await getAgentFilterValuesMapToSearchBarSuggestion( - previousField, + field, currentValue, {q: 'id!=000'} ); break; case 'id': return await getAgentFilterValuesMapToSearchBarSuggestion( - previousField, + field, currentValue, {q: 'id!=000'} ); break; case 'ip': return await getAgentFilterValuesMapToSearchBarSuggestion( - previousField, + field, currentValue, {q: 'id!=000'} ); break; case 'group': return await getAgentFilterValuesMapToSearchBarSuggestion( - previousField, + field, currentValue, {q: 'id!=000'} ); @@ -701,42 +701,42 @@ export const AgentsTable = withErrorBoundary( break; case 'lastKeepAlive': return await getAgentFilterValuesMapToSearchBarSuggestion( - previousField, + field, currentValue, {q: 'id!=000'} ); break; case 'manager': return await getAgentFilterValuesMapToSearchBarSuggestion( - previousField, + field, currentValue, {q: 'id!=000'} ); break; case 'mergedSum': return await getAgentFilterValuesMapToSearchBarSuggestion( - previousField, + field, currentValue, {q: 'id!=000'} ); break; case 'name': return await getAgentFilterValuesMapToSearchBarSuggestion( - previousField, + field, currentValue, {q: 'id!=000'} ); break; case 'node_name': return await getAgentFilterValuesMapToSearchBarSuggestion( - previousField, + field, currentValue, {q: 'id!=000'} ); break; case 'os.platform': return await getAgentFilterValuesMapToSearchBarSuggestion( - previousField, + field, currentValue, {q: 'id!=000'} ); @@ -750,7 +750,7 @@ export const AgentsTable = withErrorBoundary( break; case 'version': return await getAgentFilterValuesMapToSearchBarSuggestion( - previousField, + field, currentValue, {q: 'id!=000'} ); From c300fa36e15ec90ae992f584f515eb5e2edf1c42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Tue, 11 Apr 2023 08:44:56 +0200 Subject: [PATCH 20/76] fix(tests): update snapshot --- .../tables/__snapshots__/table-with-search-bar.test.tsx.snap | 1 + 1 file changed, 1 insertion(+) diff --git a/public/components/common/tables/__snapshots__/table-with-search-bar.test.tsx.snap b/public/components/common/tables/__snapshots__/table-with-search-bar.test.tsx.snap index 43dbaa0096..0c57fd5b26 100644 --- a/public/components/common/tables/__snapshots__/table-with-search-bar.test.tsx.snap +++ b/public/components/common/tables/__snapshots__/table-with-search-bar.test.tsx.snap @@ -108,6 +108,7 @@ exports[`Table With Search Bar component renders correctly to match the snapshot anchorPosition="downLeft" attachToAnchor={true} closePopover={[Function]} + disableFocusTrap={false} display="block" fullWidth={true} id="popover" From ef85c548986f47c2e8defae81b7f52833a24b157 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Tue, 11 Apr 2023 10:12:16 +0200 Subject: [PATCH 21/76] fix(search-bar): fix documentation link for WQL --- public/components/search-bar/query-language/wql.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/components/search-bar/query-language/wql.tsx b/public/components/search-bar/query-language/wql.tsx index 984ece8c01..b6a9bbe14e 100644 --- a/public/components/search-bar/query-language/wql.tsx +++ b/public/components/search-bar/query-language/wql.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { EuiButtonEmpty, EuiPopover, EuiText, EuiCode } from '@elastic/eui'; import { tokenizer as tokenizerUQL } from './aql'; -import { pluginPlatform } from '../../../../package.json'; +import { PLUGIN_VERSION_SHORT } from '../../../../common/constants'; /* UI Query language https://documentation.wazuh.com/current/user-manual/api/queries.html @@ -732,7 +732,7 @@ export const WQL = { id: 'wql', label: 'WQL', description: 'WQL (Wazuh Query Language) offers a human query syntax based on the Wazuh API query language.', - documentationLink: `https://github.com/wazuh/wazuh-kibana-app/blob/v${pluginPlatform.version.split('.').splice(0,2).join('.')}/public/components/search-bar/query-language/wql.md`, + documentationLink: `https://github.com/wazuh/wazuh-kibana-app/blob/v${PLUGIN_VERSION_SHORT}/public/components/search-bar/query-language/wql.md`, getConfiguration() { return { isOpenPopoverImplicitFilter: false, From 17053c037ce24efc88144dbf788c2421fae0b73d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Tue, 11 Apr 2023 10:57:31 +0200 Subject: [PATCH 22/76] fix(search-bar): remove example usage of SearchBar component in Agents --- .../agent/components/agents-table.js | 196 +----------------- 1 file changed, 2 insertions(+), 194 deletions(-) diff --git a/public/controllers/agent/components/agents-table.js b/public/controllers/agent/components/agents-table.js index d71608441a..432269d1d3 100644 --- a/public/controllers/agent/components/agents-table.js +++ b/public/controllers/agent/components/agents-table.js @@ -41,7 +41,6 @@ import { UI_ERROR_SEVERITIES } from '../../../react-services/error-orchestrator/ import { getErrorOrchestrator } from '../../../react-services/common-services'; import { AgentStatus } from '../../../components/agents/agent_status'; import { AgentSynced } from '../../../components/agents/agent-synced'; -import { SearchBar } from '../../../components/search-bar'; import { compressIPv6 } from '../../../services/ipv6-services'; export const AgentsTable = withErrorBoundary( @@ -63,8 +62,7 @@ export const AgentsTable = withErrorBoundary( isFilterColumnOpen: false, filters: sessionStorage.getItem('agents_preview_selected_options') ? JSON.parse(sessionStorage.getItem('agents_preview_selected_options')) - : [], - query: '' + : [] }; this.suggestions = [ { @@ -212,7 +210,7 @@ export const AgentsTable = withErrorBoundary( this.props.filters && this.props.filters.length ) { - this.setState({ filters: this.props.filters, pageIndex: 0, query: this.props.filters.find(({field}) => field === 'q')?.value || '' }); + this.setState({ filters: this.props.filters, pageIndex: 0 }); this.props.removeFilters(); } } @@ -609,188 +607,6 @@ export const AgentsTable = withErrorBoundary( } placeholder='Filter or search agent' /> - {/** Example implementation */} - { - switch (field) { - case 'configSum': - return await getAgentFilterValuesMapToSearchBarSuggestion( - field, - currentValue, - {q: 'id!=000'} - ); - break; - case 'dateAdd': - return await getAgentFilterValuesMapToSearchBarSuggestion( - field, - currentValue, - {q: 'id!=000'} - ); - break; - case 'id': - return await getAgentFilterValuesMapToSearchBarSuggestion( - field, - currentValue, - {q: 'id!=000'} - ); - break; - case 'ip': - return await getAgentFilterValuesMapToSearchBarSuggestion( - field, - currentValue, - {q: 'id!=000'} - ); - break; - case 'group': - return await getAgentFilterValuesMapToSearchBarSuggestion( - field, - currentValue, - {q: 'id!=000'} - ); - break; - case 'group_config_status': - return [AGENT_SYNCED_STATUS.SYNCED, AGENT_SYNCED_STATUS.NOT_SYNCED].map( - (status) => ({ - label: status, - }), - ); - break; - case 'lastKeepAlive': - return await getAgentFilterValuesMapToSearchBarSuggestion( - field, - currentValue, - {q: 'id!=000'} - ); - break; - case 'manager': - return await getAgentFilterValuesMapToSearchBarSuggestion( - field, - currentValue, - {q: 'id!=000'} - ); - break; - case 'mergedSum': - return await getAgentFilterValuesMapToSearchBarSuggestion( - field, - currentValue, - {q: 'id!=000'} - ); - break; - case 'name': - return await getAgentFilterValuesMapToSearchBarSuggestion( - field, - currentValue, - {q: 'id!=000'} - ); - break; - case 'node_name': - return await getAgentFilterValuesMapToSearchBarSuggestion( - field, - currentValue, - {q: 'id!=000'} - ); - break; - case 'os.platform': - return await getAgentFilterValuesMapToSearchBarSuggestion( - field, - currentValue, - {q: 'id!=000'} - ); - break; - case 'status': - return UI_ORDER_AGENT_STATUS.map( - (status) => ({ - label: status, - }), - ); - break; - case 'version': - return await getAgentFilterValuesMapToSearchBarSuggestion( - field, - currentValue, - {q: 'id!=000'} - ); - break; - default: - return []; - break; - } - }, - }, - }, - ]} - onChange={console.log} - onSearch={async ({language, unifiedQuery, error}) => { - if(error){ - return; - } - try{ - this.setState({isLoading: true}); - const response = await this.props.wzReq('GET', '/agents', { params: { - limit: this.state.pageSize, - offset: 0, - q: unifiedQuery, - sort: this.buildSortFilter() - }}); - - const formatedAgents = response?.data?.data?.affected_items?.map( - this.formatAgent.bind(this) - ); - - this._isMount && this.setState({ - agents: formatedAgents, - totalItems: response?.data?.data?.total_affected_items, - isLoading: false, - }); - }catch(error){ - this.setState({isLoading: false}); - }; - }} - /> { - try{ - return (await getAgentFilterValues(key, value, params)).map(label => ({label})); - }catch(error){ - return []; - }; -}; From cfbf19b04b049ba57d832f2dad90e6c730ba6637 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Wed, 12 Apr 2023 09:10:28 +0200 Subject: [PATCH 23/76] fix(search-bar): fix an error using the value suggestions in WQL Fix an error when the last token in the input was a value and used a value suggestion whose label contains whitespaces, the value was not wrapped with quotes. --- public/components/search-bar/query-language/wql.test.tsx | 4 +++- public/components/search-bar/query-language/wql.tsx | 7 +++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/public/components/search-bar/query-language/wql.test.tsx b/public/components/search-bar/query-language/wql.test.tsx index 4c645a8939..f40d81bf22 100644 --- a/public/components/search-bar/query-language/wql.test.tsx +++ b/public/components/search-bar/query-language/wql.test.tsx @@ -268,6 +268,8 @@ describe('Query language - WQL', () => { ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with < value'}} | ${'field="with < value"'} ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with ~ value'}} | ${'field="with ~ value"' /** ~ character is not supported as value in the q query parameter */} ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: '"value'}} | ${'field="\\"value"'} + ${'field="with spaces"'} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value'}} | ${'field=value'} + ${'field="with spaces"'} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'other spaces'}} | ${'field="other spaces"'} ${''} | ${{type: { iconType: 'tokenDenseVector', color: 'tint3' }, label: '('}} | ${'('} ${'('} | ${{type: { iconType: 'kqlField', color: 'tint4' }, label: 'field'}} | ${'(field'} ${'(field'} | ${{type: { iconType: 'kqlField', color: 'tint4' }, label: 'field2'}} | ${'(field2'} @@ -357,7 +359,7 @@ describe('Query language - WQL', () => { */ // Validate the tokens - it.only.each` + it.each` WQL | validationError ${''} | ${undefined} ${'field1'} | ${undefined} diff --git a/public/components/search-bar/query-language/wql.tsx b/public/components/search-bar/query-language/wql.tsx index b6a9bbe14e..c5ea2d0839 100644 --- a/public/components/search-bar/query-language/wql.tsx +++ b/public/components/search-bar/query-language/wql.tsx @@ -811,8 +811,11 @@ export const WQL = { lastToken && suggestionMappingLanguageTokenType[lastToken.type].iconType === item.type.iconType ) { - // replace the value of last token - lastToken.value = item.label; + // replace the value of last token with the current one. + // if the current token is a value, then transform it + lastToken.value = item.type.iconType === suggestionMappingLanguageTokenType.value.iconType + ? transformQLValue(item.label) + : item.label; } else { // add a whitespace for conjunction !(/\s$/.test(input)) From 57d11d64430c4ef7913950ded87aa001b2cd026e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Tue, 25 Apr 2023 11:02:54 +0200 Subject: [PATCH 24/76] feat(search-bar): add search function suggestion when the input is empty --- public/components/search-bar/query-language/wql.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/public/components/search-bar/query-language/wql.tsx b/public/components/search-bar/query-language/wql.tsx index c5ea2d0839..e9b9c2ba16 100644 --- a/public/components/search-bar/query-language/wql.tsx +++ b/public/components/search-bar/query-language/wql.tsx @@ -190,7 +190,7 @@ type QLOptionSuggestionEntityItem = { type QLOptionSuggestionEntityItemTyped = QLOptionSuggestionEntityItem - & { type: 'operator_group'|'field'|'operator_compare'|'value'|'conjunction' }; + & { type: 'operator_group'|'field'|'operator_compare'|'value'|'conjunction'|'function_search' }; type SuggestItem = QLOptionSuggestionEntityItem & { type: { iconType: string, color: string } @@ -298,6 +298,12 @@ export async function getSuggestions(tokens: ITokens, options: OptionsQL): Promi // If it can't get a token with value, then returns fields and open operator group if(!lastToken?.type){ return [ + // Search function + { + type: 'function_search', + label: 'Search', + description: 'run the search query', + }, // fields ...(await options.suggestions.field()).map(mapSuggestionCreatorField), { From ce4bc92af7e44cf0e73401441310980b9f353cc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Tue, 25 Apr 2023 11:04:08 +0200 Subject: [PATCH 25/76] fix(search-bar): ensure the query language output changed to trigger the onChange handler --- public/components/search-bar/index.tsx | 50 +++++++++++++++----------- 1 file changed, 29 insertions(+), 21 deletions(-) diff --git a/public/components/search-bar/index.tsx b/public/components/search-bar/index.tsx index f4e7fc4038..a2b771d89e 100644 --- a/public/components/search-bar/index.tsx +++ b/public/components/search-bar/index.tsx @@ -11,6 +11,7 @@ import { } from '@elastic/eui'; import { EuiSuggest } from '../eui-suggest'; import { searchBarQueryLanguages } from './query-language'; +import _ from 'lodash'; type SearchBarProps = { defaultMode?: string; @@ -48,6 +49,8 @@ export const SearchBar = ({ searchBarProps: { suggestions: [] }, output: undefined, }); + // Cache the previous output + const queryLanguageOutputRunPreviousOutput = useRef(queryLanguageOutputRun.output); // Controls when the suggestion popover is open/close const [isOpenSuggestionPopover, setIsOpenSuggestionPopover] = useState(false); @@ -85,31 +88,36 @@ export const SearchBar = ({ useEffect(() => { (async () => { // Set the query language output - setQueryLanguageOutputRun( - await searchBarQueryLanguages[queryLanguage.id].run(input, { - onSearch: _onSearch, - setInput, - closeSuggestionPopover: () => setIsOpenSuggestionPopover(false), - openSuggestionPopover: () => setIsOpenSuggestionPopover(true), - setQueryLanguageConfiguration: (configuration: any) => - setQueryLanguage(state => ({ - ...state, - configuration: - configuration?.(state.configuration) || configuration, - })), - setQueryLanguageOutput: setQueryLanguageOutputRun, - inputRef, - queryLanguage: { - configuration: queryLanguage.configuration, - parameters: modes.find(({ id }) => id === queryLanguage.id), - }, - }), - ); + const queryLanguageOutput = await searchBarQueryLanguages[queryLanguage.id].run(input, { + onSearch: _onSearch, + setInput, + closeSuggestionPopover: () => setIsOpenSuggestionPopover(false), + openSuggestionPopover: () => setIsOpenSuggestionPopover(true), + setQueryLanguageConfiguration: (configuration: any) => + setQueryLanguage(state => ({ + ...state, + configuration: + configuration?.(state.configuration) || configuration, + })), + setQueryLanguageOutput: setQueryLanguageOutputRun, + inputRef, + queryLanguage: { + configuration: queryLanguage.configuration, + parameters: modes.find(({ id }) => id === queryLanguage.id), + }, + }); + queryLanguageOutputRunPreviousOutput.current = { + ...queryLanguageOutputRun.output + }; + setQueryLanguageOutputRun(queryLanguageOutput); })(); }, [input, queryLanguage]); useEffect(() => { - onChange && onChange(queryLanguageOutputRun.output); + onChange + // Ensure the previous output is different to the new one + && !_.isEqual(queryLanguageOutputRun.output, queryLanguageOutputRunPreviousOutput.current) + && onChange(queryLanguageOutputRun.output); }, [queryLanguageOutputRun.output]); const onQueryLanguagePopoverSwitch = () => From b547d985f239dbb26bc39d712e50c8848276c428 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Thu, 27 Apr 2023 08:53:38 +0200 Subject: [PATCH 26/76] feat(search-bar): allow the API query output can be redone when the search term fields changed - Search bar: - Add a dependency to run the query language output - Adapt search bar documentation to the changes - WQL - Create a new parameter called `options` - Moved the `implicitFilter` and `searchTerm` settings to `options` - Update tests - Update documentation --- public/components/search-bar/README.md | 23 +++++++------ public/components/search-bar/index.tsx | 6 ++-- .../search-bar/query-language/wql.md | 29 ++++++++--------- .../search-bar/query-language/wql.test.tsx | 14 ++++---- .../search-bar/query-language/wql.tsx | 32 +++++++++++-------- 5 files changed, 57 insertions(+), 47 deletions(-) diff --git a/public/components/search-bar/README.md b/public/components/search-bar/README.md index b017a5871d..b8e95cef2a 100644 --- a/public/components/search-bar/README.md +++ b/public/components/search-bar/README.md @@ -28,12 +28,15 @@ Basic usage: { id: 'wql', // specific query language parameters - // implicit query. Optional - // Set a implicit query that can't be changed by the user. - // Use the UQL (Unified Query Language) syntax. - implicitQuery: { - query: 'id!=000', - conjunction: ';' + options: { + // implicit query. Optional + // Set a implicit query that can't be changed by the user. + // Use the UQL (Unified Query Language) syntax. + implicitQuery: { + query: 'id!=000', + conjunction: ';' + }, + searchTermFields: ['id', 'ip'] }, suggestions: { field(currentValue) { @@ -90,9 +93,9 @@ Basic usage: // Used to define the internal input. Optional. // This could be used to change the input text from the external components. // Use the UQL (Unified Query Language) syntax. - input="" + input='' // Define the default mode. Optional. If not defined, it will use the first one mode. - defaultMode="" + defaultMode='' > ``` @@ -119,7 +122,7 @@ type SearchBarQueryLanguage = { searchBarProps: any, output: { language: string, - unifiedQuery: string, + apiQuery: string, query: string } }>; @@ -140,7 +143,7 @@ where: customization the properties that will used by the base search bar component and the output used when searching - `output`: - `language`: query language ID - - `unifiedQuery`: query in unified query syntax + - `apiQuery`: API query. - `query`: current query in the specified language - `transformUQLToQL`: method that transforms the UQL (Unified Query Language) to the specific query language. This is used when receives a external input in the Unified Query Language, the returned diff --git a/public/components/search-bar/index.tsx b/public/components/search-bar/index.tsx index a2b771d89e..eb2be4bbd1 100644 --- a/public/components/search-bar/index.tsx +++ b/public/components/search-bar/index.tsx @@ -75,6 +75,8 @@ export const SearchBar = ({ } }; + const selectedQueryLanguageParameters = modes.find(({ id }) => id === queryLanguage.id); + useEffect(() => { // React to external changes and set the internal input text. Use the `transformUQLToQL` of // the query language in use @@ -103,7 +105,7 @@ export const SearchBar = ({ inputRef, queryLanguage: { configuration: queryLanguage.configuration, - parameters: modes.find(({ id }) => id === queryLanguage.id), + parameters: selectedQueryLanguageParameters, }, }); queryLanguageOutputRunPreviousOutput.current = { @@ -111,7 +113,7 @@ export const SearchBar = ({ }; setQueryLanguageOutputRun(queryLanguageOutput); })(); - }, [input, queryLanguage]); + }, [input, queryLanguage, selectedQueryLanguageParameters?.options]); useEffect(() => { onChange diff --git a/public/components/search-bar/query-language/wql.md b/public/components/search-bar/query-language/wql.md index ce70e7e6c6..ef44107efb 100644 --- a/public/components/search-bar/query-language/wql.md +++ b/public/components/search-bar/query-language/wql.md @@ -145,30 +145,29 @@ field1~user_input,field2~user_input,field3~user_input ## Options -- `implicitQuery`: add an implicit query that is added to the user input. Optional. +- `options`: options + + - `implicitQuery`: add an implicit query that is added to the user input. Optional. This can't be changed by the user. If this is defined, will be displayed as a prepend of the search bar. - - `query`: query string in UQL (Unified Query Language) + - `query`: query string in UQL (Unified Query Language) Use UQL (Unified Query Language). - - `conjunction`: query string of the conjunction in UQL (Unified Query Language) + - `conjunction`: query string of the conjunction in UQL (Unified Query Language) + - `searchTermFields`: define the fields used to build the query for the search term mode ```ts // language options -// ID is not equal to 000 and . This is defined in UQL that is transformed internally to -// the specific query language. -implicitQuery: { - query: 'id!=000', - conjunction: ';' +options: { + // ID is not equal to 000 and . This is defined in UQL that is transformed internally to + // the specific query language. + implicitQuery: { + query: 'id!=000', + conjunction: ';' + } + searchTermFields: ['id', 'ip'] } ``` -- `searchTermFields`: define the fields used to build the query for the search term mode - -```ts -// language options -searchTermFields: ['id', 'ip'] -``` - - `suggestions`: define the suggestion handlers. This is required. - `field`: method that returns the suggestions for the fields diff --git a/public/components/search-bar/query-language/wql.test.tsx b/public/components/search-bar/query-language/wql.test.tsx index f40d81bf22..781c24c335 100644 --- a/public/components/search-bar/query-language/wql.test.tsx +++ b/public/components/search-bar/query-language/wql.test.tsx @@ -10,9 +10,11 @@ describe('SearchBar component', () => { modes: [ { id: WQL.id, - implicitQuery: { - query: 'id!=000', - conjunction: ';' + options: { + implicitQuery: { + query: 'id!=000', + conjunction: ';' + }, }, suggestions: { field(currentValue) { @@ -127,7 +129,7 @@ describe('Query language - WQL', () => { // Get suggestions it.each` input | suggestions - ${''} | ${[{ description: 'Field', label: 'field', type: 'field' }, { description: 'Field2', label: 'field2', type: 'field' }, { description: 'open group', label: '(', type: 'operator_group' }]} + ${''} | ${[{ description: 'run the search query', label: 'Search', type: 'function_search' }, { description: 'Field', label: 'field', type: 'field' }, { description: 'Field2', label: 'field2', type: 'field' }, { description: 'open group', label: '(', type: 'operator_group' }]} ${'w'} | ${[]} ${'f'} | ${[{ description: 'Field', label: 'field', type: 'field' }, { description: 'Field2', label: 'field2', type: 'field' }]} ${'field'} | ${[{ description: 'Field2', label: 'field2', type: 'field' }, { description: 'equality', label: '=', type: 'operator_compare' }, { description: 'not equality', label: '!=', type: 'operator_compare' }, { description: 'bigger', label: '>', type: 'operator_compare' }, { description: 'smaller', label: '<', type: 'operator_compare' }, { description: 'like as', label: '~', type: 'operator_compare' }]} @@ -295,7 +297,7 @@ describe('Query language - WQL', () => { setInput: (value: string): void => { input = value; }, queryLanguage: { parameters: { - implicitQuery: '', + options: {}, suggestions: { field: () => ([]), value: () => ([]) @@ -399,7 +401,7 @@ describe('Query language - WQL', () => { const qlOutput = await WQL.run(currentInput, { queryLanguage: { parameters: { - implicitQuery: '', + options: {}, suggestions: { field: () => (['field1', 'field2'].map(label => ({label}))), value: () => ([]) diff --git a/public/components/search-bar/query-language/wql.tsx b/public/components/search-bar/query-language/wql.tsx index e9b9c2ba16..e63db6fa99 100644 --- a/public/components/search-bar/query-language/wql.tsx +++ b/public/components/search-bar/query-language/wql.tsx @@ -209,8 +209,10 @@ type OptionsQLImplicitQuery = { conjunction: string } type OptionsQL = { - implicitQuery?: OptionsQLImplicitQuery - searchTermFields?: string[] + options?: { + implicitQuery?: OptionsQLImplicitQuery + searchTermFields?: string[] + } suggestions: { field: QLOptionSuggestionHandler; value: QLOptionSuggestionHandler; @@ -579,13 +581,13 @@ export function transformSpecificQLToUnifiedQL(input: string, searchTermFields: */ function getOutput(input: string, options: OptionsQL) { // Implicit query - const implicitQueryAsUQL = options?.implicitQuery?.query ?? ''; + const implicitQueryAsUQL = options?.options?.implicitQuery?.query ?? ''; const implicitQueryAsQL = transformUQLToQL( implicitQueryAsUQL ); // Implicit query conjunction - const implicitQueryConjunctionAsUQL = options?.implicitQuery?.conjunction ?? ''; + const implicitQueryConjunctionAsUQL = options?.options?.implicitQuery?.conjunction ?? ''; const implicitQueryConjunctionAsQL = transformUQLToQL( implicitQueryConjunctionAsUQL ); @@ -594,16 +596,18 @@ function getOutput(input: string, options: OptionsQL) { const inputQueryAsQL = input; const inputQueryAsUQL = transformSpecificQLToUnifiedQL( inputQueryAsQL, - options?.searchTermFields ?? [] + options?.options?.searchTermFields ?? [] ); return { language: WQL.id, - unifiedQuery: [ - implicitQueryAsUQL, - implicitQueryAsUQL && inputQueryAsUQL ? implicitQueryConjunctionAsUQL : '', - implicitQueryAsUQL && inputQueryAsUQL ? `(${inputQueryAsUQL})`: inputQueryAsUQL - ].join(''), + apiQuery: { + q: [ + implicitQueryAsUQL, + implicitQueryAsUQL && inputQueryAsUQL ? implicitQueryConjunctionAsUQL : '', + implicitQueryAsUQL && inputQueryAsUQL ? `(${inputQueryAsUQL})`: inputQueryAsUQL + ].join(''), + }, query: [ implicitQueryAsQL, implicitQueryAsQL && inputQueryAsQL ? implicitQueryConjunctionAsQL : '', @@ -749,17 +753,17 @@ export const WQL = { const tokens: ITokens = tokenizer(input); // Get the implicit query as query language syntax - const implicitQueryAsQL = params.queryLanguage.parameters.implicitQuery + const implicitQueryAsQL = params.queryLanguage.parameters?.options?.implicitQuery ? transformUQLToQL( - params.queryLanguage.parameters.implicitQuery.query - + params.queryLanguage.parameters.implicitQuery.conjunction + params.queryLanguage.parameters.options.implicitQuery.query + + params.queryLanguage.parameters.options.implicitQuery.conjunction ) : ''; - // Validate the user input const fieldsSuggestion: string[] = await params.queryLanguage.parameters.suggestions.field() .map(({label}) => label); + // Validate the user input const validationPartial = validatePartial(tokens, { field: fieldsSuggestion }); From a3c3681dae1cabd3bf23e188a19f266f75eb5e46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Thu, 27 Apr 2023 16:44:09 +0200 Subject: [PATCH 27/76] feat(search-bar): enhance the validation of value token in WQL --- .../search-bar/query-language/wql.test.tsx | 5 ++ .../search-bar/query-language/wql.tsx | 71 +++++++++++++------ 2 files changed, 54 insertions(+), 22 deletions(-) diff --git a/public/components/search-bar/query-language/wql.test.tsx b/public/components/search-bar/query-language/wql.test.tsx index 781c24c335..3ec1f1d61f 100644 --- a/public/components/search-bar/query-language/wql.test.tsx +++ b/public/components/search-bar/query-language/wql.test.tsx @@ -374,6 +374,11 @@ describe('Query language - WQL', () => { ${'field2=value'} | ${undefined} ${'field=value'} | ${['"field" is not a valid field.']} ${'custom=value'} | ${['"custom" is not a valid field.']} + ${'field1=value!test'} | ${['"value!test" is not a valid value. Invalid characters found: !']} + ${'field1=value&test'} | ${['"value&test" is not a valid value. Invalid characters found: &']} + ${'field1=value!value&test'} | ${['"value!value&test" is not a valid value. Invalid characters found: !, &']} + ${'field1=value!value!test'} | ${['"value!value!test" is not a valid value. Invalid characters found: !']} + ${'field1=value!value!t$&st'} | ${['"value!value!t$&st" is not a valid value. Invalid characters found: !, $, &']} ${'field1=value and'} | ${['There is no whitespace after conjunction "and".', 'There is no sentence after conjunction "and".']} ${'field2=value and'} | ${['There is no whitespace after conjunction "and".', 'There is no sentence after conjunction "and".']} ${'field=value and'} | ${['"field" is not a valid field.', 'There is no whitespace after conjunction "and".', 'There is no sentence after conjunction "and".']} diff --git a/public/components/search-bar/query-language/wql.tsx b/public/components/search-bar/query-language/wql.tsx index e63db6fa99..6935f50102 100644 --- a/public/components/search-bar/query-language/wql.tsx +++ b/public/components/search-bar/query-language/wql.tsx @@ -616,6 +616,51 @@ function getOutput(input: string, options: OptionsQL) { }; }; +/** + * Validate the token value + * @param token + * @returns + */ +function validateTokenValue(token: IToken): string | undefined { + // Usage of API regular expression + const re = new RegExp( + // Value: A string. + '^(?(?:(?:\\((?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|[\\[\\]\\w _\\-.:?\\/\'"=@%<>{}]*)\\))*' + + '(?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|[\\[\\]\\w _\\-.:?\\\\/\'"=@%<>{}]+)' + + '(?:\\((?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|[\\[\\]\\w _\\-.:?\\\\/\'"=@%<>{}]*)\\))*)+)$' + ); + + /* WARN: the validation for the value token is complex, this supports some characters in + certain circumstances. + + Ideally a character validation helps to the user to identify the problem in the query, but + as the original regular expression is so complex, the logic to get this can be + complicated. + + The original regular expression has a common schema of allowed characters, these and other + characters of the original regular expression can be used to check each character. This + approach can identify some invalid characters despite this is not the ideal way. + + The ideal solution will be check each subset of the complex regex against the allowed + characters. + */ + + const invalidCharacters: string[] = token.value.split('') + .filter((character) => !(new RegExp('[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}\\(\\)]').test(character))) + .filter((value, index, array) => array.indexOf(value) === index); + + const match = token.value.match(re); + return match?.groups?.value === token.value + ? undefined + : [ + `"${token.value}" is not a valid value.`, + ...(invalidCharacters.length + ? [`Invalid characters found: ${invalidCharacters.join(', ')}`] + : [] + ) + ].join(' '); +}; + /** * Validate the tokens while the user is building the query * @param tokens @@ -643,18 +688,7 @@ function validatePartial(tokens: ITokens, options: {field: string[]}): undefined }; // Check if the value is allowed if(token.type === 'value'){ - // Usage of API regular expression - const re = new RegExp( - // Value: A string. - '^(?(?:(?:\\((?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|[\\[\\]\\w _\\-.:?\\/\'"=@%<>{}]*)\\))*' + - '(?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|[\\[\\]\\w _\\-.:?\\\\/\'"=@%<>{}]+)' + - '(?:\\((?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|[\\[\\]\\w _\\-.:?\\\\/\'"=@%<>{}]*)\\))*)+)$' - ); - - const match = token.value.match(re); - return match?.groups?.value === token.value - ? undefined - : `"${token.value}" is not a valid value222.`; + return validateTokenValue(token); } }; }) @@ -697,21 +731,14 @@ function validate(tokens: ITokens, options: {field: string[]}): undefined | stri }; // Check if the value is allowed if(token.type === 'value'){ - // Usage of API regular expression - const re = new RegExp( - // Value: A string. - '^(?(?:(?:\\((?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|[\\[\\]\\w _\\-.:?\\/\'"=@%<>{}]*)\\))*' + - '(?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|[\\[\\]\\w _\\-.:?\\\\/\'"=@%<>{}]+)' + - '(?:\\((?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|[\\[\\]\\w _\\-.:?\\\\/\'"=@%<>{}]*)\\))*)+)$' - ); + const validationError = validateTokenValue(token); - const match = token.value.match(re); - match?.groups?.value !== token.value && errors.push(`"${token.value}" is not a valid value.`); + validationError && errors.push(validationError); }; // Check if the value is allowed if(token.type === 'conjunction'){ - + const tokenWhitespaceNearToFieldNext = getTokenNearTo( tokens, 'whitespace', From 1b390bece053e6f2dae441fedb4de0493a50309e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Fri, 12 May 2023 12:51:36 +0200 Subject: [PATCH 28/76] feat(search-bar): enhance search bar and WQL Search bar: - Add the possibility to render buttons to the right of the input - Minor changes WQL: - Add options.filterButtons to render filter buttons and component rendering - Extract the quoted value for the quoted token values - Add the `validate` parameter to validate the tokens (only available for `value` token) - Enhance language description - Add test related to value token validation - Update language documentation with this changes --- public/components/search-bar/index.tsx | 179 ++++++++++-------- .../search-bar/query-language/wql.md | 92 +++++---- .../search-bar/query-language/wql.test.tsx | 34 ++-- .../search-bar/query-language/wql.tsx | 113 ++++++++--- 4 files changed, 264 insertions(+), 154 deletions(-) diff --git a/public/components/search-bar/index.tsx b/public/components/search-bar/index.tsx index eb2be4bbd1..0b4ce9909a 100644 --- a/public/components/search-bar/index.tsx +++ b/public/components/search-bar/index.tsx @@ -8,6 +8,8 @@ import { EuiSpacer, EuiSelect, EuiText, + EuiFlexGroup, + EuiFlexItem } from '@elastic/eui'; import { EuiSuggest } from '../eui-suggest'; import { searchBarQueryLanguages } from './query-language'; @@ -43,7 +45,7 @@ export const SearchBar = ({ const [isOpenPopoverQueryLanguage, setIsOpenPopoverQueryLanguage] = useState(false); // Input field - const [input, setInput] = useState(''); + const [input, setInput] = useState(rest.input || ''); // Query language output of run method const [queryLanguageOutputRun, setQueryLanguageOutputRun] = useState({ searchBarProps: { suggestions: [] }, @@ -80,7 +82,7 @@ export const SearchBar = ({ useEffect(() => { // React to external changes and set the internal input text. Use the `transformUQLToQL` of // the query language in use - rest.input && setInput( + rest.input && searchBarQueryLanguages[queryLanguage.id]?.transformUQLToQL && setInput( searchBarQueryLanguages[queryLanguage.id]?.transformUQLToQL?.( rest.input, ), @@ -125,86 +127,97 @@ export const SearchBar = ({ const onQueryLanguagePopoverSwitch = () => setIsOpenPopoverQueryLanguage(state => !state); - return ( - {}} /* This method is run by EuiSuggest when there is a change in - a div wrapper of the input and should be defined. Defining this - property prevents an error. */ - suggestions={[]} - isPopoverOpen={ - queryLanguageOutputRun?.searchBarProps?.suggestions?.length > 0 && - isOpenSuggestionPopover - } - onClosePopover={() => setIsOpenSuggestionPopover(false)} - onPopoverFocus={() => setIsOpenSuggestionPopover(true)} - placeholder={'Search'} - append={ - - {searchBarQueryLanguages[queryLanguage.id].label} - - } - isOpen={isOpenPopoverQueryLanguage} - closePopover={onQueryLanguagePopoverSwitch} - > - SYNTAX OPTIONS -
- - {searchBarQueryLanguages[queryLanguage.id].description} - - {searchBarQueryLanguages[queryLanguage.id].documentationLink && ( - <> - -
- - Documentation - -
- - )} - {modes?.length > 1 && ( - <> - - - ({ - value: id, - text: searchBarQueryLanguages[id].label, - }))} - value={queryLanguage.id} - onChange={(event: React.ChangeEvent) => { - const queryLanguageID: string = event.target.value; - setQueryLanguage({ - id: queryLanguageID, - configuration: - searchBarQueryLanguages[ - queryLanguageID - ]?.getConfiguration?.() || {}, - }); - setInput(''); - }} - aria-label='query-language-selector' - /> - - - )} -
-
- } - {...queryLanguageOutputRun.searchBarProps} - /> + const searchBar = ( + <> + {}} /* This method is run by EuiSuggest when there is a change in + a div wrapper of the input and should be defined. Defining this + property prevents an error. */ + suggestions={[]} + isPopoverOpen={ + queryLanguageOutputRun?.searchBarProps?.suggestions?.length > 0 && + isOpenSuggestionPopover + } + onClosePopover={() => setIsOpenSuggestionPopover(false)} + onPopoverFocus={() => setIsOpenSuggestionPopover(true)} + placeholder={'Search'} + append={ + + {searchBarQueryLanguages[queryLanguage.id].label} + + } + isOpen={isOpenPopoverQueryLanguage} + closePopover={onQueryLanguagePopoverSwitch} + > + SYNTAX OPTIONS +
+ + {searchBarQueryLanguages[queryLanguage.id].description} + + {searchBarQueryLanguages[queryLanguage.id].documentationLink && ( + <> + +
+ + Documentation + +
+ + )} + {modes?.length > 1 && ( + <> + + + ({ + value: id, + text: searchBarQueryLanguages[id].label, + }))} + value={queryLanguage.id} + onChange={(event: React.ChangeEvent) => { + const queryLanguageID: string = event.target.value; + setQueryLanguage({ + id: queryLanguageID, + configuration: + searchBarQueryLanguages[ + queryLanguageID + ]?.getConfiguration?.() || {}, + }); + setInput(''); + }} + aria-label='query-language-selector' + /> + + + )} +
+
+ } + {...queryLanguageOutputRun.searchBarProps} + /> + ); + return rest.buttonsRender || queryLanguageOutputRun.filterButtons + ? ( + + {searchBar} + {rest.buttonsRender && {rest.buttonsRender()}} + {queryLanguageOutputRun.filterButtons && {queryLanguageOutputRun.filterButtons}} + + ) + : searchBar; }; diff --git a/public/components/search-bar/query-language/wql.md b/public/components/search-bar/query-language/wql.md index ef44107efb..108c942d32 100644 --- a/public/components/search-bar/query-language/wql.md +++ b/public/components/search-bar/query-language/wql.md @@ -153,6 +153,7 @@ field1~user_input,field2~user_input,field3~user_input Use UQL (Unified Query Language). - `conjunction`: query string of the conjunction in UQL (Unified Query Language) - `searchTermFields`: define the fields used to build the query for the search term mode + - `filterButtons`: define a list of buttons to filter in the search bar ```ts @@ -165,6 +166,9 @@ options: { conjunction: ';' } searchTermFields: ['id', 'ip'] + filterButtons: [ + {id: 'status-active', input: 'status=active', label: 'Active'} + ] } ``` @@ -197,45 +201,63 @@ options: { } ``` +- `validate`: define validation methods for the field types. Optional + - `value`: method to validate the value token + + ```ts + validate: { + value: (token, {field, operator_compare}) => { + if(field === 'field1'){ + const value = token.formattedValue || token.value + return /\d+/ ? undefined : `Invalid value for field ${field}, only digits are supported: "${value}"` + } + } + } + ``` + ## Language workflow ```mermaid graph TD; - user_input[User input]-->tokenizer; - subgraph tokenizer - tokenize_regex[Query language regular expression] - end - - tokenizer-->tokens; - tokens-->validate; - - tokens-->searchBarProps; - subgraph searchBarProps; - searchBarProps_suggestions[suggestions]-->searchBarProps_suggestions_input_isvalid{Input is valid} - searchBarProps_suggestions_input_isvalid{Is input valid?}-->searchBarProps_suggestions_input_isvalid_success[Yes] - searchBarProps_suggestions_input_isvalid{Is input valid?}-->searchBarProps_suggestions_input_isvalid_fail[No] - searchBarProps_suggestions_input_isvalid_success[Yes]--->searchBarProps_suggestions_get_last_token_with_value[Get last token with value]-->searchBarProps_suggestions__result[Suggestions] - searchBarProps_suggestions__result[Suggestions]-->EuiSuggestItem - searchBarProps_suggestions_input_isvalid_fail[No]-->searchBarProps_suggestions_invalid[Invalid with error message] - searchBarProps_suggestions_invalid[Invalid with error message]-->EuiSuggestItem - searchBarProps_prepend[prepend]-->searchBarProps_prepend_implicitQuery{implicitQuery} - searchBarProps_prepend_implicitQuery{implicitQuery}-->searchBarProps_prepend_implicitQuery_yes[Yes]-->EuiButton - searchBarProps_prepend_implicitQuery{implicitQuery}-->searchBarProps_prepend_implicitQuery_no[No]-->null - searchBarProps_disableFocusTrap:true[disableFocusTrap = true] - searchBarProps_onItemClick[onItemClickSuggestion]-->searchBarProps_onItemClick_suggestion_search[Search suggestion] - searchBarProps_onItemClick_suggestion_search[Search suggestion]-->searchBarProps_onItemClick_suggestion_search_run[Run search] - searchBarProps_onItemClick[onItemClickSuggestion]-->searchBarProps_onItemClick_suggestion_edit_current_token[Edit current token]-->searchBarProps_onItemClick_build_input[Build input] - searchBarProps_onItemClick[onItemClickSuggestion]-->searchBarProps_onItemClick_suggestion_add_new_token[Add new token]-->searchBarProps_onItemClick_build_input[Build input] - searchBarProps_onItemClick[onItemClickSuggestion]-->searchBarProps_onItemClick_suggestion_error[Error] - searchBarProps_isInvalid[isInvalid] - end - - tokens-->output; - subgraph output[output]; - output_result[implicitFilter + user input] - end - - output-->output_search_bar[Output] + user_input[User input]-->ql_run; + ql_run-->filterButtons[filterButtons]; + ql_run-->tokenizer-->tokens; + tokens-->searchBarProps; + tokens-->output; + + subgraph tokenizer + tokenize_regex[Query language regular expression: decomposition and extract quoted values] + end + + subgraph searchBarProps; + searchBarProps_suggestions[suggestions]-->searchBarProps_suggestions_input_isvalid{Input is valid} + searchBarProps_suggestions_input_isvalid{Is input valid?}-->searchBarProps_suggestions_input_isvalid_success[Yes] + searchBarProps_suggestions_input_isvalid{Is input valid?}-->searchBarProps_suggestions_input_isvalid_fail[No] + searchBarProps_suggestions_input_isvalid_success[Yes]--->searchBarProps_suggestions_get_last_token_with_value[Get last token with value]-->searchBarProps_suggestions__result[Suggestions] + searchBarProps_suggestions__result[Suggestions]-->EuiSuggestItem + searchBarProps_suggestions_input_isvalid_fail[No]-->searchBarProps_suggestions_invalid[Invalid with error message] + searchBarProps_suggestions_invalid[Invalid with error message]-->EuiSuggestItem + searchBarProps_prepend[prepend]-->searchBarProps_prepend_implicitQuery{options.implicitQuery} + searchBarProps_prepend_implicitQuery{options.implicitQuery}-->searchBarProps_prepend_implicitQuery_yes[Yes]-->EuiButton + searchBarProps_prepend_implicitQuery{options.implicitQuery}-->searchBarProps_prepend_implicitQuery_no[No]-->null + searchBarProps_disableFocusTrap:true[disableFocusTrap = true] + searchBarProps_onItemClick[onItemClickSuggestion]-->searchBarProps_onItemClick_suggestion_search[Search suggestion] + searchBarProps_onItemClick_suggestion_search[Search suggestion]-->searchBarProps_onItemClick_suggestion_search_run[Run search] + searchBarProps_onItemClick[onItemClickSuggestion]-->searchBarProps_onItemClick_suggestion_edit_current_token[Edit current token]-->searchBarProps_onItemClick_build_input[Build input] + searchBarProps_onItemClick[onItemClickSuggestion]-->searchBarProps_onItemClick_suggestion_add_new_token[Add new token]-->searchBarProps_onItemClick_build_input[Build input] + searchBarProps_onItemClick[onItemClickSuggestion]-->searchBarProps_onItemClick_suggestion_error[Error] + searchBarProps_isInvalid[isInvalid]-->searchBarProps_validate_input[validate input] + end + + subgraph output[output]; + output_input_options_implicitFilter[options.implicitFilter]-->output_input_options_result["{apiQuery: { q }, error, language: 'wql', query}"] + output_input_user_input_QL[User input in QL]-->output_input_user_input_UQL[User input in UQL]-->output_input_options_result["{apiQuery: { q }, error, language: 'wql', query}"] + end + + subgraph filterButtons; + filterButtons_optional{options.filterButtons}-->filterButtons_optional_yes[Yes]-->filterButtons_optional_yes_component[Render fitter button] + filterButtons_optional{options.filterButtons}-->filterButtons_optional_no[No]-->filterButtons_optional_no_null[null] + end ``` ## Notes diff --git a/public/components/search-bar/query-language/wql.test.tsx b/public/components/search-bar/query-language/wql.test.tsx index 3ec1f1d61f..56f11d9256 100644 --- a/public/components/search-bar/query-language/wql.test.tsx +++ b/public/components/search-bar/query-language/wql.test.tsx @@ -50,15 +50,15 @@ describe('SearchBar component', () => { /* eslint-disable max-len */ describe('Query language - WQL', () => { // Tokenize the input - function tokenCreator({type, value}){ - return {type, value}; + function tokenCreator({type, value, formattedValue}){ + return {type, value, ...(formattedValue ? { formattedValue } : {})}; }; const t = { opGroup: (value = undefined) => tokenCreator({type: 'operator_group', value}), opCompare: (value = undefined) => tokenCreator({type: 'operator_compare', value}), field: (value = undefined) => tokenCreator({type: 'field', value}), - value: (value = undefined) => tokenCreator({type: 'value', value}), + value: (value = undefined, formattedValue = undefined) => tokenCreator({type: 'value', value, formattedValue}), whitespace: (value = undefined) => tokenCreator({type: 'whitespace', value}), conjunction: (value = undefined) => tokenCreator({type: 'conjunction', value}) }; @@ -107,20 +107,20 @@ describe('Query language - WQL', () => { ${'field=value~'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value~'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie] /** ~ character is not supported as value in the q query parameter */} ${'field="'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('"'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} ${'field="value and'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('"value and'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} - ${'field="value and value2"'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('"value and value2"'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} - ${'field="value or value2"'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('"value or value2"'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} - ${'field="value = value2"'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('"value = value2"'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} - ${'field="value != value2"'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('"value != value2"'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} - ${'field="value > value2"'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('"value > value2"'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} - ${'field="value < value2"'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('"value < value2"'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} - ${'field="value ~ value2"'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('"value ~ value2"'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie] /** ~ character is not supported as value in the q query parameter */} + ${'field="value and value2"'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('"value and value2"', 'value and value2'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field="value or value2"'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('"value or value2"', 'value or value2'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field="value = value2"'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('"value = value2"', 'value = value2'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field="value != value2"'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('"value != value2"', 'value != value2'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field="value > value2"'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('"value > value2"', 'value > value2'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field="value < value2"'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('"value < value2"', 'value < value2'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field="value ~ value2"'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('"value ~ value2"', 'value ~ value2'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie] /** ~ character is not supported as value in the q query parameter */} ${'field=value and'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.conjunction('and'), tu.whitespace, ...tuBlankSerie]} ${'field=value and '} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.conjunction('and'), t.whitespace(' '), ...tuBlankSerie]} ${'field=value and field2'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.conjunction('and'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.field('field2'), tu.whitespace, tu.opCompare, tu.whitespace, tu.value, tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} ${'field=value and field2!='} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.conjunction('and'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.field('field2'), tu.whitespace, t.opCompare('!='), tu.whitespace, tu.value, tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} ${'field=value and field2!="'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.conjunction('and'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.field('field2'), tu.whitespace, t.opCompare('!='), tu.whitespace, t.value('"'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} ${'field=value and field2!="value'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.conjunction('and'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.field('field2'), tu.whitespace, t.opCompare('!='), tu.whitespace, t.value('"value'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} - ${'field=value and field2!="value"'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.conjunction('and'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.field('field2'), tu.whitespace, t.opCompare('!='), tu.whitespace, t.value('"value"'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field=value and field2!="value"'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.conjunction('and'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.field('field2'), tu.whitespace, t.opCompare('!='), tu.whitespace, t.value('"value"', 'value'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} ${'field=value and field2!=value2 and'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.conjunction('and'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.field('field2'), tu.whitespace, t.opCompare('!='), tu.whitespace, t.value('value2'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.conjunction('and'), tu.whitespace, ...tuBlankSerie]} `(`Tokenizer API input $input`, ({input, tokens}) => { expect(tokenizer(input)).toEqual(tokens); @@ -371,6 +371,8 @@ describe('Query language - WQL', () => { ${'field='} | ${['"field" is not a valid field.']} ${'custom='} | ${['"custom" is not a valid field.']} ${'field1=value'} | ${undefined} + ${'field1=1'} | ${['Numbers are not valid for field1']} + ${'field1=value1'} | ${['Numbers are not valid for field1']} ${'field2=value'} | ${undefined} ${'field=value'} | ${['"field" is not a valid field.']} ${'custom=value'} | ${['"custom" is not a valid field.']} @@ -410,6 +412,16 @@ describe('Query language - WQL', () => { suggestions: { field: () => (['field1', 'field2'].map(label => ({label}))), value: () => ([]) + }, + validate: { + value: (token, {field, operator_compare}) => { + if(field === 'field1'){ + const value = token.formattedValue || token.value; + return /\d/.test(value) + ? `Numbers are not valid for ${field}` + : undefined + } + } } } } diff --git a/public/components/search-bar/query-language/wql.tsx b/public/components/search-bar/query-language/wql.tsx index 6935f50102..17fda7ec48 100644 --- a/public/components/search-bar/query-language/wql.tsx +++ b/public/components/search-bar/query-language/wql.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { EuiButtonEmpty, EuiPopover, EuiText, EuiCode } from '@elastic/eui'; +import { EuiButtonEmpty, EuiButtonGroup, EuiPopover, EuiText, EuiCode } from '@elastic/eui'; import { tokenizer as tokenizerUQL } from './aql'; import { PLUGIN_VERSION_SHORT } from '../../../../common/constants'; @@ -171,15 +171,20 @@ export function tokenizer(input: string): ITokens{ return [ ...input.matchAll(re) ].map( - ({groups}) => Object.entries(groups) - .map(([key, value]) => ({ - type: key.startsWith('operator_group') // Transform operator_group group match - ? 'operator_group' - : (key.startsWith('whitespace') // Transform whitespace group match - ? 'whitespace' - : key), - value}) + ({groups}) => Object.entries(groups) + .map(([key, value]) => ({ + type: key.startsWith('operator_group') // Transform operator_group group match + ? 'operator_group' + : (key.startsWith('whitespace') // Transform whitespace group match + ? 'whitespace' + : key), + value, + ...( key === 'value' && value && /^"(.+)"$/.test(value) + ? { formattedValue: value.match(/^"(.+)"$/)[1]} + : {} ) + }) + ) ).flat(); }; @@ -661,13 +666,14 @@ function validateTokenValue(token: IToken): string | undefined { ].join(' '); }; +type ITokenValidator = (tokenValue: IToken, proximityTokens: any) => string | undefined; /** * Validate the tokens while the user is building the query * @param tokens - * @param options + * @param validate * @returns */ -function validatePartial(tokens: ITokens, options: {field: string[]}): undefined | string{ +function validatePartial(tokens: ITokens, validate: {field: ITokenValidator, value: ITokenValidator}): undefined | string{ // Ensure is not in search term mode if (!shouldUseSearchTerm(tokens)){ return tokens.map((token: IToken, index) => { @@ -682,13 +688,28 @@ function validatePartial(tokens: ITokens, options: {field: string[]}): undefined { tokenReferencePosition: index, tokenFoundShouldHaveValue: true } ); return tokenOperatorNearToField - && !options.field.includes(token.value) - ? `"${token.value}" is not a valid field.` + ? validate.field(token) : undefined; }; // Check if the value is allowed if(token.type === 'value'){ - return validateTokenValue(token); + const tokenFieldNearToValue = getTokenNearTo( + tokens, + 'field', + 'previous', + { tokenReferencePosition: index, tokenFoundShouldHaveValue: true } + ); + const tokenOperatorCompareNearToValue = getTokenNearTo( + tokens, + 'operator_compare', + 'previous', + { tokenReferencePosition: index, tokenFoundShouldHaveValue: true } + ); + return validateTokenValue(token) + || (tokenFieldNearToValue && tokenOperatorCompareNearToValue && validate.value ? validate.value(token, { + field: tokenFieldNearToValue?.value, + operator: tokenOperatorCompareNearToValue?.value + }) : undefined); } }; }) @@ -700,10 +721,10 @@ function validatePartial(tokens: ITokens, options: {field: string[]}): undefined /** * Validate the tokens if they are a valid syntax * @param tokens - * @param options + * @param validate * @returns */ -function validate(tokens: ITokens, options: {field: string[]}): undefined | string[]{ +function validate(tokens: ITokens, validate: {field: ITokenValidator, value: ITokenValidator}): undefined | string[]{ if (!shouldUseSearchTerm(tokens)){ const errors = tokens.map((token: IToken, index) => { const errors = []; @@ -721,7 +742,7 @@ function validate(tokens: ITokens, options: {field: string[]}): undefined | stri 'next', { tokenReferencePosition: index, tokenFoundShouldHaveValue: true } ); - if(!options.field.includes(token.value)){ + if(validate.field(token)){ errors.push(`"${token.value}" is not a valid field.`); }else if(!tokenOperatorNearToField){ errors.push(`The operator for field "${token.value}" is missing.`); @@ -731,7 +752,23 @@ function validate(tokens: ITokens, options: {field: string[]}): undefined | stri }; // Check if the value is allowed if(token.type === 'value'){ - const validationError = validateTokenValue(token); + const tokenFieldNearToValue = getTokenNearTo( + tokens, + 'field', + 'previous', + { tokenReferencePosition: index, tokenFoundShouldHaveValue: true } + ); + const tokenOperatorCompareNearToValue = getTokenNearTo( + tokens, + 'operator_compare', + 'previous', + { tokenReferencePosition: index, tokenFoundShouldHaveValue: true } + ); + const validationError = validateTokenValue(token) + || (tokenFieldNearToValue && tokenOperatorCompareNearToValue && validate.value ? validate.value(token, { + field: tokenFieldNearToValue?.value, + operator: tokenOperatorCompareNearToValue?.value + }) : undefined);; validationError && errors.push(validationError); }; @@ -768,7 +805,7 @@ function validate(tokens: ITokens, options: {field: string[]}): undefined | stri export const WQL = { id: 'wql', label: 'WQL', - description: 'WQL (Wazuh Query Language) offers a human query syntax based on the Wazuh API query language.', + description: 'WQL (Wazuh Query Language) provides a human query syntax based on the Wazuh API query language.', documentationLink: `https://github.com/wazuh/wazuh-kibana-app/blob/v${PLUGIN_VERSION_SHORT}/public/components/search-bar/query-language/wql.md`, getConfiguration() { return { @@ -790,14 +827,17 @@ export const WQL = { const fieldsSuggestion: string[] = await params.queryLanguage.parameters.suggestions.field() .map(({label}) => label); + const validators = { + field: ({value}) => fieldsSuggestion.includes(value) ? undefined : `"${value}" is not valid field.`, + ...(params.queryLanguage.parameters?.validate?.value ? { + value: params.queryLanguage.parameters?.validate?.value + } : {}) + }; + // Validate the user input - const validationPartial = validatePartial(tokens, { - field: fieldsSuggestion - }); + const validationPartial = validatePartial(tokens, validators); - const validationStrict = validate(tokens, { - field: fieldsSuggestion - }); + const validationStrict = validate(tokens, validators); // Get the output of query language const output = { @@ -822,6 +862,29 @@ export const WQL = { }; return { + filterButtons: params.queryLanguage.parameters?.options?.filterButtons + ? ( + { id, label } + ))} + idToSelectedMap={{}} + type="multi" + onChange={(id: string) => { + const buttonParams = params.queryLanguage.parameters?.options?.filterButtons.find(({id: buttonID}) => buttonID === id); + if(buttonParams){ + params.setInput(buttonParams.input); + const output = { + ...getOutput(buttonParams.input, params.queryLanguage.parameters), + error: undefined + }; + params.onSearch(output); + } + }} + /> + : null, searchBarProps: { // Props that will be used by the EuiSuggest component // Suggestions From 1e42463f8f23e70e0eeb9b82a108e629dc4d0476 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Mon, 15 May 2023 09:41:41 +0200 Subject: [PATCH 29/76] feat(search-bar): replace search bar in TableWzAPI Replace search bar in TableWzAPI Replace each usage of TableWzAPI Adapt external filters --- public/components/agents/sca/inventory.tsx | 6 +- .../sca/inventory/agent-policies-table.tsx | 12 +- .../agents/sca/inventory/checks-table.tsx | 179 ++++++------- public/components/agents/vuls/inventory.tsx | 13 +- .../agents/vuls/inventory/table.tsx | 167 +++++-------- .../tables/components/export-table-csv.tsx | 4 +- .../common/tables/table-default.tsx | 2 - .../common/tables/table-with-search-bar.tsx | 34 ++- .../components/common/tables/table-wz-api.tsx | 19 +- .../intelligence.tsx | 33 ++- .../mitre_attack_intelligence/resource.tsx | 14 +- .../mitre_attack_intelligence/resources.tsx | 53 ++-- .../cdblists/components/cdblists-table.tsx | 45 +++- .../components/decoders-suggestions.ts | 84 ++++--- .../decoders/components/decoders-table.tsx | 118 +++++---- .../decoders/views/decoder-info.tsx | 4 +- .../ruleset/components/ruleset-suggestions.ts | 235 ++++++++---------- .../ruleset/components/ruleset-table.tsx | 46 +++- .../management/ruleset/views/rule-info.tsx | 14 +- 19 files changed, 547 insertions(+), 535 deletions(-) diff --git a/public/components/agents/sca/inventory.tsx b/public/components/agents/sca/inventory.tsx index 28b2a0c885..43d4d5c223 100644 --- a/public/components/agents/sca/inventory.tsx +++ b/public/components/agents/sca/inventory.tsx @@ -61,7 +61,7 @@ type InventoryState = { loading: boolean; checksIsLoading: boolean; redirect: boolean; - filters: object[]; + filters: string; pageTableChecks: { pageIndex: number; pageSize?: number }; policies: object[]; checks: object[]; @@ -81,7 +81,7 @@ export class Inventory extends Component { itemIdToExpandedRowMap: {}, showMoreInfo: false, loading: false, - filters: [], + filters: {}, pageTableChecks: { pageIndex: 0 }, policies: [], checks: [], @@ -370,7 +370,7 @@ export class Inventory extends Component { buttonStat(text, field, value) { return ( - ); diff --git a/public/components/agents/sca/inventory/agent-policies-table.tsx b/public/components/agents/sca/inventory/agent-policies-table.tsx index 2ab8309e7e..5327d08fc2 100644 --- a/public/components/agents/sca/inventory/agent-policies-table.tsx +++ b/public/components/agents/sca/inventory/agent-policies-table.tsx @@ -16,18 +16,18 @@ export default function SCAPoliciesTable(props: Props) { 'data-test-subj': `sca-row-${idx}`, className: 'customRowClass', onClick: rowProps ? () => rowProps(item) : null - } - } + }; + }; return ( <> - + /> ); } diff --git a/public/components/agents/sca/inventory/checks-table.tsx b/public/components/agents/sca/inventory/checks-table.tsx index 8f8641797f..036ba570b1 100644 --- a/public/components/agents/sca/inventory/checks-table.tsx +++ b/public/components/agents/sca/inventory/checks-table.tsx @@ -20,9 +20,42 @@ type State = { pageTableChecks: { pageIndex: 0 }; }; +const searchBarWQLFieldSuggestions = [ + { label: 'condition', description: 'filter by check condition' }, + { label: 'description', description: 'filter by check description' }, + { label: 'file', description: 'filter by check file' }, + { label: 'rationale', description: 'filter by check rationale' }, + { label: 'reason', description: 'filter by check reason' }, + { label: 'registry', description: 'filter by check registry' }, + { label: 'remediation', description: 'filter by check remediation' }, + { label: 'result', description: 'filter by check result' }, + { label: 'title', description: 'filter by check title' } +]; + +const searchBarWQLOptions = { + searchTermFields: [ + 'command', + 'compliance.key', + 'compliance.value', + 'description', + 'directory', + 'file', + 'id', + 'title', + 'process', + 'registry', + 'rationale', + 'reason', + 'references', + 'remediation', + 'result', + 'rules.type', + 'rules.rule', + ] +}; + export class InventoryPolicyChecksTable extends Component { _isMount = false; - suggestions: IWzSuggestItem[] = []; columnsChecks: any; constructor(props) { super(props); @@ -31,108 +64,9 @@ export class InventoryPolicyChecksTable extends Component { agent, lookingPolicy, itemIdToExpandedRowMap: {}, - filters: filters || [], + filters: filters || '', pageTableChecks: { pageIndex: 0 }, }; - this.suggestions = [ - { - type: 'params', - label: 'condition', - description: 'Filter by check condition', - operators: ['=', '!='], - values: (value) => - getFilterValues( - 'condition', - value, - this.props.agent.id, - this.props.lookingPolicy.policy_id - ), - }, - { - type: 'params', - label: 'description', - description: 'Filter by check description', - operators: ['=', '!='], - values: (value) => - getFilterValues( - 'description', - value, - this.props.agent.id, - this.props.lookingPolicy.policy_id - ), - }, - { - type: 'params', - label: 'file', - description: 'Filter by check file', - operators: ['=', '!='], - values: (value) => - getFilterValues('file', value, this.props.agent.id, this.props.lookingPolicy.policy_id), - }, - { - type: 'params', - label: 'registry', - description: 'Filter by check registry', - operators: ['=', '!='], - values: (value) => - getFilterValues( - 'registry', - value, - this.props.agent.id, - this.props.lookingPolicy.policy_id - ), - }, - { - type: 'params', - label: 'rationale', - description: 'Filter by check rationale', - operators: ['=', '!='], - values: (value) => - getFilterValues( - 'rationale', - value, - this.props.agent.id, - this.props.lookingPolicy.policy_id - ), - }, - { - type: 'params', - label: 'reason', - description: 'Filter by check reason', - operators: ['=', '!='], - values: (value) => - getFilterValues('reason', value, this.props.agent.id, this.props.lookingPolicy.policy_id), - }, - { - type: 'params', - label: 'remediation', - description: 'Filter by check remediation', - operators: ['=', '!='], - values: (value) => - getFilterValues( - 'remediation', - value, - this.props.agent.id, - this.props.lookingPolicy.policy_id - ), - }, - { - type: 'params', - label: 'result', - description: 'Filter by check result', - operators: ['=', '!='], - values: (value) => - getFilterValues('result', value, this.props.agent.id, this.props.lookingPolicy.policy_id), - }, - { - type: 'params', - label: 'title', - description: 'Filter by check title', - operators: ['=', '!='], - values: (value) => - getFilterValues('title', value, this.props.agent.id, this.props.lookingPolicy.policy_id), - }, - ]; this.columnsChecks = [ { field: 'id', @@ -200,8 +134,6 @@ export class InventoryPolicyChecksTable extends Component { ]; } - async componentDidMount() {} - async componentDidUpdate(prevProps) { const { filters } = this.props if (filters !== prevProps.filters) { @@ -306,15 +238,18 @@ export class InventoryPolicyChecksTable extends Component { }; }; + const { filters } = this.state; + const agentID = this.props?.agent?.id; + const scaPolicyID = this.props?.lookingPolicy?.policy_id; + return ( <> { }} downloadCsv showReload - filters={this.state.filters} - onFiltersChange={(filters) => this.setState({ filters })} - tablePageSizeOptions={[10, 25, 50, 100]} + filters={filters} + searchTable + searchBarProps={{ + modes: [ + { + id: 'wql', + options: searchBarWQLOptions, + suggestions: { + field(currentValue) { + return searchBarWQLFieldSuggestions; + }, + value: async (currentValue, { field }) => { + try{ + return await getFilterValues( + field, + currentValue, + agentID, + scaPolicyID, + {}, + (item) => ({label: item}) + ); + }catch(error){ + return []; + }; + }, + }, + } + ] + }} /> ); diff --git a/public/components/agents/vuls/inventory.tsx b/public/components/agents/vuls/inventory.tsx index c9f796e3aa..01cc260038 100644 --- a/public/components/agents/vuls/inventory.tsx +++ b/public/components/agents/vuls/inventory.tsx @@ -59,7 +59,7 @@ interface TitleColors { export class Inventory extends Component { _isMount = false; state: { - filters: []; + filters: object; isLoading: boolean; isLoadingStats: boolean; customBadges: ICustomBadges[]; @@ -82,7 +82,7 @@ export class Inventory extends Component { isLoading: true, isLoadingStats: true, customBadges: [], - filters: [], + filters: {}, stats: [ { title: 0, @@ -167,12 +167,9 @@ export class Inventory extends Component { } buildFilterQuery(field = '', selectedItem = '') { - return [ - { - field: 'q', - value: `${field}=${selectedItem}`, - }, - ]; + return { + q: `${field}=${selectedItem}` + }; } async loadAgent() { diff --git a/public/components/agents/vuls/inventory/table.tsx b/public/components/agents/vuls/inventory/table.tsx index 2c5c604905..77936f5acb 100644 --- a/public/components/agents/vuls/inventory/table.tsx +++ b/public/components/agents/vuls/inventory/table.tsx @@ -11,86 +11,35 @@ */ import React, { Component } from 'react'; -import { Direction } from '@elastic/eui'; import { FlyoutDetail } from './flyout'; -import { filtersToObject, IFilter, IWzSuggestItem } from '../../../wz-search-bar'; import { TableWzAPI } from '../../../../components/common/tables'; import { getFilterValues } from './lib'; import { formatUIDate } from '../../../../react-services/time-service'; +import { EuiIconTip } from '@elastic/eui'; + +const searchBarWQLOptions = { + searchTermFields: [ + 'name', + 'cve', + 'version', + 'architecture', + 'severity', + 'cvss2_score', + 'cvss3_score' + ] +}; export class InventoryTable extends Component { state: { error?: string; - pageIndex: number; - pageSize: number; - sortField: string; isFlyoutVisible: Boolean; - sortDirection: Direction; isLoading: boolean; currentItem: {}; }; - suggestions: IWzSuggestItem[] = [ - { - type: 'q', - label: 'name', - description: 'Filter by package ID', - operators: ['=', '!=', '~'], - values: async (value) => getFilterValues('name', value, this.props.agent.id), - }, - { - type: 'q', - label: 'cve', - description: 'Filter by CVE ID', - operators: ['=', '!=', '~'], - values: async (value) => getFilterValues('cve', value, this.props.agent.id), - }, - { - type: 'q', - label: 'version', - description: 'Filter by CVE version', - operators: ['=', '!=', '~'], - values: async (value) => getFilterValues('version', value, this.props.agent.id), - }, - { - type: 'q', - label: 'architecture', - description: 'Filter by architecture', - operators: ['=', '!=', '~'], - values: async (value) => getFilterValues('architecture', value, this.props.agent.id), - }, - { - type: 'q', - label: 'severity', - description: 'Filter by Severity', - operators: ['=', '!=', '~'], - values: async (value) => getFilterValues('severity', value, this.props.agent.id), - }, - { - type: 'q', - label: 'cvss2_score', - description: 'Filter by CVSS2', - operators: ['=', '!=', '~'], - values: async (value) => getFilterValues('cvss2_score', value, this.props.agent.id), - }, - { - type: 'q', - label: 'cvss3_score', - description: 'Filter by CVSS3', - operators: ['=', '!=', '~'], - values: async (value) => getFilterValues('cvss3_score', value, this.props.agent.id), - }, - { - type: 'q', - label: 'detection_time', - description: 'Filter by Detection Time', - operators: ['=', '!=', '~'], - values: async (value) => getFilterValues('detection_time', value, this.props.agent.id), - }, - ]; props!: { - filters: IFilter[]; + filters: string; agent: any; items: []; onFiltersChange: Function; @@ -100,13 +49,8 @@ export class InventoryTable extends Component { super(props); this.state = { - pageIndex: 0, - pageSize: 15, - sortField: 'name', - sortDirection: 'asc', - isLoading: false, isFlyoutVisible: false, - currentItem: {}, + currentItem: {} }; } @@ -121,31 +65,6 @@ export class InventoryTable extends Component { ); } - async componentDidUpdate(prevProps) { - const { filters } = this.props; - if (JSON.stringify(filters) !== JSON.stringify(prevProps.filters)) { - this.setState({ pageIndex: 0, isLoading: true }); - } - } - - buildSortFilter() { - const { sortField, sortDirection } = this.state; - const direction = sortDirection === 'asc' ? '+' : '-'; - - return direction + sortField; - } - - buildFilter() { - const { pageIndex, pageSize } = this.state; - const filters = filtersToObject(this.props.filters); - const filter = { - ...filters, - offset: pageIndex * pageSize, - limit: pageSize, - sort: this.buildSortFilter(), - }; - return filter; - } columns() { let width; @@ -199,7 +118,16 @@ export class InventoryTable extends Component { }, { field: 'detection_time', - name: 'Detection Time', + name: ( + Detection Time{' '} + + + ), sortable: true, width: `100px`, render: formatUIDate, @@ -217,7 +145,6 @@ export class InventoryTable extends Component { }; const { error } = this.state; - const { filters, onFiltersChange } = this.props; const columns = this.columns(); const selectFields = `select=${[ 'cve', @@ -235,13 +162,13 @@ export class InventoryTable extends Component { 'external_references' ].join(',')}`; + const agentID = this.props.agent.id; + return ( { + try{ + return await getFilterValues(field, currentValue, agentID, {}, label => ({label})); + }catch(error){ + return []; + }; + }, + }, + } + ] + }} /> ); } diff --git a/public/components/common/tables/components/export-table-csv.tsx b/public/components/common/tables/components/export-table-csv.tsx index 01486f31e6..d4bc30b2dc 100644 --- a/public/components/common/tables/components/export-table-csv.tsx +++ b/public/components/common/tables/components/export-table-csv.tsx @@ -15,7 +15,6 @@ import { EuiFlexItem, EuiButtonEmpty } from '@elastic/eui'; -import { filtersToObject } from '../../../wz-search-bar/'; import exportCsv from '../../../../react-services/wz-csv'; import { getToasts } from '../../../../kibana-services'; import { UI_ERROR_SEVERITIES } from '../../../../react-services/error-orchestrator/types'; @@ -34,8 +33,7 @@ export function ExportTableCsv({ endpoint, totalItems, filters, title }) { const downloadCsv = async () => { try { - const filtersObject = filtersToObject(filters); - const formatedFilters = Object.keys(filtersObject).map(key => ({name: key, value: filtersObject[key]})); + const formatedFilters = Object.entries(filters).map(([name, value]) => ({name, value})); showToast('success', 'Your download should begin automatically...', 3000); await exportCsv( endpoint, diff --git a/public/components/common/tables/table-default.tsx b/public/components/common/tables/table-default.tsx index 97e3d50974..0ec7451b8a 100644 --- a/public/components/common/tables/table-default.tsx +++ b/public/components/common/tables/table-default.tsx @@ -111,7 +111,6 @@ export function TableDefault({ hidePerPageOptions }; return ( - <> - ); } diff --git a/public/components/common/tables/table-with-search-bar.tsx b/public/components/common/tables/table-with-search-bar.tsx index 6804dcb1a8..308f634062 100644 --- a/public/components/common/tables/table-with-search-bar.tsx +++ b/public/components/common/tables/table-with-search-bar.tsx @@ -13,10 +13,10 @@ import React, { useState, useEffect, useRef } from 'react'; import { EuiBasicTable, EuiSpacer } from '@elastic/eui'; import _ from 'lodash'; -import { WzSearchBar } from '../../wz-search-bar/'; import { UI_ERROR_SEVERITIES } from '../../../react-services/error-orchestrator/types'; import { UI_LOGGER_LEVELS } from '../../../../common/constants'; import { getErrorOrchestrator } from '../../../react-services/common-services'; +import { SearchBar } from '../../search-bar'; export function TableWithSearchBar({ onSearch, @@ -36,21 +36,26 @@ export function TableWithSearchBar({ const [loading, setLoading] = useState(false); const [items, setItems] = useState([]); const [totalItems, setTotalItems] = useState(0); - const [filters, setFilters] = useState(rest.filters || []); + const [filters, setFilters] = useState(rest.filters || {}); const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: tablePageSizeOptions[0], }); - const [sorting, setSorting] = useState({ sort: { field: tableInitialSortingField, direction: tableInitialSortingDirection, }, }); + const [refresh, setRefresh] = useState(Date.now()); const isMounted = useRef(false); + function updateRefresh() { + setPagination({ pageIndex: 0, pageSize: pagination.pageSize }); + setRefresh(Date.now()); + }; + function tableOnChange({ page = {}, sort = {} }) { if (isMounted.current) { const { index: pageIndex, size: pageSize } = page; @@ -73,9 +78,9 @@ export function TableWithSearchBar({ // We don't want to set the pagination state because there is another effect that has this dependency // and will cause the effect is triggered (redoing the onSearch function). if (isMounted.current) { - // Reset the page index when the endpoint changes. + // Reset the page index when the endpoint or reload changes. // This will cause that onSearch function is triggered because to changes in pagination in the another effect. - setPagination({ pageIndex: 0, pageSize: pagination.pageSize }); + updateRefresh(); } }, [endpoint, reload]); @@ -103,14 +108,15 @@ export function TableWithSearchBar({ } setLoading(false); })(); - }, [filters, pagination, sorting]); + }, [filters, pagination, sorting, refresh]); useEffect(() => { // This effect is triggered when the component is mounted because of how to the useEffect hook works. // We don't want to set the filters state because there is another effect that has this dependency // and will cause the effect is triggered (redoing the onSearch function). if (isMounted.current && !_.isEqual(rest.filters, filters)) { - setFilters(rest.filters || []); + setFilters(rest.filters || {}); + updateRefresh(); } }, [rest.filters]); @@ -128,13 +134,15 @@ export function TableWithSearchBar({ }; return ( <> - { + // Set the query, reset the page index and update the refresh + setFilters(apiQuery); + updateRefresh(); + }} /> { + const { default: defaultFilters, ...restFilters } = filters; + return Object.keys(restFilters).length ? restFilters : defaultFilters; +}; + export function TableWzAPI({ actionButtons, ...rest @@ -44,7 +49,7 @@ export function TableWzAPI({ actionButtons?: ReactNode | ReactNode[]; title?: string; description?: string; - downloadCsv?: boolean; + downloadCsv?: boolean | string; searchTable?: boolean; endpoint: string; buttonOptions?: CustomFilterButton[]; @@ -54,7 +59,7 @@ export function TableWzAPI({ reload?: boolean; }) { const [totalItems, setTotalItems] = useState(0); - const [filters, setFilters] = useState([]); + const [filters, setFilters] = useState({}); const [isLoading, setIsLoading] = useState(false); const onFiltersChange = (filters) => typeof rest.onFiltersChange === 'function' ? rest.onFiltersChange(filters) : null; @@ -72,7 +77,7 @@ export function TableWzAPI({ setFilters(filters); onFiltersChange(filters); const params = { - ...filtersToObject(filters), + ...getFilters(filters), offset: pageIndex * pageSize, limit: pageSize, sort: `${direction === 'asc' ? '+' : '-'}${field}`, @@ -131,7 +136,6 @@ export function TableWzAPI({ const ReloadButton = ( triggerReload()} > @@ -164,8 +168,8 @@ export function TableWzAPI({ )} @@ -182,6 +186,7 @@ export function TableWzAPI({ return ( <> {header} + {rest.description && } {table} ); diff --git a/public/components/overview/mitre_attack_intelligence/intelligence.tsx b/public/components/overview/mitre_attack_intelligence/intelligence.tsx index 5e64bf018e..7eaa3c39dd 100644 --- a/public/components/overview/mitre_attack_intelligence/intelligence.tsx +++ b/public/components/overview/mitre_attack_intelligence/intelligence.tsx @@ -29,7 +29,7 @@ export const ModuleMitreAttackIntelligence = compose( const [selectedResource, setSelectedResource] = useState(MitreAttackResources[0].id); const [searchTermAllResources, setSearchTermAllResources] = useState(''); const searchTermAllResourcesLastSearch = useRef(''); - const [resourceFilters, setResourceFilters] = useState([]); + const [resourceFilters, setResourceFilters] = useState({}); const searchTermAllResourcesUsed = useRef(false); const searchTermAllResourcesAction = useAsyncAction( async (searchTerm) => { @@ -37,11 +37,23 @@ export const ModuleMitreAttackIntelligence = compose( searchTermAllResourcesUsed.current = true; searchTermAllResourcesLastSearch.current = searchTerm; const limitResults = 5; + const fields = ['name', 'description', 'external_id']; return ( await Promise.all( MitreAttackResources.map(async (resource) => { const response = await WzRequest.apiReq('GET', resource.apiEndpoint, { - params: { search: searchTerm, limit: limitResults }, + params: { + ...( + searchTerm + ? { + q: fields + .map(key => `${key}~${searchTerm}`) + .join(',') + } + : {} + ), + limit: limitResults + } }); return { id: resource.id, @@ -53,9 +65,18 @@ export const ModuleMitreAttackIntelligence = compose( response?.data?.data?.total_affected_items && response?.data?.data?.total_affected_items > limitResults && (() => { - setResourceFilters([ - { field: 'search', value: searchTermAllResourcesLastSearch.current }, - ]); + setResourceFilters({ + ...( + searchTermAllResourcesLastSearch.current + ? { + q: fields + .map(key => `${key}~${searchTermAllResourcesLastSearch.current}`) + .join(',') + } + : {} + ) + } + ); setSelectedResource(resource.id); }), }; @@ -76,7 +97,7 @@ export const ModuleMitreAttackIntelligence = compose( const onSelectResource = useCallback( (resourceID) => { - setResourceFilters([]); + setResourceFilters({}); setSelectedResource((prevSelectedResource) => prevSelectedResource === resourceID && searchTermAllResourcesUsed.current ? null diff --git a/public/components/overview/mitre_attack_intelligence/resource.tsx b/public/components/overview/mitre_attack_intelligence/resource.tsx index 9dbf394150..2711aa9625 100644 --- a/public/components/overview/mitre_attack_intelligence/resource.tsx +++ b/public/components/overview/mitre_attack_intelligence/resource.tsx @@ -21,7 +21,7 @@ import { getErrorOrchestrator } from '../../../react-services/common-services'; export const ModuleMitreAttackIntelligenceResource = ({ label, - searchBarSuggestions, + searchBar, apiEndpoint, tableColumnsCreator, initialSortingField, @@ -64,7 +64,7 @@ export const ModuleMitreAttackIntelligenceResource = ({ getErrorOrchestrator().handleError(options); } }; - + const tableColumns = useMemo(() => tableColumnsCreator(setDetails), []); const closeFlyout = useCallback(() => { @@ -79,10 +79,18 @@ export const ModuleMitreAttackIntelligenceResource = ({ tableColumns={tableColumns} tableInitialSortingField={initialSortingField} searchBarPlaceholder={`Search in ${label}`} - searchBarSuggestions={searchBarSuggestions} endpoint={apiEndpoint} tablePageSizeOptions={[10, 15, 25, 50, 100]} filters={resourceFilters} + searchBarProps={{ + modes: [ + { + id: 'wql', + options: searchBar.wql.options, + suggestions: searchBar.wql.suggestions, + } + ] + }} /> {details && ( async (input: string) => { try{ - const response = await WzRequest.apiReq('GET', endpoint, {}); + const response = await WzRequest.apiReq('GET', endpoint, {}); // TODO: change to use distinct return response?.data?.data.affected_items .map(item => item[field]) - .filter(item => item && item.toLowerCase().includes(input.toLowerCase())) + .filter(item => input ? (item && item.toLowerCase().includes(input.toLowerCase())) : true) .sort() - .slice(0,9) + .slice(0, 9) + .map(label => ({label})); }catch(error){ const options = { context: `${ModuleMitreAttackIntelligenceResource.name}.getMitreItemToRedirect`, @@ -49,32 +50,34 @@ const getMitreAttackIntelligenceSuggestions = (endpoint: string, field: string) function buildResource(label: string, labelResource: string){ const id = label.toLowerCase(); const endpoint: string = `/mitre/${id}`; + const fieldsMitreAttactResource = [ + { field: 'description', name: 'description' }, + { field: 'external_id', name: 'external ID' }, + { field: 'name', name: 'name' } + ]; return { label: label, id, - searchBarSuggestions: [ - { - type: 'q', - label: 'description', - description: `${labelResource} description`, - operators: ['~'], - values: (input) => input ? [input] : [] - }, - { - type: 'q', - label: 'name', - description: `${labelResource} name`, - operators: ['=', '!='], - values: getMitreAttackIntelligenceSuggestions(endpoint, 'name') - }, - { - type: 'q', - label: 'external_id', - description: `${labelResource} ID`, - operators: ['=', '!='], - values: getMitreAttackIntelligenceSuggestions(endpoint, 'external_id') + searchBar: { + wql: { + options: { + searchTermFields: fieldsMitreAttactResource.map(({field}) => field) + }, + suggestions: { + field(currentValue) { + return fieldsMitreAttactResource + .map(({field, name}) => ({label: field, description: `filter by ${name}`})); + }, + value: async (currentValue, { field }) => { + try{ // TODO: distinct + return await (getMitreAttackIntelligenceSuggestions(endpoint, field))(currentValue); + }catch(error){ + return []; + }; + }, + } } - ], + }, apiEndpoint: endpoint, fieldName: 'name', initialSortingField: 'name', diff --git a/public/controllers/management/components/management/cdblists/components/cdblists-table.tsx b/public/controllers/management/components/management/cdblists/components/cdblists-table.tsx index db27928072..dd56bf947a 100644 --- a/public/controllers/management/components/management/cdblists/components/cdblists-table.tsx +++ b/public/controllers/management/components/management/cdblists/components/cdblists-table.tsx @@ -29,18 +29,21 @@ import { AddNewFileButton, AddNewCdbListButton, UploadFilesButton, -} from '../../common/actions-buttons' +} from '../../common/actions-buttons'; + +const searchBarWQLOptions = { + searchTermFields: ['filename', 'relative_dirname'], + filterButtons: [ + {id: 'relative-dirname', input: 'relative_dirname=etc/lists', label: 'Custom lists'} + ] +}; function CDBListsTable(props) { - const [filters, setFilters] = useState([]); const [showingFiles, setShowingFiles] = useState(false); const [tableFootprint, setTableFootprint] = useState(0); const resourcesHandler = new ResourcesHandler(ResourcesConstants.LISTS); - const updateFilters = (filters) => { - setFilters(filters); - } const toggleShowFiles = () => { setShowingFiles(!showingFiles); @@ -169,15 +172,35 @@ function CDBListsTable(props) { description={`From here you can manage your lists.`} tableColumns={columns} tableInitialSortingField={'filename'} - searchTable={true} - searchBarSuggestions={[]} + searchTable + searchBarProps={{ + modes: [ + { + id: 'wql', + options: searchBarWQLOptions, + suggestions: { + field(currentValue) { + return [ + {label: 'filename', description: 'filter by filename'}, + {label: 'relative_dirname', description: 'filter by relative path'}, + ]; + }, + value: async (currentValue, { field }) => { + try{ // TODO: distinct + return []; + }catch(error){ + return []; + }; + }, + }, + } + ] + }} endpoint={'/lists'} isExpandable={true} rowProps={getRowProps} - downloadCsv={true} - showReload={true} - filters={filters} - onFiltersChange={updateFilters} + downloadCsv + showReload tablePageSizeOptions={[10, 25, 50, 100]} /> diff --git a/public/controllers/management/components/management/decoders/components/decoders-suggestions.ts b/public/controllers/management/components/management/decoders/components/decoders-suggestions.ts index fbd4ed6999..17cbf37fdd 100644 --- a/public/controllers/management/components/management/decoders/components/decoders-suggestions.ts +++ b/public/controllers/management/components/management/decoders/components/decoders-suggestions.ts @@ -1,44 +1,62 @@ import { WzRequest } from '../../../../../../react-services/wz-request'; -const decodersItems = [ - { - type: 'params', - label: 'filename', - description: 'Filters the decoders by file name.', - values: async value => { - const filter = { limit: 30 }; - if (value) { - filter['search'] = value; - } - const result = await WzRequest.apiReq('GET', '/decoders/files', filter); - return (((result || {}).data || {}).data || {}).affected_items.map((item) => { return item.filename }); - }, +const decodersItems = { + field(currentValue) { + return [ + {label: 'filename', description: 'filter by filename'}, + {label: 'relative_dirname', description: 'filter by relative path'}, + {label: 'status', description: 'filter by status'}, + ]; }, - { - type: 'params', - label: 'relative_dirname', - description: 'Path of the decoders files.', - values: async () => { - const result = await WzRequest.apiReq('GET', '/manager/configuration', { - params: { - section: 'ruleset', - field: 'decoder_dir' + value: async (currentValue, { field }) => { + try{ // TODO: distinct + switch(field){ + case 'filename': { + const filter = { limit: 30 }; + if (value) { + filter['search'] = value; + } + const result = await WzRequest.apiReq('GET', '/decoders/files', filter); + return result?.data?.data?.affected_items.map(item => ({label: item.filename})); + } + case 'relative_dirname': { + const result = await WzRequest.apiReq('GET', '/manager/configuration', { + params: { + section: 'ruleset', + field: 'decoder_dir' + } + }); + return result?.data?.data?.affected_items[0].ruleset.decoder_dir.map(label => ({label})); + } + case 'status': { + return ['enabled', 'disabled'].map(label => ({label})); } - }); - return (((result || {}).data || {}).data || {}).affected_items[0].ruleset.decoder_dir; - } + default: { + return []; + } + } + }catch(error){ + return []; + }; }, - { - type: 'params', - label: 'status', - description: 'Filters the decoders by status.', - values: ['enabled', 'disabled'] - } -]; +}; + +const decodersFiles = { + field(currentValue) { + return []; // TODO: fields + }, + value: async (currentValue, { field }) => { + try{ // TODO: distinct + return []; + }catch(error){ + return []; + }; + }, +}; const apiSuggestsItems = { items: decodersItems, - files: [], + files: decodersFiles, }; export default apiSuggestsItems; \ No newline at end of file diff --git a/public/controllers/management/components/management/decoders/components/decoders-table.tsx b/public/controllers/management/components/management/decoders/components/decoders-table.tsx index 19e2357c64..7d4736a10f 100644 --- a/public/controllers/management/components/management/decoders/components/decoders-table.tsx +++ b/public/controllers/management/components/management/decoders/components/decoders-table.tsx @@ -32,39 +32,49 @@ import { import apiSuggestsItems from './decoders-suggestions'; +const searchBarWQLOptions = { + searchTermFields: [], // TODO: add search term fields + filterButtons: [ + {id: 'relative-dirname', input: 'relative_dirname=etc/decoders', label: 'Custom decoders'} + ] +}; + /*************************************** * Render tables */ const FilesTable = ({ actionButtons, - buttonOptions, columns, searchBarSuggestions, filters, - updateFilters, reload }) => + reload={reload} + actionButtons={actionButtons} + title='Decoders files' + description='From here you can manage your decoders files.' + tableColumns={columns} + tableInitialSortingField='filename' + searchTable={true} + searchBarProps={{ + modes: [ + { + id: 'wql', + options: searchBarWQLOptions, + suggestions: searchBarSuggestions, + } + ] + }} + endpoint='/decoders/files' + isExpandable={true} + downloadCsv={true} + showReload={true} + filters={filters} + tablePageSizeOptions={[10, 25, 50, 100]} +/>; const DecodersFlyoutTable = ({ actionButtons, - buttonOptions, columns, searchBarSuggestions, getRowProps, @@ -76,37 +86,43 @@ const DecodersFlyoutTable = ({ cleanFilters, ...props }) => <> - + {isFlyoutVisible && ( + - {isFlyoutVisible && ( - - )} - + )} +; /*************************************** * Main component @@ -121,9 +137,6 @@ export default compose( const resourcesHandler = new ResourcesHandler(ResourcesConstants.DECODERS); - // Table custom filter options - const buttonOptions = [{ label: "Custom decoders", field: "relative_dirname", value: "etc/decoders" },]; - const updateFilters = (filters) => { setFilters(filters); } @@ -251,17 +264,14 @@ export default compose( {showingFiles ? ( ) : ( - this.setNewFiltersAndBack([{ field: 'filename', value: file }]) + this.setNewFiltersAndBack({q: `filename=${file}`}) } >  {file} @@ -143,7 +143,7 @@ export default class WzDecoderInfo extends Component { - this.setNewFiltersAndBack([{ field: 'relative_dirname', value: path }]) + this.setNewFiltersAndBack({q: `relative_dirname=${path}`}) } >  {path} diff --git a/public/controllers/management/components/management/ruleset/components/ruleset-suggestions.ts b/public/controllers/management/components/management/ruleset/components/ruleset-suggestions.ts index 84e4115185..87d821e124 100644 --- a/public/controllers/management/components/management/ruleset/components/ruleset-suggestions.ts +++ b/public/controllers/management/components/management/ruleset/components/ruleset-suggestions.ts @@ -1,137 +1,122 @@ import { WzRequest } from '../../../../../../react-services/wz-request'; -const rulesItems = [ - { - type: 'params', - label: 'status', - description: 'Filters the rules by status.', - values: ['enabled', 'disabled'] +const rulesItems = { + field(currentValue) { + return [ + {label: 'filename', description: 'filter by filename'}, + {label: 'gdpr', description: 'filter by GDPR requirement'}, + {label: 'gpg13', description: 'filter by GPG requirement'}, + {label: 'groups', description: 'filter by group'}, + {label: 'hipaa', description: 'filter by HIPAA requirement'}, + {label: 'level', description: 'filter by level'}, + {label: 'mitre', description: 'filter by MITRE ATT&CK requirement'}, + {label: 'pci_dss', description: 'filter by PCI DSS requirement'}, + {label: 'relative_dirname', description: 'filter by relative dirname'}, + {label: 'status', description: 'filter by status'}, + {label: 'tsc', description: 'filter by TSC requirement'}, + {label: 'nist-800-53', description: 'filter by NIST requirement'}, + ]; }, - { - type: 'params', - label: 'group', - description: 'Filters the rules by group', - values: async value => { - const filter = { limit: 30 }; - if (value) { - filter['search'] = value; + value: async (currentValue, { field }) => { + try{ // TODO: distinct + switch (field) { + case 'status': { + return ['enabled', 'disabled'].map(label => ({label})); + } + case 'groups': { + const filter = { limit: 30 }; + if (currentValue) { + filter['search'] = currentValue; + } + const result = await WzRequest.apiReq('GET', '/rules/groups', filter); + return result?.data?.data?.affected_items.map(label => ({label})); + } + case 'level': { + const filter = { limit: 30 }; + if (currentValue) { + filter['search'] = currentValue; + } + const result = await WzRequest.apiReq('GET', '/rules/groups', filter); + return [...Array(16).keys()].map(label => ({label})); + } + case 'filename': { + const filter = { limit: 30 }; + if (currentValue) { + filter['search'] = currentValue; + } + const result = await WzRequest.apiReq('GET', '/rules/files', filter); + return result?.data?.data?.affected_items?.map((item) => ({label: item.filename})); + } + case 'relative_dirname': { + const result = await WzRequest.apiReq('GET', '/manager/configuration', { + params: { + section: 'ruleset', + field: 'rule_dir' + } + }); + return result?.data?.data?.affected_items?.[0].ruleset.rule_dir.map(label => ({label})); + } + case 'hipaa': { + const result = await WzRequest.apiReq('GET', '/rules/requirement/hipaa', {}); + return result?.data?.data?.affected_items.map(label => ({label})); + } + case 'gdpr': { + const result = await WzRequest.apiReq('GET', '/rules/requirement/gdpr', {}); + return result?.data?.data?.affected_items.map(label => ({label})); + } + case 'nist-800-53': { + const result = await WzRequest.apiReq('GET', '/rules/requirement/nist-800-53', {}); + return result?.data?.data?.affected_items.map(label => ({label})); + } + case 'gpg13': { + const result = await WzRequest.apiReq('GET', '/rules/requirement/gpg13', {}); + return result?.data?.data?.affected_items.map(label => ({label})); + } + case 'pci_dss': { + const result = await WzRequest.apiReq('GET', '/rules/requirement/pci_dss', {}); + return result?.data?.data?.affected_items.map(label => ({label})); + } + case 'tsc': { + const result = await WzRequest.apiReq('GET', '/rules/requirement/tsc', {}); + return result?.data?.data?.affected_items.map(label => ({label})); + } + case 'mitre': { + const result = await WzRequest.apiReq('GET', '/rules/requirement/mitre', {}); + return result?.data?.data?.affected_items.map(label => ({label})); + } + default: + return []; } - const result = await WzRequest.apiReq('GET', '/rules/groups', filter); - return result?.data?.data?.affected_items; - }, + }catch(error){ + return []; + }; }, - { - type: 'params', - label: 'level', - description: 'Filters the rules by level', - values: [...Array(16).keys()] - }, - { - type: 'params', - label: 'filename', - description: 'Filters the rules by file name.', - values: async value => { - const filter = { limit: 30 }; - if (value) { - filter['search'] = value; - } - const result = await WzRequest.apiReq('GET', '/rules/files', filter); - return result?.data?.data?.affected_items?.map((item) => { return item.filename }); - }, +}; + +const rulesFiles = { + field(currentValue) { + return ['filename'].map(label => ({label})); }, - { - type: 'params', - label: 'relative_dirname', - description: 'Path of the rules files', - values: async () => { - const result = await WzRequest.apiReq('GET', '/manager/configuration', { - params: { - section: 'ruleset', - field: 'rule_dir' + value: async (currentValue, { field }) => { + try{ // TODO: distinct + switch (field) { + case 'filename':{ + const filter = { limit: 30 }; + if (currentValue) { + filter['search'] = currentValue; + } + const result = await WzRequest.apiReq('GET', '/rules/files', filter); + return result?.data?.data?.affected_items?.map((item) => ({label: item.filename})); + break; } - }); - return result?.data?.data?.affected_items?.[0].ruleset.rule_dir; - } - }, - { - type: 'params', - label: 'hipaa', - description: 'Filters the rules by HIPAA requirement', - values: async () => { - const result = await WzRequest.apiReq('GET', '/rules/requirement/hipaa', {}); - return result?.data?.data?.affected_items; - } - }, - { - type: 'params', - label: 'gdpr', - description: 'Filters the rules by GDPR requirement', - values: async () => { - const result = await WzRequest.apiReq('GET', '/rules/requirement/gdpr', {}); - return result?.data?.data?.affected_items; - } - }, - { - type: 'params', - label: 'nist-800-53', - description: 'Filters the rules by NIST requirement', - values: async () => { - const result = await WzRequest.apiReq('GET', '/rules/requirement/nist-800-53', {}); - return result?.data?.data?.affected_items; - } - }, - { - type: 'params', - label: 'gpg13', - description: 'Filters the rules by GPG requirement', - values: async () => { - const result = await WzRequest.apiReq('GET', '/rules/requirement/gpg13', {}); - return result?.data?.data?.affected_items; - } - }, - { - type: 'params', - label: 'pci_dss', - description: 'Filters the rules by PCI DSS requirement', - values: async () => { - const result = await WzRequest.apiReq('GET', '/rules/requirement/pci_dss', {}); - return result?.data?.data?.affected_items; - } - }, - { - type: 'params', - label: 'tsc', - description: 'Filters the rules by TSC requirement', - values: async () => { - const result = await WzRequest.apiReq('GET', '/rules/requirement/tsc', {}); - return result?.data?.data?.affected_items; - } - }, - { - type: 'params', - label: 'mitre', - description: 'Filters the rules by MITRE requirement', - values: async () => { - const result = await WzRequest.apiReq('GET', '/rules/requirement/mitre', {}); - return result?.data?.data?.affected_items; - } - } -]; -const rulesFiles = [ - { - type: 'params', - label: 'filename', - description: 'Filters the rules by file name.', - values: async value => { - const filter = { limit: 30 }; - if (value) { - filter['search'] = value; + default: + return []; } - const result = await WzRequest.apiReq('GET', '/rules/files', filter); - return result?.data?.data?.affected_items?.map((item) => { return item.filename }); - }, + }catch(error){ + return []; + }; }, -]; +}; const apiSuggestsItems = { items: rulesItems, diff --git a/public/controllers/management/components/management/ruleset/components/ruleset-table.tsx b/public/controllers/management/components/management/ruleset/components/ruleset-table.tsx index ee5ff82bcd..74173f5659 100644 --- a/public/controllers/management/components/management/ruleset/components/ruleset-table.tsx +++ b/public/controllers/management/components/management/ruleset/components/ruleset-table.tsx @@ -32,6 +32,13 @@ import { import apiSuggestsItems from './ruleset-suggestions'; +const searchBarWQLOptions = { + searchTermFields: [], // TODO: add search term fields + filterButtons: [ + {id: 'relative-dirname', input: 'relative_dirname=etc/rules', label: 'Custom rules'} + ] +}; + /*************************************** * Render tables */ @@ -46,19 +53,25 @@ const FilesTable = ({ }) => @@ -78,14 +91,21 @@ const RulesFlyoutTable = ({ }) => <> this.setNewFiltersAndBack([{ field: 'rule_ids', value: id }])} + onClick={async () => this.setNewFiltersAndBack({q: `id=${id}`})} > {id} @@ -298,7 +298,7 @@ export default class WzRuleInfo extends Component { this.setNewFiltersAndBack([{ field: 'level', value: level }])} + onClick={async () => this.setNewFiltersAndBack({q: `level=${level}`})} > {level} @@ -311,7 +311,7 @@ export default class WzRuleInfo extends Component { - this.setNewFiltersAndBack([{ field: 'filename', value: file }]) + this.setNewFiltersAndBack({q: `filename=${file}`}) } > {file} @@ -325,7 +325,7 @@ export default class WzRuleInfo extends Component { - this.setNewFiltersAndBack([{ field: 'relative_dirname', value: path }]) + this.setNewFiltersAndBack({q: `relative_dirname=${path}`}) } > {path} @@ -422,7 +422,7 @@ export default class WzRuleInfo extends Component { listGroups.push( this.setNewFiltersAndBack([{ field: 'group', value: group }])} + onClick={async () => this.setNewFiltersAndBack({q: `group=${group}`})} > {group} @@ -527,7 +527,7 @@ export default class WzRuleInfo extends Component { return ( this.setNewFiltersAndBack([{ field: key, value: element }])} + onClick={async () => this.setNewFiltersAndBack({q: `${key}=${element}`})} > {element} @@ -553,7 +553,7 @@ export default class WzRuleInfo extends Component { - this.setNewFiltersAndBack([{ field: 'mitre', value: this.state.mitreIds[index] }]) + this.setNewFiltersAndBack({q: `mitre=${this.state.mitreIds[index]}`}) } > From 78b23e79d2d25ffdaaf240c851ab282d582830bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Mon, 15 May 2023 11:12:23 +0200 Subject: [PATCH 30/76] feat(vulnerabilities): change filter by severity tooltip --- public/components/agents/vuls/inventory.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/components/agents/vuls/inventory.tsx b/public/components/agents/vuls/inventory.tsx index 01cc260038..4476d2a56f 100644 --- a/public/components/agents/vuls/inventory.tsx +++ b/public/components/agents/vuls/inventory.tsx @@ -217,7 +217,7 @@ export class Inventory extends Component { textAlign='center' isLoading={isLoadingStats} title={ - + Date: Mon, 15 May 2023 11:26:25 +0200 Subject: [PATCH 31/76] fix(test): update test and snapthost --- .../table-with-search-bar.test.tsx.snap | 656 +++++++++++++----- .../tables/table-with-search-bar.test.tsx | 18 + 2 files changed, 490 insertions(+), 184 deletions(-) diff --git a/public/components/common/tables/__snapshots__/table-with-search-bar.test.tsx.snap b/public/components/common/tables/__snapshots__/table-with-search-bar.test.tsx.snap index 0c57fd5b26..6274f8c7ee 100644 --- a/public/components/common/tables/__snapshots__/table-with-search-bar.test.tsx.snap +++ b/public/components/common/tables/__snapshots__/table-with-search-bar.test.tsx.snap @@ -5,6 +5,22 @@ exports[`Table With Search Bar component renders correctly to match the snapshot onSearch={[Function]} reload={[Function]} rowProps={[Function]} + searchBarProps={ + Object { + "modes": Array [ + Object { + "id": "wql", + "options": Object { + "searchTermFields": Array [], + }, + "suggestions": Object { + "field": [Function], + "value": [Function], + }, + }, + ], + } + } searchBarSuggestions={Array []} tableColumns={ Array [ @@ -44,222 +60,494 @@ exports[`Table With Search Bar component renders correctly to match the snapshot } tableProps={Object {}} > - - -
- + + WQL + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelPaddingSize="m" + > + + SYNTAX OPTIONS +
- + + WQL (Wazuh Query Language) provides a human query syntax based on the Wazuh API query language. + + + +
+ + Documentation + +
+
+
+ + } + inputRef={ + Object { + "current": , + } + } + isPopoverOpen={false} + onChange={[Function]} + onClosePopover={[Function]} + onInputChange={[Function]} + onKeyPress={[Function]} + onPopoverFocus={[Function]} + placeholder="Search" + suggestions={Array []} + value="" + > +
+ + WQL + } - status="unchanged" - suggestions={Array []} - value="" + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelPaddingSize="m" > + + SYNTAX OPTIONS +
- + style={ + Object { + "width": "350px", } - sendValue={[Function]} - status="unchanged" - suggestions={Array []} - value="" - > -
- - } - value="" - /> - } - isOpen={false} - panelPaddingSize="none" + } + > + + WQL (Wazuh Query Language) provides a human query syntax based on the Wazuh API query language. + + + +
+ + Documentation + +
+
+
+ + } + inputRef={ + Object { + "current": , + } + } + isPopoverOpen={false} + onChange={[Function]} + onClosePopover={[Function]} + onKeyPress={[Function]} + onPopoverFocus={[Function]} + placeholder="Search" + sendValue={[Function]} + status="unchanged" + suggestions={Array []} + value="" + > +
+ - [Function] - + WQL + } - buttonRef={[Function]} - className="euiInputPopover euiInputPopover--fullWidth" closePopover={[Function]} - display="block" + display="inlineBlock" hasArrow={true} - id="popover" isOpen={false} - ownFocus={false} - panelPaddingSize="none" - panelRef={[Function]} + ownFocus={true} + panelPaddingSize="m" > - + SYNTAX OPTIONS + +
-
+ WQL (Wazuh Query Language) provides a human query syntax based on the Wazuh API query language. + + + +
+ + Documentation + +
+
+
+ , + ] + } + fullWidth={true} + inputRef={ + Object { + "current": , + } + } + isLoading={false} + onChange={[Function]} + onFocus={[Function]} + onKeyPress={[Function]} + placeholder="Search" + value="" + /> + } + isOpen={false} + panelPaddingSize="none" + > + + [Function] + + } + buttonRef={[Function]} + className="euiInputPopover euiInputPopover--fullWidth" + closePopover={[Function]} + display="block" + hasArrow={true} + id="popover" + isOpen={false} + ownFocus={false} + panelPaddingSize="none" + panelRef={[Function]} + > + +
+
+ +
+ + WQL + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelPaddingSize="m" + > + + SYNTAX OPTIONS + +
+ + WQL (Wazuh Query Language) provides a human query syntax based on the Wazuh API query language. + + + +
+ + Documentation + +
+
+
+ , + ] + } + fullWidth={true} + inputRef={ + Object { + "current": , + } + } + isLoading={false} + onChange={[Function]} + onFocus={[Function]} + onKeyPress={[Function]} + placeholder="Search" + value="" > -
+ WQL + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelPaddingSize="m" + > + + SYNTAX OPTIONS + +
+ + WQL (Wazuh Query Language) provides a human query syntax based on the Wazuh API query language. + + + +
+ + Documentation + +
+
+
+ , + ] + } + fullWidth={true} + isLoading={false} > - -
- + + + + - } - value="" + /> +
+ + WQL + + } + className="euiFormControlLayout__append" + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + isOpen={false} + key="0/.0" + ownFocus={true} + panelPaddingSize="m" + > +
- - } +
-
- -
- -
- - - - -
-
- - -
- -
-
- -
- + + + WQL + + + + + +
+
+ +
+ + +
+ +
-
-
- + + +
-
+
-
-
+
+ diff --git a/public/components/common/tables/table-with-search-bar.test.tsx b/public/components/common/tables/table-with-search-bar.test.tsx index 263b98b51b..32c79e3945 100644 --- a/public/components/common/tables/table-with-search-bar.test.tsx +++ b/public/components/common/tables/table-with-search-bar.test.tsx @@ -73,6 +73,24 @@ const tableProps = { reload: () => {}, searchBarSuggestions: [], rowProps: () => {}, + searchBarProps: { + modes: [ + { + id: 'wql', + options: { + searchTermFields: [] + }, + suggestions: { + field(currentValue) { + return []; + }, + value: async (currentValue, { field }) => { + return []; + }, + }, + } + ] + } }; describe('Table With Search Bar component', () => { From b5b0c05762b3eaf3423d5ac11b52bd73376af06f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Mon, 15 May 2023 14:26:25 +0200 Subject: [PATCH 32/76] fix(test): update snapshot --- .../__snapshots__/intelligence.test.tsx.snap | 61 +++++++++++-------- 1 file changed, 36 insertions(+), 25 deletions(-) diff --git a/public/components/overview/mitre_attack_intelligence/__snapshots__/intelligence.test.tsx.snap b/public/components/overview/mitre_attack_intelligence/__snapshots__/intelligence.test.tsx.snap index ce40ecc0ea..adf70d0cc4 100644 --- a/public/components/overview/mitre_attack_intelligence/__snapshots__/intelligence.test.tsx.snap +++ b/public/components/overview/mitre_attack_intelligence/__snapshots__/intelligence.test.tsx.snap @@ -163,40 +163,51 @@ exports[`Module Mitre Att&ck intelligence container should render the component /> -
+
-
+
-
+
-
+
+ +
+
-
-
- -
+ + + WQL + + +
From c57f8a3cb63d1f74b4daaabe00c03492780208e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Tue, 16 May 2023 09:42:54 +0200 Subject: [PATCH 33/76] feat(search-bar): replace search bar and table component on Explore agent modal - Replace the search bar and table component with TableWzAPI - Remove unused code --- .../agents-selection-table.js | 671 +++--------------- 1 file changed, 113 insertions(+), 558 deletions(-) diff --git a/public/controllers/overview/components/overview-actions/agents-selection-table.js b/public/controllers/overview/components/overview-actions/agents-selection-table.js index 99d6277b13..2141a82ddd 100644 --- a/public/controllers/overview/components/overview-actions/agents-selection-table.js +++ b/public/controllers/overview/components/overview-actions/agents-selection-table.js @@ -1,531 +1,87 @@ import React, { Component, Fragment } from 'react'; import { EuiButtonIcon, - EuiCheckbox, EuiFlexGroup, EuiFlexItem, - EuiHealth, EuiSpacer, - EuiTable, - EuiTableBody, - EuiTableFooterCell, - EuiTableHeader, - EuiTableHeaderCell, - EuiTableHeaderCellCheckbox, - EuiTableHeaderMobile, - EuiTablePagination, - EuiTableRow, - EuiTableRowCell, - EuiTableRowCellCheckbox, - EuiTableSortMobile, EuiToolTip, } from '@elastic/eui'; import { WzRequest } from '../../../../react-services/wz-request'; -import { LEFT_ALIGNMENT } from '@elastic/eui/lib/services'; import { updateCurrentAgentData } from '../../../../redux/actions/appStateActions'; import store from '../../../../redux/store'; import { GroupTruncate } from '../../../../components/common/util/agent-group-truncate/'; -import { filtersToObject, WzSearchBar } from '../../../../components/wz-search-bar'; import { getAgentFilterValues } from '../../../../controllers/management/components/management/groups/get-agents-filters-values'; import _ from 'lodash'; import { UI_LOGGER_LEVELS, UI_ORDER_AGENT_STATUS } from '../../../../../common/constants'; import { UI_ERROR_SEVERITIES } from '../../../../react-services/error-orchestrator/types'; import { getErrorOrchestrator } from '../../../../react-services/common-services'; import { AgentStatus } from '../../../../components/agents/agent_status'; +import { TableWzAPI } from '../../../../components/common/tables'; -const checkField = field => { - return field !== undefined ? field : '-'; +const searchBarWQLOptions = { + searchTermFields: [], + implicitQuery: { + query: 'id!=000', + conjunction: ';' + } }; export class AgentSelectionTable extends Component { constructor(props) { super(props); this.state = { - itemIdToSelectedMap: {}, - itemIdToOpenActionsPopoverMap: {}, - sortedColumn: 'title', - itemsPerPage: 10, - pageIndex: 0, - totalItems: 0, - isLoading: false, - sortDirection: 'asc', - sortField: 'id', - agents: [], - selectedOptions: [], - filters: [] + filters: { default: {q: 'id!=000'}} }; this.columns = [ { - id: 'id', - label: 'ID', - alignment: LEFT_ALIGNMENT, + field: 'id', + name: 'ID', width: '60px', - mobileOptions: { - show: true, - }, - isSortable: true, + sortable: true, }, { - id: 'name', - label: 'Name', - alignment: LEFT_ALIGNMENT, - mobileOptions: { - show: true, - }, - isSortable: true + field: 'name', + name: 'Name', + sortable: true }, { - id: 'group', - label: 'Group', - alignment: LEFT_ALIGNMENT, - mobileOptions: { - show: false, - }, - isSortable: true, + field: 'group', + name: 'Group', + sortable: true, render: groups => this.renderGroups(groups) }, { - id: 'version', - label: 'Version', + field: 'version', + name: 'Version', width: '80px', - alignment: LEFT_ALIGNMENT, - mobileOptions: { - show: true, - }, - isSortable: true, + sortable: true, }, { - id: 'os', - label: 'Operating system', - alignment: LEFT_ALIGNMENT, - mobileOptions: { - show: false, - }, - isSortable: true, - render: os => this.addIconPlatformRender(os) + field: 'os.name,os.version', + name: 'Operating system', + sortable: true, + render: (field, agentData) => this.addIconPlatformRender(agentData) }, { - id: 'status', - label: 'Status', - alignment: LEFT_ALIGNMENT, - mobileOptions: { - show: true, - }, - isSortable: true, + field: 'status', + name: 'Status', + sortable: true, width: 'auto', render: status => , }, ]; - this.suggestions = [ - { type: 'q', label: 'status', description: 'Filter by agent connection status', operators: ['=', '!=',], values: UI_ORDER_AGENT_STATUS }, - { type: 'q', label: 'os.platform', description: 'Filter by operating system platform', operators: ['=', '!=',], values: async (value) => getAgentFilterValues('os.platform', value, { q: 'id!=000'})}, - { type: 'q', label: 'ip', description: 'Filter by agent IP address', operators: ['=', '!=',], values: async (value) => getAgentFilterValues('ip', value, { q: 'id!=000'})}, - { type: 'q', label: 'name', description: 'Filter by agent name', operators: ['=', '!=',], values: async (value) => getAgentFilterValues('name', value, { q: 'id!=000'})}, - { type: 'q', label: 'id', description: 'Filter by agent id', operators: ['=', '!=',], values: async (value) => getAgentFilterValues('id', value, { q: 'id!=000'})}, - { type: 'q', label: 'group', description: 'Filter by agent group', operators: ['=', '!=',], values: async (value) => getAgentFilterValues('group', value, { q: 'id!=000'})}, - { type: 'q', label: 'node_name', description: 'Filter by node name', operators: ['=', '!=',], values: async (value) => getAgentFilterValues('node_name', value, { q: 'id!=000'})}, - { type: 'q', label: 'manager', description: 'Filter by manager', operators: ['=', '!=',], values: async (value) => getAgentFilterValues('manager', value, { q: 'id!=000'})}, - { type: 'q', label: 'version', description: 'Filter by agent version', operators: ['=', '!=',], values: async (value) => getAgentFilterValues('version', value, { q: 'id!=000'})}, - { type: 'q', label: 'configSum', description: 'Filter by agent config sum', operators: ['=', '!=',], values: async (value) => getAgentFilterValues('configSum', value, { q: 'id!=000'})}, - { type: 'q', label: 'mergedSum', description: 'Filter by agent merged sum', operators: ['=', '!=',], values: async (value) => getAgentFilterValues('mergedSum', value, { q: 'id!=000'})}, - { type: 'q', label: 'dateAdd', description: 'Filter by add date', operators: ['=', '!=',], values: async (value) => getAgentFilterValues('dateAdd', value, { q: 'id!=000'})}, - { type: 'q', label: 'lastKeepAlive', description: 'Filter by last keep alive', operators: ['=', '!=',], values: async (value) => getAgentFilterValues('lastKeepAlive', value, { q: 'id!=000'})}, - ]; - } - - onChangeItemsPerPage = async itemsPerPage => { - this._isMounted && this.setState({ itemsPerPage }, async () => await this.getItems()); - }; - - onChangePage = async pageIndex => { - this._isMounted && this.setState({ pageIndex }, async () => await this.getItems()); - }; - - async componentDidMount() { - this._isMounted = true; - const tmpSelectedAgents = {}; - if(!store.getState().appStateReducers.currentAgentData.id){ - tmpSelectedAgents[store.getState().appStateReducers.currentAgentData.id] = true; - } - this._isMounted && this.setState({itemIdToSelectedMap: this.props.selectedAgents}); - await this.getItems(); - } - - componentWillUnmount(){ - this._isMounted = false; - } - - async componentDidUpdate(prevProps, prevState) { - if(!(_.isEqual(prevState.filters,this.state.filters))){ - await this.getItems(); - } - } - - getArrayFormatted(arrayText) { - try { - const stringText = arrayText.toString(); - const splitString = stringText.split(','); - return splitString.join(', '); - } catch (error) { - const options = { - context: `${AgentSelectionTable.name}.getArrayFormatted`, - level: UI_LOGGER_LEVELS.ERROR, - severity: UI_ERROR_SEVERITIES.UI, - error: { - error: error, - message: error.message || error, - title: error.name || error, - }, - }; - - getErrorOrchestrator().handleError(options); - return arrayText; - } - } - - async getItems() { - try { - this._isMounted && this.setState({ isLoading: true }); - const rawData = await WzRequest.apiReq('GET', '/agents', { params: this.buildFilter() }); - const data = (((rawData || {}).data || {}).data || {}).affected_items; - const totalItems = (((rawData || {}).data || {}).data || {}).total_affected_items; - const formattedData = data.map((item, id) => { - return { - id: item.id, - name: item.name, - version: item.version !== undefined ? item.version.split(' ')[1] : '-', - os: item.os || '-', - status: item.status, - group: item.group || '-', - }; - }); - this._isMounted && this.setState({ agents: formattedData, totalItems, isLoading: false }); - } catch (error) { - this._isMounted && this.setState({ isLoading: false }); - const options = { - context: `${AgentSelectionTable.name}.getItems`, - level: UI_LOGGER_LEVELS.ERROR, - severity: UI_ERROR_SEVERITIES.BUSINESS, - error: { - error: error, - message: error.message || error, - title: error.name || error, - }, - }; - - getErrorOrchestrator().handleError(options); - } - } - - buildFilter() { - const { itemsPerPage, pageIndex, filters } = this.state; - const filter = { - ...filtersToObject(filters), - offset: (pageIndex * itemsPerPage) || 0, - limit: pageIndex * itemsPerPage + itemsPerPage, - ...this.buildSortFilter() - }; - filter.q = !filter.q ? `id!=000` : `id!=000;${filter.q}`; - return filter; - } - - buildSortFilter() { - const { sortDirection, sortField } = this.state; - const sortFilter = {}; - if (sortField) { - const direction = sortDirection === 'asc' ? '+' : '-'; - sortFilter['sort'] = direction + (sortField === 'os'? 'os.name,os.version' : sortField); - } - - return sortFilter; - } - - onSort = async prop => { - const sortField = prop; - const sortDirection = - this.state.sortField === prop && this.state.sortDirection === 'asc' - ? 'desc' - : this.state.sortDirection === 'asc' - ? 'desc' - : 'asc'; - - this._isMounted && this.setState({ sortField, sortDirection }, async () => await this.getItems()); - }; - - toggleItem = itemId => { - this._isMounted && this.setState(previousState => { - const newItemIdToSelectedMap = { - [itemId]: !previousState.itemIdToSelectedMap[itemId], - }; - - return { - itemIdToSelectedMap: newItemIdToSelectedMap, - }; - }); - }; - - toggleAll = () => { - const allSelected = this.areAllItemsSelected(); - const newItemIdToSelectedMap = {}; - this.state.agents.forEach(item => (newItemIdToSelectedMap[item.id] = !allSelected)); - this._isMounted && this.setState({ - itemIdToSelectedMap: newItemIdToSelectedMap, - }); - }; - - isItemSelected = itemId => { - return this.state.itemIdToSelectedMap[itemId]; - }; - - areAllItemsSelected = () => { - const indexOfUnselectedItem = this.state.agents.findIndex(item => !this.isItemSelected(item.id)); - return indexOfUnselectedItem === -1; - }; - - areAnyRowsSelected = () => { - return ( - Object.keys(this.state.itemIdToSelectedMap).findIndex(id => { - return this.state.itemIdToSelectedMap[id]; - }) !== -1 - ); - }; - - togglePopover = itemId => { - this._isMounted && this.setState(previousState => { - const newItemIdToOpenActionsPopoverMap = { - ...previousState.itemIdToOpenActionsPopoverMap, - [itemId]: !previousState.itemIdToOpenActionsPopoverMap[itemId], - }; - - return { - itemIdToOpenActionsPopoverMap: newItemIdToOpenActionsPopoverMap, - }; - }); - }; - - closePopover = itemId => { - // only update the state if this item's popover is open - if (this.isPopoverOpen(itemId)) { - this._isMounted && this.setState(previousState => { - const newItemIdToOpenActionsPopoverMap = { - ...previousState.itemIdToOpenActionsPopoverMap, - [itemId]: false, - }; - - return { - itemIdToOpenActionsPopoverMap: newItemIdToOpenActionsPopoverMap, - }; - }); - } - }; - - isPopoverOpen = itemId => { - return this.state.itemIdToOpenActionsPopoverMap[itemId]; - }; - - renderSelectAll = mobile => { - if (!this.state.isLoading && this.state.agents.length) { - return ( - - ); - } - }; - - getTableMobileSortItems() { - const items = []; - this.columns.forEach(column => { - if (column.isCheckbox || !column.isSortable) { - return; - } - items.push({ - name: column.label, - key: column.id, - onSort: this.onSort.bind(this, column.id), - isSorted: this.state.sortField === column.id, - isSortAscending: this.state.sortDirection === 'asc', - }); - }); - return items.length ? items : null; - } - - renderHeaderCells() { - const headers = []; - - this.columns.forEach((column, columnIndex) => { - if (column.isCheckbox) { - headers.push( - - - ); - } else { - headers.push( - - {column.label} - - ); - } - }); - return headers.length ? headers : null; - } - - renderRows() { - const renderRow = item => { - const cells = this.columns.map(column => { - const cell = item[column.id]; - - let child; - - if (column.isCheckbox) { - return ( - - {}} - type="inList" - /> - - ); - } - - if (column.render) { - child = column.render(item[column.id]); - } else { - child = cell; - } - - return ( - - {child} - - ); - }); - - return ( - await this.selectAgentAndApply(item.id)} - hasActions={true} - > - {cells} - - ); - }; - - const rows = []; - - for ( - let itemIndex = (this.state.pageIndex * this.state.itemsPerPage) % this.state.itemsPerPage; - itemIndex < - ((this.state.pageIndex * this.state.itemsPerPage) % this.state.itemsPerPage) + - this.state.itemsPerPage && this.state.agents[itemIndex]; - itemIndex++ - ) { - const item = this.state.agents[itemIndex]; - rows.push(renderRow(item)); - } - - return rows; - } - - renderFooterCells() { - const footers = []; - - const items = this.state.agents; - const pagination = { - pageIndex: this.state.pageIndex, - pageSize: this.state.itemsPerPage, - totalItemCount: this.state.totalItems, - pageSizeOptions: [10, 25, 50, 100] - }; - - this.columns.forEach(column => { - const footer = this.getColumnFooter(column, { items, pagination }); - if (column.mobileOptions && column.mobileOptions.only) { - return; // exclude columns that only exist for mobile headers - } - - if (footer) { - footers.push( - - {footer} - - ); - } else { - footers.push( - - {undefined} - - ); - } - }); - return footers; - } - - getColumnFooter = (column, { items, pagination }) => { - if (column.footer === null) { - return null; - } - if (column.footer) { - return column.footer; - } - - return undefined; - }; - - async onQueryChange(result) { - this._isMounted && - this.setState({ isLoading: true, ...result }, async () => { - await this.getItems(); - }); - } - - getSelectedItems(){ - return Object.keys(this.state.itemIdToSelectedMap).filter(x => { - return (this.state.itemIdToSelectedMap[x] === true) - }) } unselectAgents(){ - this._isMounted && this.setState({itemIdToSelectedMap: {}}); store.dispatch(updateCurrentAgentData({})); this.props.removeAgentsFilter(); } - getSelectedCount(){ - return this.getSelectedItems().length; - } - async selectAgentAndApply(agentID){ try{ const data = await WzRequest.apiReq('GET', '/agents', { params: { q: 'id=' + agentID}}); - const formattedData = data.data.data.affected_items[0] //TODO: do it correctly + const formattedData = data?.data?.data?.affected_items?.[0] store.dispatch(updateCurrentAgentData(formattedData)); this.props.updateAgentSearch([agentID]); } catch(error) { @@ -546,50 +102,39 @@ export class AgentSelectionTable extends Component { } } - showContextMenu(id){ - this._isMounted && this.setState({contextMenuId: id}) - } - addIconPlatformRender(os) { - if(typeof os === "string" ){ return os}; - let icon = false; + addIconPlatformRender(agent) { + let icon = ''; + const os = agent?.os || {}; - if (((os || {}).uname || '').includes('Linux')) { + if ((os?.uname || '').includes('Linux')) { icon = 'linux'; - } else if ((os || {}).platform === 'windows') { + } else if (os?.platform === 'windows') { icon = 'windows'; - } else if ((os || {}).platform === 'darwin') { + } else if (os?.platform === 'darwin') { icon = 'apple'; } - const os_name = - checkField((os || {}).name) + - ' ' + - checkField((os || {}).version); + const os_name = `${agent?.os?.name || ''} ${agent?.os?.version || ''}`; + return ( - - + {' '} - {os_name === '--' ? '-' : os_name} - + >{' '} + {os_name.trim() || '-'} + ); } filterGroupBadge = (group) => { - const { filters } = this.state; - let auxFilters = filters.map( filter => filter.value.match(/group=(.*S?)/)[1] ); - if (filters.length > 0) { - !auxFilters.includes(group) ? - this.setState({ - filters: [...filters, {field: "q", value: `group=${group}`}], - }) : false; - } else { - this.setState({ - filters: [...filters, {field: "q", value: `group=${group}`}], - }) - } - } + this.setState({ + filters: { + default: {q: 'id!=000'}, + q: `group=${group}`, + } + }); + }; renderGroups(groups){ return Array.isArray(groups) ? ( @@ -604,30 +149,18 @@ export class AgentSelectionTable extends Component { } render() { - const pagination = { - pageIndex: this.state.pageIndex, - pageSize: this.state.itemsPerPage, - totalItemCount: this.state.totalItems, - pageCount: - this.state.totalItems % this.state.itemsPerPage === 0 - ? this.state.totalItems / this.state.itemsPerPage - : parseInt(this.state.totalItems / this.state.itemsPerPage) + 1, - }; const selectedAgent = store.getState().appStateReducers.currentAgentData; + const getRowProps = (item, idx) => { + return { + 'data-test-subj': `explore-agent-${idx}`, + className: 'customRowClass', + onClick: () => this.selectAgentAndApply(item.id), + }; + }; + return (
- - - this.setState({filters, pageIndex: 0})} - placeholder="Filter or search agent" - /> - - - {selectedAgent && Object.keys(selectedAgent).length > 0 && ( @@ -652,41 +185,63 @@ export class AgentSelectionTable extends Component { )} - - - - - - - - - - - {this.renderHeaderCells()} - {(this.state.agents.length && ( - - {this.renderRows()} - - )) || ( - - - - {this.state.isLoading ? 'Loading agents' : 'No results found'} - - - - )} - - - - - { + return { + ...item, + /* + The agent version contains the Wazuh word, this get the string starting with + v + */ + ...(typeof item.version === 'string' + ? {version: item.version.match(/(v\d.+)/)?.[1]} + : {version: '-'} + ) + }; + }} + rowProps={getRowProps} + filters={this.state.filters} + searchTable + searchBarProps={{ + modes: [ + { + id: 'wql', + options: searchBarWQLOptions, + suggestions: { + field(currentValue) { + return [ + {label: 'id', description: 'filter by id'}, + {label: 'group', description: 'filter by group'}, + {label: 'os.name', description: 'filter by operating system name'}, + {label: 'os.version', description: 'filter by operating system version'}, + {label: 'status', description: 'filter by status'}, + {label: 'name', description: 'filter by name'}, + {label: 'version', description: 'filter by version'}, + ]; + }, + value: async (currentValue, { field }) => { + try{ + switch (field) { + case 'status': + return UI_ORDER_AGENT_STATUS.map(status => ({label: status})) + break; + default: + return (await getAgentFilterValues(field, currentValue, { q: 'id!=000'})) + .map(status => ({label: status})); + break; + } + }catch(error){ + return []; + }; + }, + }, + } + ] + }} />
); From 47255d6176fc1f246de70964c9d063a054ee473b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Wed, 17 May 2023 11:29:34 +0200 Subject: [PATCH 34/76] feat(table-wz-api): add field selection to the TableWzAPI component Add field selection to the TableWzAPI Possibility to save the selected fields on storage (localStorage, sessionStorage) Create useStateStorage that allows save the state in localStorage or sessionStorage --- public/components/common/hooks/index.ts | 1 + .../common/hooks/use-state-storage.ts | 30 +++++ .../common/tables/table-with-search-bar.tsx | 17 ++- .../components/common/tables/table-wz-api.tsx | 112 +++++++++++++----- 4 files changed, 130 insertions(+), 30 deletions(-) create mode 100644 public/components/common/hooks/use-state-storage.ts diff --git a/public/components/common/hooks/index.ts b/public/components/common/hooks/index.ts index 944998ea1a..e3ce7584c7 100644 --- a/public/components/common/hooks/index.ts +++ b/public/components/common/hooks/index.ts @@ -27,3 +27,4 @@ export * from './use_async_action'; export * from './use_async_action_run_on_start'; export { useEsSearch } from './use-es-search'; export { useValueSuggestion, IValueSuggestion } from './use-value-suggestion'; +export * from './use-state-storage'; diff --git a/public/components/common/hooks/use-state-storage.ts b/public/components/common/hooks/use-state-storage.ts new file mode 100644 index 0000000000..3251ea1be5 --- /dev/null +++ b/public/components/common/hooks/use-state-storage.ts @@ -0,0 +1,30 @@ +import { useState } from 'react'; + +function transformValueToStorage(value: any){ + return typeof value !== 'string' ? JSON.stringify(value) : value; +}; + +function transformValueFromStorage(value: any){ + return typeof value === 'string' ? JSON.parse(value) : value; +}; + +export function useStateStorage(initialValue: any, storageSystem?: 'sessionStorage' | 'localStorage', storageKey?: string){ + const [state, setState] = useState( + (storageSystem && storageKey && window?.[storageSystem]?.getItem(storageKey)) + ? transformValueFromStorage(window?.[storageSystem]?.getItem(storageKey)) + : initialValue + ); + + function setStateStorage(value: any){ + setState((state) => { + const formattedValue = typeof value === 'function' + ? value(state) + : value; + + storageSystem && storageKey && window?.[storageSystem]?.setItem(storageKey, transformValueToStorage(formattedValue)); + return formattedValue; + }); + }; + + return [state, setStateStorage]; +}; diff --git a/public/components/common/tables/table-with-search-bar.tsx b/public/components/common/tables/table-with-search-bar.tsx index 308f634062..5f54ec010b 100644 --- a/public/components/common/tables/table-with-search-bar.tsx +++ b/public/components/common/tables/table-with-search-bar.tsx @@ -10,7 +10,7 @@ * Find more information about this on the LICENSE file. */ -import React, { useState, useEffect, useRef } from 'react'; +import React, { useState, useEffect, useRef, useMemo } from 'react'; import { EuiBasicTable, EuiSpacer } from '@elastic/eui'; import _ from 'lodash'; import { UI_ERROR_SEVERITIES } from '../../../react-services/error-orchestrator/types'; @@ -51,6 +51,13 @@ export function TableWithSearchBar({ const isMounted = useRef(false); + const searchBarWQLOptions = useMemo(() => ({ + searchTermFields: tableColumns + .filter(({field, searchable}) => searchable && rest.selectedFields.includes(field)) + .map(({field, composeField}) => ([composeField || field].flat())), + ...(rest?.searchBarWQL?.options || {}) + }), [rest?.searchBarWQL?.options, rest?.selectedFields]); + function updateRefresh() { setPagination({ pageIndex: 0, pageSize: pagination.pageSize }); setRefresh(Date.now()); @@ -137,6 +144,14 @@ export function TableWithSearchBar({ { // Set the query, reset the page index and update the refresh diff --git a/public/components/common/tables/table-wz-api.tsx b/public/components/common/tables/table-wz-api.tsx index 22c9934084..b31be28dfc 100644 --- a/public/components/common/tables/table-wz-api.tsx +++ b/public/components/common/tables/table-wz-api.tsx @@ -19,6 +19,9 @@ import { EuiText, EuiButtonEmpty, EuiSpacer, + EuiToolTip, + EuiIcon, + EuiCheckboxGroup, } from '@elastic/eui'; import { TableWithSearchBar } from './table-with-search-bar'; import { TableDefault } from './table-default'; @@ -27,6 +30,7 @@ import { ExportTableCsv } from './components/export-table-csv'; import { UI_ERROR_SEVERITIES } from '../../../react-services/error-orchestrator/types'; import { UI_LOGGER_LEVELS } from '../../../../common/constants'; import { getErrorOrchestrator } from '../../../react-services/common-services'; +import { useStateStorage } from '../hooks'; /** * Search input custom filter button @@ -69,6 +73,15 @@ export function TableWzAPI({ */ const [reloadFootprint, setReloadFootprint] = useState(rest.reload || 0); + const [selectedFields, setSelectedFields] = useStateStorage( + rest.tableColumns.some(({show}) => show) + ? rest.tableColumns.filter(({show}) => show).map(({field}) => field) + : rest.tableColumns.map(({field}) => field), + rest?.saveStateStorage?.system, + rest?.saveStateStorage?.key ? `${rest?.saveStateStorage?.key}-visible-fields` : undefined + ); + const [isOpenFieldSelector, setIsOpenFieldSelector] = useState(false); + const onSearch = useCallback(async function (endpoint, filters, pagination, sorting) { try { const { pageIndex, pageSize } = pagination; @@ -145,40 +158,81 @@ export function TableWzAPI({ ); const header = ( - - - {rest.title && ( - -

- {rest.title}{' '} - {isLoading ? : ({totalItems})} -

-
- )} - {rest.description && {rest.description}} -
- - - {/* Render optional custom action button */} - {renderActionButtons} - {/* Render optional reload button */} - {rest.showReload && ReloadButton} - {/* Render optional export to CSV button */} - {rest.downloadCsv && ( - + <> + + + {rest.title && ( + +

+ {rest.title}{' '} + {isLoading ? : ({totalItems})} +

+
)} + {rest.description && {rest.description}} +
+ + + {/* Render optional custom action button */} + {renderActionButtons} + {/* Render optional reload button */} + {rest.showReload && ReloadButton} + {/* Render optional export to CSV button */} + {rest.downloadCsv && ( + + )} + {rest.showFieldSelector && ( + + + setIsOpenFieldSelector(state => !state)}> + + + + + )} + + +
+ {isOpenFieldSelector && ( + + + ({ + id: item.field, + label: item.name, + checked: selectedFields.includes(item.field) + }))} + onChange={(optionID) => { + setSelectedFields(state => { + if(state.includes(optionID)){ + if(state.length > 1){ + return state.filter(field => field !== optionID); + } + return state; + }; + return [...state, optionID]; + } + ) + }} + className="columnsSelectedCheckboxs" + idToSelectedMap={{}} + /> + -
-
+ )} + ); + const tableColumns = rest.tableColumns + .filter(({field}) => selectedFields.includes(field)); + const table = rest.searchTable ? ( - + ) : ( ); From 06893adb850f1037d2e7a0a4a99686367317a85e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Wed, 17 May 2023 11:55:29 +0200 Subject: [PATCH 35/76] feat(search-bar): enhace search bar and WQL Search bar: - rename method from `transformUQLtoQL` to `transformInput` - add a options paramenter to `transformInput` to manage when there is an implicit filter, that this methods can remove the filter from the query - update documentation WQL: - rename method from `transformUQLtoQL` to `transformInput` - add a options paramenter to `transformInput` to manage when there is an implicit filter, that this methods can remove the filter from the query - add tests --- public/components/search-bar/README.md | 4 ++-- public/components/search-bar/index.tsx | 10 +++++++--- .../components/search-bar/query-language/index.ts | 2 +- .../search-bar/query-language/wql.test.tsx | 15 +++++++++++++-- .../components/search-bar/query-language/wql.tsx | 11 ++++++++++- 5 files changed, 33 insertions(+), 9 deletions(-) diff --git a/public/components/search-bar/README.md b/public/components/search-bar/README.md index b8e95cef2a..ce9fd0d65b 100644 --- a/public/components/search-bar/README.md +++ b/public/components/search-bar/README.md @@ -126,7 +126,7 @@ type SearchBarQueryLanguage = { query: string } }>; - transformUQLToQL: (unifiedQuery: string) => string; + transformInput: (unifiedQuery: string, options: {configuration: any, parameters: any}) => string; }; ``` @@ -145,7 +145,7 @@ where: - `language`: query language ID - `apiQuery`: API query. - `query`: current query in the specified language -- `transformUQLToQL`: method that transforms the UQL (Unified Query Language) to the specific query +- `transformInput`: method that transforms the UQL (Unified Query Language) to the specific query language. This is used when receives a external input in the Unified Query Language, the returned value is converted to the specific query language to set the new input text of the search bar component. diff --git a/public/components/search-bar/index.tsx b/public/components/search-bar/index.tsx index 0b4ce9909a..10b17b3703 100644 --- a/public/components/search-bar/index.tsx +++ b/public/components/search-bar/index.tsx @@ -80,11 +80,15 @@ export const SearchBar = ({ const selectedQueryLanguageParameters = modes.find(({ id }) => id === queryLanguage.id); useEffect(() => { - // React to external changes and set the internal input text. Use the `transformUQLToQL` of + // React to external changes and set the internal input text. Use the `transformInput` of // the query language in use - rest.input && searchBarQueryLanguages[queryLanguage.id]?.transformUQLToQL && setInput( - searchBarQueryLanguages[queryLanguage.id]?.transformUQLToQL?.( + rest.input && searchBarQueryLanguages[queryLanguage.id]?.transformInput && setInput( + searchBarQueryLanguages[queryLanguage.id]?.transformInput?.( rest.input, + { + configuration: queryLanguage.configuration, + parameters: selectedQueryLanguageParameters, + } ), ); }, [rest.input]); diff --git a/public/components/search-bar/query-language/index.ts b/public/components/search-bar/query-language/index.ts index fc95717fca..5a897d1d34 100644 --- a/public/components/search-bar/query-language/index.ts +++ b/public/components/search-bar/query-language/index.ts @@ -15,7 +15,7 @@ type SearchBarQueryLanguage = { query: string } }>; - transformUQLToQL: (unifiedQuery: string) => string; + transformInput: (unifiedQuery: string, options: {configuration: any, parameters: any}) => string; }; // Register the query languages diff --git a/public/components/search-bar/query-language/wql.test.tsx b/public/components/search-bar/query-language/wql.test.tsx index 56f11d9256..bfe284b03d 100644 --- a/public/components/search-bar/query-language/wql.test.tsx +++ b/public/components/search-bar/query-language/wql.test.tsx @@ -347,9 +347,20 @@ describe('Query language - WQL', () => { ${'(field=value,field2'} | ${'(field=value or field2'} ${'(field=value,field2>'} | ${'(field=value or field2>'} ${'(field=value,field2>value2'} | ${'(field=value or field2>value2'} - ${'(field=value,field2>value2)'} | ${'(field=value or field2>value2)'} + ${'(field=value,field2>value2)'} | ${'(field=value or field2>value2)'} + ${'implicit=value;'} | ${''} + ${'implicit=value;field'} | ${'field'} `('Transform the external input UQL to QL - UQL $UQL => $WQL', async ({UQL, WQL: changedInput}) => { - expect(WQL.transformUQLToQL(UQL)).toEqual(changedInput); + expect(WQL.transformInput(UQL, { + parameters: { + options: { + implicitQuery: { + query: 'implicit=value', + conjunction: ';' + } + } + } + })).toEqual(changedInput); }); /* The ! and ~ characters can't be part of a value that contains examples. The tests doesn't diff --git a/public/components/search-bar/query-language/wql.tsx b/public/components/search-bar/query-language/wql.tsx index 17fda7ec48..f3163a8147 100644 --- a/public/components/search-bar/query-language/wql.tsx +++ b/public/components/search-bar/query-language/wql.tsx @@ -1007,5 +1007,14 @@ export const WQL = { output }; }, - transformUQLToQL: transformUQLToQL, + transformInput: (unifiedQuery: string, {parameters}) => { + const input = unifiedQuery && parameters?.options?.implicitQuery + ? unifiedQuery.replace( + new RegExp(`^${parameters.options.implicitQuery.query}${parameters.options.implicitQuery.conjunction}`), + '' + ) + : unifiedQuery; + + return transformUQLToQL(input); + }, }; From 3d7e0d1005a2f4d5ca7a8fd106d6cedac49d80c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Wed, 17 May 2023 12:43:32 +0200 Subject: [PATCH 36/76] feat(table-wz-api): Adapt TableWzAPI usage to the recent changes when using the search bar Enhance Management/Decoders table --- .../agents/sca/inventory/checks-table.tsx | 47 ++++++++--------- .../agents/vuls/inventory/table.tsx | 51 +++++++++---------- .../mitre_attack_intelligence/resource.tsx | 15 ++---- .../cdblists/components/cdblists-table.tsx | 45 ++++++++-------- .../components/decoders-suggestions.ts | 4 +- .../decoders/components/decoders-table.tsx | 11 ++-- .../decoders/views/decoder-info.tsx | 2 +- .../ruleset/components/ruleset-table.tsx | 11 ++-- .../management/ruleset/views/rule-info.tsx | 2 +- 9 files changed, 80 insertions(+), 108 deletions(-) diff --git a/public/components/agents/sca/inventory/checks-table.tsx b/public/components/agents/sca/inventory/checks-table.tsx index 036ba570b1..3cfe0ec9df 100644 --- a/public/components/agents/sca/inventory/checks-table.tsx +++ b/public/components/agents/sca/inventory/checks-table.tsx @@ -260,32 +260,27 @@ export class InventoryPolicyChecksTable extends Component { showReload filters={filters} searchTable - searchBarProps={{ - modes: [ - { - id: 'wql', - options: searchBarWQLOptions, - suggestions: { - field(currentValue) { - return searchBarWQLFieldSuggestions; - }, - value: async (currentValue, { field }) => { - try{ - return await getFilterValues( - field, - currentValue, - agentID, - scaPolicyID, - {}, - (item) => ({label: item}) - ); - }catch(error){ - return []; - }; - }, - }, - } - ] + searchBarWQL={{ + options: searchBarWQLOptions, + suggestions: { + field(currentValue) { + return searchBarWQLFieldSuggestions; + }, + value: async (currentValue, { field }) => { + try{ + return await getFilterValues( + field, + currentValue, + agentID, + scaPolicyID, + {}, + (item) => ({label: item}) + ); + }catch(error){ + return []; + }; + }, + }, }} /> diff --git a/public/components/agents/vuls/inventory/table.tsx b/public/components/agents/vuls/inventory/table.tsx index 77936f5acb..6ed05d6656 100644 --- a/public/components/agents/vuls/inventory/table.tsx +++ b/public/components/agents/vuls/inventory/table.tsx @@ -187,34 +187,29 @@ export class InventoryTable extends Component { showReload tablePageSizeOptions={[10, 25, 50, 100]} filters={this.props.filters} - searchBarProps={{ - modes: [ - { - id: 'wql', - options: searchBarWQLOptions, - suggestions: { - field(currentValue) { - return [ - { label: 'architecture', description: 'filter by architecture' }, - { label: 'cve', description: 'filter by CVE ID' }, - { label: 'cvss2_score', description: 'filter by CVSS2' }, - { label: 'cvss3_score', description: 'filter by CVSS3' }, - { label: 'detection_time', description: 'filter by detection time' }, - { label: 'name', description: 'filter by package name' }, - { label: 'severity', description: 'filter by severity' }, - { label: 'version', description: 'filter by CVE version' }, - ]; - }, - value: async (currentValue, { field }) => { - try{ - return await getFilterValues(field, currentValue, agentID, {}, label => ({label})); - }catch(error){ - return []; - }; - }, - }, - } - ] + searchBarWQL={{ + options: searchBarWQLOptions, + suggestions: { + field(currentValue) { + return [ + { label: 'architecture', description: 'filter by architecture' }, + { label: 'cve', description: 'filter by CVE ID' }, + { label: 'cvss2_score', description: 'filter by CVSS2' }, + { label: 'cvss3_score', description: 'filter by CVSS3' }, + { label: 'detection_time', description: 'filter by detection time' }, + { label: 'name', description: 'filter by package name' }, + { label: 'severity', description: 'filter by severity' }, + { label: 'version', description: 'filter by CVE version' }, + ]; + }, + value: async (currentValue, { field }) => { + try{ + return await getFilterValues(field, currentValue, agentID, {}, label => ({label})); + }catch(error){ + return []; + }; + }, + }, }} /> ); diff --git a/public/components/overview/mitre_attack_intelligence/resource.tsx b/public/components/overview/mitre_attack_intelligence/resource.tsx index 2711aa9625..f8ffebf4a0 100644 --- a/public/components/overview/mitre_attack_intelligence/resource.tsx +++ b/public/components/overview/mitre_attack_intelligence/resource.tsx @@ -82,14 +82,9 @@ export const ModuleMitreAttackIntelligenceResource = ({ endpoint={apiEndpoint} tablePageSizeOptions={[10, 15, 25, 50, 100]} filters={resourceFilters} - searchBarProps={{ - modes: [ - { - id: 'wql', - options: searchBar.wql.options, - suggestions: searchBar.wql.suggestions, - } - ] + searchBarWQL={{ + options: searchBar.wql.options, + suggestions: searchBar.wql.suggestions, }} /> {details && ( @@ -99,6 +94,6 @@ export const ModuleMitreAttackIntelligenceResource = ({ onSelectResource={setDetails} /> )} - - ) + + ); }; diff --git a/public/controllers/management/components/management/cdblists/components/cdblists-table.tsx b/public/controllers/management/components/management/cdblists/components/cdblists-table.tsx index dd56bf947a..ff902a3f11 100644 --- a/public/controllers/management/components/management/cdblists/components/cdblists-table.tsx +++ b/public/controllers/management/components/management/cdblists/components/cdblists-table.tsx @@ -168,33 +168,28 @@ function CDBListsTable(props) { { - try{ // TODO: distinct - return []; - }catch(error){ - return []; - }; - }, - }, - } - ] + searchBarWQL={{ + options: searchBarWQLOptions, + suggestions: { + field(currentValue) { + return [ + {label: 'filename', description: 'filter by filename'}, + {label: 'relative_dirname', description: 'filter by relative path'}, + ]; + }, + value: async (currentValue, { field }) => { + try{ // TODO: distinct + return []; + }catch(error){ + return []; + }; + }, + }, }} endpoint={'/lists'} isExpandable={true} diff --git a/public/controllers/management/components/management/decoders/components/decoders-suggestions.ts b/public/controllers/management/components/management/decoders/components/decoders-suggestions.ts index 17cbf37fdd..9936fbe4cc 100644 --- a/public/controllers/management/components/management/decoders/components/decoders-suggestions.ts +++ b/public/controllers/management/components/management/decoders/components/decoders-suggestions.ts @@ -3,9 +3,11 @@ import { WzRequest } from '../../../../../../react-services/wz-request'; const decodersItems = { field(currentValue) { return [ + {label: 'details.order', description: 'filter by program name'}, + {label: 'details.program_name', description: 'filter by program name'}, {label: 'filename', description: 'filter by filename'}, + {label: 'name', description: 'filter by name'}, {label: 'relative_dirname', description: 'filter by relative path'}, - {label: 'status', description: 'filter by status'}, ]; }, value: async (currentValue, { field }) => { diff --git a/public/controllers/management/components/management/decoders/components/decoders-table.tsx b/public/controllers/management/components/management/decoders/components/decoders-table.tsx index 7d4736a10f..decffb537b 100644 --- a/public/controllers/management/components/management/decoders/components/decoders-table.tsx +++ b/public/controllers/management/components/management/decoders/components/decoders-table.tsx @@ -93,14 +93,9 @@ const DecodersFlyoutTable = ({ tableColumns={columns} tableInitialSortingField='filename' searchTable={true} - searchBarProps={{ - modes: [ - { - id: 'wql', - options: searchBarWQLOptions, - suggestions: searchBarSuggestions, - } - ] + searchBarWQL={{ + options: searchBarWQLOptions, + suggestions: searchBarSuggestions, }} endpoint='/decoders' isExpandable={true} diff --git a/public/controllers/management/components/management/decoders/views/decoder-info.tsx b/public/controllers/management/components/management/decoders/views/decoder-info.tsx index 9d74ab6827..bc5e6a39ac 100644 --- a/public/controllers/management/components/management/decoders/views/decoder-info.tsx +++ b/public/controllers/management/components/management/decoders/views/decoder-info.tsx @@ -328,7 +328,7 @@ export default class WzDecoderInfo extends Component { {currentDecoder?.filename && Date: Wed, 17 May 2023 16:38:22 +0200 Subject: [PATCH 37/76] fix(test): fixed test and update snapshot --- .../table-with-search-bar.test.tsx.snap | 23 ++++++-------- .../tables/table-with-search-bar.test.tsx | 31 +++++++++---------- 2 files changed, 23 insertions(+), 31 deletions(-) diff --git a/public/components/common/tables/__snapshots__/table-with-search-bar.test.tsx.snap b/public/components/common/tables/__snapshots__/table-with-search-bar.test.tsx.snap index 6274f8c7ee..499e54559b 100644 --- a/public/components/common/tables/__snapshots__/table-with-search-bar.test.tsx.snap +++ b/public/components/common/tables/__snapshots__/table-with-search-bar.test.tsx.snap @@ -5,23 +5,18 @@ exports[`Table With Search Bar component renders correctly to match the snapshot onSearch={[Function]} reload={[Function]} rowProps={[Function]} - searchBarProps={ + searchBarSuggestions={Array []} + searchBarWQL={ Object { - "modes": Array [ - Object { - "id": "wql", - "options": Object { - "searchTermFields": Array [], - }, - "suggestions": Object { - "field": [Function], - "value": [Function], - }, - }, - ], + "options": Object { + "searchTermFields": Array [], + }, + "suggestions": Object { + "field": [Function], + "value": [Function], + }, } } - searchBarSuggestions={Array []} tableColumns={ Array [ Object { diff --git a/public/components/common/tables/table-with-search-bar.test.tsx b/public/components/common/tables/table-with-search-bar.test.tsx index 32c79e3945..60d83b8a52 100644 --- a/public/components/common/tables/table-with-search-bar.test.tsx +++ b/public/components/common/tables/table-with-search-bar.test.tsx @@ -63,6 +63,10 @@ const columns = [ }, ]; +const searchBarWQLOptions = { + searchTermFields: [] +} + const tableProps = { onSearch: () => {}, tableColumns: columns, @@ -73,23 +77,16 @@ const tableProps = { reload: () => {}, searchBarSuggestions: [], rowProps: () => {}, - searchBarProps: { - modes: [ - { - id: 'wql', - options: { - searchTermFields: [] - }, - suggestions: { - field(currentValue) { - return []; - }, - value: async (currentValue, { field }) => { - return []; - }, - }, - } - ] + searchBarWQL: { + options: searchBarWQLOptions, + suggestions: { + field(currentValue) { + return []; + }, + value: async (currentValue, { field }) => { + return []; + }, + }, } }; From 1a198e8231b7d56aee8a8fea360c09a6484e5355 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Wed, 17 May 2023 16:40:03 +0200 Subject: [PATCH 38/76] fix(table-wz-api): minor fixes on TableWzAPI usage --- .../agents/sca/inventory/checks-table.tsx | 78 +++++++++---------- .../mitre_attack_intelligence/resource.tsx | 1 - 2 files changed, 38 insertions(+), 41 deletions(-) diff --git a/public/components/agents/sca/inventory/checks-table.tsx b/public/components/agents/sca/inventory/checks-table.tsx index 3cfe0ec9df..3bddc1f363 100644 --- a/public/components/agents/sca/inventory/checks-table.tsx +++ b/public/components/agents/sca/inventory/checks-table.tsx @@ -243,47 +243,45 @@ export class InventoryPolicyChecksTable extends Component { const scaPolicyID = this.props?.lookingPolicy?.policy_id; return ( - <> - { - try{ - return await getFilterValues( - field, - currentValue, - agentID, - scaPolicyID, - {}, - (item) => ({label: item}) - ); - }catch(error){ - return []; - }; - }, + - + value: async (currentValue, { field }) => { + try{ + return await getFilterValues( + field, + currentValue, + agentID, + scaPolicyID, + {}, + (item) => ({label: item}) + ); + }catch(error){ + return []; + }; + }, + }, + }} + /> ); } } diff --git a/public/components/overview/mitre_attack_intelligence/resource.tsx b/public/components/overview/mitre_attack_intelligence/resource.tsx index f8ffebf4a0..fe311cfbea 100644 --- a/public/components/overview/mitre_attack_intelligence/resource.tsx +++ b/public/components/overview/mitre_attack_intelligence/resource.tsx @@ -78,7 +78,6 @@ export const ModuleMitreAttackIntelligenceResource = ({ title={label} tableColumns={tableColumns} tableInitialSortingField={initialSortingField} - searchBarPlaceholder={`Search in ${label}`} endpoint={apiEndpoint} tablePageSizeOptions={[10, 15, 25, 50, 100]} filters={resourceFilters} From 1a67951cbba0a45d4e17b870f80470e006ede4a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Wed, 17 May 2023 16:46:22 +0200 Subject: [PATCH 39/76] fix: fixed prop type --- public/components/agents/sca/inventory.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/components/agents/sca/inventory.tsx b/public/components/agents/sca/inventory.tsx index 43d4d5c223..4540f2f0c1 100644 --- a/public/components/agents/sca/inventory.tsx +++ b/public/components/agents/sca/inventory.tsx @@ -61,7 +61,7 @@ type InventoryState = { loading: boolean; checksIsLoading: boolean; redirect: boolean; - filters: string; + filters: object; pageTableChecks: { pageIndex: number; pageSize?: number }; policies: object[]; checks: object[]; From 1ba9575c78e5477f37bf5cc57dfabb6bfecd15f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Thu, 18 May 2023 13:26:22 +0200 Subject: [PATCH 40/76] fix: adapt search bar parameters on agent selection table --- .../agents-selection-table.js | 73 ++++++++++--------- 1 file changed, 37 insertions(+), 36 deletions(-) diff --git a/public/controllers/overview/components/overview-actions/agents-selection-table.js b/public/controllers/overview/components/overview-actions/agents-selection-table.js index 2141a82ddd..580ad47c15 100644 --- a/public/controllers/overview/components/overview-actions/agents-selection-table.js +++ b/public/controllers/overview/components/overview-actions/agents-selection-table.js @@ -19,7 +19,6 @@ import { AgentStatus } from '../../../../components/agents/agent_status'; import { TableWzAPI } from '../../../../components/common/tables'; const searchBarWQLOptions = { - searchTermFields: [], implicitQuery: { query: 'id!=000', conjunction: ';' @@ -38,34 +37,41 @@ export class AgentSelectionTable extends Component { field: 'id', name: 'ID', width: '60px', + searchable: true, sortable: true, }, { field: 'name', name: 'Name', + searchable: true, sortable: true }, { field: 'group', name: 'Group', sortable: true, + searchable: true, render: groups => this.renderGroups(groups) }, { field: 'version', name: 'Version', width: '80px', + searchable: true, sortable: true, }, { field: 'os.name,os.version', + composeField: ['os.name', 'os.version'], name: 'Operating system', sortable: true, + searchable: true, render: (field, agentData) => this.addIconPlatformRender(agentData) }, { field: 'status', name: 'Status', + searchable: true, sortable: true, width: 'auto', render: status => , @@ -206,41 +212,36 @@ export class AgentSelectionTable extends Component { rowProps={getRowProps} filters={this.state.filters} searchTable - searchBarProps={{ - modes: [ - { - id: 'wql', - options: searchBarWQLOptions, - suggestions: { - field(currentValue) { - return [ - {label: 'id', description: 'filter by id'}, - {label: 'group', description: 'filter by group'}, - {label: 'os.name', description: 'filter by operating system name'}, - {label: 'os.version', description: 'filter by operating system version'}, - {label: 'status', description: 'filter by status'}, - {label: 'name', description: 'filter by name'}, - {label: 'version', description: 'filter by version'}, - ]; - }, - value: async (currentValue, { field }) => { - try{ - switch (field) { - case 'status': - return UI_ORDER_AGENT_STATUS.map(status => ({label: status})) - break; - default: - return (await getAgentFilterValues(field, currentValue, { q: 'id!=000'})) - .map(status => ({label: status})); - break; - } - }catch(error){ - return []; - }; - }, - }, - } - ] + searchBarWQL={{ + options: searchBarWQLOptions, + suggestions: { + field(currentValue) { + return [ + {label: 'id', description: 'filter by id'}, + {label: 'group', description: 'filter by group'}, + {label: 'name', description: 'filter by name'}, + {label: 'os.name', description: 'filter by operating system name'}, + {label: 'os.version', description: 'filter by operating system version'}, + {label: 'status', description: 'filter by status'}, + {label: 'version', description: 'filter by version'}, + ]; + }, + value: async (currentValue, { field }) => { + try{ + switch (field) { + case 'status': + return UI_ORDER_AGENT_STATUS.map(status => ({label: status})) + break; + default: + return (await getAgentFilterValues(field, currentValue, { q: 'id!=000'})) + .map(status => ({label: status})); + break; + } + }catch(error){ + return []; + }; + }, + } }} />
From 6257a7ff5c8fe44fa43609da6e1d9c46d116d7ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Thu, 18 May 2023 13:30:44 +0200 Subject: [PATCH 41/76] fix: fix search term field on TableWzAPI for composed column --- public/components/common/tables/table-with-search-bar.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/public/components/common/tables/table-with-search-bar.tsx b/public/components/common/tables/table-with-search-bar.tsx index 5f54ec010b..97ecbe46af 100644 --- a/public/components/common/tables/table-with-search-bar.tsx +++ b/public/components/common/tables/table-with-search-bar.tsx @@ -54,7 +54,8 @@ export function TableWithSearchBar({ const searchBarWQLOptions = useMemo(() => ({ searchTermFields: tableColumns .filter(({field, searchable}) => searchable && rest.selectedFields.includes(field)) - .map(({field, composeField}) => ([composeField || field].flat())), + .map(({field, composeField}) => ([composeField || field].flat())) + .flat(), ...(rest?.searchBarWQL?.options || {}) }), [rest?.searchBarWQL?.options, rest?.selectedFields]); From f64749cf71bc2387186773a308e45b16d9ce555c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Fri, 19 May 2023 10:17:06 +0200 Subject: [PATCH 42/76] fix(table-wz-api): enhance TableWithSearchBar types and fix error HTML attributes --- .../common/tables/table-default.tsx | 2 +- .../common/tables/table-with-search-bar.tsx | 82 ++++++++++++++++--- 2 files changed, 73 insertions(+), 11 deletions(-) diff --git a/public/components/common/tables/table-default.tsx b/public/components/common/tables/table-default.tsx index 0ec7451b8a..ed068e3d23 100644 --- a/public/components/common/tables/table-default.tsx +++ b/public/components/common/tables/table-default.tsx @@ -112,7 +112,7 @@ export function TableDefault({ }; return ( ({...rest}))} items={items} loading={loading} pagination={tablePagination} diff --git a/public/components/common/tables/table-with-search-bar.tsx b/public/components/common/tables/table-with-search-bar.tsx index 97ecbe46af..d8f0df3e7a 100644 --- a/public/components/common/tables/table-with-search-bar.tsx +++ b/public/components/common/tables/table-with-search-bar.tsx @@ -11,17 +11,79 @@ */ import React, { useState, useEffect, useRef, useMemo } from 'react'; -import { EuiBasicTable, EuiSpacer } from '@elastic/eui'; +import { EuiBasicTable, EuiBasicTableProps, EuiSpacer } from '@elastic/eui'; import _ from 'lodash'; import { UI_ERROR_SEVERITIES } from '../../../react-services/error-orchestrator/types'; import { UI_LOGGER_LEVELS } from '../../../../common/constants'; import { getErrorOrchestrator } from '../../../react-services/common-services'; -import { SearchBar } from '../../search-bar'; +import { SearchBar, SearchBarProps } from '../../search-bar'; -export function TableWithSearchBar({ +export interface ITableWithSearcHBarProps{ + /** + * Function to fetch the data + */ + onSearch: ( + endpoint: string, + filters: Record, + pagination: {pageIndex: number, pageSize: number}, + sorting: {sort: {field: string, direction: string}} + ) => Promise<{items: any[], totalItems: number}> + /** + * Properties for the search bar + */ + searchBarProps?: Omit + /** + * Columns for the table + */ + tableColumns: EuiBasicTableProps['columns'] & { + composeField?: string[], + searchable?: string + show?: boolean, + } + /** + * Table row properties for the table + */ + rowProps?: EuiBasicTableProps['rowProps'] + /** + * Table page size options + */ + tablePageSizeOptions?: number[] + /** + * Table initial sorting direction + */ + tableInitialSortingDirection?: 'asc' | 'dsc' + /** + * Table initial sorting field + */ + tableInitialSortingField?: string + /** + * Table properties + */ + tableProps?: Omit, 'columns' | 'items' | 'loading' | 'pagination' | 'sorting' | 'onChange' | 'rowProps'> + /** + * Refresh the fetch of data + */ + reload?: number + /** + * API endpoint + */ + endpoint: string + /** + * Search bar properties for WQL + */ + searchBarWQL?: any + /** + * Visible fields + */ + selectedFields: string[] + /** + * API request filters + */ + filters?: any +} + +export function TableWithSearchBar({ onSearch, - searchBarSuggestions, - searchBarPlaceholder = 'Filter or search', searchBarProps = {}, tableColumns, rowProps, @@ -32,7 +94,7 @@ export function TableWithSearchBar({ reload, endpoint, ...rest -}) { +}: ITableWithSearcHBarProps) { const [loading, setLoading] = useState(false); const [items, setItems] = useState([]); const [totalItems, setTotalItems] = useState(0); @@ -143,14 +205,14 @@ export function TableWithSearchBar({ return ( <> ({...rest}))} items={items} loading={loading} pagination={tablePagination} From d774a48f313419dadde396d7370e262b53d4035d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Fri, 19 May 2023 10:18:36 +0200 Subject: [PATCH 43/76] fix: enhance search bar and WQL types --- public/components/search-bar/index.tsx | 10 ++++++---- public/components/search-bar/query-language/wql.tsx | 10 ++++++++++ 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/public/components/search-bar/index.tsx b/public/components/search-bar/index.tsx index 10b17b3703..4a82d5d360 100644 --- a/public/components/search-bar/index.tsx +++ b/public/components/search-bar/index.tsx @@ -14,12 +14,14 @@ import { import { EuiSuggest } from '../eui-suggest'; import { searchBarQueryLanguages } from './query-language'; import _ from 'lodash'; +import { ISearchBarModeWQL } from './query-language/wql'; -type SearchBarProps = { +export interface SearchBarProps{ defaultMode?: string; - modes: { id: string; [key: string]: any }[]; + modes: ISearchBarModeWQL[]; onChange?: (params: any) => void; onSearch: (params: any) => void; + buttonsRender?: () => React.ReactNode input?: string; }; @@ -71,7 +73,7 @@ export const SearchBar = ({ setInput(event.target.value); // Handler when pressing a key - const onKeyPressHandler = event => { + const onKeyPressHandler = (event: React.KeyboardEvent) => { if (event.key === 'Enter') { _onSearch(queryLanguageOutputRun.output); } @@ -192,7 +194,7 @@ export const SearchBar = ({ text: searchBarQueryLanguages[id].label, }))} value={queryLanguage.id} - onChange={(event: React.ChangeEvent) => { + onChange={(event: React.ChangeEvent) => { const queryLanguageID: string = event.target.value; setQueryLanguage({ id: queryLanguageID, diff --git a/public/components/search-bar/query-language/wql.tsx b/public/components/search-bar/query-language/wql.tsx index f3163a8147..4776c141e7 100644 --- a/public/components/search-bar/query-language/wql.tsx +++ b/public/components/search-bar/query-language/wql.tsx @@ -217,11 +217,21 @@ type OptionsQL = { options?: { implicitQuery?: OptionsQLImplicitQuery searchTermFields?: string[] + filterButtons: {id: string, label: string, input: string}[] } suggestions: { field: QLOptionSuggestionHandler; value: QLOptionSuggestionHandler; }; + validate?: { + value?: { + [key: string]: (token: IToken, nearTokens: {field: string, operator: string}) => string | undefined + } + } +}; + +export interface ISearchBarModeWQL extends OptionsQL{ + id: 'wql' }; /** From b31e5f576b0d5ab8bc9397b3827c51f8dc14c5a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Tue, 25 Jul 2023 14:43:01 +0200 Subject: [PATCH 44/76] fix: test snapshot --- .../__snapshots__/table-with-search-bar.test.tsx.snap | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/main/public/components/common/tables/__snapshots__/table-with-search-bar.test.tsx.snap b/plugins/main/public/components/common/tables/__snapshots__/table-with-search-bar.test.tsx.snap index 499e54559b..d4a34ae9fc 100644 --- a/plugins/main/public/components/common/tables/__snapshots__/table-with-search-bar.test.tsx.snap +++ b/plugins/main/public/components/common/tables/__snapshots__/table-with-search-bar.test.tsx.snap @@ -109,7 +109,7 @@ exports[`Table With Search Bar component renders correctly to match the snapshot
@@ -178,7 +178,7 @@ exports[`Table With Search Bar component renders correctly to match the snapshot
@@ -258,7 +258,7 @@ exports[`Table With Search Bar component renders correctly to match the snapshot
@@ -368,7 +368,7 @@ exports[`Table With Search Bar component renders correctly to match the snapshot
@@ -434,7 +434,7 @@ exports[`Table With Search Bar component renders correctly to match the snapshot
From 0cf2d183e8fc0b8a18c74d166539042457cf6d03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Tue, 25 Jul 2023 15:05:04 +0200 Subject: [PATCH 45/76] fix: remove duplicated search bar --- .../search-bar/search-bar/README.md | 201 ---- .../__snapshots__/index.test.tsx.snap | 59 - .../search-bar/search-bar/index.test.tsx | 57 - .../search-bar/search-bar/index.tsx | 229 ---- .../__snapshots__/aql.test.tsx.snap | 99 -- .../__snapshots__/wql.test.tsx.snap | 99 -- .../search-bar/query-language/aql.md | 204 ---- .../search-bar/query-language/aql.test.tsx | 205 ---- .../search-bar/query-language/aql.tsx | 523 --------- .../search-bar/query-language/index.ts | 32 - .../search-bar/query-language/wql.md | 269 ----- .../search-bar/query-language/wql.test.tsx | 442 ------- .../search-bar/query-language/wql.tsx | 1030 ----------------- 13 files changed, 3449 deletions(-) delete mode 100644 plugins/main/public/components/search-bar/search-bar/README.md delete mode 100644 plugins/main/public/components/search-bar/search-bar/__snapshots__/index.test.tsx.snap delete mode 100644 plugins/main/public/components/search-bar/search-bar/index.test.tsx delete mode 100644 plugins/main/public/components/search-bar/search-bar/index.tsx delete mode 100644 plugins/main/public/components/search-bar/search-bar/query-language/__snapshots__/aql.test.tsx.snap delete mode 100644 plugins/main/public/components/search-bar/search-bar/query-language/__snapshots__/wql.test.tsx.snap delete mode 100644 plugins/main/public/components/search-bar/search-bar/query-language/aql.md delete mode 100644 plugins/main/public/components/search-bar/search-bar/query-language/aql.test.tsx delete mode 100644 plugins/main/public/components/search-bar/search-bar/query-language/aql.tsx delete mode 100644 plugins/main/public/components/search-bar/search-bar/query-language/index.ts delete mode 100644 plugins/main/public/components/search-bar/search-bar/query-language/wql.md delete mode 100644 plugins/main/public/components/search-bar/search-bar/query-language/wql.test.tsx delete mode 100644 plugins/main/public/components/search-bar/search-bar/query-language/wql.tsx diff --git a/plugins/main/public/components/search-bar/search-bar/README.md b/plugins/main/public/components/search-bar/search-bar/README.md deleted file mode 100644 index ce9fd0d65b..0000000000 --- a/plugins/main/public/components/search-bar/search-bar/README.md +++ /dev/null @@ -1,201 +0,0 @@ -# Component - -The `SearchBar` component is a base component of a search bar. - -It is designed to be extensible through the self-contained query language implementations. This means -the behavior of the search bar depends on the business logic of each query language. For example, a -query language can display suggestions according to the user input or prepend some buttons to the search bar. - -It is based on a custom `EuiSuggest` component defined in `public/components/eui-suggest/suggest.js`. So the -abilities are restricted by this one. - -## Features - -- Supports multiple query languages. -- Switch the selected query language. -- Self-contained query language implementation and ability to interact with the search bar component. -- React to external changes to set the new input. This enables to change the input from external components. - -# Usage - -Basic usage: - -```tsx - { - switch (field) { - case 'configSum': - return [ - { label: 'configSum1' }, - { label: 'configSum2' }, - ]; - break; - case 'dateAdd': - return [ - { label: 'dateAdd1' }, - { label: 'dateAdd2' }, - ]; - break; - case 'status': - return UI_ORDER_AGENT_STATUS.map( - (status) => ({ - label: status, - }), - ); - break; - default: - return []; - break; - } - }, - } - }, - ]} - // Handler fired when the input handler changes. Optional. - onChange={onChange} - // Handler fired when the user press the Enter key or custom implementations. Required. - onSearch={onSearch} - // Used to define the internal input. Optional. - // This could be used to change the input text from the external components. - // Use the UQL (Unified Query Language) syntax. - input='' - // Define the default mode. Optional. If not defined, it will use the first one mode. - defaultMode='' -> -``` - -# Query languages - -The built-in query languages are: - -- AQL: API Query Language. Based on https://documentation.wazuh.com/current/user-manual/api/queries.html. - -## How to add a new query language - -### Definition - -The language expects to take the interface: - -```ts -type SearchBarQueryLanguage = { - description: string; - documentationLink?: string; - id: string; - label: string; - getConfiguration?: () => any; - run: (input: string | undefined, params: any) => Promise<{ - searchBarProps: any, - output: { - language: string, - apiQuery: string, - query: string - } - }>; - transformInput: (unifiedQuery: string, options: {configuration: any, parameters: any}) => string; -}; -``` - -where: - -- `description`: is the description of the query language. This is displayed in a query language popover - on the right side of the search bar. Required. -- `documentationLink`: URL to the documentation link. Optional. -- `id`: identification of the query language. -- `label`: name -- `getConfiguration`: method that returns the configuration of the language. This allows custom behavior. -- `run`: method that returns: - - `searchBarProps`: properties to be passed to the search bar component. This allows the - customization the properties that will used by the base search bar component and the output used when searching - - `output`: - - `language`: query language ID - - `apiQuery`: API query. - - `query`: current query in the specified language -- `transformInput`: method that transforms the UQL (Unified Query Language) to the specific query - language. This is used when receives a external input in the Unified Query Language, the returned - value is converted to the specific query language to set the new input text of the search bar - component. - -Create a new file located in `public/components/search-bar/query-language` and define the expected interface; - -### Register - -Go to `public/components/search-bar/query-language/index.ts` and add the new query language: - -```ts -import { AQL } from './aql'; - -// Import the custom query language -import { CustomQL } from './custom'; - -// [...] - -// Register the query languages -export const searchBarQueryLanguages: { - [key: string]: SearchBarQueryLanguage; -} = [ - AQL, - CustomQL, // Add the new custom query language -].reduce((accum, item) => { - if (accum[item.id]) { - throw new Error(`Query language with id: ${item.id} already registered.`); - } - return { - ...accum, - [item.id]: item, - }; -}, {}); -``` - -## Unified Query Language - UQL - -This is an unified syntax used by the search bar component that provides a way to communicate -with the different query language implementations. - -The input and output parameters of the search bar component must use this syntax. - -This is used in: -- input: - - `input` component property -- output: - - `onChange` component handler - - `onSearch` component handler - -Its syntax is equal to Wazuh API Query Language -https://wazuh.com/./user-manual/api/queries.html - -> The AQL query language is a implementation of this syntax. \ No newline at end of file diff --git a/plugins/main/public/components/search-bar/search-bar/__snapshots__/index.test.tsx.snap b/plugins/main/public/components/search-bar/search-bar/__snapshots__/index.test.tsx.snap deleted file mode 100644 index 5602512bd0..0000000000 --- a/plugins/main/public/components/search-bar/search-bar/__snapshots__/index.test.tsx.snap +++ /dev/null @@ -1,59 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`SearchBar component Renders correctly the initial render 1`] = ` -
-
-
-
-
-
-
-
- -
-
-
- -
-
-
-
-
-
-
-
-
-`; diff --git a/plugins/main/public/components/search-bar/search-bar/index.test.tsx b/plugins/main/public/components/search-bar/search-bar/index.test.tsx deleted file mode 100644 index 31f18f6dda..0000000000 --- a/plugins/main/public/components/search-bar/search-bar/index.test.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import React from 'react'; -import { render } from '@testing-library/react'; -import { SearchBar } from './index'; - -describe('SearchBar component', () => { - const componentProps = { - defaultMode: 'wql', - input: '', - modes: [ - { - id: 'aql', - implicitQuery: 'id!=000;', - suggestions: { - field(currentValue) { - return []; - }, - value(currentValue, { field }){ - return []; - }, - }, - }, - { - id: 'wql', - implicitQuery: { - query: 'id!=000', - conjunction: ';' - }, - suggestions: { - field(currentValue) { - return []; - }, - value(currentValue, { field }){ - return []; - }, - }, - }, - ], - /* eslint-disable @typescript-eslint/no-empty-function */ - onChange: () => {}, - onSearch: () => {} - /* eslint-enable @typescript-eslint/no-empty-function */ - }; - - it('Renders correctly the initial render', async () => { - const wrapper = render( - - ); - - /* This test causes a warning about act. This is intentional, because the test pretends to get - the first rendering of the component that doesn't have the component properties coming of the - selected query language */ - expect(wrapper.container).toMatchSnapshot(); - }); -}); \ No newline at end of file diff --git a/plugins/main/public/components/search-bar/search-bar/index.tsx b/plugins/main/public/components/search-bar/search-bar/index.tsx deleted file mode 100644 index 4a82d5d360..0000000000 --- a/plugins/main/public/components/search-bar/search-bar/index.tsx +++ /dev/null @@ -1,229 +0,0 @@ -import React, { useEffect, useRef, useState } from 'react'; -import { - EuiButtonEmpty, - EuiFormRow, - EuiLink, - EuiPopover, - EuiPopoverTitle, - EuiSpacer, - EuiSelect, - EuiText, - EuiFlexGroup, - EuiFlexItem -} from '@elastic/eui'; -import { EuiSuggest } from '../eui-suggest'; -import { searchBarQueryLanguages } from './query-language'; -import _ from 'lodash'; -import { ISearchBarModeWQL } from './query-language/wql'; - -export interface SearchBarProps{ - defaultMode?: string; - modes: ISearchBarModeWQL[]; - onChange?: (params: any) => void; - onSearch: (params: any) => void; - buttonsRender?: () => React.ReactNode - input?: string; -}; - -export const SearchBar = ({ - defaultMode, - modes, - onChange, - onSearch, - ...rest -}: SearchBarProps) => { - // Query language ID and configuration - const [queryLanguage, setQueryLanguage] = useState<{ - id: string; - configuration: any; - }>({ - id: defaultMode || modes[0].id, - configuration: - searchBarQueryLanguages[ - defaultMode || modes[0].id - ]?.getConfiguration?.() || {}, - }); - // Popover query language is open - const [isOpenPopoverQueryLanguage, setIsOpenPopoverQueryLanguage] = - useState(false); - // Input field - const [input, setInput] = useState(rest.input || ''); - // Query language output of run method - const [queryLanguageOutputRun, setQueryLanguageOutputRun] = useState({ - searchBarProps: { suggestions: [] }, - output: undefined, - }); - // Cache the previous output - const queryLanguageOutputRunPreviousOutput = useRef(queryLanguageOutputRun.output); - // Controls when the suggestion popover is open/close - const [isOpenSuggestionPopover, setIsOpenSuggestionPopover] = - useState(false); - // Reference to the input - const inputRef = useRef(); - - // Handler when searching - const _onSearch = (output: any) => { - // TODO: fix when searching - onSearch(output); - setIsOpenSuggestionPopover(false); - }; - - // Handler on change the input field text - const onChangeInput = (event: React.ChangeEvent) => - setInput(event.target.value); - - // Handler when pressing a key - const onKeyPressHandler = (event: React.KeyboardEvent) => { - if (event.key === 'Enter') { - _onSearch(queryLanguageOutputRun.output); - } - }; - - const selectedQueryLanguageParameters = modes.find(({ id }) => id === queryLanguage.id); - - useEffect(() => { - // React to external changes and set the internal input text. Use the `transformInput` of - // the query language in use - rest.input && searchBarQueryLanguages[queryLanguage.id]?.transformInput && setInput( - searchBarQueryLanguages[queryLanguage.id]?.transformInput?.( - rest.input, - { - configuration: queryLanguage.configuration, - parameters: selectedQueryLanguageParameters, - } - ), - ); - }, [rest.input]); - - useEffect(() => { - (async () => { - // Set the query language output - const queryLanguageOutput = await searchBarQueryLanguages[queryLanguage.id].run(input, { - onSearch: _onSearch, - setInput, - closeSuggestionPopover: () => setIsOpenSuggestionPopover(false), - openSuggestionPopover: () => setIsOpenSuggestionPopover(true), - setQueryLanguageConfiguration: (configuration: any) => - setQueryLanguage(state => ({ - ...state, - configuration: - configuration?.(state.configuration) || configuration, - })), - setQueryLanguageOutput: setQueryLanguageOutputRun, - inputRef, - queryLanguage: { - configuration: queryLanguage.configuration, - parameters: selectedQueryLanguageParameters, - }, - }); - queryLanguageOutputRunPreviousOutput.current = { - ...queryLanguageOutputRun.output - }; - setQueryLanguageOutputRun(queryLanguageOutput); - })(); - }, [input, queryLanguage, selectedQueryLanguageParameters?.options]); - - useEffect(() => { - onChange - // Ensure the previous output is different to the new one - && !_.isEqual(queryLanguageOutputRun.output, queryLanguageOutputRunPreviousOutput.current) - && onChange(queryLanguageOutputRun.output); - }, [queryLanguageOutputRun.output]); - - const onQueryLanguagePopoverSwitch = () => - setIsOpenPopoverQueryLanguage(state => !state); - - const searchBar = ( - <> - {}} /* This method is run by EuiSuggest when there is a change in - a div wrapper of the input and should be defined. Defining this - property prevents an error. */ - suggestions={[]} - isPopoverOpen={ - queryLanguageOutputRun?.searchBarProps?.suggestions?.length > 0 && - isOpenSuggestionPopover - } - onClosePopover={() => setIsOpenSuggestionPopover(false)} - onPopoverFocus={() => setIsOpenSuggestionPopover(true)} - placeholder={'Search'} - append={ - - {searchBarQueryLanguages[queryLanguage.id].label} - - } - isOpen={isOpenPopoverQueryLanguage} - closePopover={onQueryLanguagePopoverSwitch} - > - SYNTAX OPTIONS -
- - {searchBarQueryLanguages[queryLanguage.id].description} - - {searchBarQueryLanguages[queryLanguage.id].documentationLink && ( - <> - -
- - Documentation - -
- - )} - {modes?.length > 1 && ( - <> - - - ({ - value: id, - text: searchBarQueryLanguages[id].label, - }))} - value={queryLanguage.id} - onChange={(event: React.ChangeEvent) => { - const queryLanguageID: string = event.target.value; - setQueryLanguage({ - id: queryLanguageID, - configuration: - searchBarQueryLanguages[ - queryLanguageID - ]?.getConfiguration?.() || {}, - }); - setInput(''); - }} - aria-label='query-language-selector' - /> - - - )} -
-
- } - {...queryLanguageOutputRun.searchBarProps} - /> - - ); - return rest.buttonsRender || queryLanguageOutputRun.filterButtons - ? ( - - {searchBar} - {rest.buttonsRender && {rest.buttonsRender()}} - {queryLanguageOutputRun.filterButtons && {queryLanguageOutputRun.filterButtons}} - - ) - : searchBar; -}; diff --git a/plugins/main/public/components/search-bar/search-bar/query-language/__snapshots__/aql.test.tsx.snap b/plugins/main/public/components/search-bar/search-bar/query-language/__snapshots__/aql.test.tsx.snap deleted file mode 100644 index 0ef68d2e9e..0000000000 --- a/plugins/main/public/components/search-bar/search-bar/query-language/__snapshots__/aql.test.tsx.snap +++ /dev/null @@ -1,99 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`SearchBar component Renders correctly to match the snapshot of query language 1`] = ` -
-
-
-
-
-
-
-
-
- -
-
-
- -
-
-
- -
-
-
-
-
-
-
-
-
-`; diff --git a/plugins/main/public/components/search-bar/search-bar/query-language/__snapshots__/wql.test.tsx.snap b/plugins/main/public/components/search-bar/search-bar/query-language/__snapshots__/wql.test.tsx.snap deleted file mode 100644 index f1bad4e5d4..0000000000 --- a/plugins/main/public/components/search-bar/search-bar/query-language/__snapshots__/wql.test.tsx.snap +++ /dev/null @@ -1,99 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`SearchBar component Renders correctly to match the snapshot of query language 1`] = ` -
-
-
-
-
-
-
-
-
- -
-
-
- -
-
-
- -
-
-
-
-
-
-
-
-
-`; diff --git a/plugins/main/public/components/search-bar/search-bar/query-language/aql.md b/plugins/main/public/components/search-bar/search-bar/query-language/aql.md deleted file mode 100644 index 9d144e3b15..0000000000 --- a/plugins/main/public/components/search-bar/search-bar/query-language/aql.md +++ /dev/null @@ -1,204 +0,0 @@ -**WARNING: The search bar was changed and this language needs some adaptations to work.** - -# Query Language - AQL - -AQL (API Query Language) is a query language based in the `q` query parameters of the Wazuh API -endpoints. - -Documentation: https://wazuh.com/./user-manual/api/queries.html - -The implementation is adapted to work with the search bar component defined -`public/components/search-bar/index.tsx`. - -## Features -- Suggestions for `fields` (configurable), `operators` and `values` (configurable) -- Support implicit query - -# Language syntax - -Documentation: https://wazuh.com/./user-manual/api/queries.html - -# Developer notes - -## Options - -- `implicitQuery`: add an implicit query that is added to the user input. Optional. -Use UQL (Unified Query Language). -This can't be changed by the user. If this is defined, will be displayed as a prepend of the search bar. - -```ts -// language options -// ID is not equal to 000 and . This is defined in UQL that is transformed internally to the specific query language. -implicitQuery: 'id!=000;' -``` - -- `suggestions`: define the suggestion handlers. This is required. - - - `field`: method that returns the suggestions for the fields - - ```ts - // language options - field(currentValue) { - return [ - { label: 'configSum', description: 'Config sum' }, - { label: 'dateAdd', description: 'Date add' }, - { label: 'id', description: 'ID' }, - { label: 'ip', description: 'IP address' }, - { label: 'group', description: 'Group' }, - { label: 'group_config_status', description: 'Synced configuration status' }, - { label: 'lastKeepAline', description: 'Date add' }, - { label: 'manager', description: 'Manager' }, - { label: 'mergedSum', description: 'Merged sum' }, - { label: 'name', description: 'Agent name' }, - { label: 'node_name', description: 'Node name' }, - { label: 'os.platform', description: 'Operating system platform' }, - { label: 'status', description: 'Status' }, - { label: 'version', description: 'Version' }, - ]; - } - ``` - - - `value`: method that returns the suggestion for the values - ```ts - // language options - value: async (currentValue, { previousField }) => { - switch (previousField) { - case 'configSum': - return await getAgentFilterValuesMapToSearchBarSuggestion( - previousField, - currentValue, - {q: 'id!=000'} - ); - break; - case 'dateAdd': - return await getAgentFilterValuesMapToSearchBarSuggestion( - previousField, - currentValue, - {q: 'id!=000'} - ); - break; - case 'id': - return await getAgentFilterValuesMapToSearchBarSuggestion( - previousField, - currentValue, - {q: 'id!=000'} - ); - break; - case 'ip': - return await getAgentFilterValuesMapToSearchBarSuggestion( - previousField, - currentValue, - {q: 'id!=000'} - ); - break; - case 'group': - return await getAgentFilterValuesMapToSearchBarSuggestion( - previousField, - currentValue, - {q: 'id!=000'} - ); - break; - case 'group_config_status': - return [AGENT_SYNCED_STATUS.SYNCED, AGENT_SYNCED_STATUS.NOT_SYNCED].map( - (status) => ({ - type: 'value', - label: status, - }), - ); - break; - case 'lastKeepAline': - return await getAgentFilterValuesMapToSearchBarSuggestion( - previousField, - currentValue, - {q: 'id!=000'} - ); - break; - case 'manager': - return await getAgentFilterValuesMapToSearchBarSuggestion( - previousField, - currentValue, - {q: 'id!=000'} - ); - break; - case 'mergedSum': - return await getAgentFilterValuesMapToSearchBarSuggestion( - previousField, - currentValue, - {q: 'id!=000'} - ); - break; - case 'name': - return await getAgentFilterValuesMapToSearchBarSuggestion( - previousField, - currentValue, - {q: 'id!=000'} - ); - break; - case 'node_name': - return await getAgentFilterValuesMapToSearchBarSuggestion( - previousField, - currentValue, - {q: 'id!=000'} - ); - break; - case 'os.platform': - return await getAgentFilterValuesMapToSearchBarSuggestion( - previousField, - currentValue, - {q: 'id!=000'} - ); - break; - case 'status': - return UI_ORDER_AGENT_STATUS.map( - (status) => ({ - type: 'value', - label: status, - }), - ); - break; - case 'version': - return await getAgentFilterValuesMapToSearchBarSuggestion( - previousField, - currentValue, - {q: 'id!=000'} - ); - break; - default: - return []; - break; - } - } - ``` - -## Language workflow - -```mermaid -graph TD; - user_input[User input]-->tokenizer; - subgraph tokenizer - tokenize_regex[Wazuh API `q` regular expression] - end - - tokenizer-->tokens; - - tokens-->searchBarProps; - subgraph searchBarProps; - searchBarProps_suggestions-->searchBarProps_suggestions_get_last_token_with_value[Get last token with value] - searchBarProps_suggestions_get_last_token_with_value[Get last token with value]-->searchBarProps_suggestions__result[Suggestions] - searchBarProps_suggestions__result[Suggestions]-->EuiSuggestItem - searchBarProps_prepend[prepend]-->searchBarProps_prepend_implicitQuery{implicitQuery} - searchBarProps_prepend_implicitQuery{implicitQuery}-->searchBarProps_prepend_implicitQuery_yes[Yes]-->EuiButton - searchBarProps_prepend_implicitQuery{implicitQuery}-->searchBarProps_prepend_implicitQuery_no[No]-->null - searchBarProps_disableFocusTrap:true[disableFocusTrap = true] - searchBarProps_onItemClick[onItemClickSuggestion onclick handler]-->searchBarProps_onItemClick_suggestion_search[Search suggestion] - searchBarProps_onItemClick[onItemClickSuggestion onclick handler]-->searchBarProps_onItemClick_suggestion_edit_current_token[Edit current token]-->searchBarProps_onItemClick_build_input[Build input] - searchBarProps_onItemClick[onItemClickSuggestion onclick handler]-->searchBarProps_onItemClick_suggestion_add_new_token[Add new token]-->searchBarProps_onItemClick_build_input[Build input] - end - - tokens-->output; - subgraph output[output]; - output_result[implicitFilter + user input] - end - - output-->output_search_bar[Output] -``` \ No newline at end of file diff --git a/plugins/main/public/components/search-bar/search-bar/query-language/aql.test.tsx b/plugins/main/public/components/search-bar/search-bar/query-language/aql.test.tsx deleted file mode 100644 index a5f7c7d36c..0000000000 --- a/plugins/main/public/components/search-bar/search-bar/query-language/aql.test.tsx +++ /dev/null @@ -1,205 +0,0 @@ -import { AQL, getSuggestions, tokenizer } from './aql'; -import React from 'react'; -import { render, waitFor } from '@testing-library/react'; -import { SearchBar } from '../index'; - -describe('SearchBar component', () => { - const componentProps = { - defaultMode: AQL.id, - input: '', - modes: [ - { - id: AQL.id, - implicitQuery: 'id!=000;', - suggestions: { - field(currentValue) { - return []; - }, - value(currentValue, { previousField }){ - return []; - }, - }, - } - ], - /* eslint-disable @typescript-eslint/no-empty-function */ - onChange: () => {}, - onSearch: () => {} - /* eslint-enable @typescript-eslint/no-empty-function */ - }; - - it('Renders correctly to match the snapshot of query language', async () => { - const wrapper = render( - - ); - - await waitFor(() => { - const elementImplicitQuery = wrapper.container.querySelector('.euiCodeBlock__code'); - expect(elementImplicitQuery?.innerHTML).toEqual('id!=000;'); - expect(wrapper.container).toMatchSnapshot(); - }); - }); -}); - -describe('Query language - AQL', () => { - // Tokenize the input - it.each` - input | tokens - ${''} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} - ${'f'} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'f' }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} - ${'field'} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} - ${'field='} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} - ${'field=value'} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} - ${'field=value with spaces'} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value with spaces' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} - ${'field=value with spaces<'} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value with spaces<' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} - ${'field=value with (parenthesis)'} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value with (parenthesis)' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} - ${'field=value;'} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} - ${'field=value;field2'} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: 'field2' }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} - ${'field=value;field2!='} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: 'field2' }, { type: 'operator_compare', value: '!=' }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} - ${'field=value;field2!=value2'} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: 'field2' }, { type: 'operator_compare', value: '!=' }, { type: 'value', value: 'value2' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} - ${'('} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} - ${'(field'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} - ${'(field>'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} - ${'(field>2'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: '2' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} - ${'(field>2)'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: '2' }, { type: 'operator_group', value: ')' }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} - ${'(field>2);'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: '2' }, { type: 'operator_group', value: ')' }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} - ${'(field>2;'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: '2' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} - ${'(field>2;field2'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: '2' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: 'field2' }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} - ${'(field>2;field2='} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: '2' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: 'field2' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} - ${'(field>2;field2=value2'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: '2' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: 'field2' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value2' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} - ${'(field>2;field2=value2)'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: '2' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: 'field2' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value2' }, { type: 'operator_group', value: ')' }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} - ${'(field>2;field2=custom value())'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: '2' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: 'field2' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'custom value()' }, { type: 'operator_group', value: ')' }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} - `(`Tokenizer API input $input`, ({input, tokens}) => { - expect(tokenizer(input)).toEqual(tokens); - }); - - // Get suggestions - it.each` - input | suggestions - ${''} | ${[{ description: 'Field', label: 'field', type: 'field' }, { description: 'Field2', label: 'field2', type: 'field' }, { description: 'open group', label: '(', type: 'operator_group' }]} - ${'w'} | ${[]} - ${'f'} | ${[{ description: 'Field', label: 'field', type: 'field' }, { description: 'Field2', label: 'field2', type: 'field' }]} - ${'field'} | ${[{ description: 'Field2', label: 'field2', type: 'field' }, { description: 'equality', label: '=', type: 'operator_compare' }, { description: 'not equality', label: '!=', type: 'operator_compare' }, { description: 'bigger', label: '>', type: 'operator_compare' }, { description: 'smaller', label: '<', type: 'operator_compare' }, { description: 'like as', label: '~', type: 'operator_compare' }]} - ${'field='} | ${[{ label: 'value', type: 'value' }, { label: 'value2', type: 'value' }, { label: 'value3', type: 'value' }, { label: 'value4', type: 'value' }]} - ${'field=v'} | ${[{ description: 'run the search query', label: 'Search', type: 'function_search' }, { label: 'value', type: 'value' }, { label: 'value2', type: 'value' }, { label: 'value3', type: 'value' }, { label: 'value4', type: 'value' }, { description: 'and', label: ';', type: 'conjunction' }, { description: 'or', label: ',', type: 'conjunction' }, { description: 'close group', label: ')', type: 'operator_group' }]} - ${'field=value'} | ${[{ description: 'run the search query', label: 'Search', type: 'function_search' }, { label: 'value', type: 'value' }, { label: 'value2', type: 'value' }, { label: 'value3', type: 'value' }, { label: 'value4', type: 'value' }, { description: 'and', label: ';', type: 'conjunction' }, { description: 'or', label: ',', type: 'conjunction' }, { description: 'close group', label: ')', type: 'operator_group' }]} - ${'field=value;'} | ${[{ description: 'Field', label: 'field', type: 'field' }, { description: 'Field2', label: 'field2', type: 'field' }, { description: 'open group', label: '(', type: 'operator_group' }]} - ${'field=value;field2'} | ${[{ description: 'equality', label: '=', type: 'operator_compare' }, { description: 'not equality', label: '!=', type: 'operator_compare' }, { description: 'bigger', label: '>', type: 'operator_compare' }, { description: 'smaller', label: '<', type: 'operator_compare' }, { description: 'like as', label: '~', type: 'operator_compare' }]} - ${'field=value;field2='} | ${[{ label: '127.0.0.1', type: 'value' }, { label: '127.0.0.2', type: 'value' }, { label: '190.0.0.1', type: 'value' }, { label: '190.0.0.2', type: 'value' }]} - ${'field=value;field2=127'} | ${[{ description: 'run the search query', label: 'Search', type: 'function_search' }, { label: '127.0.0.1', type: 'value' }, { label: '127.0.0.2', type: 'value' }, { description: 'and', label: ';', type: 'conjunction' }, { description: 'or', label: ',', type: 'conjunction' }, { description: 'close group', label: ')', type: 'operator_group' }]} - `('Get suggestion from the input: $input', async ({ input, suggestions }) => { - expect( - await getSuggestions(tokenizer(input), { - id: 'aql', - suggestions: { - field(currentValue) { - return [ - { label: 'field', description: 'Field' }, - { label: 'field2', description: 'Field2' }, - ].map(({ label, description }) => ({ - type: 'field', - label, - description, - })); - }, - value(currentValue = '', { previousField }) { - switch (previousField) { - case 'field': - return ['value', 'value2', 'value3', 'value4'] - .filter(value => value.startsWith(currentValue)) - .map(value => ({ type: 'value', label: value })); - break; - case 'field2': - return ['127.0.0.1', '127.0.0.2', '190.0.0.1', '190.0.0.2'] - .filter(value => value.startsWith(currentValue)) - .map(value => ({ type: 'value', label: value })); - break; - default: - return []; - break; - } - }, - }, - }), - ).toEqual(suggestions); - }); - - // When a suggestion is clicked, change the input text - it.each` - AQL | clikedSuggestion | changedInput - ${''} | ${{type: { iconType: 'kqlField', color: 'tint4' }, label: 'field'}} | ${'field'} - ${'field'} | ${{type: { iconType: 'kqlField', color: 'tint4' }, label: 'field2'}} | ${'field2'} - ${'field'} | ${{type: { iconType: 'kqlOperand', color: 'tint1' }, label: '='}} | ${'field='} - ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value'}} | ${'field=value'} - ${'field='} | ${{type: { iconType: 'kqlOperand', color: 'tint1' }, label: '!='}} | ${'field!='} - ${'field=value'} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value2'}} | ${'field=value2'} - ${'field=value'} | ${{type: { iconType: 'kqlSelector', color: 'tint3' }, label: ';'}} | ${'field=value;'} - ${'field=value;'} | ${{type: { iconType: 'kqlField', color: 'tint4' }, label: 'field2'}} | ${'field=value;field2'} - ${'field=value;field2'} | ${{type: { iconType: 'kqlOperand', color: 'tint1' }, label: '>'}} | ${'field=value;field2>'} - ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with spaces'}} | ${'field=with spaces'} - ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with "spaces'}} | ${'field=with "spaces'} - ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: '"value'}} | ${'field="value'} - ${''} | ${{type: { iconType: 'tokenDenseVector', color: 'tint3' }, label: '('}} | ${'('} - ${'('} | ${{type: { iconType: 'kqlField', color: 'tint4' }, label: 'field'}} | ${'(field'} - ${'(field'} | ${{type: { iconType: 'kqlField', color: 'tint4' }, label: 'field2'}} | ${'(field2'} - ${'(field'} | ${{type: { iconType: 'kqlOperand', color: 'tint1' }, label: '='}} | ${'(field='} - ${'(field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value'}} | ${'(field=value'} - ${'(field=value'} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value2'}} | ${'(field=value2'} - ${'(field=value'} | ${{type: { iconType: 'kqlSelector', color: 'tint3' }, label: ','}} | ${'(field=value,'} - ${'(field=value,'} | ${{type: { iconType: 'kqlField', color: 'tint4' }, label: 'field2'}} | ${'(field=value,field2'} - ${'(field=value,field2'} | ${{type: { iconType: 'kqlOperand', color: 'tint1' }, label: '>'}} | ${'(field=value,field2>'} - ${'(field=value,field2>'} | ${{type: { iconType: 'kqlOperand', color: 'tint1' }, label: '>'}} | ${'(field=value,field2>'} - ${'(field=value,field2>'} | ${{type: { iconType: 'kqlOperand', color: 'tint1' }, label: '~'}} | ${'(field=value,field2~'} - ${'(field=value,field2>'} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value2'}} | ${'(field=value,field2>value2'} - ${'(field=value,field2>value2'} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value3'}} | ${'(field=value,field2>value3'} - ${'(field=value,field2>value2'} | ${{type: { iconType: 'tokenDenseVector', color: 'tint3' }, label: ')'}} | ${'(field=value,field2>value2)'} - `('click suggestion - AQL $AQL => $changedInput', async ({AQL: currentInput, clikedSuggestion, changedInput}) => { - // Mock input - let input = currentInput; - - const qlOutput = await AQL.run(input, { - setInput: (value: string): void => { input = value; }, - queryLanguage: { - parameters: { - implicitQuery: '', - suggestions: { - field: () => ([]), - value: () => ([]) - } - } - } - }); - qlOutput.searchBarProps.onItemClick(clikedSuggestion); - expect(input).toEqual(changedInput); - }); - - // Transform the external input in UQL (Unified Query Language) to QL - it.each` - UQL | AQL - ${''} | ${''} - ${'field'} | ${'field'} - ${'field='} | ${'field='} - ${'field!='} | ${'field!='} - ${'field>'} | ${'field>'} - ${'field<'} | ${'field<'} - ${'field~'} | ${'field~'} - ${'field=value'} | ${'field=value'} - ${'field=value;'} | ${'field=value;'} - ${'field=value;field2'} | ${'field=value;field2'} - ${'field="'} | ${'field="'} - ${'field=with spaces'} | ${'field=with spaces'} - ${'field=with "spaces'} | ${'field=with "spaces'} - ${'('} | ${'('} - ${'(field'} | ${'(field'} - ${'(field='} | ${'(field='} - ${'(field=value'} | ${'(field=value'} - ${'(field=value,'} | ${'(field=value,'} - ${'(field=value,field2'} | ${'(field=value,field2'} - ${'(field=value,field2>'} | ${'(field=value,field2>'} - ${'(field=value,field2>value2'} | ${'(field=value,field2>value2'} - ${'(field=value,field2>value2)'} | ${'(field=value,field2>value2)'} - `('Transform the external input UQL to QL - UQL $UQL => $AQL', async ({UQL, AQL: changedInput}) => { - expect(AQL.transformUQLToQL(UQL)).toEqual(changedInput); - }); -}); diff --git a/plugins/main/public/components/search-bar/search-bar/query-language/aql.tsx b/plugins/main/public/components/search-bar/search-bar/query-language/aql.tsx deleted file mode 100644 index 8c898af3e2..0000000000 --- a/plugins/main/public/components/search-bar/search-bar/query-language/aql.tsx +++ /dev/null @@ -1,523 +0,0 @@ -import React from 'react'; -import { EuiButtonEmpty, EuiPopover, EuiText, EuiCode } from '@elastic/eui'; -import { webDocumentationLink } from '../../../../common/services/web_documentation'; - -type ITokenType = - | 'field' - | 'operator_compare' - | 'operator_group' - | 'value' - | 'conjunction'; -type IToken = { type: ITokenType; value: string }; -type ITokens = IToken[]; - -/* API Query Language -Define the API Query Language to use in the search bar. -It is based in the language used by the q query parameter. -https://documentation.wazuh.com/current/user-manual/api/queries.html - -Use the regular expression of API with some modifications to allow the decomposition of -input in entities that doesn't compose a valid query. It allows get not-completed queries. - -API schema: -??? - -Implemented schema: -?????? -*/ - -// Language definition -export const language = { - // Tokens - tokens: { - // eslint-disable-next-line camelcase - operator_compare: { - literal: { - '=': 'equality', - '!=': 'not equality', - '>': 'bigger', - '<': 'smaller', - '~': 'like as', - }, - }, - conjunction: { - literal: { - ';': 'and', - ',': 'or', - }, - }, - // eslint-disable-next-line camelcase - operator_group: { - literal: { - '(': 'open group', - ')': 'close group', - }, - }, - }, -}; - -// Suggestion mapper by language token type -const suggestionMappingLanguageTokenType = { - field: { iconType: 'kqlField', color: 'tint4' }, - // eslint-disable-next-line camelcase - operator_compare: { iconType: 'kqlOperand', color: 'tint1' }, - value: { iconType: 'kqlValue', color: 'tint0' }, - conjunction: { iconType: 'kqlSelector', color: 'tint3' }, - // eslint-disable-next-line camelcase - operator_group: { iconType: 'tokenDenseVector', color: 'tint3' }, - // eslint-disable-next-line camelcase - function_search: { iconType: 'search', color: 'tint5' }, -}; - -/** - * Creator of intermediate interface of EuiSuggestItem - * @param type - * @returns - */ -function mapSuggestionCreator(type: ITokenType ){ - return function({...params}){ - return { - type, - ...params - }; - }; -}; - -const mapSuggestionCreatorField = mapSuggestionCreator('field'); -const mapSuggestionCreatorValue = mapSuggestionCreator('value'); - - -/** - * Tokenize the input string. Returns an array with the tokens. - * @param input - * @returns - */ -export function tokenizer(input: string): ITokens{ - // API regular expression - // https://github.com/wazuh/wazuh/blob/v4.4.0-rc1/framework/wazuh/core/utils.py#L1242-L1257 - // self.query_regex = re.compile( - // # A ( character. - // r"(\()?" + - // # Field name: name of the field to look on DB. - // r"([\w.]+)" + - // # Operator: looks for '=', '!=', '<', '>' or '~'. - // rf"([{''.join(self.query_operators.keys())}]{{1,2}})" + - // # Value: A string. - // r"((?:(?:\((?:\[[\[\]\w _\-.,:?\\/'\"=@%<>{}]*]|[\[\]\w _\-.:?\\/'\"=@%<>{}]*)\))*" - // r"(?:\[[\[\]\w _\-.,:?\\/'\"=@%<>{}]*]|[\[\]\w _\-.:?\\/'\"=@%<>{}]+)" - // r"(?:\((?:\[[\[\]\w _\-.,:?\\/'\"=@%<>{}]*]|[\[\]\w _\-.:?\\/'\"=@%<>{}]*)\))*)+)" + - // # A ) character. - // r"(\))?" + - // # Separator: looks for ';', ',' or nothing. - // rf"([{''.join(self.query_separators.keys())}])?" - // ) - - const re = new RegExp( - // The following regular expression is based in API one but was modified to use named groups - // and added the optional operator to allow matching the entities when the query is not - // completed. This helps to tokenize the query and manage when the input is not completed. - // A ( character. - '(?\\()?' + - // Field name: name of the field to look on DB. - '(?[\\w.]+)?' + // Added an optional find - // Operator: looks for '=', '!=', '<', '>' or '~'. - // This seems to be a bug because is not searching the literal valid operators. - // I guess the operator is validated after the regular expression matches - `(?[${Object.keys(language.tokens.operator_compare.literal)}]{1,2})?` + // Added an optional find - // Value: A string. - '(?(?:(?:\\((?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|[\\[\\]\\w _\\-.:?\\/\'"=@%<>{}]*)\\))*' + - '(?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|[\\[\\]\\w _\\-.:?\\\\/\'"=@%<>{}]+)' + - '(?:\\((?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|[\\[\\]\\w _\\-.:?\\\\/\'"=@%<>{}]*)\\))*)+)?' + // Added an optional find - // A ) character. - '(?\\))?' + - `(?[${Object.keys(language.tokens.conjunction.literal)}])?`, - 'g' - ); - - return [ - ...input.matchAll(re)] - .map( - ({groups}) => Object.entries(groups) - .map(([key, value]) => ({ - type: key.startsWith('operator_group') ? 'operator_group' : key, - value}) - ) - ).flat(); -}; - -type QLOptionSuggestionEntityItem = { - description?: string - label: string -}; - -type QLOptionSuggestionEntityItemTyped = - QLOptionSuggestionEntityItem - & { type: 'operator_group'|'field'|'operator_compare'|'value'|'conjunction' }; - -type SuggestItem = QLOptionSuggestionEntityItem & { - type: { iconType: string, color: string } -}; - -type QLOptionSuggestionHandler = ( - currentValue: string | undefined, - { - previousField, - previousOperatorCompare, - }: { previousField: string; previousOperatorCompare: string }, -) => Promise; - -type optionsQL = { - suggestions: { - field: QLOptionSuggestionHandler; - value: QLOptionSuggestionHandler; - }; -}; - -/** - * Get the last token with value - * @param tokens Tokens - * @param tokenType token type to search - * @returns - */ -function getLastTokenWithValue( - tokens: ITokens -): IToken | undefined { - // Reverse the tokens array and use the Array.protorype.find method - const shallowCopyArray = Array.from([...tokens]); - const shallowCopyArrayReversed = shallowCopyArray.reverse(); - const tokenFound = shallowCopyArrayReversed.find( - ({ value }) => value, - ); - return tokenFound; -} - -/** - * Get the last token with value by type - * @param tokens Tokens - * @param tokenType token type to search - * @returns - */ -function getLastTokenWithValueByType( - tokens: ITokens, - tokenType: ITokenType, -): IToken | undefined { - // Find the last token by type - // Reverse the tokens array and use the Array.protorype.find method - const shallowCopyArray = Array.from([...tokens]); - const shallowCopyArrayReversed = shallowCopyArray.reverse(); - const tokenFound = shallowCopyArrayReversed.find( - ({ type, value }) => type === tokenType && value, - ); - return tokenFound; -} - -/** - * Get the suggestions from the tokens - * @param tokens - * @param language - * @param options - * @returns - */ -export async function getSuggestions(tokens: ITokens, options: optionsQL): Promise { - if (!tokens.length) { - return []; - } - - // Get last token - const lastToken = getLastTokenWithValue(tokens); - - // If it can't get a token with value, then returns fields and open operator group - if(!lastToken?.type){ - return [ - // fields - ...(await options.suggestions.field()).map(mapSuggestionCreatorField), - { - type: 'operator_group', - label: '(', - description: language.tokens.operator_group.literal['('], - } - ]; - }; - - switch (lastToken.type) { - case 'field': - return [ - // fields that starts with the input but is not equals - ...(await options.suggestions.field()).filter( - ({ label }) => - label.startsWith(lastToken.value) && label !== lastToken.value, - ).map(mapSuggestionCreatorField), - // operators if the input field is exact - ...((await options.suggestions.field()).some( - ({ label }) => label === lastToken.value, - ) - ? [ - ...Object.keys(language.tokens.operator_compare.literal).map( - operator => ({ - type: 'operator_compare', - label: operator, - description: - language.tokens.operator_compare.literal[operator], - }), - ), - ] - : []), - ]; - break; - case 'operator_compare': - return [ - ...Object.keys(language.tokens.operator_compare.literal) - .filter( - operator => - operator.startsWith(lastToken.value) && - operator !== lastToken.value, - ) - .map(operator => ({ - type: 'operator_compare', - label: operator, - description: language.tokens.operator_compare.literal[operator], - })), - ...(Object.keys(language.tokens.operator_compare.literal).some( - operator => operator === lastToken.value, - ) - ? [ - ...(await options.suggestions.value(undefined, { - previousField: getLastTokenWithValueByType(tokens, 'field')!.value, - previousOperatorCompare: getLastTokenWithValueByType( - tokens, - 'operator_compare', - )!.value, - })).map(mapSuggestionCreatorValue), - ] - : []), - ]; - break; - case 'value': - return [ - ...(lastToken.value - ? [ - { - type: 'function_search', - label: 'Search', - description: 'run the search query', - }, - ] - : []), - ...(await options.suggestions.value(lastToken.value, { - previousField: getLastTokenWithValueByType(tokens, 'field')!.value, - previousOperatorCompare: getLastTokenWithValueByType( - tokens, - 'operator_compare', - )!.value, - })).map(mapSuggestionCreatorValue), - ...Object.entries(language.tokens.conjunction.literal).map( - ([ conjunction, description]) => ({ - type: 'conjunction', - label: conjunction, - description, - }), - ), - { - type: 'operator_group', - label: ')', - description: language.tokens.operator_group.literal[')'], - }, - ]; - break; - case 'conjunction': - return [ - ...Object.keys(language.tokens.conjunction.literal) - .filter( - conjunction => - conjunction.startsWith(lastToken.value) && - conjunction !== lastToken.value, - ) - .map(conjunction => ({ - type: 'conjunction', - label: conjunction, - description: language.tokens.conjunction.literal[conjunction], - })), - // fields if the input field is exact - ...(Object.keys(language.tokens.conjunction.literal).some( - conjunction => conjunction === lastToken.value, - ) - ? [ - ...(await options.suggestions.field()).map(mapSuggestionCreatorField), - ] - : []), - { - type: 'operator_group', - label: '(', - description: language.tokens.operator_group.literal['('], - }, - ]; - break; - case 'operator_group': - if (lastToken.value === '(') { - return [ - // fields - ...(await options.suggestions.field()).map(mapSuggestionCreatorField), - ]; - } else if (lastToken.value === ')') { - return [ - // conjunction - ...Object.keys(language.tokens.conjunction.literal).map( - conjunction => ({ - type: 'conjunction', - label: conjunction, - description: language.tokens.conjunction.literal[conjunction], - }), - ), - ]; - } - break; - default: - return []; - break; - } - - return []; -} - -/** - * Transform the suggestion object to the expected object by EuiSuggestItem - * @param param0 - * @returns - */ -export function transformSuggestionToEuiSuggestItem(suggestion: QLOptionSuggestionEntityItemTyped): SuggestItem{ - const { type, ...rest} = suggestion; - return { - type: { ...suggestionMappingLanguageTokenType[type] }, - ...rest - }; -}; - -/** - * Transform the suggestion object to the expected object by EuiSuggestItem - * @param suggestions - * @returns - */ -function transformSuggestionsToEuiSuggestItem( - suggestions: QLOptionSuggestionEntityItemTyped[] -): SuggestItem[] { - return suggestions.map(transformSuggestionToEuiSuggestItem); -}; - -/** - * Get the output from the input - * @param input - * @returns - */ -function getOutput(input: string, options: {implicitQuery?: string} = {}) { - const unifiedQuery = `${options?.implicitQuery ?? ''}${options?.implicitQuery ? `(${input})` : input}`; - return { - language: AQL.id, - query: unifiedQuery, - unifiedQuery - }; -}; - -export const AQL = { - id: 'aql', - label: 'AQL', - description: 'API Query Language (AQL) allows to do queries.', - documentationLink: webDocumentationLink('user-manual/api/queries.html'), - getConfiguration() { - return { - isOpenPopoverImplicitFilter: false, - }; - }, - async run(input, params) { - // Get the tokens from the input - const tokens: ITokens = tokenizer(input); - - return { - searchBarProps: { - // Props that will be used by the EuiSuggest component - // Suggestions - suggestions: transformSuggestionsToEuiSuggestItem( - await getSuggestions(tokens, params.queryLanguage.parameters) - ), - // Handler to manage when clicking in a suggestion item - onItemClick: item => { - // When the clicked item has the `search` iconType, run the `onSearch` function - if (item.type.iconType === 'search') { - // Execute the search action - params.onSearch(getOutput(input, params.queryLanguage.parameters)); - } else { - // When the clicked item has another iconType - const lastToken: IToken = getLastTokenWithValue(tokens); - // if the clicked suggestion is of same type of last token - if ( - lastToken && suggestionMappingLanguageTokenType[lastToken.type].iconType === - item.type.iconType - ) { - // replace the value of last token - lastToken.value = item.label; - } else { - // add a new token of the selected type and value - tokens.push({ - type: Object.entries(suggestionMappingLanguageTokenType).find( - ([, { iconType }]) => iconType === item.type.iconType, - )[0], - value: item.label, - }); - }; - - // Change the input - params.setInput(tokens - .filter(({ value }) => value) // Ensure the input is rebuilt using tokens with value. - // The input tokenization can contain tokens with no value due to the used - // regular expression. - .map(({ value }) => value) - .join('')); - } - }, - prepend: params.queryLanguage.parameters.implicitQuery ? ( - - params.setQueryLanguageConfiguration(state => ({ - ...state, - isOpenPopoverImplicitFilter: - !state.isOpenPopoverImplicitFilter, - })) - } - iconType='filter' - > - - {params.queryLanguage.parameters.implicitQuery} - - - } - isOpen={ - params.queryLanguage.configuration.isOpenPopoverImplicitFilter - } - closePopover={() => - params.setQueryLanguageConfiguration(state => ({ - ...state, - isOpenPopoverImplicitFilter: false, - })) - } - > - - Implicit query:{' '} - {params.queryLanguage.parameters.implicitQuery} - - This query is added to the input. - - ) : null, - // Disable the focus trap in the EuiInputPopover. - // This causes when using the Search suggestion, the suggestion popover can be closed. - // If this is disabled, then the suggestion popover is open after a short time for this - // use case. - disableFocusTrap: true - }, - output: getOutput(input, params.queryLanguage.parameters), - }; - }, - transformUQLToQL(unifiedQuery: string): string { - return unifiedQuery; - }, -}; diff --git a/plugins/main/public/components/search-bar/search-bar/query-language/index.ts b/plugins/main/public/components/search-bar/search-bar/query-language/index.ts deleted file mode 100644 index 5a897d1d34..0000000000 --- a/plugins/main/public/components/search-bar/search-bar/query-language/index.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { AQL } from './aql'; -import { WQL } from './wql'; - -type SearchBarQueryLanguage = { - description: string; - documentationLink?: string; - id: string; - label: string; - getConfiguration?: () => any; - run: (input: string | undefined, params: any) => Promise<{ - searchBarProps: any, - output: { - language: string, - unifiedQuery: string, - query: string - } - }>; - transformInput: (unifiedQuery: string, options: {configuration: any, parameters: any}) => string; -}; - -// Register the query languages -export const searchBarQueryLanguages: { - [key: string]: SearchBarQueryLanguage; -} = [AQL, WQL].reduce((accum, item) => { - if (accum[item.id]) { - throw new Error(`Query language with id: ${item.id} already registered.`); - } - return { - ...accum, - [item.id]: item, - }; -}, {}); diff --git a/plugins/main/public/components/search-bar/search-bar/query-language/wql.md b/plugins/main/public/components/search-bar/search-bar/query-language/wql.md deleted file mode 100644 index 108c942d32..0000000000 --- a/plugins/main/public/components/search-bar/search-bar/query-language/wql.md +++ /dev/null @@ -1,269 +0,0 @@ -# Query Language - WQL - -WQL (Wazuh Query Language) is a query language based in the `q` query parameter of the Wazuh API -endpoints. - -Documentation: https://wazuh.com/./user-manual/api/queries.html - -The implementation is adapted to work with the search bar component defined -`public/components/search-bar/index.tsx`. - -# Language syntax - -It supports 2 modes: - -- `explicit`: define the field, operator and value -- `search term`: use a term to search in the available fields - -Theses modes can not be combined. - -`explicit` mode is enabled when it finds a field and operator tokens. - -## Mode: explicit - -### Schema - -``` -???????????? -``` - -### Fields - -Regular expression: /[\\w.]+/ - -Examples: - -``` -field -field.custom -``` - -### Operators - -#### Compare - -- `=` equal to -- `!=` not equal to -- `>` bigger -- `<` smaller -- `~` like - -#### Group - -- `(` open -- `)` close - -#### Conjunction (logical) - -- `and` intersection -- `or` union - -#### Values - -- Value without spaces can be literal -- Value with spaces should be wrapped by `"`. The `"` can be escaped using `\"`. - -Examples: -``` -value_without_whitespace -"value with whitespaces" -"value with whitespaces and escaped \"quotes\"" -``` - -### Notes - -- The tokens can be separated by whitespaces. - -### Examples - -- Simple query - -``` -id=001 -id = 001 -``` - -- Complex query (logical operator) -``` -status=active and os.platform~linux -status = active and os.platform ~ linux -``` - -``` -status!=never_connected and ip~240 or os.platform~linux -status != never_connected and ip ~ 240 or os.platform ~ linux -``` - -- Complex query (logical operators and group operator) -``` -(status!=never_connected and ip~240) or id=001 -( status != never_connected and ip ~ 240 ) or id = 001 -``` - -## Mode: search term - -Search the term in the available fields. - -This mode is used when there is no a `field` and `operator` according to the regular expression -of the **explicit** mode. - -### Examples: - -``` -linux -``` - -If the available fields are `id` and `ip`, then the input will be translated under the hood to the -following UQL syntax: - -``` -id~linux,ip~linux -``` - -## Developer notes - -## Features -- Support suggestions for each token entity. `fields` and `values` are customizable. -- Support implicit query. -- Support for search term mode. It enables to search a term in multiple fields. - The query is built under the hoods. This mode requires there are `field` and `operator_compare`. - -### Implicit query - -This a query that can't be added, edited or removed by the user. It is added to the user input. - -### Search term mode - -This mode enables to search in multiple fields using a search term. The fields to use must be defined. - -Use an union expression of each field with the like as operation `~`. - -The user input is transformed to something as: -``` -field1~user_input,field2~user_input,field3~user_input -``` - -## Options - -- `options`: options - - - `implicitQuery`: add an implicit query that is added to the user input. Optional. - This can't be changed by the user. If this is defined, will be displayed as a prepend of the search bar. - - `query`: query string in UQL (Unified Query Language) -Use UQL (Unified Query Language). - - `conjunction`: query string of the conjunction in UQL (Unified Query Language) - - `searchTermFields`: define the fields used to build the query for the search term mode - - `filterButtons`: define a list of buttons to filter in the search bar - - -```ts -// language options -options: { - // ID is not equal to 000 and . This is defined in UQL that is transformed internally to - // the specific query language. - implicitQuery: { - query: 'id!=000', - conjunction: ';' - } - searchTermFields: ['id', 'ip'] - filterButtons: [ - {id: 'status-active', input: 'status=active', label: 'Active'} - ] -} -``` - -- `suggestions`: define the suggestion handlers. This is required. - - - `field`: method that returns the suggestions for the fields - - ```ts - // language options - field(currentValue) { - // static or async fetching is allowed - return [ - { label: 'field1', description: 'Description' }, - { label: 'field2', description: 'Description' } - ]; - } - ``` - - - `value`: method that returns the suggestion for the values - ```ts - // language options - value: async (currentValue, { field }) => { - // static or async fetching is allowed - // async fetching data - // const response = await fetchData(); - return [ - { label: 'value1' }, - { label: 'value2' } - ] - } - ``` - -- `validate`: define validation methods for the field types. Optional - - `value`: method to validate the value token - - ```ts - validate: { - value: (token, {field, operator_compare}) => { - if(field === 'field1'){ - const value = token.formattedValue || token.value - return /\d+/ ? undefined : `Invalid value for field ${field}, only digits are supported: "${value}"` - } - } - } - ``` - -## Language workflow - -```mermaid -graph TD; - user_input[User input]-->ql_run; - ql_run-->filterButtons[filterButtons]; - ql_run-->tokenizer-->tokens; - tokens-->searchBarProps; - tokens-->output; - - subgraph tokenizer - tokenize_regex[Query language regular expression: decomposition and extract quoted values] - end - - subgraph searchBarProps; - searchBarProps_suggestions[suggestions]-->searchBarProps_suggestions_input_isvalid{Input is valid} - searchBarProps_suggestions_input_isvalid{Is input valid?}-->searchBarProps_suggestions_input_isvalid_success[Yes] - searchBarProps_suggestions_input_isvalid{Is input valid?}-->searchBarProps_suggestions_input_isvalid_fail[No] - searchBarProps_suggestions_input_isvalid_success[Yes]--->searchBarProps_suggestions_get_last_token_with_value[Get last token with value]-->searchBarProps_suggestions__result[Suggestions] - searchBarProps_suggestions__result[Suggestions]-->EuiSuggestItem - searchBarProps_suggestions_input_isvalid_fail[No]-->searchBarProps_suggestions_invalid[Invalid with error message] - searchBarProps_suggestions_invalid[Invalid with error message]-->EuiSuggestItem - searchBarProps_prepend[prepend]-->searchBarProps_prepend_implicitQuery{options.implicitQuery} - searchBarProps_prepend_implicitQuery{options.implicitQuery}-->searchBarProps_prepend_implicitQuery_yes[Yes]-->EuiButton - searchBarProps_prepend_implicitQuery{options.implicitQuery}-->searchBarProps_prepend_implicitQuery_no[No]-->null - searchBarProps_disableFocusTrap:true[disableFocusTrap = true] - searchBarProps_onItemClick[onItemClickSuggestion]-->searchBarProps_onItemClick_suggestion_search[Search suggestion] - searchBarProps_onItemClick_suggestion_search[Search suggestion]-->searchBarProps_onItemClick_suggestion_search_run[Run search] - searchBarProps_onItemClick[onItemClickSuggestion]-->searchBarProps_onItemClick_suggestion_edit_current_token[Edit current token]-->searchBarProps_onItemClick_build_input[Build input] - searchBarProps_onItemClick[onItemClickSuggestion]-->searchBarProps_onItemClick_suggestion_add_new_token[Add new token]-->searchBarProps_onItemClick_build_input[Build input] - searchBarProps_onItemClick[onItemClickSuggestion]-->searchBarProps_onItemClick_suggestion_error[Error] - searchBarProps_isInvalid[isInvalid]-->searchBarProps_validate_input[validate input] - end - - subgraph output[output]; - output_input_options_implicitFilter[options.implicitFilter]-->output_input_options_result["{apiQuery: { q }, error, language: 'wql', query}"] - output_input_user_input_QL[User input in QL]-->output_input_user_input_UQL[User input in UQL]-->output_input_options_result["{apiQuery: { q }, error, language: 'wql', query}"] - end - - subgraph filterButtons; - filterButtons_optional{options.filterButtons}-->filterButtons_optional_yes[Yes]-->filterButtons_optional_yes_component[Render fitter button] - filterButtons_optional{options.filterButtons}-->filterButtons_optional_no[No]-->filterButtons_optional_no_null[null] - end -``` - -## Notes - -- The value that contains the following characters: `!`, `~` are not supported by the AQL and this -could cause problems when do the request to the API. -- The value with spaces are wrapped with `"`. If the value contains the `\"` sequence this is -replaced by `"`. This could cause a problem with values that are intended to have the mentioned -sequence. \ No newline at end of file diff --git a/plugins/main/public/components/search-bar/search-bar/query-language/wql.test.tsx b/plugins/main/public/components/search-bar/search-bar/query-language/wql.test.tsx deleted file mode 100644 index bfe284b03d..0000000000 --- a/plugins/main/public/components/search-bar/search-bar/query-language/wql.test.tsx +++ /dev/null @@ -1,442 +0,0 @@ -import { getSuggestions, tokenizer, transformSpecificQLToUnifiedQL, WQL } from './wql'; -import React from 'react'; -import { render, waitFor } from '@testing-library/react'; -import { SearchBar } from '../index'; - -describe('SearchBar component', () => { - const componentProps = { - defaultMode: WQL.id, - input: '', - modes: [ - { - id: WQL.id, - options: { - implicitQuery: { - query: 'id!=000', - conjunction: ';' - }, - }, - suggestions: { - field(currentValue) { - return []; - }, - value(currentValue, { field }){ - return []; - }, - }, - } - ], - /* eslint-disable @typescript-eslint/no-empty-function */ - onChange: () => {}, - onSearch: () => {} - /* eslint-enable @typescript-eslint/no-empty-function */ - }; - - it('Renders correctly to match the snapshot of query language', async () => { - const wrapper = render( - - ); - - await waitFor(() => { - const elementImplicitQuery = wrapper.container.querySelector('.euiCodeBlock__code'); - expect(elementImplicitQuery?.innerHTML).toEqual('id!=000 and '); - expect(wrapper.container).toMatchSnapshot(); - }); - }); -}); - -/* eslint-disable max-len */ -describe('Query language - WQL', () => { - // Tokenize the input - function tokenCreator({type, value, formattedValue}){ - return {type, value, ...(formattedValue ? { formattedValue } : {})}; - }; - - const t = { - opGroup: (value = undefined) => tokenCreator({type: 'operator_group', value}), - opCompare: (value = undefined) => tokenCreator({type: 'operator_compare', value}), - field: (value = undefined) => tokenCreator({type: 'field', value}), - value: (value = undefined, formattedValue = undefined) => tokenCreator({type: 'value', value, formattedValue}), - whitespace: (value = undefined) => tokenCreator({type: 'whitespace', value}), - conjunction: (value = undefined) => tokenCreator({type: 'conjunction', value}) - }; - - // Token undefined - const tu = { - opGroup: tokenCreator({type: 'operator_group', value: undefined}), - opCompare: tokenCreator({type: 'operator_compare', value: undefined}), - whitespace: tokenCreator({type: 'whitespace', value: undefined}), - field: tokenCreator({type: 'field', value: undefined}), - value: tokenCreator({type: 'value', value: undefined}), - conjunction: tokenCreator({type: 'conjunction', value: undefined}) - }; - - const tuBlankSerie = [ - tu.opGroup, - tu.whitespace, - tu.field, - tu.whitespace, - tu.opCompare, - tu.whitespace, - tu.value, - tu.whitespace, - tu.opGroup, - tu.whitespace, - tu.conjunction, - tu.whitespace - ]; - - - it.each` - input | tokens - ${''} | ${tuBlankSerie} - ${'f'} | ${[tu.opGroup, tu.whitespace, t.field('f'), tu.whitespace, tu.opCompare, tu.whitespace, tu.value, tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} - ${'field'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, tu.opCompare, tu.whitespace, tu.value, tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} - ${'field='} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, tu.value, tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} - ${'field=value'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} - ${'field=and'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('and'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} - ${'field=or'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('or'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} - ${'field=valueand'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('valueand'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} - ${'field=valueor'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('valueor'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} - ${'field=value='} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value='), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} - ${'field=value!='} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value!='), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} - ${'field=value>'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value>'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} - ${'field=value<'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value<'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} - ${'field=value~'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value~'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie] /** ~ character is not supported as value in the q query parameter */} - ${'field="'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('"'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} - ${'field="value and'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('"value and'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} - ${'field="value and value2"'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('"value and value2"', 'value and value2'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} - ${'field="value or value2"'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('"value or value2"', 'value or value2'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} - ${'field="value = value2"'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('"value = value2"', 'value = value2'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} - ${'field="value != value2"'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('"value != value2"', 'value != value2'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} - ${'field="value > value2"'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('"value > value2"', 'value > value2'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} - ${'field="value < value2"'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('"value < value2"', 'value < value2'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} - ${'field="value ~ value2"'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('"value ~ value2"', 'value ~ value2'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie] /** ~ character is not supported as value in the q query parameter */} - ${'field=value and'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.conjunction('and'), tu.whitespace, ...tuBlankSerie]} - ${'field=value and '} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.conjunction('and'), t.whitespace(' '), ...tuBlankSerie]} - ${'field=value and field2'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.conjunction('and'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.field('field2'), tu.whitespace, tu.opCompare, tu.whitespace, tu.value, tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} - ${'field=value and field2!='} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.conjunction('and'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.field('field2'), tu.whitespace, t.opCompare('!='), tu.whitespace, tu.value, tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} - ${'field=value and field2!="'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.conjunction('and'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.field('field2'), tu.whitespace, t.opCompare('!='), tu.whitespace, t.value('"'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} - ${'field=value and field2!="value'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.conjunction('and'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.field('field2'), tu.whitespace, t.opCompare('!='), tu.whitespace, t.value('"value'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} - ${'field=value and field2!="value"'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.conjunction('and'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.field('field2'), tu.whitespace, t.opCompare('!='), tu.whitespace, t.value('"value"', 'value'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} - ${'field=value and field2!=value2 and'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.conjunction('and'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.field('field2'), tu.whitespace, t.opCompare('!='), tu.whitespace, t.value('value2'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.conjunction('and'), tu.whitespace, ...tuBlankSerie]} - `(`Tokenizer API input $input`, ({input, tokens}) => { - expect(tokenizer(input)).toEqual(tokens); - }); - - // Get suggestions - it.each` - input | suggestions - ${''} | ${[{ description: 'run the search query', label: 'Search', type: 'function_search' }, { description: 'Field', label: 'field', type: 'field' }, { description: 'Field2', label: 'field2', type: 'field' }, { description: 'open group', label: '(', type: 'operator_group' }]} - ${'w'} | ${[]} - ${'f'} | ${[{ description: 'Field', label: 'field', type: 'field' }, { description: 'Field2', label: 'field2', type: 'field' }]} - ${'field'} | ${[{ description: 'Field2', label: 'field2', type: 'field' }, { description: 'equality', label: '=', type: 'operator_compare' }, { description: 'not equality', label: '!=', type: 'operator_compare' }, { description: 'bigger', label: '>', type: 'operator_compare' }, { description: 'smaller', label: '<', type: 'operator_compare' }, { description: 'like as', label: '~', type: 'operator_compare' }]} - ${'field='} | ${[{ label: 'value', type: 'value' }, { label: 'value2', type: 'value' }, { label: 'value3', type: 'value' }, { label: 'value4', type: 'value' }]} - ${'field=v'} | ${[{ description: 'run the search query', label: 'Search', type: 'function_search' }, { label: 'value', type: 'value' }, { label: 'value2', type: 'value' }, { label: 'value3', type: 'value' }, { label: 'value4', type: 'value' }, { description: 'and', label: 'and', type: 'conjunction' }, { description: 'or', label: 'or', type: 'conjunction' }, { description: 'close group', label: ')', type: 'operator_group' }]} - ${'field=value'} | ${[{ description: 'run the search query', label: 'Search', type: 'function_search' }, { label: 'value', type: 'value' }, { label: 'value2', type: 'value' }, { label: 'value3', type: 'value' }, { label: 'value4', type: 'value' }, { description: 'and', label: 'and', type: 'conjunction' }, { description: 'or', label: 'or', type: 'conjunction' }, { description: 'close group', label: ')', type: 'operator_group' }]} - ${'field=value and'} | ${[{ description: 'Field', label: 'field', type: 'field' }, { description: 'Field2', label: 'field2', type: 'field' }, { description: 'open group', label: '(', type: 'operator_group' }]} - `('Get suggestion from the input: $input', async ({ input, suggestions }) => { - expect( - await getSuggestions(tokenizer(input), { - id: 'aql', - suggestions: { - field(currentValue) { - return [ - { label: 'field', description: 'Field' }, - { label: 'field2', description: 'Field2' }, - ].map(({ label, description }) => ({ - type: 'field', - label, - description, - })); - }, - value(currentValue = '', { field }) { - switch (field) { - case 'field': - return ['value', 'value2', 'value3', 'value4'] - .filter(value => value.startsWith(currentValue)) - .map(value => ({ type: 'value', label: value })); - break; - case 'field2': - return ['127.0.0.1', '127.0.0.2', '190.0.0.1', '190.0.0.2'] - .filter(value => value.startsWith(currentValue)) - .map(value => ({ type: 'value', label: value })); - break; - default: - return []; - break; - } - }, - }, - }), - ).toEqual(suggestions); - }); - - // Transform specific query language to UQL (Unified Query Language) - it.each` - WQL | UQL - ${'field'} | ${'field'} - ${'field='} | ${'field='} - ${'field=value'} | ${'field=value'} - ${'field=value()'} | ${'field=value()'} - ${'field=valueand'} | ${'field=valueand'} - ${'field=valueor'} | ${'field=valueor'} - ${'field=value='} | ${'field=value='} - ${'field=value!='} | ${'field=value!='} - ${'field=value>'} | ${'field=value>'} - ${'field=value<'} | ${'field=value<'} - ${'field=value~'} | ${'field=value~' /** ~ character is not supported as value in the q query parameter */} - ${'field="custom value"'} | ${'field=custom value'} - ${'field="custom value()"'} | ${'field=custom value()'} - ${'field="value and value2"'} | ${'field=value and value2'} - ${'field="value or value2"'} | ${'field=value or value2'} - ${'field="value = value2"'} | ${'field=value = value2'} - ${'field="value != value2"'} | ${'field=value != value2'} - ${'field="value > value2"'} | ${'field=value > value2'} - ${'field="value < value2"'} | ${'field=value < value2'} - ${'field="value ~ value2"'} | ${'field=value ~ value2' /** ~ character is not supported as value in the q query parameter */} - ${'field="custom \\"value"'} | ${'field=custom "value'} - ${'field="custom \\"value\\""'} | ${'field=custom "value"'} - ${'field=value and'} | ${'field=value;'} - ${'field="custom value" and'} | ${'field=custom value;'} - ${'(field=value'} | ${'(field=value'} - ${'(field=value)'} | ${'(field=value)'} - ${'(field=value) and'} | ${'(field=value);'} - ${'(field=value) and field2'} | ${'(field=value);field2'} - ${'(field=value) and field2>'} | ${'(field=value);field2>'} - ${'(field=value) and field2>"wrappedcommas"'} | ${'(field=value);field2>wrappedcommas'} - ${'(field=value) and field2>"value with spaces"'} | ${'(field=value);field2>value with spaces'} - ${'field ='} | ${'field='} - ${'field = value'} | ${'field=value'} - ${'field = value()'} | ${'field=value()'} - ${'field = valueand'} | ${'field=valueand'} - ${'field = valueor'} | ${'field=valueor'} - ${'field = value='} | ${'field=value='} - ${'field = value!='} | ${'field=value!='} - ${'field = value>'} | ${'field=value>'} - ${'field = value<'} | ${'field=value<'} - ${'field = value~'} | ${'field=value~' /** ~ character is not supported as value in the q query parameter */} - ${'field = "custom value"'} | ${'field=custom value'} - ${'field = "custom value()"'} | ${'field=custom value()'} - ${'field = "value and value2"'} | ${'field=value and value2'} - ${'field = "value or value2"'} | ${'field=value or value2'} - ${'field = "value = value2"'} | ${'field=value = value2'} - ${'field = "value != value2"'} | ${'field=value != value2'} - ${'field = "value > value2"'} | ${'field=value > value2'} - ${'field = "value < value2"'} | ${'field=value < value2'} - ${'field = "value ~ value2"'} | ${'field=value ~ value2' /** ~ character is not supported as value in the q query parameter */} - ${'field = value or'} | ${'field=value,'} - ${'field = value or field2'} | ${'field=value,field2'} - ${'field = value or field2 <'} | ${'field=value,field2<'} - ${'field = value or field2 < value2'} | ${'field=value,field2 "custom value" '} | ${'(field=value);field2>custom value'} - `('transformSpecificQLToUnifiedQL - WQL $WQL TO UQL $UQL', ({WQL, UQL}) => { - expect(transformSpecificQLToUnifiedQL(WQL)).toEqual(UQL); - }); - - // When a suggestion is clicked, change the input text - it.each` - WQL | clikedSuggestion | changedInput - ${''} | ${{type: { iconType: 'kqlField', color: 'tint4' }, label: 'field'}} | ${'field'} - ${'field'} | ${{type: { iconType: 'kqlField', color: 'tint4' }, label: 'field2'}} | ${'field2'} - ${'field'} | ${{type: { iconType: 'kqlOperand', color: 'tint1' }, label: '='}} | ${'field='} - ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value'}} | ${'field=value'} - ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value()'}} | ${'field=value()'} - ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'valueand'}} | ${'field=valueand'} - ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'valueor'}} | ${'field=valueor'} - ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value='}} | ${'field=value='} - ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value!='}} | ${'field=value!='} - ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value>'}} | ${'field=value>'} - ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value<'}} | ${'field=value<'} - ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value~'}} | ${'field=value~' /** ~ character is not supported as value in the q query parameter */} - ${'field='} | ${{type: { iconType: 'kqlOperand', color: 'tint1' }, label: '!='}} | ${'field!='} - ${'field=value'} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value2'}} | ${'field=value2'} - ${'field=value'} | ${{type: { iconType: 'kqlSelector', color: 'tint3' }, label: 'and'}} | ${'field=value and '} - ${'field=value and'} | ${{type: { iconType: 'kqlSelector', color: 'tint3' }, label: 'or'}} | ${'field=value or'} - ${'field=value and'} | ${{type: { iconType: 'kqlField', color: 'tint4' }, label: 'field2'}} | ${'field=value and field2'} - ${'field=value and '} | ${{type: { iconType: 'kqlSelector', color: 'tint3' }, label: 'or'}} | ${'field=value or '} - ${'field=value and '} | ${{type: { iconType: 'kqlField', color: 'tint4' }, label: 'field2'}} | ${'field=value and field2'} - ${'field=value and field2'} | ${{type: { iconType: 'kqlOperand', color: 'tint1' }, label: '>'}} | ${'field=value and field2>'} - ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with spaces'}} | ${'field="with spaces"'} - ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with "spaces'}} | ${'field="with \\"spaces"'} - ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with value()'}} | ${'field="with value()"'} - ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with and value'}}| ${'field="with and value"'} - ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with or value'}} | ${'field="with or value"'} - ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with = value'}} | ${'field="with = value"'} - ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with != value'}} | ${'field="with != value"'} - ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with > value'}} | ${'field="with > value"'} - ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with < value'}} | ${'field="with < value"'} - ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with ~ value'}} | ${'field="with ~ value"' /** ~ character is not supported as value in the q query parameter */} - ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: '"value'}} | ${'field="\\"value"'} - ${'field="with spaces"'} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value'}} | ${'field=value'} - ${'field="with spaces"'} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'other spaces'}} | ${'field="other spaces"'} - ${''} | ${{type: { iconType: 'tokenDenseVector', color: 'tint3' }, label: '('}} | ${'('} - ${'('} | ${{type: { iconType: 'kqlField', color: 'tint4' }, label: 'field'}} | ${'(field'} - ${'(field'} | ${{type: { iconType: 'kqlField', color: 'tint4' }, label: 'field2'}} | ${'(field2'} - ${'(field'} | ${{type: { iconType: 'kqlOperand', color: 'tint1' }, label: '='}} | ${'(field='} - ${'(field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value'}} | ${'(field=value'} - ${'(field=value'} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value2'}} | ${'(field=value2'} - ${'(field=value'} | ${{type: { iconType: 'kqlSelector', color: 'tint3' }, label: 'or'}} | ${'(field=value or '} - ${'(field=value or'} | ${{type: { iconType: 'kqlSelector', color: 'tint3' }, label: 'and'}} | ${'(field=value and'} - ${'(field=value or'} | ${{type: { iconType: 'kqlField', color: 'tint4' }, label: 'field2'}} | ${'(field=value or field2'} - ${'(field=value or '} | ${{type: { iconType: 'kqlSelector', color: 'tint3' }, label: 'and'}} | ${'(field=value and '} - ${'(field=value or '} | ${{type: { iconType: 'kqlField', color: 'tint4' }, label: 'field2'}} | ${'(field=value or field2'} - ${'(field=value or field2'} | ${{type: { iconType: 'kqlOperand', color: 'tint1' }, label: '>'}} | ${'(field=value or field2>'} - ${'(field=value or field2>'} | ${{type: { iconType: 'kqlOperand', color: 'tint1' }, label: '>'}} | ${'(field=value or field2>'} - ${'(field=value or field2>'} | ${{type: { iconType: 'kqlOperand', color: 'tint1' }, label: '~'}} | ${'(field=value or field2~'} - ${'(field=value or field2>'} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value2'}} | ${'(field=value or field2>value2'} - ${'(field=value or field2>value2'}| ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value3'}} | ${'(field=value or field2>value3'} - ${'(field=value or field2>value2'}| ${{type: { iconType: 'tokenDenseVector', color: 'tint3' }, label: ')'}} | ${'(field=value or field2>value2)'} - `('click suggestion - WQL $WQL => $changedInput', async ({WQL: currentInput, clikedSuggestion, changedInput}) => { - // Mock input - let input = currentInput; - - const qlOutput = await WQL.run(input, { - setInput: (value: string): void => { input = value; }, - queryLanguage: { - parameters: { - options: {}, - suggestions: { - field: () => ([]), - value: () => ([]) - } - } - } - }); - qlOutput.searchBarProps.onItemClick(clikedSuggestion); - expect(input).toEqual(changedInput); - }); - - // Transform the external input in UQL (Unified Query Language) to QL - it.each` - UQL | WQL - ${''} | ${''} - ${'field'} | ${'field'} - ${'field='} | ${'field='} - ${'field=()'} | ${'field=()'} - ${'field=valueand'} | ${'field=valueand'} - ${'field=valueor'} | ${'field=valueor'} - ${'field=value='} | ${'field=value='} - ${'field=value!='} | ${'field=value!='} - ${'field=value>'} | ${'field=value>'} - ${'field=value<'} | ${'field=value<'} - ${'field=value~'} | ${'field=value~'} - ${'field!='} | ${'field!='} - ${'field>'} | ${'field>'} - ${'field<'} | ${'field<'} - ${'field~'} | ${'field~'} - ${'field=value'} | ${'field=value'} - ${'field=value;'} | ${'field=value and '} - ${'field=value;field2'} | ${'field=value and field2'} - ${'field="'} | ${'field="\\""'} - ${'field=with spaces'} | ${'field="with spaces"'} - ${'field=with "spaces'} | ${'field="with \\"spaces"'} - ${'field=value ()'} | ${'field="value ()"'} - ${'field=with and value'} | ${'field="with and value"'} - ${'field=with or value'} | ${'field="with or value"'} - ${'field=with = value'} | ${'field="with = value"'} - ${'field=with > value'} | ${'field="with > value"'} - ${'field=with < value'} | ${'field="with < value"'} - ${'('} | ${'('} - ${'(field'} | ${'(field'} - ${'(field='} | ${'(field='} - ${'(field=value'} | ${'(field=value'} - ${'(field=value,'} | ${'(field=value or '} - ${'(field=value,field2'} | ${'(field=value or field2'} - ${'(field=value,field2>'} | ${'(field=value or field2>'} - ${'(field=value,field2>value2'} | ${'(field=value or field2>value2'} - ${'(field=value,field2>value2)'} | ${'(field=value or field2>value2)'} - ${'implicit=value;'} | ${''} - ${'implicit=value;field'} | ${'field'} - `('Transform the external input UQL to QL - UQL $UQL => $WQL', async ({UQL, WQL: changedInput}) => { - expect(WQL.transformInput(UQL, { - parameters: { - options: { - implicitQuery: { - query: 'implicit=value', - conjunction: ';' - } - } - } - })).toEqual(changedInput); - }); - - /* The ! and ~ characters can't be part of a value that contains examples. The tests doesn't - include these cases. - - Value examples: - - with != value - - with ~ value - */ - - // Validate the tokens - it.each` - WQL | validationError - ${''} | ${undefined} - ${'field1'} | ${undefined} - ${'field2'} | ${undefined} - ${'field1='} | ${['The value for field "field1" is missing.']} - ${'field2='} | ${['The value for field "field2" is missing.']} - ${'field='} | ${['"field" is not a valid field.']} - ${'custom='} | ${['"custom" is not a valid field.']} - ${'field1=value'} | ${undefined} - ${'field1=1'} | ${['Numbers are not valid for field1']} - ${'field1=value1'} | ${['Numbers are not valid for field1']} - ${'field2=value'} | ${undefined} - ${'field=value'} | ${['"field" is not a valid field.']} - ${'custom=value'} | ${['"custom" is not a valid field.']} - ${'field1=value!test'} | ${['"value!test" is not a valid value. Invalid characters found: !']} - ${'field1=value&test'} | ${['"value&test" is not a valid value. Invalid characters found: &']} - ${'field1=value!value&test'} | ${['"value!value&test" is not a valid value. Invalid characters found: !, &']} - ${'field1=value!value!test'} | ${['"value!value!test" is not a valid value. Invalid characters found: !']} - ${'field1=value!value!t$&st'} | ${['"value!value!t$&st" is not a valid value. Invalid characters found: !, $, &']} - ${'field1=value and'} | ${['There is no whitespace after conjunction "and".', 'There is no sentence after conjunction "and".']} - ${'field2=value and'} | ${['There is no whitespace after conjunction "and".', 'There is no sentence after conjunction "and".']} - ${'field=value and'} | ${['"field" is not a valid field.', 'There is no whitespace after conjunction "and".', 'There is no sentence after conjunction "and".']} - ${'custom=value and'} | ${['"custom" is not a valid field.', 'There is no whitespace after conjunction "and".', 'There is no sentence after conjunction "and".']} - ${'field1=value and '} | ${['There is no sentence after conjunction "and".']} - ${'field2=value and '} | ${['There is no sentence after conjunction "and".']} - ${'field=value and '} | ${['"field" is not a valid field.', 'There is no sentence after conjunction "and".']} - ${'custom=value and '} | ${['"custom" is not a valid field.', 'There is no sentence after conjunction "and".']} - ${'field1=value and field2'} | ${['The operator for field \"field2\" is missing.']} - ${'field2=value and field1'} | ${['The operator for field \"field1\" is missing.']} - ${'field1=value and field'} | ${['"field" is not a valid field.']} - ${'field2=value and field'} | ${['"field" is not a valid field.']} - ${'field=value and custom'} | ${['"field" is not a valid field.', '"custom" is not a valid field.']} - ${'('} | ${undefined} - ${'(field'} | ${undefined} - ${'(field='} | ${['"field" is not a valid field.']} - ${'(field=value'} | ${['"field" is not a valid field.']} - ${'(field=value or'} | ${['"field" is not a valid field.', 'There is no whitespace after conjunction "or".', 'There is no sentence after conjunction "or".']} - ${'(field=value or '} | ${['"field" is not a valid field.', 'There is no sentence after conjunction "or".']} - ${'(field=value or field2'} | ${['"field" is not a valid field.', 'The operator for field \"field2\" is missing.']} - ${'(field=value or field2>'} | ${['"field" is not a valid field.', 'The value for field "field2" is missing.']} - ${'(field=value or field2>value2'}| ${['"field" is not a valid field.']} - `('validate the tokens - WQL $WQL => $validationError', async ({WQL: currentInput, validationError}) => { - - const qlOutput = await WQL.run(currentInput, { - queryLanguage: { - parameters: { - options: {}, - suggestions: { - field: () => (['field1', 'field2'].map(label => ({label}))), - value: () => ([]) - }, - validate: { - value: (token, {field, operator_compare}) => { - if(field === 'field1'){ - const value = token.formattedValue || token.value; - return /\d/.test(value) - ? `Numbers are not valid for ${field}` - : undefined - } - } - } - } - } - }); - expect(qlOutput.output.error).toEqual(validationError); - }); -}); diff --git a/plugins/main/public/components/search-bar/search-bar/query-language/wql.tsx b/plugins/main/public/components/search-bar/search-bar/query-language/wql.tsx deleted file mode 100644 index 4776c141e7..0000000000 --- a/plugins/main/public/components/search-bar/search-bar/query-language/wql.tsx +++ /dev/null @@ -1,1030 +0,0 @@ -import React from 'react'; -import { EuiButtonEmpty, EuiButtonGroup, EuiPopover, EuiText, EuiCode } from '@elastic/eui'; -import { tokenizer as tokenizerUQL } from './aql'; -import { PLUGIN_VERSION_SHORT } from '../../../../common/constants'; - -/* UI Query language -https://documentation.wazuh.com/current/user-manual/api/queries.html - -// Example of another query language definition -*/ - -type ITokenType = - | 'field' - | 'operator_compare' - | 'operator_group' - | 'value' - | 'conjunction' - | 'whitespace'; -type IToken = { type: ITokenType; value: string }; -type ITokens = IToken[]; - -/* API Query Language -Define the API Query Language to use in the search bar. -It is based in the language used by the q query parameter. -https://documentation.wazuh.com/current/user-manual/api/queries.html - -Use the regular expression of API with some modifications to allow the decomposition of -input in entities that doesn't compose a valid query. It allows get not-completed queries. - -API schema: -??? - -Implemented schema: -???????????? -*/ - -// Language definition -const language = { - // Tokens - tokens: { - // eslint-disable-next-line camelcase - operator_compare: { - literal: { - '=': 'equality', - '!=': 'not equality', - '>': 'bigger', - '<': 'smaller', - '~': 'like as', - }, - }, - conjunction: { - literal: { - 'and': 'and', - 'or': 'or', - }, - }, - // eslint-disable-next-line camelcase - operator_group: { - literal: { - '(': 'open group', - ')': 'close group', - }, - }, - }, - equivalencesToUQL:{ - conjunction:{ - literal:{ - 'and': ';', - 'or': ',', - } - } - } -}; - -// Suggestion mapper by language token type -const suggestionMappingLanguageTokenType = { - field: { iconType: 'kqlField', color: 'tint4' }, - // eslint-disable-next-line camelcase - operator_compare: { iconType: 'kqlOperand', color: 'tint1' }, - value: { iconType: 'kqlValue', color: 'tint0' }, - conjunction: { iconType: 'kqlSelector', color: 'tint3' }, - // eslint-disable-next-line camelcase - operator_group: { iconType: 'tokenDenseVector', color: 'tint3' }, - // eslint-disable-next-line camelcase - function_search: { iconType: 'search', color: 'tint5' }, - // eslint-disable-next-line camelcase - validation_error: { iconType: 'alert', color: 'tint2' } -}; - -/** - * Creator of intermediate interface of EuiSuggestItem - * @param type - * @returns - */ -function mapSuggestionCreator(type: ITokenType ){ - return function({...params}){ - return { - type, - ...params - }; - }; -}; - -const mapSuggestionCreatorField = mapSuggestionCreator('field'); -const mapSuggestionCreatorValue = mapSuggestionCreator('value'); - -/** - * Transform the conjunction to the query language syntax - * @param conjunction - * @returns - */ -function transformQLConjunction(conjunction: string): string{ - // If the value has a whitespace or comma, then - return conjunction === language.equivalencesToUQL.conjunction.literal['and'] - ? ` ${language.tokens.conjunction.literal['and']} ` - : ` ${language.tokens.conjunction.literal['or']} `; -}; - -/** - * Transform the value to the query language syntax - * @param value - * @returns - */ -function transformQLValue(value: string): string{ - // If the value has a whitespace or comma, then - return /[\s|"]/.test(value) - // Escape the commas (") => (\") and wraps the string with commas ("") - ? `"${value.replace(/"/, '\\"')}"` - // Raw value - : value; -}; - -/** - * Tokenize the input string. Returns an array with the tokens. - * @param input - * @returns - */ -export function tokenizer(input: string): ITokens{ - const re = new RegExp( - // A ( character. - '(?\\()?' + - // Whitespace - '(?\\s+)?' + - // Field name: name of the field to look on DB. - '(?[\\w.]+)?' + // Added an optional find - // Whitespace - '(?\\s+)?' + - // Operator: looks for '=', '!=', '<', '>' or '~'. - // This seems to be a bug because is not searching the literal valid operators. - // I guess the operator is validated after the regular expression matches - `(?[${Object.keys(language.tokens.operator_compare.literal)}]{1,2})?` + // Added an optional find - // Whitespace - '(?\\s+)?' + - // Value: A string. - // Simple value - // Quoted ", "value, "value", "escaped \"quote" - // Escape quoted string with escaping quotes: https://stackoverflow.com/questions/249791/regex-for-quoted-string-with-escaping-quotes - '(?(?:(?:[^"\\s]+|(?:"(?:[^"\\\\]|\\\\")*")|(?:"(?:[^"\\\\]|\\\\")*)|")))?' + - // Whitespace - '(?\\s+)?' + - // A ) character. - '(?\\))?' + - // Whitespace - '(?\\s+)?' + - `(?${Object.keys(language.tokens.conjunction.literal).join('|')})?` + - // Whitespace - '(?\\s+)?', - 'g' - ); - - return [ - ...input.matchAll(re) - ].map( - ({groups}) => Object.entries(groups) - .map(([key, value]) => ({ - type: key.startsWith('operator_group') // Transform operator_group group match - ? 'operator_group' - : (key.startsWith('whitespace') // Transform whitespace group match - ? 'whitespace' - : key), - value, - ...( key === 'value' && value && /^"(.+)"$/.test(value) - ? { formattedValue: value.match(/^"(.+)"$/)[1]} - : {} - ) - }) - ) - ).flat(); -}; - -type QLOptionSuggestionEntityItem = { - description?: string - label: string -}; - -type QLOptionSuggestionEntityItemTyped = - QLOptionSuggestionEntityItem - & { type: 'operator_group'|'field'|'operator_compare'|'value'|'conjunction'|'function_search' }; - -type SuggestItem = QLOptionSuggestionEntityItem & { - type: { iconType: string, color: string } -}; - -type QLOptionSuggestionHandler = ( - currentValue: string | undefined, - { - field, - operatorCompare, - }: { field: string; operatorCompare: string }, -) => Promise; - -type OptionsQLImplicitQuery = { - query: string - conjunction: string -} -type OptionsQL = { - options?: { - implicitQuery?: OptionsQLImplicitQuery - searchTermFields?: string[] - filterButtons: {id: string, label: string, input: string}[] - } - suggestions: { - field: QLOptionSuggestionHandler; - value: QLOptionSuggestionHandler; - }; - validate?: { - value?: { - [key: string]: (token: IToken, nearTokens: {field: string, operator: string}) => string | undefined - } - } -}; - -export interface ISearchBarModeWQL extends OptionsQL{ - id: 'wql' -}; - -/** - * Get the last token with value - * @param tokens Tokens - * @param tokenType token type to search - * @returns - */ -function getLastTokenDefined( - tokens: ITokens -): IToken | undefined { - // Reverse the tokens array and use the Array.protorype.find method - const shallowCopyArray = Array.from([...tokens]); - const shallowCopyArrayReversed = shallowCopyArray.reverse(); - const tokenFound = shallowCopyArrayReversed.find( - ({ type, value }) => type !== 'whitespace' && value, - ); - return tokenFound; -} - -/** - * Get the last token with value by type - * @param tokens Tokens - * @param tokenType token type to search - * @returns - */ -function getLastTokenDefinedByType( - tokens: ITokens, - tokenType: ITokenType, -): IToken | undefined { - // Find the last token by type - // Reverse the tokens array and use the Array.protorype.find method - const shallowCopyArray = Array.from([...tokens]); - const shallowCopyArrayReversed = shallowCopyArray.reverse(); - const tokenFound = shallowCopyArrayReversed.find( - ({ type, value }) => type === tokenType && value, - ); - return tokenFound; -}; - -/** - * Get the token that is near to a token position of the token type. - * @param tokens - * @param tokenReferencePosition - * @param tokenType - * @param mode - * @returns - */ -function getTokenNearTo( - tokens: ITokens, - tokenType: ITokenType, - mode : 'previous' | 'next' = 'previous', - options : {tokenReferencePosition?: number, tokenFoundShouldHaveValue?: boolean} = {} -): IToken | undefined { - const shallowCopyTokens = Array.from([...tokens]); - const computedShallowCopyTokens = mode === 'previous' - ? shallowCopyTokens.slice(0, options?.tokenReferencePosition || tokens.length).reverse() - : shallowCopyTokens.slice(options?.tokenReferencePosition || 0); - return computedShallowCopyTokens - .find(({type, value}) => - type === tokenType - && (options?.tokenFoundShouldHaveValue ? value : true) - ); -}; - -/** - * Get the suggestions from the tokens - * @param tokens - * @param language - * @param options - * @returns - */ -export async function getSuggestions(tokens: ITokens, options: OptionsQL): Promise { - if (!tokens.length) { - return []; - } - - // Get last token - const lastToken = getLastTokenDefined(tokens); - - // If it can't get a token with value, then returns fields and open operator group - if(!lastToken?.type){ - return [ - // Search function - { - type: 'function_search', - label: 'Search', - description: 'run the search query', - }, - // fields - ...(await options.suggestions.field()).map(mapSuggestionCreatorField), - { - type: 'operator_group', - label: '(', - description: language.tokens.operator_group.literal['('], - } - ]; - }; - - switch (lastToken.type) { - case 'field': - return [ - // fields that starts with the input but is not equals - ...(await options.suggestions.field()).filter( - ({ label }) => - label.startsWith(lastToken.value) && label !== lastToken.value, - ).map(mapSuggestionCreatorField), - // operators if the input field is exact - ...((await options.suggestions.field()).some( - ({ label }) => label === lastToken.value, - ) - ? [ - ...Object.keys(language.tokens.operator_compare.literal).map( - operator => ({ - type: 'operator_compare', - label: operator, - description: - language.tokens.operator_compare.literal[operator], - }), - ), - ] - : []), - ]; - break; - case 'operator_compare':{ - const field = getLastTokenDefinedByType(tokens, 'field')?.value; - const operatorCompare = getLastTokenDefinedByType( - tokens, - 'operator_compare', - )?.value; - - // If there is no a previous field, then no return suggestions because it would be an syntax - // error - if(!field){ - return []; - }; - - return [ - ...Object.keys(language.tokens.operator_compare.literal) - .filter( - operator => - operator.startsWith(lastToken.value) && - operator !== lastToken.value, - ) - .map(operator => ({ - type: 'operator_compare', - label: operator, - description: language.tokens.operator_compare.literal[operator], - })), - ...(Object.keys(language.tokens.operator_compare.literal).some( - operator => operator === lastToken.value, - ) - ? [ - ...(await options.suggestions.value(undefined, { - field, - operatorCompare, - })).map(mapSuggestionCreatorValue), - ] - : []), - ]; - break; - } - case 'value':{ - const field = getLastTokenDefinedByType(tokens, 'field')?.value; - const operatorCompare = getLastTokenDefinedByType( - tokens, - 'operator_compare', - )?.value; - - /* If there is no a previous field or operator_compare, then no return suggestions because - it would be an syntax error */ - if(!field || !operatorCompare){ - return []; - }; - - return [ - ...(lastToken.value - ? [ - { - type: 'function_search', - label: 'Search', - description: 'run the search query', - }, - ] - : []), - ...(await options.suggestions.value(lastToken.value, { - field, - operatorCompare, - })).map(mapSuggestionCreatorValue), - ...Object.entries(language.tokens.conjunction.literal).map( - ([ conjunction, description]) => ({ - type: 'conjunction', - label: conjunction, - description, - }), - ), - { - type: 'operator_group', - label: ')', - description: language.tokens.operator_group.literal[')'], - }, - ]; - break; - } - case 'conjunction': - return [ - ...Object.keys(language.tokens.conjunction.literal) - .filter( - conjunction => - conjunction.startsWith(lastToken.value) && - conjunction !== lastToken.value, - ) - .map(conjunction => ({ - type: 'conjunction', - label: conjunction, - description: language.tokens.conjunction.literal[conjunction], - })), - // fields if the input field is exact - ...(Object.keys(language.tokens.conjunction.literal).some( - conjunction => conjunction === lastToken.value, - ) - ? [ - ...(await options.suggestions.field()).map(mapSuggestionCreatorField), - ] - : []), - { - type: 'operator_group', - label: '(', - description: language.tokens.operator_group.literal['('], - }, - ]; - break; - case 'operator_group': - if (lastToken.value === '(') { - return [ - // fields - ...(await options.suggestions.field()).map(mapSuggestionCreatorField), - ]; - } else if (lastToken.value === ')') { - return [ - // conjunction - ...Object.keys(language.tokens.conjunction.literal).map( - conjunction => ({ - type: 'conjunction', - label: conjunction, - description: language.tokens.conjunction.literal[conjunction], - }), - ), - ]; - } - break; - default: - return []; - break; - } - - return []; -} - -/** - * Transform the suggestion object to the expected object by EuiSuggestItem - * @param param0 - * @returns - */ -export function transformSuggestionToEuiSuggestItem(suggestion: QLOptionSuggestionEntityItemTyped): SuggestItem{ - const { type, ...rest} = suggestion; - return { - type: { ...suggestionMappingLanguageTokenType[type] }, - ...rest - }; -}; - -/** - * Transform the suggestion object to the expected object by EuiSuggestItem - * @param suggestions - * @returns - */ -function transformSuggestionsToEuiSuggestItem( - suggestions: QLOptionSuggestionEntityItemTyped[] -): SuggestItem[] { - return suggestions.map(transformSuggestionToEuiSuggestItem); -}; - -/** - * Transform the UQL (Unified Query Language) to QL - * @param input - * @returns - */ -export function transformUQLToQL(input: string){ - const tokens = tokenizerUQL(input); - return tokens - .filter(({value}) => value) - .map(({type, value}) => { - switch (type) { - case 'conjunction': - return transformQLConjunction(value); - break; - case 'value': - return transformQLValue(value); - break; - default: - return value; - break; - } - } - ).join(''); -}; - -export function shouldUseSearchTerm(tokens: ITokens): boolean{ - return !( - tokens.some(({type, value}) => type === 'operator_compare' && value ) - && tokens.some(({type, value}) => type === 'field' && value ) - ); -}; - -export function transformToSearchTerm(searchTermFields: string[], input: string): string{ - return searchTermFields.map(searchTermField => `${searchTermField}~${input}`).join(','); -}; - -/** - * Transform the input in QL to UQL (Unified Query Language) - * @param input - * @returns - */ -export function transformSpecificQLToUnifiedQL(input: string, searchTermFields: string[]){ - const tokens = tokenizer(input); - - if(input && searchTermFields && shouldUseSearchTerm(tokens)){ - return transformToSearchTerm(searchTermFields, input); - }; - - return tokens - .filter(({type, value}) => type !== 'whitespace' && value) - .map(({type, value}) => { - switch (type) { - case 'value':{ - // Value is wrapped with " - let [ _, extractedValue ] = value.match(/^"(.+)"$/) || [ null, null ]; - // Replace the escaped comma (\") by comma (") - // WARN: This could cause a problem with value that contains this sequence \" - extractedValue && (extractedValue = extractedValue.replace(/\\"/g, '"')); - return extractedValue || value; - break; - } - case 'conjunction': - return value === 'and' - ? language.equivalencesToUQL.conjunction.literal['and'] - : language.equivalencesToUQL.conjunction.literal['or']; - break; - default: - return value; - break; - } - } - ).join(''); -}; - -/** - * Get the output from the input - * @param input - * @returns - */ -function getOutput(input: string, options: OptionsQL) { - // Implicit query - const implicitQueryAsUQL = options?.options?.implicitQuery?.query ?? ''; - const implicitQueryAsQL = transformUQLToQL( - implicitQueryAsUQL - ); - - // Implicit query conjunction - const implicitQueryConjunctionAsUQL = options?.options?.implicitQuery?.conjunction ?? ''; - const implicitQueryConjunctionAsQL = transformUQLToQL( - implicitQueryConjunctionAsUQL - ); - - // User input query - const inputQueryAsQL = input; - const inputQueryAsUQL = transformSpecificQLToUnifiedQL( - inputQueryAsQL, - options?.options?.searchTermFields ?? [] - ); - - return { - language: WQL.id, - apiQuery: { - q: [ - implicitQueryAsUQL, - implicitQueryAsUQL && inputQueryAsUQL ? implicitQueryConjunctionAsUQL : '', - implicitQueryAsUQL && inputQueryAsUQL ? `(${inputQueryAsUQL})`: inputQueryAsUQL - ].join(''), - }, - query: [ - implicitQueryAsQL, - implicitQueryAsQL && inputQueryAsQL ? implicitQueryConjunctionAsQL : '', - implicitQueryAsQL && inputQueryAsQL ? `(${inputQueryAsQL})`: inputQueryAsQL - ].join('') - }; -}; - -/** - * Validate the token value - * @param token - * @returns - */ -function validateTokenValue(token: IToken): string | undefined { - // Usage of API regular expression - const re = new RegExp( - // Value: A string. - '^(?(?:(?:\\((?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|[\\[\\]\\w _\\-.:?\\/\'"=@%<>{}]*)\\))*' + - '(?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|[\\[\\]\\w _\\-.:?\\\\/\'"=@%<>{}]+)' + - '(?:\\((?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|[\\[\\]\\w _\\-.:?\\\\/\'"=@%<>{}]*)\\))*)+)$' - ); - - /* WARN: the validation for the value token is complex, this supports some characters in - certain circumstances. - - Ideally a character validation helps to the user to identify the problem in the query, but - as the original regular expression is so complex, the logic to get this can be - complicated. - - The original regular expression has a common schema of allowed characters, these and other - characters of the original regular expression can be used to check each character. This - approach can identify some invalid characters despite this is not the ideal way. - - The ideal solution will be check each subset of the complex regex against the allowed - characters. - */ - - const invalidCharacters: string[] = token.value.split('') - .filter((character) => !(new RegExp('[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}\\(\\)]').test(character))) - .filter((value, index, array) => array.indexOf(value) === index); - - const match = token.value.match(re); - return match?.groups?.value === token.value - ? undefined - : [ - `"${token.value}" is not a valid value.`, - ...(invalidCharacters.length - ? [`Invalid characters found: ${invalidCharacters.join(', ')}`] - : [] - ) - ].join(' '); -}; - -type ITokenValidator = (tokenValue: IToken, proximityTokens: any) => string | undefined; -/** - * Validate the tokens while the user is building the query - * @param tokens - * @param validate - * @returns - */ -function validatePartial(tokens: ITokens, validate: {field: ITokenValidator, value: ITokenValidator}): undefined | string{ - // Ensure is not in search term mode - if (!shouldUseSearchTerm(tokens)){ - return tokens.map((token: IToken, index) => { - if(token.value){ - if(token.type === 'field'){ - // Ensure there is a operator next to field to check if the fields is valid or not. - // This allows the user can type the field token and get the suggestions for the field. - const tokenOperatorNearToField = getTokenNearTo( - tokens, - 'operator_compare', - 'next', - { tokenReferencePosition: index, tokenFoundShouldHaveValue: true } - ); - return tokenOperatorNearToField - ? validate.field(token) - : undefined; - }; - // Check if the value is allowed - if(token.type === 'value'){ - const tokenFieldNearToValue = getTokenNearTo( - tokens, - 'field', - 'previous', - { tokenReferencePosition: index, tokenFoundShouldHaveValue: true } - ); - const tokenOperatorCompareNearToValue = getTokenNearTo( - tokens, - 'operator_compare', - 'previous', - { tokenReferencePosition: index, tokenFoundShouldHaveValue: true } - ); - return validateTokenValue(token) - || (tokenFieldNearToValue && tokenOperatorCompareNearToValue && validate.value ? validate.value(token, { - field: tokenFieldNearToValue?.value, - operator: tokenOperatorCompareNearToValue?.value - }) : undefined); - } - }; - }) - .filter(t => typeof t !== 'undefined') - .join('\n') || undefined; - } -}; - -/** - * Validate the tokens if they are a valid syntax - * @param tokens - * @param validate - * @returns - */ -function validate(tokens: ITokens, validate: {field: ITokenValidator, value: ITokenValidator}): undefined | string[]{ - if (!shouldUseSearchTerm(tokens)){ - const errors = tokens.map((token: IToken, index) => { - const errors = []; - if(token.value){ - if(token.type === 'field'){ - const tokenOperatorNearToField = getTokenNearTo( - tokens, - 'operator_compare', - 'next', - { tokenReferencePosition: index, tokenFoundShouldHaveValue: true } - ); - const tokenValueNearToField = getTokenNearTo( - tokens, - 'value', - 'next', - { tokenReferencePosition: index, tokenFoundShouldHaveValue: true } - ); - if(validate.field(token)){ - errors.push(`"${token.value}" is not a valid field.`); - }else if(!tokenOperatorNearToField){ - errors.push(`The operator for field "${token.value}" is missing.`); - }else if(!tokenValueNearToField){ - errors.push(`The value for field "${token.value}" is missing.`); - } - }; - // Check if the value is allowed - if(token.type === 'value'){ - const tokenFieldNearToValue = getTokenNearTo( - tokens, - 'field', - 'previous', - { tokenReferencePosition: index, tokenFoundShouldHaveValue: true } - ); - const tokenOperatorCompareNearToValue = getTokenNearTo( - tokens, - 'operator_compare', - 'previous', - { tokenReferencePosition: index, tokenFoundShouldHaveValue: true } - ); - const validationError = validateTokenValue(token) - || (tokenFieldNearToValue && tokenOperatorCompareNearToValue && validate.value ? validate.value(token, { - field: tokenFieldNearToValue?.value, - operator: tokenOperatorCompareNearToValue?.value - }) : undefined);; - - validationError && errors.push(validationError); - }; - - // Check if the value is allowed - if(token.type === 'conjunction'){ - - const tokenWhitespaceNearToFieldNext = getTokenNearTo( - tokens, - 'whitespace', - 'next', - { tokenReferencePosition: index } - ); - const tokenFieldNearToFieldNext = getTokenNearTo( - tokens, - 'field', - 'next', - { tokenReferencePosition: index, tokenFoundShouldHaveValue: true } - ); - !tokenWhitespaceNearToFieldNext?.value?.length - && errors.push(`There is no whitespace after conjunction "${token.value}".`); - !tokenFieldNearToFieldNext?.value?.length - && errors.push(`There is no sentence after conjunction "${token.value}".`); - }; - }; - return errors.length ? errors : undefined; - }).filter(errors => errors) - .flat() - return errors.length ? errors : undefined; - }; - return undefined; -}; - -export const WQL = { - id: 'wql', - label: 'WQL', - description: 'WQL (Wazuh Query Language) provides a human query syntax based on the Wazuh API query language.', - documentationLink: `https://github.com/wazuh/wazuh-kibana-app/blob/v${PLUGIN_VERSION_SHORT}/public/components/search-bar/query-language/wql.md`, - getConfiguration() { - return { - isOpenPopoverImplicitFilter: false, - }; - }, - async run(input, params) { - // Get the tokens from the input - const tokens: ITokens = tokenizer(input); - - // Get the implicit query as query language syntax - const implicitQueryAsQL = params.queryLanguage.parameters?.options?.implicitQuery - ? transformUQLToQL( - params.queryLanguage.parameters.options.implicitQuery.query - + params.queryLanguage.parameters.options.implicitQuery.conjunction - ) - : ''; - - const fieldsSuggestion: string[] = await params.queryLanguage.parameters.suggestions.field() - .map(({label}) => label); - - const validators = { - field: ({value}) => fieldsSuggestion.includes(value) ? undefined : `"${value}" is not valid field.`, - ...(params.queryLanguage.parameters?.validate?.value ? { - value: params.queryLanguage.parameters?.validate?.value - } : {}) - }; - - // Validate the user input - const validationPartial = validatePartial(tokens, validators); - - const validationStrict = validate(tokens, validators); - - // Get the output of query language - const output = { - ...getOutput(input, params.queryLanguage.parameters), - error: validationStrict - }; - - const onSearch = () => { - if(output?.error){ - params.setQueryLanguageOutput((state) => ({ - ...state, - searchBarProps: { - ...state.searchBarProps, - suggestions: transformSuggestionsToEuiSuggestItem( - output.error.map(error => ({type: 'validation_error', label: 'Invalid', description: error})) - ) - } - })); - }else{ - params.onSearch(output); - }; - }; - - return { - filterButtons: params.queryLanguage.parameters?.options?.filterButtons - ? ( - { id, label } - ))} - idToSelectedMap={{}} - type="multi" - onChange={(id: string) => { - const buttonParams = params.queryLanguage.parameters?.options?.filterButtons.find(({id: buttonID}) => buttonID === id); - if(buttonParams){ - params.setInput(buttonParams.input); - const output = { - ...getOutput(buttonParams.input, params.queryLanguage.parameters), - error: undefined - }; - params.onSearch(output); - } - }} - /> - : null, - searchBarProps: { - // Props that will be used by the EuiSuggest component - // Suggestions - suggestions: transformSuggestionsToEuiSuggestItem( - validationPartial - ? [{ type: 'validation_error', label: 'Invalid', description: validationPartial}] - : await getSuggestions(tokens, params.queryLanguage.parameters) - ), - // Handler to manage when clicking in a suggestion item - onItemClick: item => { - // There is an error, clicking on the item does nothing - if (item.type.iconType === 'alert'){ - return; - }; - // When the clicked item has the `search` iconType, run the `onSearch` function - if (item.type.iconType === 'search') { - // Execute the search action - onSearch(); - } else { - // When the clicked item has another iconType - const lastToken: IToken | undefined = getLastTokenDefined(tokens); - // if the clicked suggestion is of same type of last token - if ( - lastToken && suggestionMappingLanguageTokenType[lastToken.type].iconType === - item.type.iconType - ) { - // replace the value of last token with the current one. - // if the current token is a value, then transform it - lastToken.value = item.type.iconType === suggestionMappingLanguageTokenType.value.iconType - ? transformQLValue(item.label) - : item.label; - } else { - // add a whitespace for conjunction - !(/\s$/.test(input)) - && ( - item.type.iconType === suggestionMappingLanguageTokenType.conjunction.iconType - || lastToken?.type === 'conjunction' - ) - && tokens.push({ - type: 'whitespace', - value: ' ' - }); - - // add a new token of the selected type and value - tokens.push({ - type: Object.entries(suggestionMappingLanguageTokenType).find( - ([, { iconType }]) => iconType === item.type.iconType, - )[0], - value: item.type.iconType === suggestionMappingLanguageTokenType.value.iconType - ? transformQLValue(item.label) - : item.label, - }); - - // add a whitespace for conjunction - item.type.iconType === suggestionMappingLanguageTokenType.conjunction.iconType - && tokens.push({ - type: 'whitespace', - value: ' ' - }); - }; - - // Change the input - params.setInput(tokens - .filter(value => value) // Ensure the input is rebuilt using tokens with value. - // The input tokenization can contain tokens with no value due to the used - // regular expression. - .map(({ value }) => value) - .join('')); - } - }, - prepend: implicitQueryAsQL ? ( - - params.setQueryLanguageConfiguration(state => ({ - ...state, - isOpenPopoverImplicitFilter: - !state.isOpenPopoverImplicitFilter, - })) - } - iconType='filter' - > - - {implicitQueryAsQL} - - - } - isOpen={ - params.queryLanguage.configuration.isOpenPopoverImplicitFilter - } - closePopover={() => - params.setQueryLanguageConfiguration(state => ({ - ...state, - isOpenPopoverImplicitFilter: false, - })) - } - > - - Implicit query:{' '} - {implicitQueryAsQL} - - This query is added to the input. - - ) : null, - // Disable the focus trap in the EuiInputPopover. - // This causes when using the Search suggestion, the suggestion popover can be closed. - // If this is disabled, then the suggestion popover is open after a short time for this - // use case. - disableFocusTrap: true, - // Show the input is invalid - isInvalid: Boolean(validationStrict), - // Define the handler when the a key is pressed while the input is focused - onKeyPress: (event) => { - if (event.key === 'Enter') { - onSearch(); - }; - } - }, - output - }; - }, - transformInput: (unifiedQuery: string, {parameters}) => { - const input = unifiedQuery && parameters?.options?.implicitQuery - ? unifiedQuery.replace( - new RegExp(`^${parameters.options.implicitQuery.query}${parameters.options.implicitQuery.conjunction}`), - '' - ) - : unifiedQuery; - - return transformUQLToQL(input); - }, -}; From eafd20c85d264d6182890057a41644d21619da5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Tue, 25 Jul 2023 15:35:29 +0200 Subject: [PATCH 46/76] update: test snapshots --- .../__snapshots__/table-with-search-bar.test.tsx.snap | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/main/public/components/common/tables/__snapshots__/table-with-search-bar.test.tsx.snap b/plugins/main/public/components/common/tables/__snapshots__/table-with-search-bar.test.tsx.snap index 499e54559b..d4a34ae9fc 100644 --- a/plugins/main/public/components/common/tables/__snapshots__/table-with-search-bar.test.tsx.snap +++ b/plugins/main/public/components/common/tables/__snapshots__/table-with-search-bar.test.tsx.snap @@ -109,7 +109,7 @@ exports[`Table With Search Bar component renders correctly to match the snapshot
@@ -178,7 +178,7 @@ exports[`Table With Search Bar component renders correctly to match the snapshot
@@ -258,7 +258,7 @@ exports[`Table With Search Bar component renders correctly to match the snapshot
@@ -368,7 +368,7 @@ exports[`Table With Search Bar component renders correctly to match the snapshot
@@ -434,7 +434,7 @@ exports[`Table With Search Bar component renders correctly to match the snapshot
From 4c6c38d8e86beb888ad5884b8d958d395d26d4ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Mon, 31 Jul 2023 15:49:48 +0200 Subject: [PATCH 47/76] feat: add the distinct values for the search bar suggestions in some sections - Add the distinct values for the search bar suggestions in some sections: - Modules > Security Configuration Assessment policy checks table - Modules > Vulnerabilities > Inventory table - Modules > MITRE ATT&CK > Inventory table - Management > Rules table - Management > Decoders table - Management > CDB Lists table - Add Path column to Rules files table - Add Path column to Decoders files table --- .../agents/sca/inventory/checks-table.tsx | 48 ++-- .../agents/sca/inventory/lib/api-request.ts | 20 +- .../agents/vuls/inventory/lib/api-requests.ts | 36 ++- .../agents/vuls/inventory/table.tsx | 61 +++-- .../mitre_attack_intelligence/resources.tsx | 100 ++++---- .../cdblists/components/cdblists-table.tsx | 103 +++++--- .../decoders/components/columns.tsx | 141 ++++++---- .../components/decoders-suggestions.ts | 158 +++++++++--- .../decoders/components/decoders-table.tsx | 241 ++++++++++-------- .../management/ruleset/components/columns.tsx | 184 ++++++++----- .../ruleset/components/ruleset-suggestions.ts | 223 +++++++++++----- .../ruleset/components/ruleset-table.tsx | 170 +++++++----- 12 files changed, 961 insertions(+), 524 deletions(-) diff --git a/plugins/main/public/components/agents/sca/inventory/checks-table.tsx b/plugins/main/public/components/agents/sca/inventory/checks-table.tsx index 3bddc1f363..e1c5d32279 100644 --- a/plugins/main/public/components/agents/sca/inventory/checks-table.tsx +++ b/plugins/main/public/components/agents/sca/inventory/checks-table.tsx @@ -2,7 +2,6 @@ import { EuiButtonIcon, EuiDescriptionList, EuiHealth } from '@elastic/eui'; import React, { Component } from 'react'; import { MODULE_SCA_CHECK_RESULT_LABEL } from '../../../../../common/constants'; import { TableWzAPI } from '../../../common/tables'; -import { IWzSuggestItem } from '../../../wz-search-bar'; import { ComplianceText, RuleText } from '../components'; import { getFilterValues } from './lib'; @@ -29,7 +28,7 @@ const searchBarWQLFieldSuggestions = [ { label: 'registry', description: 'filter by check registry' }, { label: 'remediation', description: 'filter by check remediation' }, { label: 'result', description: 'filter by check result' }, - { label: 'title', description: 'filter by check title' } + { label: 'title', description: 'filter by check title' }, ]; const searchBarWQLOptions = { @@ -51,7 +50,7 @@ const searchBarWQLOptions = { 'result', 'rules.type', 'rules.rule', - ] + ], }; export class InventoryPolicyChecksTable extends Component { @@ -83,7 +82,7 @@ export class InventoryPolicyChecksTable extends Component { { name: 'Target', truncateText: true, - render: (item) => ( + render: item => (
{item.file ? ( @@ -123,11 +122,17 @@ export class InventoryPolicyChecksTable extends Component { align: 'right', width: '40px', isExpander: true, - render: (item) => ( + render: item => ( this.toggleDetails(item)} - aria-label={this.state.itemIdToExpandedRowMap[item.id] ? 'Collapse' : 'Expand'} - iconType={this.state.itemIdToExpandedRowMap[item.id] ? 'arrowUp' : 'arrowDown'} + aria-label={ + this.state.itemIdToExpandedRowMap[item.id] ? 'Collapse' : 'Expand' + } + iconType={ + this.state.itemIdToExpandedRowMap[item.id] + ? 'arrowUp' + : 'arrowDown' + } /> ), }, @@ -135,7 +140,7 @@ export class InventoryPolicyChecksTable extends Component { } async componentDidUpdate(prevProps) { - const { filters } = this.props + const { filters } = this.props; if (filters !== prevProps.filters) { this.setState({ filters: filters }); } @@ -149,7 +154,7 @@ export class InventoryPolicyChecksTable extends Component { * * @param item */ - toggleDetails = (item) => { + toggleDetails = item => { const itemIdToExpandedRowMap = { ...this.state.itemIdToExpandedRowMap }; if (itemIdToExpandedRowMap[item.id]) { @@ -160,7 +165,7 @@ export class InventoryPolicyChecksTable extends Component { checks += item.condition ? ` (Condition: ${item.condition})` : ''; const complianceText = item.compliance && item.compliance.length - ? item.compliance.map((el) => `${el.key}: ${el.value}`).join('\n') + ? item.compliance.map(el => `${el.key}: ${el.value}`).join('\n') : ''; const listItems = [ { @@ -192,10 +197,12 @@ export class InventoryPolicyChecksTable extends Component { description: , }, ]; - const itemsToShow = listItems.filter((x) => { + const itemsToShow = listItems.filter(x => { return x.description; }); - itemIdToExpandedRowMap[item.id] = ; + itemIdToExpandedRowMap[item.id] = ( + + ); } this.setState({ itemIdToExpandedRowMap }); }; @@ -244,10 +251,10 @@ export class InventoryPolicyChecksTable extends Component { return ( { return searchBarWQLFieldSuggestions; }, value: async (currentValue, { field }) => { - try{ + try { return await getFilterValues( field, - currentValue, agentID, scaPolicyID, - {}, - (item) => ({label: item}) + { + ...(currentValue ? { q: `${field}~${currentValue}` } : {}), + }, + item => ({ label: item }), ); - }catch(error){ + } catch (error) { return []; - }; + } }, }, }} diff --git a/plugins/main/public/components/agents/sca/inventory/lib/api-request.ts b/plugins/main/public/components/agents/sca/inventory/lib/api-request.ts index 5d7844d49f..0d3eacd92e 100644 --- a/plugins/main/public/components/agents/sca/inventory/lib/api-request.ts +++ b/plugins/main/public/components/agents/sca/inventory/lib/api-request.ts @@ -2,25 +2,27 @@ import { WzRequest } from '../../../../../react-services/wz-request'; export async function getFilterValues( field: string, - value: string, agentId: string, policyId: string, filters: { [key: string]: string } = {}, - format = (item) => item) { + format = item => item, +) { const filter = { ...filters, distinct: true, select: field, + sort: `+${field}`, limit: 30, }; - if (value) { - filter['search'] = value; - } - const result = await WzRequest.apiReq('GET', `/sca/${agentId}/checks/${policyId}`, { - params: filter, - }); + const result = await WzRequest.apiReq( + 'GET', + `/sca/${agentId}/checks/${policyId}`, + { + params: filter, + }, + ); return ( - result?.data?.data?.affected_items?.map((item) => { + result?.data?.data?.affected_items?.map(item => { return format(item[field]); }) || [] ); diff --git a/plugins/main/public/components/agents/vuls/inventory/lib/api-requests.ts b/plugins/main/public/components/agents/vuls/inventory/lib/api-requests.ts index 3edb4c5f16..c567b46f9b 100644 --- a/plugins/main/public/components/agents/vuls/inventory/lib/api-requests.ts +++ b/plugins/main/public/components/agents/vuls/inventory/lib/api-requests.ts @@ -11,31 +11,47 @@ */ import { WzRequest } from '../../../../../react-services/wz-request'; -export async function getAggregation(agentId: string, field: string = 'severity', limit: number | null = null) { - const result = await WzRequest.apiReq('GET', `/vulnerability/${agentId}/summary/${field}`, limit ? { params: { limit } } : {}); +export async function getAggregation( + agentId: string, + field: string = 'severity', + limit: number | null = null, +) { + const result = await WzRequest.apiReq( + 'GET', + `/vulnerability/${agentId}/summary/${field}`, + limit ? { params: { limit } } : {}, + ); return result?.data?.data; } -export async function getFilterValues(field, value, agentId, filters = {}, format = (item) => item) { - +export async function getFilterValues( + field, + agentId, + filters = {}, + format = item => item, +) { const filter = { ...filters, distinct: true, select: field, + sort: `+${field}`, limit: 30, }; - if (value) { - filter['search'] = value; - } - const result = await WzRequest.apiReq('GET', `/vulnerability/${agentId}`, { params: filter }); - return result?.data?.data?.affected_items?.map((item) => { return format(item[field]) }) || []; + const result = await WzRequest.apiReq('GET', `/vulnerability/${agentId}`, { + params: filter, + }); + return ( + result?.data?.data?.affected_items?.map(item => { + return format(item[field]); + }) || [] + ); } export async function getLastScan(agentId: string = '000') { const response = await WzRequest.apiReq( 'GET', `/vulnerability/${agentId}/last_scan`, - {} + {}, ); return response?.data?.data?.affected_items[0] || {}; } diff --git a/plugins/main/public/components/agents/vuls/inventory/table.tsx b/plugins/main/public/components/agents/vuls/inventory/table.tsx index 6ed05d6656..345f012675 100644 --- a/plugins/main/public/components/agents/vuls/inventory/table.tsx +++ b/plugins/main/public/components/agents/vuls/inventory/table.tsx @@ -25,8 +25,8 @@ const searchBarWQLOptions = { 'architecture', 'severity', 'cvss2_score', - 'cvss3_score' - ] + 'cvss3_score', + ], }; export class InventoryTable extends Component { @@ -37,7 +37,6 @@ export class InventoryTable extends Component { currentItem: {}; }; - props!: { filters: string; agent: any; @@ -50,7 +49,7 @@ export class InventoryTable extends Component { this.state = { isFlyoutVisible: false, - currentItem: {} + currentItem: {}, }; } @@ -61,11 +60,10 @@ export class InventoryTable extends Component { async showFlyout(item, redirect = false) { //if a flyout is opened, we close it and open a new one, so the components are correctly updated on start. this.setState({ isFlyoutVisible: false }, () => - this.setState({ isFlyoutVisible: true, currentItem: item }) + this.setState({ isFlyoutVisible: true, currentItem: item }), ); } - columns() { let width; (((this.props.agent || {}).os || {}).platform || false) === 'windows' @@ -119,7 +117,8 @@ export class InventoryTable extends Component { { field: 'detection_time', name: ( - Detection Time{' '} + + Detection Time{' '} { + const getRowProps = item => { const id = `${item.name}-${item.cve}-${item.architecture}-${item.version}-${item.severity}-${item.cvss2_score}-${item.cvss3_score}-${item.detection_time}`; return { 'data-test-subj': `row-${id}`, @@ -159,27 +158,27 @@ export class InventoryTable extends Component { 'condition', 'updated', 'published', - 'external_references' + 'external_references', ].join(',')}`; const agentID = this.props.agent.id; return ( ({ + mapResponseItem={item => ({ ...item, // Some vulnerability data could not contain the external_references field. // This causes the rendering of them can crash when opening the flyout with the details. // So, we ensure the fields are defined with the expected data structure. - external_references: Array.isArray(item?.external_references) - ? item?.external_references - : [] + external_references: Array.isArray(item?.external_references) + ? item?.external_references + : [], })} error={error} searchTable @@ -192,22 +191,36 @@ export class InventoryTable extends Component { suggestions: { field(currentValue) { return [ - { label: 'architecture', description: 'filter by architecture' }, + { + label: 'architecture', + description: 'filter by architecture', + }, { label: 'cve', description: 'filter by CVE ID' }, { label: 'cvss2_score', description: 'filter by CVSS2' }, { label: 'cvss3_score', description: 'filter by CVSS3' }, - { label: 'detection_time', description: 'filter by detection time' }, + { + label: 'detection_time', + description: 'filter by detection time', + }, { label: 'name', description: 'filter by package name' }, { label: 'severity', description: 'filter by severity' }, { label: 'version', description: 'filter by CVE version' }, ]; }, value: async (currentValue, { field }) => { - try{ - return await getFilterValues(field, currentValue, agentID, {}, label => ({label})); - }catch(error){ + try { + return await getFilterValues( + field, + currentValue, + agentID, + { + ...(currentValue ? { q: `${field}~${currentValue}` } : {}), + }, + label => ({ label }), + ); + } catch (error) { return []; - }; + } }, }, }} @@ -218,7 +231,7 @@ export class InventoryTable extends Component { render() { const table = this.renderTable(); return ( -
+
{table} {this.state.isFlyoutVisible && ( this.closeFlyout()} - type="vulnerability" - view="inventory" + type='vulnerability' + view='inventory' showViewInEvents={true} outsideClickCloses={true} {...this.props} diff --git a/plugins/main/public/components/overview/mitre_attack_intelligence/resources.tsx b/plugins/main/public/components/overview/mitre_attack_intelligence/resources.tsx index bbef33ef8f..87749e9557 100644 --- a/plugins/main/public/components/overview/mitre_attack_intelligence/resources.tsx +++ b/plugins/main/public/components/overview/mitre_attack_intelligence/resources.tsx @@ -20,16 +20,24 @@ import { UI_LOGGER_LEVELS } from '../../../../common/constants'; import { UI_ERROR_SEVERITIES } from '../../../react-services/error-orchestrator/types'; import { getErrorOrchestrator } from '../../../react-services/common-services'; -const getMitreAttackIntelligenceSuggestions = (endpoint: string, field: string) => async (input: string) => { - try{ - const response = await WzRequest.apiReq('GET', endpoint, {}); // TODO: change to use distinct - return response?.data?.data.affected_items - .map(item => item[field]) - .filter(item => input ? (item && item.toLowerCase().includes(input.toLowerCase())) : true) - .sort() - .slice(0, 9) - .map(label => ({label})); - }catch(error){ +const getMitreAttackIntelligenceSuggestions = async ( + endpoint: string, + field: string, + currentValue: string, +) => { + try { + const params = { + distinct: true, + limit: 30, + select: field, + sort: `+${field}`, + ...(currentValue ? { q: `${field}~${currentValue}` } : {}), + }; + const response = await WzRequest.apiReq('GET', endpoint, { params }); + return response?.data?.data.affected_items.map(item => ({ + label: item[field], + })); + } catch (error) { const options = { context: `${ModuleMitreAttackIntelligenceResource.name}.getMitreItemToRedirect`, level: UI_LOGGER_LEVELS.ERROR, @@ -44,16 +52,16 @@ const getMitreAttackIntelligenceSuggestions = (endpoint: string, field: string) }; getErrorOrchestrator().handleError(options); return []; - }; + } }; -function buildResource(label: string, labelResource: string){ +function buildResource(label: string) { const id = label.toLowerCase(); const endpoint: string = `/mitre/${id}`; const fieldsMitreAttactResource = [ { field: 'description', name: 'description' }, { field: 'external_id', name: 'external ID' }, - { field: 'name', name: 'name' } + { field: 'name', name: 'name' }, ]; return { label: label, @@ -61,47 +69,57 @@ function buildResource(label: string, labelResource: string){ searchBar: { wql: { options: { - searchTermFields: fieldsMitreAttactResource.map(({field}) => field) + searchTermFields: fieldsMitreAttactResource.map(({ field }) => field), }, suggestions: { field(currentValue) { - return fieldsMitreAttactResource - .map(({field, name}) => ({label: field, description: `filter by ${name}`})); + return fieldsMitreAttactResource.map(({ field, name }) => ({ + label: field, + description: `filter by ${name}`, + })); }, value: async (currentValue, { field }) => { - try{ // TODO: distinct - return await (getMitreAttackIntelligenceSuggestions(endpoint, field))(currentValue); - }catch(error){ + try { + return await getMitreAttackIntelligenceSuggestions( + endpoint, + field, + currentValue, + ); + } catch (error) { return []; - }; + } }, - } - } + }, + }, }, apiEndpoint: endpoint, fieldName: 'name', initialSortingField: 'name', - tableColumnsCreator: (openResourceDetails) => [ + tableColumnsCreator: openResourceDetails => [ { field: 'external_id', name: 'ID', width: '12%', - render: (value, item) => openResourceDetails(item)}>{value} + render: (value, item) => ( + openResourceDetails(item)}>{value} + ), }, { field: 'name', name: 'Name', sortable: true, width: '30%', - render: (value, item) => openResourceDetails(item)}>{value} + render: (value, item) => ( + openResourceDetails(item)}>{value} + ), }, { field: 'description', name: 'Description', sortable: true, - render: (value) => value ? : '', - truncateText: true - } + render: value => (value ? : ''), + truncateText: true, + }, ], mitreFlyoutHeaderProperties: [ { @@ -110,34 +128,30 @@ function buildResource(label: string, labelResource: string){ }, { label: 'Name', - id: 'name' + id: 'name', }, { label: 'Created Time', id: 'created_time', - render: (value) => value ? ( - formatUIDate(value) - ) : '' + render: value => (value ? formatUIDate(value) : ''), }, { label: 'Modified Time', id: 'modified_time', - render: (value) => value ? ( - formatUIDate(value) - ) : '' + render: value => (value ? formatUIDate(value) : ''), }, { label: 'Version', - id: 'mitre_version' + id: 'mitre_version', }, ], - } -}; + }; +} export const MitreAttackResources = [ - buildResource('Groups', 'Group'), - buildResource('Mitigations', 'Mitigation'), - buildResource('Software', 'Software'), - buildResource('Tactics', 'Tactic'), - buildResource('Techniques', 'Technique') + buildResource('Groups'), + buildResource('Mitigations'), + buildResource('Software'), + buildResource('Tactics'), + buildResource('Techniques'), ]; diff --git a/plugins/main/public/controllers/management/components/management/cdblists/components/cdblists-table.tsx b/plugins/main/public/controllers/management/components/management/cdblists/components/cdblists-table.tsx index c49d556005..32aa651f09 100644 --- a/plugins/main/public/controllers/management/components/management/cdblists/components/cdblists-table.tsx +++ b/plugins/main/public/controllers/management/components/management/cdblists/components/cdblists-table.tsx @@ -13,12 +13,19 @@ import React, { useState } from 'react'; import { TableWzAPI } from '../../../../../../components/common/tables'; import { getToasts } from '../../../../../../kibana-services'; -import { resourceDictionary, ResourcesConstants, ResourcesHandler } from '../../common/resources-handler'; +import { + resourceDictionary, + ResourcesConstants, + ResourcesHandler, +} from '../../common/resources-handler'; import { getErrorOrchestrator } from '../../../../../../react-services/common-services'; import { UI_ERROR_SEVERITIES } from '../../../../../../react-services/error-orchestrator/types'; import { UI_LOGGER_LEVELS } from '../../../../../../../common/constants'; -import { SECTION_CDBLIST_SECTION, SECTION_CDBLIST_KEY } from '../../common/constants'; +import { + SECTION_CDBLIST_SECTION, + SECTION_CDBLIST_KEY, +} from '../../common/constants'; import CDBListsColumns from './columns'; import { withUserPermissions } from '../../../../../../components/common/hocs/withUserPermissions'; @@ -30,12 +37,17 @@ import { AddNewCdbListButton, UploadFilesButton, } from '../../common/actions-buttons'; +import { WzRequest } from '../../../../../../react-services'; -const searchBarWQLOptions = { +const searchBarWQLOptions = { searchTermFields: ['filename', 'relative_dirname'], filterButtons: [ - {id: 'relative-dirname', input: 'relative_dirname=etc/lists', label: 'Custom lists'} - ] + { + id: 'relative-dirname', + input: 'relative_dirname=etc/lists', + label: 'Custom lists', + }, + ], }; function CDBListsTable(props) { @@ -44,31 +56,30 @@ function CDBListsTable(props) { const resourcesHandler = new ResourcesHandler(ResourcesConstants.LISTS); - const toggleShowFiles = () => { setShowingFiles(!showingFiles); - } - + }; const getColumns = () => { const cdblistsColumns = new CDBListsColumns({ removeItems: removeItems, state: { section: SECTION_CDBLIST_KEY, - defaultItems: [] - }, ...props + defaultItems: [], + }, + ...props, }).columns; const columns = cdblistsColumns[SECTION_CDBLIST_KEY]; return columns; - } + }; /** * Columns and Rows properties */ - const getRowProps = (item) => { + const getRowProps = item => { const { id, name } = item; - const getRequiredPermissions = (item) => { + const getRequiredPermissions = item => { const { permissionResource } = resourceDictionary[SECTION_CDBLIST_KEY]; return [ { @@ -83,17 +94,17 @@ function CDBListsTable(props) { className: 'customRowClass', onClick: !WzUserPermissions.checkMissingUserPermissions( getRequiredPermissions(item), - props.userPermissions + props.userPermissions, ) - ? async (ev) => { - const result = await resourcesHandler.getFileContent(item.filename); - const file = { - name: item.filename, - content: result, - path: item.relative_dirname, - }; - updateListContent(file); - } + ? async ev => { + const result = await resourcesHandler.getFileContent(item.filename); + const file = { + name: item.filename, + content: result, + path: item.relative_dirname, + }; + updateListContent(file); + } : undefined, }; }; @@ -101,13 +112,13 @@ function CDBListsTable(props) { /** * Remove files method */ - const removeItems = async (items) => { + const removeItems = async items => { try { const results = items.map(async (item, i) => { await resourcesHandler.deleteFile(item.filename || item.name); }); - Promise.all(results).then((completed) => { + Promise.all(results).then(completed => { setTableFootprint(Date.now()); getToasts().add({ color: 'success', @@ -129,7 +140,7 @@ function CDBListsTable(props) { }; getErrorOrchestrator().handleError(options); } - } + }; const { updateRestartClusterManager, updateListContent } = props; const columns = getColumns(); @@ -156,14 +167,14 @@ function CDBListsTable(props) { { updateRestartClusterManager && updateRestartClusterManager() }} + onSuccess={() => { + updateRestartClusterManager && updateRestartClusterManager(); + }} />, ]; - - return ( -
+
{ - try{ // TODO: distinct + try { + const response = await WzRequest.apiReq('GET', '/lists', { + params: { + distinct: true, + limit: 30, + select: field, + sort: `+${field}`, + ...(currentValue ? { q: `${field}~${currentValue}` } : {}), + }, + }); + return response?.data?.data.affected_items.map(item => ({ + label: item[field], + })); + } catch (error) { return []; - }catch(error){ - return []; - }; + } }, }, }} @@ -199,10 +224,6 @@ function CDBListsTable(props) { />
); - } - -export default compose( - withUserPermissions -)(CDBListsTable); +export default compose(withUserPermissions)(CDBListsTable); diff --git a/plugins/main/public/controllers/management/components/management/decoders/components/columns.tsx b/plugins/main/public/controllers/management/components/management/decoders/components/columns.tsx index 9e3e160425..a8b24717c2 100644 --- a/plugins/main/public/controllers/management/components/management/decoders/components/columns.tsx +++ b/plugins/main/public/controllers/management/components/management/decoders/components/columns.tsx @@ -1,5 +1,9 @@ import React from 'react'; -import { resourceDictionary, ResourcesHandler, ResourcesConstants } from '../../common/resources-handler'; +import { + resourceDictionary, + ResourcesHandler, + ResourcesConstants, +} from '../../common/resources-handler'; import { WzButtonPermissions } from '../../../../../../components/common/permissions/button'; import { WzButtonPermissionsModalConfirm } from '../../../../../../components/common/buttons'; import { getErrorOrchestrator } from '../../../../../../react-services/common-services'; @@ -7,9 +11,7 @@ import { UIErrorLog } from '../../../../../../react-services/error-orchestrator/ import { getErrorOptions } from '../../common/error-helper'; import { Columns } from '../../common/interfaces'; - export default class DecodersColumns { - columns: Columns = {}; constructor(props) { @@ -24,19 +26,19 @@ export default class DecodersColumns { field: 'name', name: 'Name', align: 'left', - sortable: true + sortable: true, }, { field: 'details.program_name', name: 'Program name', align: 'left', - sortable: false + sortable: false, }, { field: 'details.order', name: 'Order', align: 'left', - sortable: false + sortable: false, }, { field: 'filename', @@ -49,39 +51,52 @@ export default class DecodersColumns { buttonType='link' permissions={getReadButtonPermissions(item)} tooltip={{ position: 'top', content: `Show ${value} content` }} - onClick={async (ev) => { + onClick={async ev => { try { ev.stopPropagation(); - const resourcesHandler = new ResourcesHandler(ResourcesConstants.DECODERS); + const resourcesHandler = new ResourcesHandler( + ResourcesConstants.DECODERS, + ); const result = await resourcesHandler.getFileContent(value); - const file = { name: value, content: result, path: item.relative_dirname }; + const file = { + name: value, + content: result, + path: item.relative_dirname, + }; this.props.updateFileContent(file); } catch (error) { const options: UIErrorLog = getErrorOptions( error, - 'Decoders.readFileContent' + 'Decoders.readFileContent', ); getErrorOrchestrator().handleError(options); } - }}> + }} + > {value} ); - } + }, }, { field: 'relative_dirname', name: 'Path', align: 'left', - sortable: true - } + sortable: true, + }, ], files: [ { field: 'filename', name: 'File', align: 'left', - sortable: true + sortable: true, + }, + { + field: 'relative_dirname', + name: 'Path', + align: 'left', + sortable: true, }, { name: 'Actions', @@ -92,25 +107,36 @@ export default class DecodersColumns { { try { ev.stopPropagation(); - const resourcesHandler = new ResourcesHandler(ResourcesConstants.DECODERS); - const result = await resourcesHandler.getFileContent(item.filename); - const file = { name: item.filename, content: result, path: item.relative_dirname }; + const resourcesHandler = new ResourcesHandler( + ResourcesConstants.DECODERS, + ); + const result = await resourcesHandler.getFileContent( + item.filename, + ); + const file = { + name: item.filename, + content: result, + path: item.relative_dirname, + }; this.props.updateFileContent(file); } catch (error) { const options: UIErrorLog = getErrorOptions( error, - 'Decoders.readFileContent' + 'Decoders.readFileContent', ); getErrorOrchestrator().handleError(options); } }} - color="primary" + color='primary' /> ); } else { @@ -119,44 +145,58 @@ export default class DecodersColumns { { try { ev.stopPropagation(); - const resourcesHandler = new ResourcesHandler(ResourcesConstants.DECODERS); - const result = await resourcesHandler.getFileContent(item.filename); - const file = { name: item.filename, content: result, path: item.relative_dirname }; + const resourcesHandler = new ResourcesHandler( + ResourcesConstants.DECODERS, + ); + const result = await resourcesHandler.getFileContent( + item.filename, + ); + const file = { + name: item.filename, + content: result, + path: item.relative_dirname, + }; this.props.updateFileContent(file); } catch (error) { const options: UIErrorLog = getErrorOptions( error, - 'Files.editFileContent' + 'Files.editFileContent', ); getErrorOrchestrator().handleError(options); } }} - color="primary" + color='primary' /> { try { this.props.removeItems([item]); } catch (error) { const options: UIErrorLog = getErrorOptions( error, - 'Files.deleteFile' + 'Files.deleteFile', ); getErrorOrchestrator().handleError(options); } }} - color="danger" + color='danger' modalTitle={'Are you sure?'} modalProps={{ buttonColor: 'danger', @@ -165,13 +205,14 @@ export default class DecodersColumns {
); } - } - } - ] + }, + }, + ], }; - const getReadButtonPermissions = (item) => { - const { permissionResource } = resourceDictionary[ResourcesConstants.DECODERS]; + const getReadButtonPermissions = item => { + const { permissionResource } = + resourceDictionary[ResourcesConstants.DECODERS]; return [ { action: `${ResourcesConstants.DECODERS}:read`, @@ -180,19 +221,24 @@ export default class DecodersColumns { ]; }; - const getEditButtonPermissions = (item) => { - const { permissionResource } = resourceDictionary[ResourcesConstants.DECODERS]; + const getEditButtonPermissions = item => { + const { permissionResource } = + resourceDictionary[ResourcesConstants.DECODERS]; return [ { action: `${ResourcesConstants.DECODERS}:read`, resource: permissionResource(item.filename), }, - { action: `${ResourcesConstants.DECODERS}:update`, resource: permissionResource(item.filename) }, + { + action: `${ResourcesConstants.DECODERS}:update`, + resource: permissionResource(item.filename), + }, ]; }; - const getDeleteButtonPermissions = (item) => { - const { permissionResource } = resourceDictionary[ResourcesConstants.DECODERS]; + const getDeleteButtonPermissions = item => { + const { permissionResource } = + resourceDictionary[ResourcesConstants.DECODERS]; return [ { action: `${ResourcesConstants.DECODERS}:delete`, @@ -200,6 +246,5 @@ export default class DecodersColumns { }, ]; }; - } } diff --git a/plugins/main/public/controllers/management/components/management/decoders/components/decoders-suggestions.ts b/plugins/main/public/controllers/management/components/management/decoders/components/decoders-suggestions.ts index 9936fbe4cc..675b691685 100644 --- a/plugins/main/public/controllers/management/components/management/decoders/components/decoders-suggestions.ts +++ b/plugins/main/public/controllers/management/components/management/decoders/components/decoders-suggestions.ts @@ -3,56 +3,154 @@ import { WzRequest } from '../../../../../../react-services/wz-request'; const decodersItems = { field(currentValue) { return [ - {label: 'details.order', description: 'filter by program name'}, - {label: 'details.program_name', description: 'filter by program name'}, - {label: 'filename', description: 'filter by filename'}, - {label: 'name', description: 'filter by name'}, - {label: 'relative_dirname', description: 'filter by relative path'}, + { label: 'details.order', description: 'filter by program name' }, + { label: 'details.program_name', description: 'filter by program name' }, + { label: 'filename', description: 'filter by filename' }, + { label: 'name', description: 'filter by name' }, + { label: 'relative_dirname', description: 'filter by relative path' }, ]; }, value: async (currentValue, { field }) => { - try{ // TODO: distinct - switch(field){ + try { + switch (field) { + case 'details.order': { + const filter = { + distinct: true, + limit: 30, + select: field, + ...(currentValue ? { q: `${field}~${currentValue}` } : {}), + }; + const result = await WzRequest.apiReq('GET', '/decoders', { + params: filter, + }); + return ( + result?.data?.data?.affected_items + // There are some affected items that doesn't return any value for the selected property + ?.filter(item => typeof item?.details?.order === 'string') + ?.map(item => ({ + label: item?.details?.order, + })) + ); + } + case 'details.program_name': { + const filter = { + distinct: true, + limit: 30, + select: field, + ...(currentValue ? { q: `${field}~${currentValue}` } : {}), + }; + const result = await WzRequest.apiReq('GET', '/decoders', { + params: filter, + }); + // FIX: this breaks the search bar component because returns a non-string value. + return result?.data?.data?.affected_items + ?.filter(item => item?.details?.program_name) + .map(item => ({ + label: item?.details?.program_name, + })); + } case 'filename': { - const filter = { limit: 30 }; - if (value) { - filter['search'] = value; - } - const result = await WzRequest.apiReq('GET', '/decoders/files', filter); - return result?.data?.data?.affected_items.map(item => ({label: item.filename})); + const filter = { + distinct: true, + limit: 30, + select: field, + sort: `+${field}`, + ...(currentValue ? { q: `${field}~${currentValue}` } : {}), + }; + const result = await WzRequest.apiReq('GET', '/decoders/files', { + params: filter, + }); + return result?.data?.data?.affected_items?.map(item => ({ + label: item[field], + })); } - case 'relative_dirname': { - const result = await WzRequest.apiReq('GET', '/manager/configuration', { - params: { - section: 'ruleset', - field: 'decoder_dir' - } + case 'name': { + const filter = { + distinct: true, + limit: 30, + select: field, + sort: `+${field}`, + ...(currentValue ? { q: `${field}~${currentValue}` } : {}), + }; + const result = await WzRequest.apiReq('GET', '/decoders', { + params: filter, }); - return result?.data?.data?.affected_items[0].ruleset.decoder_dir.map(label => ({label})); + return result?.data?.data?.affected_items?.map(item => ({ + label: item[field], + })); } - case 'status': { - return ['enabled', 'disabled'].map(label => ({label})); + case 'relative_dirname': { + const filter = { + distinct: true, + limit: 30, + select: field, + sort: `+${field}`, + ...(currentValue ? { q: `${field}~${currentValue}` } : {}), + }; + const result = await WzRequest.apiReq('GET', '/decoders', { + params: filter, + }); + return result?.data?.data?.affected_items.map(item => ({ + label: item[field], + })); } default: { return []; } } - }catch(error){ + } catch (error) { return []; - }; + } }, }; const decodersFiles = { field(currentValue) { - return []; // TODO: fields + return [ + { label: 'filename', description: 'filter by filename' }, + { label: 'relative_dirname', description: 'filter by relative dirname' }, + ]; }, value: async (currentValue, { field }) => { - try{ // TODO: distinct - return []; - }catch(error){ + try { + switch (field) { + case 'filename': { + const filter = { + distinct: true, + limit: 30, + select: field, + sort: `+${field}`, + ...(currentValue ? { q: `${field}~${currentValue}` } : {}), + }; + const result = await WzRequest.apiReq('GET', '/decoders/files', { + params: filter, + }); + return result?.data?.data?.affected_items?.map(item => ({ + label: item[field], + })); + break; + } + case 'relative_dirname': { + const filter = { + distinct: true, + limit: 30, + select: field, + sort: `+${field}`, + ...(currentValue ? { q: `${field}~${currentValue}` } : {}), + }; + const result = await WzRequest.apiReq('GET', '/decoders', { + params: filter, + }); + return result?.data?.data?.affected_items.map(item => ({ + label: item[field], + })); + } + default: + return []; + } + } catch (error) { return []; - }; + } }, }; @@ -61,4 +159,4 @@ const apiSuggestsItems = { files: decodersFiles, }; -export default apiSuggestsItems; \ No newline at end of file +export default apiSuggestsItems; diff --git a/plugins/main/public/controllers/management/components/management/decoders/components/decoders-table.tsx b/plugins/main/public/controllers/management/components/management/decoders/components/decoders-table.tsx index 4dd560fdb4..f626e875d1 100644 --- a/plugins/main/public/controllers/management/components/management/decoders/components/decoders-table.tsx +++ b/plugins/main/public/controllers/management/components/management/decoders/components/decoders-table.tsx @@ -13,7 +13,11 @@ import React, { useState, useCallback } from 'react'; import { TableWzAPI } from '../../../../../../components/common/tables'; import { getToasts } from '../../../../../../kibana-services'; -import { resourceDictionary, ResourcesConstants, ResourcesHandler } from '../../common/resources-handler'; +import { + resourceDictionary, + ResourcesConstants, + ResourcesHandler, +} from '../../common/resources-handler'; import { getErrorOrchestrator } from '../../../../../../react-services/common-services'; import { UI_ERROR_SEVERITIES } from '../../../../../../react-services/error-orchestrator/types'; import { UI_LOGGER_LEVELS } from '../../../../../../../common/constants'; @@ -23,20 +27,44 @@ import { FlyoutDetail } from './flyout-detail'; import { withUserPermissions } from '../../../../../../components/common/hocs/withUserPermissions'; import { WzUserPermissions } from '../../../../../../react-services/wz-user-permissions'; import { compose } from 'redux'; -import { SECTION_DECODERS_SECTION, SECTION_DECODERS_KEY } from '../../common/constants'; +import { + SECTION_DECODERS_SECTION, + SECTION_DECODERS_KEY, +} from '../../common/constants'; import { ManageFiles, AddNewFileButton, UploadFilesButton, -} from '../../common/actions-buttons' +} from '../../common/actions-buttons'; import apiSuggestsItems from './decoders-suggestions'; -const searchBarWQLOptions = { - searchTermFields: [], // TODO: add search term fields +const searchBarWQLOptions = { + searchTermFields: [ + 'details.order', + 'details.program_name', + 'filename', + 'name', + 'relative_dirname', + ], + filterButtons: [ + { + id: 'relative-dirname', + input: 'relative_dirname=etc/decoders', + label: 'Custom decoders', + }, + ], +}; + +const searchBarWQLOptionsFiles = { + searchTermFields: ['filename', 'relative_dirname'], filterButtons: [ - {id: 'relative-dirname', input: 'relative_dirname=etc/decoders', label: 'Custom decoders'} - ] + { + id: 'relative-dirname', + input: 'relative_dirname=etc/rules', + label: 'Custom rules', + }, + ], }; /*************************************** @@ -47,84 +75,85 @@ const FilesTable = ({ columns, searchBarSuggestions, filters, - reload -}) => ; - -const DecodersFlyoutTable = ({ - actionButtons, - columns, - searchBarSuggestions, - getRowProps, - filters, - updateFilters, - isFlyoutVisible, - currentItem, - closeFlyout, - cleanFilters, - ...props -}) => <> + reload, +}) => ( - {isFlyoutVisible && ( - ( + <> + - )} -; + {isFlyoutVisible && ( + + )} + +); /*************************************** * Main component */ -export default compose( - withUserPermissions -)(function DecodersTable({ setShowingFiles, showingFiles, ...props }) { +export default compose(withUserPermissions)(function DecodersTable({ + setShowingFiles, + showingFiles, + ...props +}) { const [filters, setFilters] = useState([]); const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); const [currentItem, setCurrentItem] = useState(null); @@ -132,23 +161,23 @@ export default compose( const resourcesHandler = new ResourcesHandler(ResourcesConstants.DECODERS); - const updateFilters = (filters) => { + const updateFilters = filters => { setFilters(filters); - } + }; const cleanFilters = () => { setFilters([]); - } + }; const toggleShowFiles = () => { setFilters([]); setShowingFiles(!showingFiles); - } + }; const closeFlyout = () => { setIsFlyoutVisible(false); setCurrentItem(null); - } + }; /** * Columns and Rows properties @@ -157,15 +186,17 @@ export default compose( const decodersColumns = new DecodersColumns({ removeItems: removeItems, state: { - section: SECTION_DECODERS_KEY - }, ...props + section: SECTION_DECODERS_KEY, + }, + ...props, }).columns; - const columns = decodersColumns[showingFiles ? 'files' : SECTION_DECODERS_KEY]; + const columns = + decodersColumns[showingFiles ? 'files' : SECTION_DECODERS_KEY]; return columns; - } + }; - const getRowProps = (item) => { - const getRequiredPermissions = (item) => { + const getRowProps = item => { + const getRequiredPermissions = item => { const { permissionResource } = resourceDictionary[SECTION_DECODERS_KEY]; return [ { @@ -180,12 +211,12 @@ export default compose( className: 'customRowClass', onClick: !WzUserPermissions.checkMissingUserPermissions( getRequiredPermissions(item), - props.userPermissions + props.userPermissions, ) ? () => { - setCurrentItem(item) - setIsFlyoutVisible(true); - } + setCurrentItem(item); + setIsFlyoutVisible(true); + } : undefined, }; }; @@ -193,13 +224,13 @@ export default compose( /** * Remove files method */ - const removeItems = async (items) => { + const removeItems = async items => { try { const results = items.map(async (item, i) => { await resourcesHandler.deleteFile(item.filename || item.name); }); - Promise.all(results).then((completed) => { + Promise.all(results).then(completed => { setTableFootprint(Date.now()); getToasts().add({ color: 'success', @@ -221,7 +252,7 @@ export default compose( }; getErrorOrchestrator().handleError(options); } - } + }; const { updateRestartClusterManager, updateFileContent } = props; const columns = getColumns(); @@ -243,18 +274,22 @@ export default compose( />, ]; if (showingFiles) - buttons.push( { updateRestartClusterManager && updateRestartClusterManager() }} - />); + buttons.push( + { + updateRestartClusterManager && updateRestartClusterManager(); + }} + />, + ); return buttons; }, [showingFiles]); const actionButtons = buildActionButtons(); return ( -
+
{showingFiles ? ( ) : ( - - )} + + )}
); }); diff --git a/plugins/main/public/controllers/management/components/management/ruleset/components/columns.tsx b/plugins/main/public/controllers/management/components/management/ruleset/components/columns.tsx index ba57e1760f..2f1e5fd712 100644 --- a/plugins/main/public/controllers/management/components/management/ruleset/components/columns.tsx +++ b/plugins/main/public/controllers/management/components/management/ruleset/components/columns.tsx @@ -1,6 +1,10 @@ import React from 'react'; import { EuiToolTip, EuiBadge } from '@elastic/eui'; -import { resourceDictionary, ResourcesHandler, ResourcesConstants } from '../../common/resources-handler'; +import { + resourceDictionary, + ResourcesHandler, + ResourcesConstants, +} from '../../common/resources-handler'; import { WzButtonPermissions } from '../../../../../../components/common/permissions/button'; import { WzButtonPermissionsModalConfirm } from '../../../../../../components/common/buttons'; import { getErrorOrchestrator } from '../../../../../../react-services/common-services'; @@ -8,9 +12,7 @@ import { UIErrorLog } from '../../../../../../react-services/error-orchestrator/ import { getErrorOptions } from '../../common/error-helper'; import { Columns } from '../../common/interfaces'; - export default class RulesetColumns { - columns: Columns = {}; constructor(props) { @@ -26,7 +28,7 @@ export default class RulesetColumns { name: 'ID', align: 'left', sortable: true, - width: '5%' + width: '5%', }, { field: 'description', @@ -44,40 +46,44 @@ export default class RulesetColumns { haveTooltip = true; toolTipDescription = value; for (const oldValue of result) { - let newValue = oldValue.replace('$(', ``); + let newValue = oldValue.replace( + '$(', + ``, + ); newValue = newValue.replace(')', ' '); value = value.replace(oldValue, newValue); } } return (
- {haveTooltip === false ? - : - + {haveTooltip === false ? ( + + ) : ( + - } + )}
); - } + }, }, { field: 'groups', name: 'Groups', align: 'left', sortable: false, - width: '10%' + width: '10%', }, { name: 'Regulatory compliance', - render: this.buildComplianceBadges + render: this.buildComplianceBadges, }, { field: 'level', name: 'Level', align: 'left', sortable: true, - width: '5%' + width: '5%', }, { field: 'filename', @@ -91,40 +97,54 @@ export default class RulesetColumns { buttonType='link' permissions={getReadButtonPermissions(item)} tooltip={{ position: 'top', content: `Show ${value} content` }} - onClick={async (ev) => { - try{ + onClick={async ev => { + try { ev.stopPropagation(); - const resourcesHandler = new ResourcesHandler(ResourcesConstants.RULES); + const resourcesHandler = new ResourcesHandler( + ResourcesConstants.RULES, + ); const result = await resourcesHandler.getFileContent(value); - const file = { name: value, content: result, path: item.relative_dirname }; + const file = { + name: value, + content: result, + path: item.relative_dirname, + }; this.props.updateFileContent(file); - }catch(error){ + } catch (error) { const options: UIErrorLog = getErrorOptions( error, - 'Rules.readFileContent' + 'Rules.readFileContent', ); getErrorOrchestrator().handleError(options); } - }}> + }} + > {value} ); - } + }, }, { field: 'relative_dirname', name: 'Path', align: 'left', sortable: true, - width: '10%' - } + width: '10%', + }, ], files: [ { field: 'filename', name: 'File', align: 'left', - sortable: true + sortable: true, + }, + { + field: 'relative_dirname', + name: 'Path', + align: 'left', + sortable: true, + width: '10%', }, { name: 'Actions', @@ -135,25 +155,36 @@ export default class RulesetColumns { { - try{ + try { ev.stopPropagation(); - const resourcesHandler = new ResourcesHandler(this.props.state.section); - const result = await resourcesHandler.getFileContent(item.filename); - const file = { name: item.filename, content: result, path: item.relative_dirname }; + const resourcesHandler = new ResourcesHandler( + this.props.state.section, + ); + const result = await resourcesHandler.getFileContent( + item.filename, + ); + const file = { + name: item.filename, + content: result, + path: item.relative_dirname, + }; this.props.updateFileContent(file); - }catch(error){ + } catch (error) { const options: UIErrorLog = getErrorOptions( error, - 'Files.readFileContent' + 'Files.readFileContent', ); getErrorOrchestrator().handleError(options); } }} - color="primary" + color='primary' /> ); } else { @@ -162,44 +193,58 @@ export default class RulesetColumns { { try { ev.stopPropagation(); - const resourcesHandler = new ResourcesHandler(ResourcesConstants.RULES); - const result = await resourcesHandler.getFileContent(item.filename); - const file = { name: item.filename, content: result, path: item.relative_dirname }; + const resourcesHandler = new ResourcesHandler( + ResourcesConstants.RULES, + ); + const result = await resourcesHandler.getFileContent( + item.filename, + ); + const file = { + name: item.filename, + content: result, + path: item.relative_dirname, + }; this.props.updateFileContent(file); } catch (error) { const options: UIErrorLog = getErrorOptions( error, - 'Files.editFileContent' + 'Files.editFileContent', ); getErrorOrchestrator().handleError(options); } }} - color="primary" + color='primary' /> { try { this.props.removeItems([item]); } catch (error) { const options: UIErrorLog = getErrorOptions( error, - 'Files.deleteFile' + 'Files.deleteFile', ); getErrorOrchestrator().handleError(options); } }} - color="danger" + color='danger' modalTitle={'Are you sure?'} modalProps={{ buttonColor: 'danger', @@ -208,13 +253,14 @@ export default class RulesetColumns {
); } - } - } - ] + }, + }, + ], }; - const getReadButtonPermissions = (item) => { - const { permissionResource } = resourceDictionary[ResourcesConstants.RULES]; + const getReadButtonPermissions = item => { + const { permissionResource } = + resourceDictionary[ResourcesConstants.RULES]; return [ { action: `${ResourcesConstants.RULES}:read`, @@ -223,19 +269,24 @@ export default class RulesetColumns { ]; }; - const getEditButtonPermissions = (item) => { - const { permissionResource } = resourceDictionary[ResourcesConstants.RULES]; + const getEditButtonPermissions = item => { + const { permissionResource } = + resourceDictionary[ResourcesConstants.RULES]; return [ { action: `${ResourcesConstants.RULES}:read`, resource: permissionResource(item.filename), }, - { action: `${ResourcesConstants.RULES}:update`, resource: permissionResource(item.filename) }, + { + action: `${ResourcesConstants.RULES}:update`, + resource: permissionResource(item.filename), + }, ]; }; - const getDeleteButtonPermissions = (item) => { - const { permissionResource } = resourceDictionary[ResourcesConstants.RULES]; + const getDeleteButtonPermissions = item => { + const { permissionResource } = + resourceDictionary[ResourcesConstants.RULES]; return [ { action: `${ResourcesConstants.RULES}:delete`, @@ -247,18 +298,25 @@ export default class RulesetColumns { buildComplianceBadges(item) { const badgeList = []; - const fields = ['pci_dss', 'gpg13', 'hipaa', 'gdpr', 'nist_800_53', 'tsc', 'mitre']; + const fields = [ + 'pci_dss', + 'gpg13', + 'hipaa', + 'gdpr', + 'nist_800_53', + 'tsc', + 'mitre', + ]; const buildBadge = field => { - return ( ev.stopPropagation()} onClickAriaLabel={field.toUpperCase()} style={{ margin: '1px 2px' }} @@ -274,7 +332,7 @@ export default class RulesetColumns { badgeList.push(buildBadge(field)); } } - } catch (error) { } + } catch (error) {} return
{badgeList}
; } diff --git a/plugins/main/public/controllers/management/components/management/ruleset/components/ruleset-suggestions.ts b/plugins/main/public/controllers/management/components/management/ruleset/components/ruleset-suggestions.ts index 87d821e124..31a64e7de2 100644 --- a/plugins/main/public/controllers/management/components/management/ruleset/components/ruleset-suggestions.ts +++ b/plugins/main/public/controllers/management/components/management/ruleset/components/ruleset-suggestions.ts @@ -3,118 +3,209 @@ import { WzRequest } from '../../../../../../react-services/wz-request'; const rulesItems = { field(currentValue) { return [ - {label: 'filename', description: 'filter by filename'}, - {label: 'gdpr', description: 'filter by GDPR requirement'}, - {label: 'gpg13', description: 'filter by GPG requirement'}, - {label: 'groups', description: 'filter by group'}, - {label: 'hipaa', description: 'filter by HIPAA requirement'}, - {label: 'level', description: 'filter by level'}, - {label: 'mitre', description: 'filter by MITRE ATT&CK requirement'}, - {label: 'pci_dss', description: 'filter by PCI DSS requirement'}, - {label: 'relative_dirname', description: 'filter by relative dirname'}, - {label: 'status', description: 'filter by status'}, - {label: 'tsc', description: 'filter by TSC requirement'}, - {label: 'nist-800-53', description: 'filter by NIST requirement'}, + { label: 'filename', description: 'filter by filename' }, + { label: 'gdpr', description: 'filter by GDPR requirement' }, + { label: 'gpg13', description: 'filter by GPG requirement' }, + { label: 'groups', description: 'filter by group' }, + { label: 'hipaa', description: 'filter by HIPAA requirement' }, + { label: 'level', description: 'filter by level' }, + { label: 'mitre', description: 'filter by MITRE ATT&CK requirement' }, + { label: 'nist_800_53', description: 'filter by NIST requirement' }, + { label: 'pci_dss', description: 'filter by PCI DSS requirement' }, + { label: 'relative_dirname', description: 'filter by relative dirname' }, + { label: 'status', description: 'filter by status' }, + { label: 'tsc', description: 'filter by TSC requirement' }, ]; }, value: async (currentValue, { field }) => { - try{ // TODO: distinct + try { switch (field) { case 'status': { - return ['enabled', 'disabled'].map(label => ({label})); + return ['enabled', 'disabled'].map(label => ({ label })); } case 'groups': { - const filter = { limit: 30 }; - if (currentValue) { - filter['search'] = currentValue; - } - const result = await WzRequest.apiReq('GET', '/rules/groups', filter); - return result?.data?.data?.affected_items.map(label => ({label})); + const filter = { + limit: 30, + ...(currentValue ? { search: currentValue } : {}), + }; + const result = await WzRequest.apiReq('GET', '/rules/groups', { + params: filter, + }); + return result?.data?.data?.affected_items.map(label => ({ label })); } case 'level': { - const filter = { limit: 30 }; - if (currentValue) { - filter['search'] = currentValue; - } - const result = await WzRequest.apiReq('GET', '/rules/groups', filter); - return [...Array(16).keys()].map(label => ({label})); + return [...Array(16).keys()].map(label => ({ label })); } case 'filename': { - const filter = { limit: 30 }; - if (currentValue) { - filter['search'] = currentValue; - } - const result = await WzRequest.apiReq('GET', '/rules/files', filter); - return result?.data?.data?.affected_items?.map((item) => ({label: item.filename})); + const filter = { + distinct: true, + limit: 30, + select: field, + sort: `+${field}`, + ...(currentValue ? { q: `${field}~${currentValue}` } : {}), + }; + const result = await WzRequest.apiReq('GET', '/rules/files', { + params: filter, + }); + return result?.data?.data?.affected_items?.map(item => ({ + label: item[field], + })); } case 'relative_dirname': { - const result = await WzRequest.apiReq('GET', '/manager/configuration', { - params: { - section: 'ruleset', - field: 'rule_dir' - } + const filter = { + distinct: true, + limit: 30, + select: field, + sort: `+${field}`, + ...(currentValue ? { q: `${field}~${currentValue}` } : {}), + }; + const result = await WzRequest.apiReq('GET', '/rules', { + params: filter, }); - return result?.data?.data?.affected_items?.[0].ruleset.rule_dir.map(label => ({label})); + return result?.data?.data?.affected_items.map(item => ({ + label: item[field], + })); } case 'hipaa': { - const result = await WzRequest.apiReq('GET', '/rules/requirement/hipaa', {}); - return result?.data?.data?.affected_items.map(label => ({label})); + const filter = { + limit: 30, + ...(currentValue ? { search: currentValue } : {}), + }; + const result = await WzRequest.apiReq( + 'GET', + '/rules/requirement/hipaa', + { params: filter }, + ); + return result?.data?.data?.affected_items.map(label => ({ label })); } case 'gdpr': { - const result = await WzRequest.apiReq('GET', '/rules/requirement/gdpr', {}); - return result?.data?.data?.affected_items.map(label => ({label})); + const filter = { + limit: 30, + ...(currentValue ? { search: currentValue } : {}), + }; + const result = await WzRequest.apiReq( + 'GET', + '/rules/requirement/gdpr', + { params: filter }, + ); + return result?.data?.data?.affected_items.map(label => ({ label })); } - case 'nist-800-53': { - const result = await WzRequest.apiReq('GET', '/rules/requirement/nist-800-53', {}); - return result?.data?.data?.affected_items.map(label => ({label})); + case 'nist_800_53': { + const filter = { + limit: 30, + ...(currentValue ? { search: currentValue } : {}), + }; + const result = await WzRequest.apiReq( + 'GET', + '/rules/requirement/nist-800-53', + { params: filter }, + ); + return result?.data?.data?.affected_items.map(label => ({ label })); } case 'gpg13': { - const result = await WzRequest.apiReq('GET', '/rules/requirement/gpg13', {}); - return result?.data?.data?.affected_items.map(label => ({label})); + const filter = { + limit: 30, + ...(currentValue ? { search: currentValue } : {}), + }; + const result = await WzRequest.apiReq( + 'GET', + '/rules/requirement/gpg13', + { params: filter }, + ); + return result?.data?.data?.affected_items.map(label => ({ label })); } case 'pci_dss': { - const result = await WzRequest.apiReq('GET', '/rules/requirement/pci_dss', {}); - return result?.data?.data?.affected_items.map(label => ({label})); + const filter = { + limit: 30, + ...(currentValue ? { search: currentValue } : {}), + }; + const result = await WzRequest.apiReq( + 'GET', + '/rules/requirement/pci_dss', + { params: filter }, + ); + return result?.data?.data?.affected_items.map(label => ({ label })); } case 'tsc': { - const result = await WzRequest.apiReq('GET', '/rules/requirement/tsc', {}); - return result?.data?.data?.affected_items.map(label => ({label})); + const filter = { + limit: 30, + ...(currentValue ? { search: currentValue } : {}), + }; + const result = await WzRequest.apiReq( + 'GET', + '/rules/requirement/tsc', + { params: filter }, + ); + return result?.data?.data?.affected_items.map(label => ({ label })); } case 'mitre': { - const result = await WzRequest.apiReq('GET', '/rules/requirement/mitre', {}); - return result?.data?.data?.affected_items.map(label => ({label})); + const filter = { + limit: 30, + ...(currentValue ? { search: currentValue } : {}), + }; + const result = await WzRequest.apiReq( + 'GET', + '/rules/requirement/mitre', + { params: filter }, + ); + return result?.data?.data?.affected_items.map(label => ({ label })); } default: return []; } - }catch(error){ + } catch (error) { return []; - }; + } }, }; const rulesFiles = { field(currentValue) { - return ['filename'].map(label => ({label})); + return [ + { label: 'filename', description: 'filter by filename' }, + { label: 'relative_dirname', description: 'filter by relative dirname' }, + ]; }, value: async (currentValue, { field }) => { - try{ // TODO: distinct + try { switch (field) { - case 'filename':{ - const filter = { limit: 30 }; - if (currentValue) { - filter['search'] = currentValue; - } - const result = await WzRequest.apiReq('GET', '/rules/files', filter); - return result?.data?.data?.affected_items?.map((item) => ({label: item.filename})); + case 'filename': { + const filter = { + distinct: true, + limit: 30, + select: field, + sort: `+${field}`, + ...(currentValue ? { q: `${field}~${currentValue}` } : {}), + }; + const result = await WzRequest.apiReq('GET', '/rules/files', { + params: filter, + }); + return result?.data?.data?.affected_items?.map(item => ({ + label: item[field], + })); break; } + case 'relative_dirname': { + const filter = { + distinct: true, + limit: 30, + select: field, + sort: `+${field}`, + ...(currentValue ? { q: `${field}~${currentValue}` } : {}), + }; + const result = await WzRequest.apiReq('GET', '/rules', { + params: filter, + }); + return result?.data?.data?.affected_items.map(item => ({ + label: item[field], + })); + } default: return []; } - }catch(error){ + } catch (error) { return []; - }; + } }, }; diff --git a/plugins/main/public/controllers/management/components/management/ruleset/components/ruleset-table.tsx b/plugins/main/public/controllers/management/components/management/ruleset/components/ruleset-table.tsx index 6ca1e4ce35..df8f432735 100644 --- a/plugins/main/public/controllers/management/components/management/ruleset/components/ruleset-table.tsx +++ b/plugins/main/public/controllers/management/components/management/ruleset/components/ruleset-table.tsx @@ -12,13 +12,20 @@ import React, { useEffect, useState, useCallback } from 'react'; import { getToasts } from '../../../../../../kibana-services'; -import { resourceDictionary, ResourcesConstants, ResourcesHandler } from '../../common/resources-handler'; +import { + resourceDictionary, + ResourcesConstants, + ResourcesHandler, +} from '../../common/resources-handler'; import { getErrorOrchestrator } from '../../../../../../react-services/common-services'; import { UI_ERROR_SEVERITIES } from '../../../../../../react-services/error-orchestrator/types'; import { UI_LOGGER_LEVELS } from '../../../../../../../common/constants'; import { TableWzAPI } from '../../../../../../components/common/tables'; -import { SECTION_RULES_SECTION, SECTION_RULES_KEY } from '../../common/constants'; +import { + SECTION_RULES_SECTION, + SECTION_RULES_KEY, +} from '../../common/constants'; import RulesetColumns from './columns'; import { FlyoutDetail } from './flyout-detail'; import { withUserPermissions } from '../../../../../../components/common/hocs/withUserPermissions'; @@ -28,15 +35,43 @@ import { ManageFiles, AddNewFileButton, UploadFilesButton, -} from '../../common/actions-buttons' +} from '../../common/actions-buttons'; import apiSuggestsItems from './ruleset-suggestions'; -const searchBarWQLOptions = { - searchTermFields: [], // TODO: add search term fields +const searchBarWQLOptions = { + searchTermFields: [ + 'id', + 'description', + 'filename', + 'gdpr', + 'gpg13', + 'groups', + 'level', + 'mitre', + 'nist_800_53', + 'pci_dss', + 'relative_dirname', + 'tsc', + ], + filterButtons: [ + { + id: 'relative-dirname', + input: 'relative_dirname=etc/rules', + label: 'Custom rules', + }, + ], +}; + +const searchBarWQLOptionsFiles = { + searchTermFields: ['filename', 'relative_dirname'], filterButtons: [ - {id: 'relative-dirname', input: 'relative_dirname=etc/rules', label: 'Custom rules'} - ] + { + id: 'relative-dirname', + input: 'relative_dirname=etc/rules', + label: 'Custom rules', + }, + ], }; /*************************************** @@ -49,8 +84,9 @@ const FilesTable = ({ searchBarSuggestions, filters, updateFilters, - reload -}) => ( + +); const RulesFlyoutTable = ({ actionButtons, @@ -88,7 +120,8 @@ const RulesFlyoutTable = ({ closeFlyout, cleanFilters, ...props -}) => <> +}) => ( + <> )} +); /*************************************** * Main component @@ -139,42 +173,43 @@ function RulesetTable({ setShowingFiles, showingFiles, ...props }) { const regex = new RegExp('redirectRule=' + '[^&]*'); const match = window.location.href.match(regex); if (match && match[0]) { - setCurrentItem(parseInt(match[0].split('=')[1])) - setIsFlyoutVisible(true) + setCurrentItem(parseInt(match[0].split('=')[1])); + setIsFlyoutVisible(true); } - }, []) + }, []); // Table custom filter options - const buttonOptions = [{ label: "Custom rules", field: "relative_dirname", value: "etc/rules" },]; + const buttonOptions = [ + { label: 'Custom rules', field: 'relative_dirname', value: 'etc/rules' }, + ]; - const updateFilters = (filters) => { + const updateFilters = filters => { setFilters(filters); - } + }; const cleanFilters = () => { setFilters([]); - } + }; const toggleShowFiles = () => { setFilters([]); setShowingFiles(!showingFiles); - } + }; const closeFlyout = () => { setIsFlyoutVisible(false); - } - + }; /** * Remove files method */ - const removeItems = async (items) => { + const removeItems = async items => { try { const results = items.map(async (item, i) => { await resourcesHandler.deleteFile(item.filename || item.name); }); - Promise.all(results).then((completed) => { + Promise.all(results).then(completed => { setTableFootprint(Date.now()); getToasts().add({ color: 'success', @@ -196,7 +231,7 @@ function RulesetTable({ setShowingFiles, showingFiles, ...props }) { }; getErrorOrchestrator().handleError(options); } - } + }; /** * Columns and Rows properties @@ -205,17 +240,18 @@ function RulesetTable({ setShowingFiles, showingFiles, ...props }) { const rulesetColumns = new RulesetColumns({ removeItems: removeItems, state: { - section: SECTION_RULES_KEY - }, ...props + section: SECTION_RULES_KEY, + }, + ...props, }).columns; const columns = rulesetColumns[showingFiles ? 'files' : SECTION_RULES_KEY]; return columns; - } + }; - const getRowProps = (item) => { + const getRowProps = item => { const { id, name } = item; - const getRequiredPermissions = (item) => { + const getRequiredPermissions = item => { const { permissionResource } = resourceDictionary[SECTION_RULES_KEY]; return [ { @@ -230,12 +266,12 @@ function RulesetTable({ setShowingFiles, showingFiles, ...props }) { className: 'customRowClass', onClick: !WzUserPermissions.checkMissingUserPermissions( getRequiredPermissions(item), - props.userPermissions + props.userPermissions, ) - ? (item) => { - setCurrentItem(id) - setIsFlyoutVisible(true); - } + ? item => { + setCurrentItem(id); + setIsFlyoutVisible(true); + } : undefined, }; }; @@ -260,18 +296,22 @@ function RulesetTable({ setShowingFiles, showingFiles, ...props }) { />, ]; if (showingFiles) - buttons.push( { updateRestartClusterManager && updateRestartClusterManager() }} - />); + buttons.push( + { + updateRestartClusterManager && updateRestartClusterManager(); + }} + />, + ); return buttons; }, [showingFiles]); const actionButtons = buildActionButtons(); return ( -
+
{showingFiles ? ( ) : ( - - )} + + )}
); - } - -export default compose( - withUserPermissions -)(RulesetTable); +export default compose(withUserPermissions)(RulesetTable); From 0cfe1d543ed97ce076fc64f7fa2550424d3e0a2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Tue, 1 Aug 2023 12:02:52 +0200 Subject: [PATCH 48/76] fix: remove exact validation for the token value due to performance problems --- .../search-bar/query-language/wql.tsx | 1014 +++++++++-------- 1 file changed, 562 insertions(+), 452 deletions(-) diff --git a/plugins/main/public/components/search-bar/query-language/wql.tsx b/plugins/main/public/components/search-bar/query-language/wql.tsx index 4776c141e7..8830ff0175 100644 --- a/plugins/main/public/components/search-bar/query-language/wql.tsx +++ b/plugins/main/public/components/search-bar/query-language/wql.tsx @@ -1,5 +1,11 @@ import React from 'react'; -import { EuiButtonEmpty, EuiButtonGroup, EuiPopover, EuiText, EuiCode } from '@elastic/eui'; +import { + EuiButtonEmpty, + EuiButtonGroup, + EuiPopover, + EuiText, + EuiCode, +} from '@elastic/eui'; import { tokenizer as tokenizerUQL } from './aql'; import { PLUGIN_VERSION_SHORT } from '../../../../common/constants'; @@ -50,8 +56,8 @@ const language = { }, conjunction: { literal: { - 'and': 'and', - 'or': 'or', + and: 'and', + or: 'or', }, }, // eslint-disable-next-line camelcase @@ -62,14 +68,14 @@ const language = { }, }, }, - equivalencesToUQL:{ - conjunction:{ - literal:{ - 'and': ';', - 'or': ',', - } - } - } + equivalencesToUQL: { + conjunction: { + literal: { + and: ';', + or: ',', + }, + }, + }, }; // Suggestion mapper by language token type @@ -84,155 +90,162 @@ const suggestionMappingLanguageTokenType = { // eslint-disable-next-line camelcase function_search: { iconType: 'search', color: 'tint5' }, // eslint-disable-next-line camelcase - validation_error: { iconType: 'alert', color: 'tint2' } + validation_error: { iconType: 'alert', color: 'tint2' }, }; /** * Creator of intermediate interface of EuiSuggestItem - * @param type - * @returns + * @param type + * @returns */ -function mapSuggestionCreator(type: ITokenType ){ - return function({...params}){ +function mapSuggestionCreator(type: ITokenType) { + return function ({ ...params }) { return { type, - ...params + ...params, }; }; -}; +} const mapSuggestionCreatorField = mapSuggestionCreator('field'); const mapSuggestionCreatorValue = mapSuggestionCreator('value'); /** * Transform the conjunction to the query language syntax - * @param conjunction - * @returns + * @param conjunction + * @returns */ -function transformQLConjunction(conjunction: string): string{ +function transformQLConjunction(conjunction: string): string { // If the value has a whitespace or comma, then return conjunction === language.equivalencesToUQL.conjunction.literal['and'] ? ` ${language.tokens.conjunction.literal['and']} ` : ` ${language.tokens.conjunction.literal['or']} `; -}; +} /** * Transform the value to the query language syntax - * @param value - * @returns + * @param value + * @returns */ -function transformQLValue(value: string): string{ +function transformQLValue(value: string): string { // If the value has a whitespace or comma, then return /[\s|"]/.test(value) - // Escape the commas (") => (\") and wraps the string with commas ("") - ? `"${value.replace(/"/, '\\"')}"` - // Raw value - : value; -}; + ? // Escape the commas (") => (\") and wraps the string with commas ("") + `"${value.replace(/"/, '\\"')}"` + : // Raw value + value; +} /** * Tokenize the input string. Returns an array with the tokens. * @param input * @returns */ -export function tokenizer(input: string): ITokens{ +export function tokenizer(input: string): ITokens { const re = new RegExp( // A ( character. '(?\\()?' + - // Whitespace - '(?\\s+)?' + - // Field name: name of the field to look on DB. - '(?[\\w.]+)?' + // Added an optional find - // Whitespace - '(?\\s+)?' + - // Operator: looks for '=', '!=', '<', '>' or '~'. - // This seems to be a bug because is not searching the literal valid operators. - // I guess the operator is validated after the regular expression matches - `(?[${Object.keys(language.tokens.operator_compare.literal)}]{1,2})?` + // Added an optional find - // Whitespace - '(?\\s+)?' + - // Value: A string. - // Simple value - // Quoted ", "value, "value", "escaped \"quote" - // Escape quoted string with escaping quotes: https://stackoverflow.com/questions/249791/regex-for-quoted-string-with-escaping-quotes - '(?(?:(?:[^"\\s]+|(?:"(?:[^"\\\\]|\\\\")*")|(?:"(?:[^"\\\\]|\\\\")*)|")))?' + - // Whitespace - '(?\\s+)?' + - // A ) character. - '(?\\))?' + - // Whitespace - '(?\\s+)?' + - `(?${Object.keys(language.tokens.conjunction.literal).join('|')})?` + - // Whitespace - '(?\\s+)?', - 'g' + // Whitespace + '(?\\s+)?' + + // Field name: name of the field to look on DB. + '(?[\\w.]+)?' + // Added an optional find + // Whitespace + '(?\\s+)?' + + // Operator: looks for '=', '!=', '<', '>' or '~'. + // This seems to be a bug because is not searching the literal valid operators. + // I guess the operator is validated after the regular expression matches + `(?[${Object.keys( + language.tokens.operator_compare.literal, + )}]{1,2})?` + // Added an optional find + // Whitespace + '(?\\s+)?' + + // Value: A string. + // Simple value + // Quoted ", "value, "value", "escaped \"quote" + // Escape quoted string with escaping quotes: https://stackoverflow.com/questions/249791/regex-for-quoted-string-with-escaping-quotes + '(?(?:(?:[^"\\s]+|(?:"(?:[^"\\\\]|\\\\")*")|(?:"(?:[^"\\\\]|\\\\")*)|")))?' + + // Whitespace + '(?\\s+)?' + + // A ) character. + '(?\\))?' + + // Whitespace + '(?\\s+)?' + + `(?${Object.keys(language.tokens.conjunction.literal).join( + '|', + )})?` + + // Whitespace + '(?\\s+)?', + 'g', ); - return [ - ...input.matchAll(re) - ].map( - ({groups}) => Object.entries(groups) - .map(([key, value]) => ({ - type: key.startsWith('operator_group') // Transform operator_group group match - ? 'operator_group' - : (key.startsWith('whitespace') // Transform whitespace group match - ? 'whitespace' - : key), - value, - ...( key === 'value' && value && /^"(.+)"$/.test(value) - ? { formattedValue: value.match(/^"(.+)"$/)[1]} - : {} - ) - }) - ) - ).flat(); -}; + return [...input.matchAll(re)] + .map(({ groups }) => + Object.entries(groups).map(([key, value]) => ({ + type: key.startsWith('operator_group') // Transform operator_group group match + ? 'operator_group' + : key.startsWith('whitespace') // Transform whitespace group match + ? 'whitespace' + : key, + value, + ...(key === 'value' && value && /^"(.+)"$/.test(value) + ? { formattedValue: value.match(/^"(.+)"$/)[1] } + : {}), + })), + ) + .flat(); +} type QLOptionSuggestionEntityItem = { - description?: string - label: string + description?: string; + label: string; }; -type QLOptionSuggestionEntityItemTyped = - QLOptionSuggestionEntityItem - & { type: 'operator_group'|'field'|'operator_compare'|'value'|'conjunction'|'function_search' }; +type QLOptionSuggestionEntityItemTyped = QLOptionSuggestionEntityItem & { + type: + | 'operator_group' + | 'field' + | 'operator_compare' + | 'value' + | 'conjunction' + | 'function_search'; +}; type SuggestItem = QLOptionSuggestionEntityItem & { - type: { iconType: string, color: string } + type: { iconType: string; color: string }; }; type QLOptionSuggestionHandler = ( currentValue: string | undefined, - { - field, - operatorCompare, - }: { field: string; operatorCompare: string }, + { field, operatorCompare }: { field: string; operatorCompare: string }, ) => Promise; type OptionsQLImplicitQuery = { - query: string - conjunction: string -} + query: string; + conjunction: string; +}; type OptionsQL = { options?: { - implicitQuery?: OptionsQLImplicitQuery - searchTermFields?: string[] - filterButtons: {id: string, label: string, input: string}[] - } + implicitQuery?: OptionsQLImplicitQuery; + searchTermFields?: string[]; + filterButtons: { id: string; label: string; input: string }[]; + }; suggestions: { field: QLOptionSuggestionHandler; value: QLOptionSuggestionHandler; }; validate?: { value?: { - [key: string]: (token: IToken, nearTokens: {field: string, operator: string}) => string | undefined - } - } + [key: string]: ( + token: IToken, + nearTokens: { field: string; operator: string }, + ) => string | undefined; + }; + }; }; -export interface ISearchBarModeWQL extends OptionsQL{ - id: 'wql' -}; +export interface ISearchBarModeWQL extends OptionsQL { + id: 'wql'; +} /** * Get the last token with value @@ -240,9 +253,7 @@ export interface ISearchBarModeWQL extends OptionsQL{ * @param tokenType token type to search * @returns */ -function getLastTokenDefined( - tokens: ITokens -): IToken | undefined { +function getLastTokenDefined(tokens: ITokens): IToken | undefined { // Reverse the tokens array and use the Array.protorype.find method const shallowCopyArray = Array.from([...tokens]); const shallowCopyArrayReversed = shallowCopyArray.reverse(); @@ -270,32 +281,37 @@ function getLastTokenDefinedByType( ({ type, value }) => type === tokenType && value, ); return tokenFound; -}; +} /** * Get the token that is near to a token position of the token type. - * @param tokens - * @param tokenReferencePosition - * @param tokenType - * @param mode - * @returns + * @param tokens + * @param tokenReferencePosition + * @param tokenType + * @param mode + * @returns */ function getTokenNearTo( tokens: ITokens, tokenType: ITokenType, - mode : 'previous' | 'next' = 'previous', - options : {tokenReferencePosition?: number, tokenFoundShouldHaveValue?: boolean} = {} + mode: 'previous' | 'next' = 'previous', + options: { + tokenReferencePosition?: number; + tokenFoundShouldHaveValue?: boolean; + } = {}, ): IToken | undefined { const shallowCopyTokens = Array.from([...tokens]); - const computedShallowCopyTokens = mode === 'previous' - ? shallowCopyTokens.slice(0, options?.tokenReferencePosition || tokens.length).reverse() - : shallowCopyTokens.slice(options?.tokenReferencePosition || 0); - return computedShallowCopyTokens - .find(({type, value}) => - type === tokenType - && (options?.tokenFoundShouldHaveValue ? value : true) - ); -}; + const computedShallowCopyTokens = + mode === 'previous' + ? shallowCopyTokens + .slice(0, options?.tokenReferencePosition || tokens.length) + .reverse() + : shallowCopyTokens.slice(options?.tokenReferencePosition || 0); + return computedShallowCopyTokens.find( + ({ type, value }) => + type === tokenType && (options?.tokenFoundShouldHaveValue ? value : true), + ); +} /** * Get the suggestions from the tokens @@ -304,7 +320,10 @@ function getTokenNearTo( * @param options * @returns */ -export async function getSuggestions(tokens: ITokens, options: OptionsQL): Promise { +export async function getSuggestions( + tokens: ITokens, + options: OptionsQL, +): Promise { if (!tokens.length) { return []; } @@ -313,8 +332,8 @@ export async function getSuggestions(tokens: ITokens, options: OptionsQL): Promi const lastToken = getLastTokenDefined(tokens); // If it can't get a token with value, then returns fields and open operator group - if(!lastToken?.type){ - return [ + if (!lastToken?.type) { + return [ // Search function { type: 'function_search', @@ -327,36 +346,38 @@ export async function getSuggestions(tokens: ITokens, options: OptionsQL): Promi type: 'operator_group', label: '(', description: language.tokens.operator_group.literal['('], - } + }, ]; - }; + } switch (lastToken.type) { case 'field': return [ // fields that starts with the input but is not equals - ...(await options.suggestions.field()).filter( - ({ label }) => - label.startsWith(lastToken.value) && label !== lastToken.value, - ).map(mapSuggestionCreatorField), + ...(await options.suggestions.field()) + .filter( + ({ label }) => + label.startsWith(lastToken.value) && label !== lastToken.value, + ) + .map(mapSuggestionCreatorField), // operators if the input field is exact ...((await options.suggestions.field()).some( ({ label }) => label === lastToken.value, ) ? [ - ...Object.keys(language.tokens.operator_compare.literal).map( - operator => ({ - type: 'operator_compare', - label: operator, - description: - language.tokens.operator_compare.literal[operator], - }), - ), - ] + ...Object.keys(language.tokens.operator_compare.literal).map( + operator => ({ + type: 'operator_compare', + label: operator, + description: + language.tokens.operator_compare.literal[operator], + }), + ), + ] : []), ]; break; - case 'operator_compare':{ + case 'operator_compare': { const field = getLastTokenDefinedByType(tokens, 'field')?.value; const operatorCompare = getLastTokenDefinedByType( tokens, @@ -365,9 +386,9 @@ export async function getSuggestions(tokens: ITokens, options: OptionsQL): Promi // If there is no a previous field, then no return suggestions because it would be an syntax // error - if(!field){ + if (!field) { return []; - }; + } return [ ...Object.keys(language.tokens.operator_compare.literal) @@ -385,16 +406,18 @@ export async function getSuggestions(tokens: ITokens, options: OptionsQL): Promi operator => operator === lastToken.value, ) ? [ - ...(await options.suggestions.value(undefined, { - field, - operatorCompare, - })).map(mapSuggestionCreatorValue), - ] + ...( + await options.suggestions.value(undefined, { + field, + operatorCompare, + }) + ).map(mapSuggestionCreatorValue), + ] : []), ]; break; } - case 'value':{ + case 'value': { const field = getLastTokenDefinedByType(tokens, 'field')?.value; const operatorCompare = getLastTokenDefinedByType( tokens, @@ -403,26 +426,28 @@ export async function getSuggestions(tokens: ITokens, options: OptionsQL): Promi /* If there is no a previous field or operator_compare, then no return suggestions because it would be an syntax error */ - if(!field || !operatorCompare){ + if (!field || !operatorCompare) { return []; - }; + } return [ ...(lastToken.value ? [ - { - type: 'function_search', - label: 'Search', - description: 'run the search query', - }, - ] + { + type: 'function_search', + label: 'Search', + description: 'run the search query', + }, + ] : []), - ...(await options.suggestions.value(lastToken.value, { - field, - operatorCompare, - })).map(mapSuggestionCreatorValue), + ...( + await options.suggestions.value(lastToken.value, { + field, + operatorCompare, + }) + ).map(mapSuggestionCreatorValue), ...Object.entries(language.tokens.conjunction.literal).map( - ([ conjunction, description]) => ({ + ([conjunction, description]) => ({ type: 'conjunction', label: conjunction, description, @@ -454,8 +479,10 @@ export async function getSuggestions(tokens: ITokens, options: OptionsQL): Promi conjunction => conjunction === lastToken.value, ) ? [ - ...(await options.suggestions.field()).map(mapSuggestionCreatorField), - ] + ...(await options.suggestions.field()).map( + mapSuggestionCreatorField, + ), + ] : []), { type: 'operator_group', @@ -493,16 +520,18 @@ export async function getSuggestions(tokens: ITokens, options: OptionsQL): Promi /** * Transform the suggestion object to the expected object by EuiSuggestItem - * @param param0 - * @returns + * @param param0 + * @returns */ -export function transformSuggestionToEuiSuggestItem(suggestion: QLOptionSuggestionEntityItemTyped): SuggestItem{ - const { type, ...rest} = suggestion; +export function transformSuggestionToEuiSuggestItem( + suggestion: QLOptionSuggestionEntityItemTyped, +): SuggestItem { + const { type, ...rest } = suggestion; return { type: { ...suggestionMappingLanguageTokenType[type] }, - ...rest + ...rest, }; -}; +} /** * Transform the suggestion object to the expected object by EuiSuggestItem @@ -510,21 +539,21 @@ export function transformSuggestionToEuiSuggestItem(suggestion: QLOptionSuggesti * @returns */ function transformSuggestionsToEuiSuggestItem( - suggestions: QLOptionSuggestionEntityItemTyped[] + suggestions: QLOptionSuggestionEntityItemTyped[], ): SuggestItem[] { return suggestions.map(transformSuggestionToEuiSuggestItem); -}; +} /** * Transform the UQL (Unified Query Language) to QL - * @param input - * @returns + * @param input + * @returns */ -export function transformUQLToQL(input: string){ +export function transformUQLToQL(input: string) { const tokens = tokenizerUQL(input); return tokens - .filter(({value}) => value) - .map(({type, value}) => { + .filter(({ value }) => value) + .map(({ type, value }) => { switch (type) { case 'conjunction': return transformQLConjunction(value); @@ -536,43 +565,52 @@ export function transformUQLToQL(input: string){ return value; break; } - } - ).join(''); -}; + }) + .join(''); +} -export function shouldUseSearchTerm(tokens: ITokens): boolean{ +export function shouldUseSearchTerm(tokens: ITokens): boolean { return !( - tokens.some(({type, value}) => type === 'operator_compare' && value ) - && tokens.some(({type, value}) => type === 'field' && value ) + tokens.some(({ type, value }) => type === 'operator_compare' && value) && + tokens.some(({ type, value }) => type === 'field' && value) ); -}; +} -export function transformToSearchTerm(searchTermFields: string[], input: string): string{ - return searchTermFields.map(searchTermField => `${searchTermField}~${input}`).join(','); -}; +export function transformToSearchTerm( + searchTermFields: string[], + input: string, +): string { + return searchTermFields + .map(searchTermField => `${searchTermField}~${input}`) + .join(','); +} /** * Transform the input in QL to UQL (Unified Query Language) - * @param input - * @returns + * @param input + * @returns */ -export function transformSpecificQLToUnifiedQL(input: string, searchTermFields: string[]){ +export function transformSpecificQLToUnifiedQL( + input: string, + searchTermFields: string[], +) { const tokens = tokenizer(input); - if(input && searchTermFields && shouldUseSearchTerm(tokens)){ + if (input && searchTermFields && shouldUseSearchTerm(tokens)) { return transformToSearchTerm(searchTermFields, input); - }; + } return tokens - .filter(({type, value}) => type !== 'whitespace' && value) - .map(({type, value}) => { + .filter(({ type, value }) => type !== 'whitespace' && value) + .map(({ type, value }) => { switch (type) { - case 'value':{ + case 'value': { // Value is wrapped with " - let [ _, extractedValue ] = value.match(/^"(.+)"$/) || [ null, null ]; + let [_, extractedValue] = value.match(/^"(.+)"$/) || [null, null]; // Replace the escaped comma (\") by comma (") // WARN: This could cause a problem with value that contains this sequence \" - extractedValue && (extractedValue = extractedValue.replace(/\\"/g, '"')); + extractedValue && + (extractedValue = extractedValue.replace(/\\"/g, '"')); return extractedValue || value; break; } @@ -585,9 +623,9 @@ export function transformSpecificQLToUnifiedQL(input: string, searchTermFields: return value; break; } - } - ).join(''); -}; + }) + .join(''); +} /** * Get the output from the input @@ -597,21 +635,20 @@ export function transformSpecificQLToUnifiedQL(input: string, searchTermFields: function getOutput(input: string, options: OptionsQL) { // Implicit query const implicitQueryAsUQL = options?.options?.implicitQuery?.query ?? ''; - const implicitQueryAsQL = transformUQLToQL( - implicitQueryAsUQL - ); + const implicitQueryAsQL = transformUQLToQL(implicitQueryAsUQL); // Implicit query conjunction - const implicitQueryConjunctionAsUQL = options?.options?.implicitQuery?.conjunction ?? ''; + const implicitQueryConjunctionAsUQL = + options?.options?.implicitQuery?.conjunction ?? ''; const implicitQueryConjunctionAsQL = transformUQLToQL( - implicitQueryConjunctionAsUQL + implicitQueryConjunctionAsUQL, ); // User input query const inputQueryAsQL = input; const inputQueryAsUQL = transformSpecificQLToUnifiedQL( inputQueryAsQL, - options?.options?.searchTermFields ?? [] + options?.options?.searchTermFields ?? [], ); return { @@ -619,203 +656,245 @@ function getOutput(input: string, options: OptionsQL) { apiQuery: { q: [ implicitQueryAsUQL, - implicitQueryAsUQL && inputQueryAsUQL ? implicitQueryConjunctionAsUQL : '', - implicitQueryAsUQL && inputQueryAsUQL ? `(${inputQueryAsUQL})`: inputQueryAsUQL + implicitQueryAsUQL && inputQueryAsUQL + ? implicitQueryConjunctionAsUQL + : '', + implicitQueryAsUQL && inputQueryAsUQL + ? `(${inputQueryAsUQL})` + : inputQueryAsUQL, ].join(''), }, query: [ implicitQueryAsQL, implicitQueryAsQL && inputQueryAsQL ? implicitQueryConjunctionAsQL : '', - implicitQueryAsQL && inputQueryAsQL ? `(${inputQueryAsQL})`: inputQueryAsQL - ].join('') + implicitQueryAsQL && inputQueryAsQL + ? `(${inputQueryAsQL})` + : inputQueryAsQL, + ].join(''), }; -}; +} /** * Validate the token value - * @param token - * @returns + * @param token + * @returns */ function validateTokenValue(token: IToken): string | undefined { - // Usage of API regular expression - const re = new RegExp( - // Value: A string. - '^(?(?:(?:\\((?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|[\\[\\]\\w _\\-.:?\\/\'"=@%<>{}]*)\\))*' + - '(?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|[\\[\\]\\w _\\-.:?\\\\/\'"=@%<>{}]+)' + - '(?:\\((?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|[\\[\\]\\w _\\-.:?\\\\/\'"=@%<>{}]*)\\))*)+)$' - ); - - /* WARN: the validation for the value token is complex, this supports some characters in - certain circumstances. - - Ideally a character validation helps to the user to identify the problem in the query, but - as the original regular expression is so complex, the logic to get this can be - complicated. - - The original regular expression has a common schema of allowed characters, these and other - characters of the original regular expression can be used to check each character. This - approach can identify some invalid characters despite this is not the ideal way. - - The ideal solution will be check each subset of the complex regex against the allowed - characters. - */ - - const invalidCharacters: string[] = token.value.split('') - .filter((character) => !(new RegExp('[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}\\(\\)]').test(character))) - .filter((value, index, array) => array.indexOf(value) === index); + const invalidCharacters: string[] = token.value + .split('') + .filter((value, index, array) => array.indexOf(value) === index) + .filter( + character => + !new RegExp('[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}\\(\\)]').test( + character, + ), + ); - const match = token.value.match(re); - return match?.groups?.value === token.value + return invalidCharacters.length ? undefined : [ - `"${token.value}" is not a valid value.`, - ...(invalidCharacters.length - ? [`Invalid characters found: ${invalidCharacters.join(', ')}`] - : [] - ) - ].join(' '); -}; + `"${token.value}" is not a valid value.`, + ...(invalidCharacters.length + ? [`Invalid characters found: ${invalidCharacters.join(', ')}`] + : []), + ].join(' '); +} -type ITokenValidator = (tokenValue: IToken, proximityTokens: any) => string | undefined; +type ITokenValidator = ( + tokenValue: IToken, + proximityTokens: any, +) => string | undefined; /** * Validate the tokens while the user is building the query - * @param tokens - * @param validate - * @returns + * @param tokens + * @param validate + * @returns */ -function validatePartial(tokens: ITokens, validate: {field: ITokenValidator, value: ITokenValidator}): undefined | string{ +function validatePartial( + tokens: ITokens, + validate: { field: ITokenValidator; value: ITokenValidator }, +): undefined | string { // Ensure is not in search term mode - if (!shouldUseSearchTerm(tokens)){ - return tokens.map((token: IToken, index) => { - if(token.value){ - if(token.type === 'field'){ - // Ensure there is a operator next to field to check if the fields is valid or not. - // This allows the user can type the field token and get the suggestions for the field. - const tokenOperatorNearToField = getTokenNearTo( - tokens, - 'operator_compare', - 'next', - { tokenReferencePosition: index, tokenFoundShouldHaveValue: true } - ); - return tokenOperatorNearToField - ? validate.field(token) - : undefined; - }; - // Check if the value is allowed - if(token.type === 'value'){ - const tokenFieldNearToValue = getTokenNearTo( - tokens, - 'field', - 'previous', - { tokenReferencePosition: index, tokenFoundShouldHaveValue: true } - ); - const tokenOperatorCompareNearToValue = getTokenNearTo( - tokens, - 'operator_compare', - 'previous', - { tokenReferencePosition: index, tokenFoundShouldHaveValue: true } - ); - return validateTokenValue(token) - || (tokenFieldNearToValue && tokenOperatorCompareNearToValue && validate.value ? validate.value(token, { - field: tokenFieldNearToValue?.value, - operator: tokenOperatorCompareNearToValue?.value - }) : undefined); - } - }; - }) - .filter(t => typeof t !== 'undefined') - .join('\n') || undefined; + if (!shouldUseSearchTerm(tokens)) { + return ( + tokens + .map((token: IToken, index) => { + if (token.value) { + if (token.type === 'field') { + // Ensure there is a operator next to field to check if the fields is valid or not. + // This allows the user can type the field token and get the suggestions for the field. + const tokenOperatorNearToField = getTokenNearTo( + tokens, + 'operator_compare', + 'next', + { + tokenReferencePosition: index, + tokenFoundShouldHaveValue: true, + }, + ); + return tokenOperatorNearToField + ? validate.field(token) + : undefined; + } + // Check if the value is allowed + if (token.type === 'value') { + const tokenFieldNearToValue = getTokenNearTo( + tokens, + 'field', + 'previous', + { + tokenReferencePosition: index, + tokenFoundShouldHaveValue: true, + }, + ); + const tokenOperatorCompareNearToValue = getTokenNearTo( + tokens, + 'operator_compare', + 'previous', + { + tokenReferencePosition: index, + tokenFoundShouldHaveValue: true, + }, + ); + return ( + validateTokenValue(token) || + (tokenFieldNearToValue && + tokenOperatorCompareNearToValue && + validate.value + ? validate.value(token, { + field: tokenFieldNearToValue?.value, + operator: tokenOperatorCompareNearToValue?.value, + }) + : undefined) + ); + } + } + }) + .filter(t => typeof t !== 'undefined') + .join('\n') || undefined + ); } -}; +} /** - * Validate the tokens if they are a valid syntax - * @param tokens - * @param validate - * @returns + * Validate the tokens if they are a valid syntax + * @param tokens + * @param validate + * @returns */ -function validate(tokens: ITokens, validate: {field: ITokenValidator, value: ITokenValidator}): undefined | string[]{ - if (!shouldUseSearchTerm(tokens)){ - const errors = tokens.map((token: IToken, index) => { - const errors = []; - if(token.value){ - if(token.type === 'field'){ - const tokenOperatorNearToField = getTokenNearTo( - tokens, - 'operator_compare', - 'next', - { tokenReferencePosition: index, tokenFoundShouldHaveValue: true } - ); - const tokenValueNearToField = getTokenNearTo( - tokens, - 'value', - 'next', - { tokenReferencePosition: index, tokenFoundShouldHaveValue: true } - ); - if(validate.field(token)){ - errors.push(`"${token.value}" is not a valid field.`); - }else if(!tokenOperatorNearToField){ - errors.push(`The operator for field "${token.value}" is missing.`); - }else if(!tokenValueNearToField){ - errors.push(`The value for field "${token.value}" is missing.`); +function validate( + tokens: ITokens, + validate: { field: ITokenValidator; value: ITokenValidator }, +): undefined | string[] { + if (!shouldUseSearchTerm(tokens)) { + const errors = tokens + .map((token: IToken, index) => { + const errors = []; + if (token.value) { + if (token.type === 'field') { + const tokenOperatorNearToField = getTokenNearTo( + tokens, + 'operator_compare', + 'next', + { + tokenReferencePosition: index, + tokenFoundShouldHaveValue: true, + }, + ); + const tokenValueNearToField = getTokenNearTo( + tokens, + 'value', + 'next', + { + tokenReferencePosition: index, + tokenFoundShouldHaveValue: true, + }, + ); + if (validate.field(token)) { + errors.push(`"${token.value}" is not a valid field.`); + } else if (!tokenOperatorNearToField) { + errors.push( + `The operator for field "${token.value}" is missing.`, + ); + } else if (!tokenValueNearToField) { + errors.push(`The value for field "${token.value}" is missing.`); + } } - }; - // Check if the value is allowed - if(token.type === 'value'){ - const tokenFieldNearToValue = getTokenNearTo( - tokens, - 'field', - 'previous', - { tokenReferencePosition: index, tokenFoundShouldHaveValue: true } - ); - const tokenOperatorCompareNearToValue = getTokenNearTo( - tokens, - 'operator_compare', - 'previous', - { tokenReferencePosition: index, tokenFoundShouldHaveValue: true } - ); - const validationError = validateTokenValue(token) - || (tokenFieldNearToValue && tokenOperatorCompareNearToValue && validate.value ? validate.value(token, { - field: tokenFieldNearToValue?.value, - operator: tokenOperatorCompareNearToValue?.value - }) : undefined);; - - validationError && errors.push(validationError); - }; - - // Check if the value is allowed - if(token.type === 'conjunction'){ - - const tokenWhitespaceNearToFieldNext = getTokenNearTo( - tokens, - 'whitespace', - 'next', - { tokenReferencePosition: index } - ); - const tokenFieldNearToFieldNext = getTokenNearTo( - tokens, - 'field', - 'next', - { tokenReferencePosition: index, tokenFoundShouldHaveValue: true } - ); - !tokenWhitespaceNearToFieldNext?.value?.length - && errors.push(`There is no whitespace after conjunction "${token.value}".`); - !tokenFieldNearToFieldNext?.value?.length - && errors.push(`There is no sentence after conjunction "${token.value}".`); - }; - }; - return errors.length ? errors : undefined; - }).filter(errors => errors) - .flat() + // Check if the value is allowed + if (token.type === 'value') { + const tokenFieldNearToValue = getTokenNearTo( + tokens, + 'field', + 'previous', + { + tokenReferencePosition: index, + tokenFoundShouldHaveValue: true, + }, + ); + const tokenOperatorCompareNearToValue = getTokenNearTo( + tokens, + 'operator_compare', + 'previous', + { + tokenReferencePosition: index, + tokenFoundShouldHaveValue: true, + }, + ); + const validationError = + validateTokenValue(token) || + (tokenFieldNearToValue && + tokenOperatorCompareNearToValue && + validate.value + ? validate.value(token, { + field: tokenFieldNearToValue?.value, + operator: tokenOperatorCompareNearToValue?.value, + }) + : undefined); + + validationError && errors.push(validationError); + } + + // Check if the value is allowed + if (token.type === 'conjunction') { + const tokenWhitespaceNearToFieldNext = getTokenNearTo( + tokens, + 'whitespace', + 'next', + { tokenReferencePosition: index }, + ); + const tokenFieldNearToFieldNext = getTokenNearTo( + tokens, + 'field', + 'next', + { + tokenReferencePosition: index, + tokenFoundShouldHaveValue: true, + }, + ); + !tokenWhitespaceNearToFieldNext?.value?.length && + errors.push( + `There is no whitespace after conjunction "${token.value}".`, + ); + !tokenFieldNearToFieldNext?.value?.length && + errors.push( + `There is no sentence after conjunction "${token.value}".`, + ); + } + } + return errors.length ? errors : undefined; + }) + .filter(errors => errors) + .flat(); return errors.length ? errors : undefined; - }; + } return undefined; -}; +} export const WQL = { id: 'wql', label: 'WQL', - description: 'WQL (Wazuh Query Language) provides a human query syntax based on the Wazuh API query language.', + description: + 'WQL (Wazuh Query Language) provides a human query syntax based on the Wazuh API query language.', documentationLink: `https://github.com/wazuh/wazuh-kibana-app/blob/v${PLUGIN_VERSION_SHORT}/public/components/search-bar/query-language/wql.md`, getConfiguration() { return { @@ -827,21 +906,29 @@ export const WQL = { const tokens: ITokens = tokenizer(input); // Get the implicit query as query language syntax - const implicitQueryAsQL = params.queryLanguage.parameters?.options?.implicitQuery + const implicitQueryAsQL = params.queryLanguage.parameters?.options + ?.implicitQuery ? transformUQLToQL( - params.queryLanguage.parameters.options.implicitQuery.query - + params.queryLanguage.parameters.options.implicitQuery.conjunction - ) + params.queryLanguage.parameters.options.implicitQuery.query + + params.queryLanguage.parameters.options.implicitQuery.conjunction, + ) : ''; - const fieldsSuggestion: string[] = await params.queryLanguage.parameters.suggestions.field() - .map(({label}) => label); + const fieldsSuggestion: string[] = + await params.queryLanguage.parameters.suggestions + .field() + .map(({ label }) => label); const validators = { - field: ({value}) => fieldsSuggestion.includes(value) ? undefined : `"${value}" is not valid field.`, - ...(params.queryLanguage.parameters?.validate?.value ? { - value: params.queryLanguage.parameters?.validate?.value - } : {}) + field: ({ value }) => + fieldsSuggestion.includes(value) + ? undefined + : `"${value}" is not valid field.`, + ...(params.queryLanguage.parameters?.validate?.value + ? { + value: params.queryLanguage.parameters?.validate?.value, + } + : {}), }; // Validate the user input @@ -852,63 +939,79 @@ export const WQL = { // Get the output of query language const output = { ...getOutput(input, params.queryLanguage.parameters), - error: validationStrict + error: validationStrict, }; const onSearch = () => { - if(output?.error){ - params.setQueryLanguageOutput((state) => ({ + if (output?.error) { + params.setQueryLanguageOutput(state => ({ ...state, searchBarProps: { ...state.searchBarProps, suggestions: transformSuggestionsToEuiSuggestItem( - output.error.map(error => ({type: 'validation_error', label: 'Invalid', description: error})) - ) - } + output.error.map(error => ({ + type: 'validation_error', + label: 'Invalid', + description: error, + })), + ), + }, })); - }else{ + } else { params.onSearch(output); - }; + } }; return { - filterButtons: params.queryLanguage.parameters?.options?.filterButtons - ? ( - { id, label } - ))} + filterButtons: params.queryLanguage.parameters?.options?.filterButtons ? ( + ({ id, label }), + )} idToSelectedMap={{}} - type="multi" + type='multi' onChange={(id: string) => { - const buttonParams = params.queryLanguage.parameters?.options?.filterButtons.find(({id: buttonID}) => buttonID === id); - if(buttonParams){ + const buttonParams = + params.queryLanguage.parameters?.options?.filterButtons.find( + ({ id: buttonID }) => buttonID === id, + ); + if (buttonParams) { params.setInput(buttonParams.input); const output = { - ...getOutput(buttonParams.input, params.queryLanguage.parameters), - error: undefined + ...getOutput( + buttonParams.input, + params.queryLanguage.parameters, + ), + error: undefined, }; params.onSearch(output); } }} /> - : null, + ) : null, searchBarProps: { // Props that will be used by the EuiSuggest component // Suggestions suggestions: transformSuggestionsToEuiSuggestItem( validationPartial - ? [{ type: 'validation_error', label: 'Invalid', description: validationPartial}] - : await getSuggestions(tokens, params.queryLanguage.parameters) + ? [ + { + type: 'validation_error', + label: 'Invalid', + description: validationPartial, + }, + ] + : await getSuggestions(tokens, params.queryLanguage.parameters), ), // Handler to manage when clicking in a suggestion item onItemClick: item => { // There is an error, clicking on the item does nothing - if (item.type.iconType === 'alert'){ + if (item.type.iconType === 'alert') { return; - }; + } // When the clicked item has the `search` iconType, run the `onSearch` function if (item.type.iconType === 'search') { // Execute the search action @@ -918,51 +1021,58 @@ export const WQL = { const lastToken: IToken | undefined = getLastTokenDefined(tokens); // if the clicked suggestion is of same type of last token if ( - lastToken && suggestionMappingLanguageTokenType[lastToken.type].iconType === - item.type.iconType + lastToken && + suggestionMappingLanguageTokenType[lastToken.type].iconType === + item.type.iconType ) { // replace the value of last token with the current one. // if the current token is a value, then transform it - lastToken.value = item.type.iconType === suggestionMappingLanguageTokenType.value.iconType - ? transformQLValue(item.label) - : item.label; + lastToken.value = + item.type.iconType === + suggestionMappingLanguageTokenType.value.iconType + ? transformQLValue(item.label) + : item.label; } else { // add a whitespace for conjunction - !(/\s$/.test(input)) - && ( - item.type.iconType === suggestionMappingLanguageTokenType.conjunction.iconType - || lastToken?.type === 'conjunction' - ) - && tokens.push({ - type: 'whitespace', - value: ' ' - }); + !/\s$/.test(input) && + (item.type.iconType === + suggestionMappingLanguageTokenType.conjunction.iconType || + lastToken?.type === 'conjunction') && + tokens.push({ + type: 'whitespace', + value: ' ', + }); // add a new token of the selected type and value tokens.push({ type: Object.entries(suggestionMappingLanguageTokenType).find( ([, { iconType }]) => iconType === item.type.iconType, )[0], - value: item.type.iconType === suggestionMappingLanguageTokenType.value.iconType - ? transformQLValue(item.label) - : item.label, + value: + item.type.iconType === + suggestionMappingLanguageTokenType.value.iconType + ? transformQLValue(item.label) + : item.label, }); // add a whitespace for conjunction - item.type.iconType === suggestionMappingLanguageTokenType.conjunction.iconType - && tokens.push({ - type: 'whitespace', - value: ' ' - }); - }; + item.type.iconType === + suggestionMappingLanguageTokenType.conjunction.iconType && + tokens.push({ + type: 'whitespace', + value: ' ', + }); + } // Change the input - params.setInput(tokens - .filter(value => value) // Ensure the input is rebuilt using tokens with value. - // The input tokenization can contain tokens with no value due to the used - // regular expression. - .map(({ value }) => value) - .join('')); + params.setInput( + tokens + .filter(value => value) // Ensure the input is rebuilt using tokens with value. + // The input tokenization can contain tokens with no value due to the used + // regular expression. + .map(({ value }) => value) + .join(''), + ); } }, prepend: implicitQueryAsQL ? ( @@ -978,9 +1088,7 @@ export const WQL = { } iconType='filter' > - - {implicitQueryAsQL} - + {implicitQueryAsQL} } isOpen={ @@ -994,8 +1102,7 @@ export const WQL = { } > - Implicit query:{' '} - {implicitQueryAsQL} + Implicit query: {implicitQueryAsQL} This query is added to the input. @@ -1008,22 +1115,25 @@ export const WQL = { // Show the input is invalid isInvalid: Boolean(validationStrict), // Define the handler when the a key is pressed while the input is focused - onKeyPress: (event) => { + onKeyPress: event => { if (event.key === 'Enter') { onSearch(); - }; - } + } + }, }, - output + output, }; }, - transformInput: (unifiedQuery: string, {parameters}) => { - const input = unifiedQuery && parameters?.options?.implicitQuery - ? unifiedQuery.replace( - new RegExp(`^${parameters.options.implicitQuery.query}${parameters.options.implicitQuery.conjunction}`), - '' - ) - : unifiedQuery; + transformInput: (unifiedQuery: string, { parameters }) => { + const input = + unifiedQuery && parameters?.options?.implicitQuery + ? unifiedQuery.replace( + new RegExp( + `^${parameters.options.implicitQuery.query}${parameters.options.implicitQuery.conjunction}`, + ), + '', + ) + : unifiedQuery; return transformUQLToQL(input); }, From 3502d0b1f341b9739e5db9cf9907943a3554cfce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Tue, 1 Aug 2023 13:49:05 +0200 Subject: [PATCH 49/76] fix: fix token value validation --- .../main/public/components/search-bar/query-language/wql.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/main/public/components/search-bar/query-language/wql.tsx b/plugins/main/public/components/search-bar/query-language/wql.tsx index 8830ff0175..a8f136b3ff 100644 --- a/plugins/main/public/components/search-bar/query-language/wql.tsx +++ b/plugins/main/public/components/search-bar/query-language/wql.tsx @@ -690,7 +690,7 @@ function validateTokenValue(token: IToken): string | undefined { ), ); - return invalidCharacters.length + return !invalidCharacters.length ? undefined : [ `"${token.value}" is not a valid value.`, From 1d9563f1e9acffcfee19e2d3c3a048020f89c904 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Wed, 2 Aug 2023 10:27:16 +0200 Subject: [PATCH 50/76] fix: add the suggestions to the search bar of the explore agent modal table --- .../agents-selection-table.js | 154 +++++++++++------- 1 file changed, 98 insertions(+), 56 deletions(-) diff --git a/plugins/main/public/controllers/overview/components/overview-actions/agents-selection-table.js b/plugins/main/public/controllers/overview/components/overview-actions/agents-selection-table.js index 580ad47c15..4cf7fc5d7b 100644 --- a/plugins/main/public/controllers/overview/components/overview-actions/agents-selection-table.js +++ b/plugins/main/public/controllers/overview/components/overview-actions/agents-selection-table.js @@ -10,9 +10,11 @@ import { WzRequest } from '../../../../react-services/wz-request'; import { updateCurrentAgentData } from '../../../../redux/actions/appStateActions'; import store from '../../../../redux/store'; import { GroupTruncate } from '../../../../components/common/util/agent-group-truncate/'; -import { getAgentFilterValues } from '../../../../controllers/management/components/management/groups/get-agents-filters-values'; -import _ from 'lodash'; -import { UI_LOGGER_LEVELS, UI_ORDER_AGENT_STATUS } from '../../../../../common/constants'; +import { get as getLodash } from 'lodash'; +import { + UI_LOGGER_LEVELS, + UI_ORDER_AGENT_STATUS, +} from '../../../../../common/constants'; import { UI_ERROR_SEVERITIES } from '../../../../react-services/error-orchestrator/types'; import { getErrorOrchestrator } from '../../../../react-services/common-services'; import { AgentStatus } from '../../../../components/agents/agent_status'; @@ -21,15 +23,15 @@ import { TableWzAPI } from '../../../../components/common/tables'; const searchBarWQLOptions = { implicitQuery: { query: 'id!=000', - conjunction: ';' - } + conjunction: ';', + }, }; export class AgentSelectionTable extends Component { constructor(props) { super(props); this.state = { - filters: { default: {q: 'id!=000'}} + filters: { default: { q: 'id!=000' } }, }; this.columns = [ @@ -44,14 +46,14 @@ export class AgentSelectionTable extends Component { field: 'name', name: 'Name', searchable: true, - sortable: true + sortable: true, }, { field: 'group', name: 'Group', sortable: true, searchable: true, - render: groups => this.renderGroups(groups) + render: groups => this.renderGroups(groups), }, { field: 'version', @@ -66,7 +68,7 @@ export class AgentSelectionTable extends Component { name: 'Operating system', sortable: true, searchable: true, - render: (field, agentData) => this.addIconPlatformRender(agentData) + render: (field, agentData) => this.addIconPlatformRender(agentData), }, { field: 'status', @@ -74,23 +76,27 @@ export class AgentSelectionTable extends Component { searchable: true, sortable: true, width: 'auto', - render: status => , + render: status => ( + + ), }, ]; } - unselectAgents(){ + unselectAgents() { store.dispatch(updateCurrentAgentData({})); this.props.removeAgentsFilter(); } - async selectAgentAndApply(agentID){ - try{ - const data = await WzRequest.apiReq('GET', '/agents', { params: { q: 'id=' + agentID}}); - const formattedData = data?.data?.data?.affected_items?.[0] + async selectAgentAndApply(agentID) { + try { + const data = await WzRequest.apiReq('GET', '/agents', { + params: { q: 'id=' + agentID }, + }); + const formattedData = data?.data?.data?.affected_items?.[0]; store.dispatch(updateCurrentAgentData(formattedData)); this.props.updateAgentSearch([agentID]); - } catch(error) { + } catch (error) { store.dispatch(updateCurrentAgentData({})); this.props.removeAgentsFilter(true); const options = { @@ -108,7 +114,6 @@ export class AgentSelectionTable extends Component { } } - addIconPlatformRender(agent) { let icon = ''; const os = agent?.os || {}; @@ -123,26 +128,28 @@ export class AgentSelectionTable extends Component { const os_name = `${agent?.os?.name || ''} ${agent?.os?.version || ''}`; return ( - - {' '} + + + + {' '} {os_name.trim() || '-'} ); } - filterGroupBadge = (group) => { + filterGroupBadge = group => { this.setState({ filters: { - default: {q: 'id!=000'}, + default: { q: 'id!=000' }, q: `group=${group}`, - } + }, }); }; - renderGroups(groups){ + renderGroups(groups) { return Array.isArray(groups) ? ( - ) : groups + {...this.props} + /> + ) : ( + groups + ); } render() { @@ -169,25 +179,31 @@ export class AgentSelectionTable extends Component {
{selectedAgent && Object.keys(selectedAgent).length > 0 && ( - + {/* agent name (agent id) Unpin button right aligned, require justifyContent="flexEnd" in the EuiFlexGroup */} - - + + {selectedAgent.name} ({selectedAgent.id}) - + this.unselectAgents()} - iconType="pinFilled" - aria-label="unpin agent" + iconType='pinFilled' + aria-label='unpin agent' /> - + )} @@ -196,7 +212,7 @@ export class AgentSelectionTable extends Component { tableColumns={this.columns} tableInitialSortingField='id' tablePageSizeOptions={[10, 25, 50, 100]} - mapResponseItem={(item) => { + mapResponseItem={item => { return { ...item, /* @@ -204,9 +220,8 @@ export class AgentSelectionTable extends Component { v */ ...(typeof item.version === 'string' - ? {version: item.version.match(/(v\d.+)/)?.[1]} - : {version: '-'} - ) + ? { version: item.version.match(/(v\d.+)/)?.[1] } + : { version: '-' }), }; }} rowProps={getRowProps} @@ -217,31 +232,58 @@ export class AgentSelectionTable extends Component { suggestions: { field(currentValue) { return [ - {label: 'id', description: 'filter by id'}, - {label: 'group', description: 'filter by group'}, - {label: 'name', description: 'filter by name'}, - {label: 'os.name', description: 'filter by operating system name'}, - {label: 'os.version', description: 'filter by operating system version'}, - {label: 'status', description: 'filter by status'}, - {label: 'version', description: 'filter by version'}, + { label: 'id', description: 'filter by id' }, + { label: 'group', description: 'filter by group' }, + { label: 'name', description: 'filter by name' }, + { + label: 'os.name', + description: 'filter by operating system name', + }, + { + label: 'os.version', + description: 'filter by operating system version', + }, + { label: 'status', description: 'filter by status' }, + { label: 'version', description: 'filter by version' }, ]; }, value: async (currentValue, { field }) => { - try{ + try { switch (field) { case 'status': - return UI_ORDER_AGENT_STATUS.map(status => ({label: status})) - break; - default: - return (await getAgentFilterValues(field, currentValue, { q: 'id!=000'})) - .map(status => ({label: status})); - break; + return UI_ORDER_AGENT_STATUS.map(status => ({ + label: status, + })); + default: { + const response = await WzRequest.apiReq( + 'GET', + '/agents', + { + params: { + distinct: true, + limit: 30, + select: field, + sort: `+${field}`, + ...(currentValue + ? { + q: `${searchBarWQLOptions.implicitQuery.query}${searchBarWQLOptions.implicitQuery.conjunction}${field}~${currentValue}`, + } + : { + q: `${searchBarWQLOptions.implicitQuery.query}`, + }), + }, + }, + ); + return response?.data?.data.affected_items.map(item => ({ + label: getLodash(item, field), + })); + } } - }catch(error){ + } catch (error) { return []; - }; + } }, - } + }, }} />
From 086782f8f8013e5db1c23248fd2f0a5e5e47048f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Wed, 2 Aug 2023 11:39:44 +0200 Subject: [PATCH 51/76] fix: extract group values for the distinct values of the search bar in the explore agent modal --- .../agents-selection-table.js | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/plugins/main/public/controllers/overview/components/overview-actions/agents-selection-table.js b/plugins/main/public/controllers/overview/components/overview-actions/agents-selection-table.js index 4cf7fc5d7b..bceecb37fc 100644 --- a/plugins/main/public/controllers/overview/components/overview-actions/agents-selection-table.js +++ b/plugins/main/public/controllers/overview/components/overview-actions/agents-selection-table.js @@ -274,6 +274,28 @@ export class AgentSelectionTable extends Component { }, }, ); + if (field === 'group') { + /* the group field is returned as an string[], + example: ['group1', 'group2'] + + Due the API request done to get the distinct values for the groups is + not returning the exepected values, as workaround, the values are + extracted in the frontend using the returned results. + + This API request to get the distint values of groups doesn't + return the unique values for the groups, else the unique combination + of groups. + */ + return response?.data?.data.affected_items + .map(item => getLodash(item, field)) + .flat() + .filter( + (item, index, array) => + array.indexOf(item) === index, + ) + .sort() + .map(group => ({ label: group })); + } return response?.data?.data.affected_items.map(item => ({ label: getLodash(item, field), })); From 0696698c358441ad2589d0f6e7faf83dad446fec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Wed, 2 Aug 2023 13:09:02 +0200 Subject: [PATCH 52/76] fix: fix Management > Rules search bar filters Add id field Fix groups filter in Rule info flyout Remove onFiltersChange handler --- .../ruleset/components/ruleset-suggestions.ts | 16 + .../ruleset/components/ruleset-table.tsx | 1 - .../management/ruleset/views/rule-info.tsx | 328 ++++++++++++------ 3 files changed, 232 insertions(+), 113 deletions(-) diff --git a/plugins/main/public/controllers/management/components/management/ruleset/components/ruleset-suggestions.ts b/plugins/main/public/controllers/management/components/management/ruleset/components/ruleset-suggestions.ts index 31a64e7de2..b63412b33e 100644 --- a/plugins/main/public/controllers/management/components/management/ruleset/components/ruleset-suggestions.ts +++ b/plugins/main/public/controllers/management/components/management/ruleset/components/ruleset-suggestions.ts @@ -3,6 +3,7 @@ import { WzRequest } from '../../../../../../react-services/wz-request'; const rulesItems = { field(currentValue) { return [ + { label: 'id', description: 'filter by ID' }, { label: 'filename', description: 'filter by filename' }, { label: 'gdpr', description: 'filter by GDPR requirement' }, { label: 'gpg13', description: 'filter by GPG requirement' }, @@ -20,6 +21,21 @@ const rulesItems = { value: async (currentValue, { field }) => { try { switch (field) { + case 'id': { + const filter = { + distinct: true, + limit: 30, + select: field, + sort: `+${field}`, + ...(currentValue ? { q: `id~${currentValue}` } : {}), + }; + const result = await WzRequest.apiReq('GET', '/rules', { + params: filter, + }); + return result?.data?.data?.affected_items.map(label => ({ + label: label[field], + })); + } case 'status': { return ['enabled', 'disabled'].map(label => ({ label })); } diff --git a/plugins/main/public/controllers/management/components/management/ruleset/components/ruleset-table.tsx b/plugins/main/public/controllers/management/components/management/ruleset/components/ruleset-table.tsx index df8f432735..2358773423 100644 --- a/plugins/main/public/controllers/management/components/management/ruleset/components/ruleset-table.tsx +++ b/plugins/main/public/controllers/management/components/management/ruleset/components/ruleset-table.tsx @@ -139,7 +139,6 @@ const RulesFlyoutTable = ({ downloadCsv={true} showReload={true} filters={filters} - onFiltersChange={updateFilters} tablePageSizeOptions={[10, 25, 50, 100]} /> {isFlyoutVisible && ( diff --git a/plugins/main/public/controllers/management/components/management/ruleset/views/rule-info.tsx b/plugins/main/public/controllers/management/components/management/ruleset/views/rule-info.tsx index 89def62c38..c2c70e3084 100644 --- a/plugins/main/public/controllers/management/components/management/ruleset/views/rule-info.tsx +++ b/plugins/main/public/controllers/management/components/management/ruleset/views/rule-info.tsx @@ -18,7 +18,10 @@ import { import { WzRequest } from '../../../../../../react-services/wz-request'; -import { ResourcesHandler, ResourcesConstants } from '../../common/resources-handler'; +import { + ResourcesHandler, + ResourcesConstants, +} from '../../common/resources-handler'; import WzTextWithTooltipTruncated from '../../../../../../components/common/wz-text-with-tooltip-if-truncated'; import { UI_ERROR_SEVERITIES } from '../../../../../../react-services/error-orchestrator/types'; import { UI_LOGGER_LEVELS } from '../../../../../../../common/constants'; @@ -46,7 +49,7 @@ export default class WzRuleInfo extends Component { mitreRuleId: '', mitreIds: [], currentRuleInfo: {}, - isLoading: true + isLoading: true, }; this.resourcesHandler = new ResourcesHandler(ResourcesConstants.RULES); @@ -95,7 +98,10 @@ export default class WzRuleInfo extends Component { let result = value.match(regex); if (result !== null) { for (const oldValue of result) { - let newValue = oldValue.replace('$(', ``); + let newValue = oldValue.replace( + '$(', + ``, + ); newValue = newValue.replace(')', ' '); value = value.replace(oldValue, newValue); } @@ -133,8 +139,10 @@ export default class WzRuleInfo extends Component { width: '15%', render: (value, item) => { return ( - - handleFileClick(event, item)}>{value} + + handleFileClick(event, item)}> + {value} + ); }, @@ -155,7 +163,7 @@ export default class WzRuleInfo extends Component { async componentDidUpdate(prevProps, prevState) { if (prevState.currentRuleId !== this.state.currentRuleId) - await this.loadRule() + await this.loadRule(); } async loadRule() { @@ -167,7 +175,9 @@ export default class WzRuleInfo extends Component { rule_ids: currentRuleId, }, }); - const currentRule = result?.data?.affected_items?.length ? result.data.affected_items[0] : {}; + const currentRule = result?.data?.affected_items?.length + ? result.data.affected_items[0] + : {}; const compliance = this.buildCompliance(currentRule); if (compliance?.mitre?.length && currentRuleId !== mitreRuleId) { @@ -179,14 +189,14 @@ export default class WzRuleInfo extends Component { mitreIds: [], mitreTactics: [], mitreTechniques: [], - } + }; } this.setState({ currentRuleInfo: currentRule, compliance: compliance, isLoading: false, - ...mitreState + ...mitreState, }); } catch (error) { const options = { @@ -201,7 +211,6 @@ export default class WzRuleInfo extends Component { }; getErrorOrchestrator().handleError(options); } - } /** * Build an object with the compliance info about a rule @@ -209,25 +218,45 @@ export default class WzRuleInfo extends Component { */ buildCompliance(ruleInfo) { const compliance = {}; - const complianceKeys = ['gdpr', 'gpg13', 'hipaa', 'nist-800-53', 'pci', 'tsc', 'mitre']; - Object.keys(ruleInfo).forEach((key) => { - if (complianceKeys.includes(key) && ruleInfo[key].length) compliance[key] = ruleInfo[key]; + const complianceKeys = [ + 'gdpr', + 'gpg13', + 'hipaa', + 'nist-800-53', + 'pci', + 'tsc', + 'mitre', + ]; + Object.keys(ruleInfo).forEach(key => { + if (complianceKeys.includes(key) && ruleInfo[key].length) + compliance[key] = ruleInfo[key]; }); return compliance || {}; } buildComplianceBadges(item) { const badgeList = []; - const fields = ['pci_dss', 'gpg13', 'hipaa', 'gdpr', 'nist_800_53', 'tsc', 'mitre']; - const buildBadge = (field) => { - + const fields = [ + 'pci_dss', + 'gpg13', + 'hipaa', + 'gdpr', + 'nist_800_53', + 'tsc', + 'mitre', + ]; + const buildBadge = field => { return ( - + ev.stopPropagation()} + onClick={ev => ev.stopPropagation()} onClickAriaLabel={field.toUpperCase()} - color="hollow" + color='hollow' style={{ margin: '1px 2px' }} > {field.toUpperCase()} @@ -250,7 +279,7 @@ export default class WzRuleInfo extends Component { error: error, message: error.message || error, title: error.name || error, - } + }, }; getErrorOrchestrator().handleError(options); } @@ -262,9 +291,10 @@ export default class WzRuleInfo extends Component { * Clean the existing filters and sets the new ones and back to the previous section */ setNewFiltersAndBack(filters) { - window.history.pushState("", + window.history.pushState( + '', window.document.title, - window.location.href.replace(new RegExp('&redirectRule=' + '[^&]*'), '') + window.location.href.replace(new RegExp('&redirectRule=' + '[^&]*'), ''), ); this.props.cleanFilters(); this.props.onFiltersChange(filters); @@ -281,37 +311,47 @@ export default class WzRuleInfo extends Component { renderInfo(id = '', level = '', file = '', path = '', groups = []) { return ( - + ID - + this.setNewFiltersAndBack({q: `id=${id}`})} + onClick={async () => + this.setNewFiltersAndBack({ q: `id=${id}` }) + } > {id} - + Level - + this.setNewFiltersAndBack({q: `level=${level}`})} + onClick={async () => + this.setNewFiltersAndBack({ q: `level=${level}` }) + } > {level} - + File - + - this.setNewFiltersAndBack({q: `filename=${file}`}) + this.setNewFiltersAndBack({ q: `filename=${file}` }) } > {file} @@ -319,13 +359,13 @@ export default class WzRuleInfo extends Component { - + Path - + - this.setNewFiltersAndBack({q: `relative_dirname=${path}`}) + this.setNewFiltersAndBack({ q: `relative_dirname=${path}` }) } > {path} @@ -333,11 +373,11 @@ export default class WzRuleInfo extends Component { - + Groups {this.renderGroups(groups)} - + ); } @@ -347,16 +387,16 @@ export default class WzRuleInfo extends Component { let link = ''; let name = ''; - value.forEach((item) => { - if (item.type === 'cve'){ + value.forEach(item => { + if (item.type === 'cve') { name = item.name; } - if (item.type === 'link'){ + if (item.type === 'link') { link = ( {item.name} @@ -369,7 +409,11 @@ export default class WzRuleInfo extends Component { {name}: {link} ); - } else if (value && typeof value === 'object' && value.constructor === Object) { + } else if ( + value && + typeof value === 'object' && + value.constructor === Object + ) { let list = []; Object.keys(value).forEach((key, idx) => { list.push( @@ -378,7 +422,7 @@ export default class WzRuleInfo extends Component { {value[key]} {idx < Object.keys(value).length - 1 && ', '}
- + , ); }); return ( @@ -387,7 +431,11 @@ export default class WzRuleInfo extends Component { ); } else { - return {value}; + return ( + + {value} + + ); } } @@ -400,13 +448,21 @@ export default class WzRuleInfo extends Component { // Exclude group key of details Object.keys(details) - .filter((key) => key !== 'group') - .forEach((key) => { + .filter(key => key !== 'group') + .forEach(key => { detailsToRender.push( - - {key} - {details[key] === '' ? 'true' : this.getFormattedDetails(details[key])} - + + + {key} + + {details[key] === '' + ? 'true' + : this.getFormattedDetails(details[key])} + , ); }); return {detailsToRender}; @@ -422,14 +478,19 @@ export default class WzRuleInfo extends Component { listGroups.push( this.setNewFiltersAndBack({q: `group=${group}`})} + onClick={async () => + this.setNewFiltersAndBack({ q: `groups=${group}` }) + } > - + {group} {index < groups.length - 1 && ', '} - + , ); }); return ( @@ -447,9 +508,10 @@ export default class WzRuleInfo extends Component { tactic_ids: tactics.toString(), }, }); - const formattedData = ((data || {}).data.data || {}).affected_items || [] || {}; + const formattedData = + ((data || {}).data.data || {}).affected_items || [] || {}; formattedData && - formattedData.forEach((item) => { + formattedData.forEach(item => { tacticsObj.push(item.name); }); return tacticsObj; @@ -465,21 +527,23 @@ export default class WzRuleInfo extends Component { const mitreName = []; const mitreIds = []; const mitreTactics = await Promise.all( - compliance.map(async (i) => { + compliance.map(async i => { const data = await WzRequest.apiReq('GET', '/mitre/techniques', { params: { q: `external_id=${i}`, }, }); - const formattedData = (((data || {}).data.data || {}).affected_items || [])[0] || {}; + const formattedData = + (((data || {}).data.data || {}).affected_items || [])[0] || {}; const tactics = this.getTacticsNames(formattedData.tactics) || []; mitreName.push(formattedData.name); mitreIds.push(i); return tactics; - }) + }), ); if (mitreTactics.length) { - let removeDuplicates = (arr) => arr.filter((v, i) => arr.indexOf(v) === i); + let removeDuplicates = arr => + arr.filter((v, i) => arr.indexOf(v) === i); const uniqueTactics = removeDuplicates(mitreTactics.flat()); Object.assign(newMitreState, { mitreRuleId: currentRuleId, @@ -515,8 +579,6 @@ export default class WzRuleInfo extends Component { ? this.state.currentRuleId : this.props.state.ruleInfo.current; - - const listCompliance = []; if (compliance.mitre) delete compliance.mitre; const keys = Object.keys(compliance); @@ -527,9 +589,11 @@ export default class WzRuleInfo extends Component { return ( this.setNewFiltersAndBack({q: `${key}=${element}`})} + onClick={async () => + this.setNewFiltersAndBack({ q: `${key}=${element}` }) + } > - + {element} @@ -539,11 +603,15 @@ export default class WzRuleInfo extends Component { }); listCompliance.push( - + {this.complianceEquivalences[key]}

{values}

- -
+ +
, ); } @@ -553,10 +621,12 @@ export default class WzRuleInfo extends Component { - this.setNewFiltersAndBack({q: `mitre=${this.state.mitreIds[index]}`}) + this.setNewFiltersAndBack({ + q: `mitre=${this.state.mitreIds[index]}`, + }) } > - + {element} @@ -565,21 +635,35 @@ export default class WzRuleInfo extends Component { ); }); listCompliance.push( - - {this.complianceEquivalences['mitreTechniques']} - {(this.state.mitreLoading && ) ||

{values}

} - -
+ + + {this.complianceEquivalences['mitreTechniques']} + + {(this.state.mitreLoading && ) || ( +

{values}

+ )} + +
, ); } if (this.state.mitreTactics && this.state.mitreTactics.length) { listCompliance.push( - - {this.complianceEquivalences['mitreTactics']} + + + {this.complianceEquivalences['mitreTactics']} +

{this.state.mitreTactics.toString()}

- -
+ +
, ); } @@ -598,7 +682,7 @@ export default class WzRuleInfo extends Component { window.location.href = window.location.href.replace( new RegExp('redirectRule=' + '[^&]*'), - `redirectRule=${ruleId}` + `redirectRule=${ruleId}`, ); this.setState({ currentRuleId: ruleId, isLoading: true }); } @@ -621,7 +705,7 @@ export default class WzRuleInfo extends Component { return value; } - onClickRow = (item) => { + onClickRow = item => { return { onClick: () => { this.changeBetweenRules(item.id); @@ -630,38 +714,47 @@ export default class WzRuleInfo extends Component { }; render() { - const { description, details, filename, relative_dirname, level, id, groups } = this.state.currentRuleInfo; + const { + description, + details, + filename, + relative_dirname, + level, + id, + groups, + } = this.state.currentRuleInfo; const compliance = this.buildCompliance(this.state.currentRuleInfo); return ( <> - + - { - description && ( - ) - } + {description && ( + + )} View alerts of this Rule - + - + {/* Cards */} @@ -669,19 +762,25 @@ export default class WzRuleInfo extends Component { {/* General info */} +

Information

} - paddingSize="none" + paddingSize='none' initialIsOpen={true} isLoading={this.state.isLoading} isLoadingMessage={''} > - - {this.renderInfo(id, level, filename, relative_dirname, groups)} + + {this.renderInfo( + id, + level, + filename, + relative_dirname, + groups, + )}
@@ -690,18 +789,20 @@ export default class WzRuleInfo extends Component { +

Details

} - paddingSize="none" + paddingSize='none' initialIsOpen={true} isLoading={this.state.isLoading} isLoadingMessage={''} > - {this.renderDetails(details)} + + {this.renderDetails(details)} +
@@ -710,18 +811,18 @@ export default class WzRuleInfo extends Component { +

Compliance

} - paddingSize="none" + paddingSize='none' initialIsOpen={true} isLoading={this.state.isLoading} isLoadingMessage={''} > - + {this.renderCompliance(compliance)}
@@ -732,29 +833,32 @@ export default class WzRuleInfo extends Component { +

Related rules

} isLoading={this.state.isLoading} isLoadingMessage={''} - paddingSize="none" + paddingSize='none' initialIsOpen={true} > - + - {this.state.currentRuleInfo?.filename && + {this.state.currentRuleInfo?.filename && ( - } + )} From bc26f9ae982f511b60a3d2cfa1a1a9e423da7e48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Wed, 2 Aug 2023 13:53:21 +0200 Subject: [PATCH 53/76] fix: update the link to the documentation of WQL --- .../main/public/components/search-bar/query-language/wql.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/main/public/components/search-bar/query-language/wql.tsx b/plugins/main/public/components/search-bar/query-language/wql.tsx index a8f136b3ff..067ac6bf20 100644 --- a/plugins/main/public/components/search-bar/query-language/wql.tsx +++ b/plugins/main/public/components/search-bar/query-language/wql.tsx @@ -7,7 +7,7 @@ import { EuiCode, } from '@elastic/eui'; import { tokenizer as tokenizerUQL } from './aql'; -import { PLUGIN_VERSION_SHORT } from '../../../../common/constants'; +import { PLUGIN_VERSION } from '../../../../common/constants'; /* UI Query language https://documentation.wazuh.com/current/user-manual/api/queries.html @@ -895,7 +895,7 @@ export const WQL = { label: 'WQL', description: 'WQL (Wazuh Query Language) provides a human query syntax based on the Wazuh API query language.', - documentationLink: `https://github.com/wazuh/wazuh-kibana-app/blob/v${PLUGIN_VERSION_SHORT}/public/components/search-bar/query-language/wql.md`, + documentationLink: `https://github.com/wazuh/wazuh-kibana-app/blob/v${PLUGIN_VERSION}/plugins/main/public/components/search-bar/query-language/wql.md`, getConfiguration() { return { isOpenPopoverImplicitFilter: false, From cd89aa5e30e62681a7a537026c36c710b93514aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Wed, 2 Aug 2023 13:55:13 +0200 Subject: [PATCH 54/76] fix(search-bar): use value of value token as the value used to get the value suggestions in the search bar instead of raw token that could include " character --- .../public/components/search-bar/query-language/wql.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/main/public/components/search-bar/query-language/wql.tsx b/plugins/main/public/components/search-bar/query-language/wql.tsx index 067ac6bf20..db85ca55b2 100644 --- a/plugins/main/public/components/search-bar/query-language/wql.tsx +++ b/plugins/main/public/components/search-bar/query-language/wql.tsx @@ -189,7 +189,7 @@ export function tokenizer(input: string): ITokens { value, ...(key === 'value' && value && /^"(.+)"$/.test(value) ? { formattedValue: value.match(/^"(.+)"$/)[1] } - : {}), + : { formattedValue: value }), })), ) .flat(); @@ -431,7 +431,7 @@ export async function getSuggestions( } return [ - ...(lastToken.value + ...(lastToken.formattedValue ? [ { type: 'function_search', @@ -441,7 +441,7 @@ export async function getSuggestions( ] : []), ...( - await options.suggestions.value(lastToken.value, { + await options.suggestions.value(lastToken.formattedValue, { field, operatorCompare, }) From d961094e5a63451158057d3d7e62d230610c2ed2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Thu, 3 Aug 2023 12:34:46 +0200 Subject: [PATCH 55/76] fix(search-bar): fix a problem extracting value for value tokens wrapped by double quotation marks that contains the new line character and remove separation of invalid characters in the value token - Fix tests --- .../search-bar/query-language/wql.test.tsx | 655 +++++++++--------- .../search-bar/query-language/wql.tsx | 29 +- 2 files changed, 358 insertions(+), 326 deletions(-) diff --git a/plugins/main/public/components/search-bar/query-language/wql.test.tsx b/plugins/main/public/components/search-bar/query-language/wql.test.tsx index bfe284b03d..5cdecb968b 100644 --- a/plugins/main/public/components/search-bar/query-language/wql.test.tsx +++ b/plugins/main/public/components/search-bar/query-language/wql.test.tsx @@ -1,4 +1,9 @@ -import { getSuggestions, tokenizer, transformSpecificQLToUnifiedQL, WQL } from './wql'; +import { + getSuggestions, + tokenizer, + transformSpecificQLToUnifiedQL, + WQL, +} from './wql'; import React from 'react'; import { render, waitFor } from '@testing-library/react'; import { SearchBar } from '../index'; @@ -13,34 +18,32 @@ describe('SearchBar component', () => { options: { implicitQuery: { query: 'id!=000', - conjunction: ';' + conjunction: ';', }, }, suggestions: { field(currentValue) { return []; }, - value(currentValue, { field }){ + value(currentValue, { field }) { return []; }, }, - } + }, ], /* eslint-disable @typescript-eslint/no-empty-function */ onChange: () => {}, - onSearch: () => {} + onSearch: () => {}, /* eslint-enable @typescript-eslint/no-empty-function */ }; it('Renders correctly to match the snapshot of query language', async () => { - const wrapper = render( - - ); + const wrapper = render(); await waitFor(() => { - const elementImplicitQuery = wrapper.container.querySelector('.euiCodeBlock__code'); + const elementImplicitQuery = wrapper.container.querySelector( + '.euiCodeBlock__code', + ); expect(elementImplicitQuery?.innerHTML).toEqual('id!=000 and '); expect(wrapper.container).toMatchSnapshot(); }); @@ -50,27 +53,40 @@ describe('SearchBar component', () => { /* eslint-disable max-len */ describe('Query language - WQL', () => { // Tokenize the input - function tokenCreator({type, value, formattedValue}){ - return {type, value, ...(formattedValue ? { formattedValue } : {})}; - }; + function tokenCreator({ type, value, formattedValue }) { + return { type, value, ...(formattedValue ? { formattedValue } : {}) }; + } const t = { - opGroup: (value = undefined) => tokenCreator({type: 'operator_group', value}), - opCompare: (value = undefined) => tokenCreator({type: 'operator_compare', value}), - field: (value = undefined) => tokenCreator({type: 'field', value}), - value: (value = undefined, formattedValue = undefined) => tokenCreator({type: 'value', value, formattedValue}), - whitespace: (value = undefined) => tokenCreator({type: 'whitespace', value}), - conjunction: (value = undefined) => tokenCreator({type: 'conjunction', value}) + opGroup: (value = undefined) => + tokenCreator({ type: 'operator_group', value }), + opCompare: (value = undefined) => + tokenCreator({ type: 'operator_compare', value }), + field: (value = undefined) => tokenCreator({ type: 'field', value }), + value: (value = undefined, formattedValue = undefined) => + tokenCreator({ + type: 'value', + value, + formattedValue: formattedValue ?? value, + }), + whitespace: (value = undefined) => + tokenCreator({ type: 'whitespace', value }), + conjunction: (value = undefined) => + tokenCreator({ type: 'conjunction', value }), }; // Token undefined const tu = { - opGroup: tokenCreator({type: 'operator_group', value: undefined}), - opCompare: tokenCreator({type: 'operator_compare', value: undefined}), - whitespace: tokenCreator({type: 'whitespace', value: undefined}), - field: tokenCreator({type: 'field', value: undefined}), - value: tokenCreator({type: 'value', value: undefined}), - conjunction: tokenCreator({type: 'conjunction', value: undefined}) + opGroup: tokenCreator({ type: 'operator_group', value: undefined }), + opCompare: tokenCreator({ type: 'operator_compare', value: undefined }), + whitespace: tokenCreator({ type: 'whitespace', value: undefined }), + field: tokenCreator({ type: 'field', value: undefined }), + value: tokenCreator({ + type: 'value', + value: undefined, + formattedValue: undefined, + }), + conjunction: tokenCreator({ type: 'conjunction', value: undefined }), }; const tuBlankSerie = [ @@ -85,58 +101,57 @@ describe('Query language - WQL', () => { tu.opGroup, tu.whitespace, tu.conjunction, - tu.whitespace + tu.whitespace, ]; - it.each` - input | tokens - ${''} | ${tuBlankSerie} - ${'f'} | ${[tu.opGroup, tu.whitespace, t.field('f'), tu.whitespace, tu.opCompare, tu.whitespace, tu.value, tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} - ${'field'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, tu.opCompare, tu.whitespace, tu.value, tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} - ${'field='} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, tu.value, tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} - ${'field=value'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} - ${'field=and'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('and'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} - ${'field=or'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('or'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} - ${'field=valueand'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('valueand'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} - ${'field=valueor'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('valueor'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} - ${'field=value='} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value='), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} - ${'field=value!='} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value!='), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} - ${'field=value>'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value>'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} - ${'field=value<'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value<'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} - ${'field=value~'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value~'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie] /** ~ character is not supported as value in the q query parameter */} - ${'field="'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('"'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} - ${'field="value and'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('"value and'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} - ${'field="value and value2"'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('"value and value2"', 'value and value2'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} - ${'field="value or value2"'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('"value or value2"', 'value or value2'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} - ${'field="value = value2"'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('"value = value2"', 'value = value2'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} - ${'field="value != value2"'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('"value != value2"', 'value != value2'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} - ${'field="value > value2"'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('"value > value2"', 'value > value2'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} - ${'field="value < value2"'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('"value < value2"', 'value < value2'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} - ${'field="value ~ value2"'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('"value ~ value2"', 'value ~ value2'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie] /** ~ character is not supported as value in the q query parameter */} - ${'field=value and'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.conjunction('and'), tu.whitespace, ...tuBlankSerie]} - ${'field=value and '} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.conjunction('and'), t.whitespace(' '), ...tuBlankSerie]} - ${'field=value and field2'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.conjunction('and'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.field('field2'), tu.whitespace, tu.opCompare, tu.whitespace, tu.value, tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} - ${'field=value and field2!='} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.conjunction('and'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.field('field2'), tu.whitespace, t.opCompare('!='), tu.whitespace, tu.value, tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} - ${'field=value and field2!="'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.conjunction('and'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.field('field2'), tu.whitespace, t.opCompare('!='), tu.whitespace, t.value('"'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} - ${'field=value and field2!="value'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.conjunction('and'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.field('field2'), tu.whitespace, t.opCompare('!='), tu.whitespace, t.value('"value'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} - ${'field=value and field2!="value"'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.conjunction('and'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.field('field2'), tu.whitespace, t.opCompare('!='), tu.whitespace, t.value('"value"', 'value'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} - ${'field=value and field2!=value2 and'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.conjunction('and'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.field('field2'), tu.whitespace, t.opCompare('!='), tu.whitespace, t.value('value2'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.conjunction('and'), tu.whitespace, ...tuBlankSerie]} - `(`Tokenizer API input $input`, ({input, tokens}) => { + input | tokens + ${''} | ${tuBlankSerie} + ${'f'} | ${[tu.opGroup, tu.whitespace, t.field('f'), tu.whitespace, tu.opCompare, tu.whitespace, tu.value, tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, tu.opCompare, tu.whitespace, tu.value, tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field='} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, tu.value, tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field=value'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field=and'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('and'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field=or'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('or'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field=valueand'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('valueand'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field=valueor'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('valueor'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field=value='} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value='), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field=value!='} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value!='), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field=value>'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value>'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field=value<'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value<'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field=value~'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value~'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie] /** ~ character is not supported as value in the q query parameter */} + ${'field="'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('"'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field="value and'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('"value and'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field="value and value2"'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('"value and value2"', 'value and value2'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field="value or value2"'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('"value or value2"', 'value or value2'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field="value = value2"'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('"value = value2"', 'value = value2'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field="value != value2"'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('"value != value2"', 'value != value2'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field="value > value2"'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('"value > value2"', 'value > value2'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field="value < value2"'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('"value < value2"', 'value < value2'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field="value ~ value2"'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('"value ~ value2"', 'value ~ value2'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie] /** ~ character is not supported as value in the q query parameter */} + ${'field=value and'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.conjunction('and'), tu.whitespace, ...tuBlankSerie]} + ${'field=value and '} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.conjunction('and'), t.whitespace(' '), ...tuBlankSerie]} + ${'field=value and field2'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.conjunction('and'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.field('field2'), tu.whitespace, tu.opCompare, tu.whitespace, tu.value, tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field=value and field2!='} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.conjunction('and'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.field('field2'), tu.whitespace, t.opCompare('!='), tu.whitespace, tu.value, tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field=value and field2!="'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.conjunction('and'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.field('field2'), tu.whitespace, t.opCompare('!='), tu.whitespace, t.value('"'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field=value and field2!="value'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.conjunction('and'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.field('field2'), tu.whitespace, t.opCompare('!='), tu.whitespace, t.value('"value'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field=value and field2!="value"'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.conjunction('and'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.field('field2'), tu.whitespace, t.opCompare('!='), tu.whitespace, t.value('"value"', 'value'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field=value and field2!=value2 and'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.conjunction('and'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.field('field2'), tu.whitespace, t.opCompare('!='), tu.whitespace, t.value('value2'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.conjunction('and'), tu.whitespace, ...tuBlankSerie]} + `(`Tokenizer API input $input`, ({ input, tokens }) => { expect(tokenizer(input)).toEqual(tokens); }); // Get suggestions it.each` - input | suggestions - ${''} | ${[{ description: 'run the search query', label: 'Search', type: 'function_search' }, { description: 'Field', label: 'field', type: 'field' }, { description: 'Field2', label: 'field2', type: 'field' }, { description: 'open group', label: '(', type: 'operator_group' }]} - ${'w'} | ${[]} - ${'f'} | ${[{ description: 'Field', label: 'field', type: 'field' }, { description: 'Field2', label: 'field2', type: 'field' }]} - ${'field'} | ${[{ description: 'Field2', label: 'field2', type: 'field' }, { description: 'equality', label: '=', type: 'operator_compare' }, { description: 'not equality', label: '!=', type: 'operator_compare' }, { description: 'bigger', label: '>', type: 'operator_compare' }, { description: 'smaller', label: '<', type: 'operator_compare' }, { description: 'like as', label: '~', type: 'operator_compare' }]} - ${'field='} | ${[{ label: 'value', type: 'value' }, { label: 'value2', type: 'value' }, { label: 'value3', type: 'value' }, { label: 'value4', type: 'value' }]} - ${'field=v'} | ${[{ description: 'run the search query', label: 'Search', type: 'function_search' }, { label: 'value', type: 'value' }, { label: 'value2', type: 'value' }, { label: 'value3', type: 'value' }, { label: 'value4', type: 'value' }, { description: 'and', label: 'and', type: 'conjunction' }, { description: 'or', label: 'or', type: 'conjunction' }, { description: 'close group', label: ')', type: 'operator_group' }]} - ${'field=value'} | ${[{ description: 'run the search query', label: 'Search', type: 'function_search' }, { label: 'value', type: 'value' }, { label: 'value2', type: 'value' }, { label: 'value3', type: 'value' }, { label: 'value4', type: 'value' }, { description: 'and', label: 'and', type: 'conjunction' }, { description: 'or', label: 'or', type: 'conjunction' }, { description: 'close group', label: ')', type: 'operator_group' }]} - ${'field=value and'} | ${[{ description: 'Field', label: 'field', type: 'field' }, { description: 'Field2', label: 'field2', type: 'field' }, { description: 'open group', label: '(', type: 'operator_group' }]} + input | suggestions + ${''} | ${[{ description: 'run the search query', label: 'Search', type: 'function_search' }, { description: 'Field', label: 'field', type: 'field' }, { description: 'Field2', label: 'field2', type: 'field' }, { description: 'open group', label: '(', type: 'operator_group' }]} + ${'w'} | ${[]} + ${'f'} | ${[{ description: 'Field', label: 'field', type: 'field' }, { description: 'Field2', label: 'field2', type: 'field' }]} + ${'field'} | ${[{ description: 'Field2', label: 'field2', type: 'field' }, { description: 'equality', label: '=', type: 'operator_compare' }, { description: 'not equality', label: '!=', type: 'operator_compare' }, { description: 'bigger', label: '>', type: 'operator_compare' }, { description: 'smaller', label: '<', type: 'operator_compare' }, { description: 'like as', label: '~', type: 'operator_compare' }]} + ${'field='} | ${[{ label: 'value', type: 'value' }, { label: 'value2', type: 'value' }, { label: 'value3', type: 'value' }, { label: 'value4', type: 'value' }]} + ${'field=v'} | ${[{ description: 'run the search query', label: 'Search', type: 'function_search' }, { label: 'value', type: 'value' }, { label: 'value2', type: 'value' }, { label: 'value3', type: 'value' }, { label: 'value4', type: 'value' }, { description: 'and', label: 'and', type: 'conjunction' }, { description: 'or', label: 'or', type: 'conjunction' }, { description: 'close group', label: ')', type: 'operator_group' }]} + ${'field=value'} | ${[{ description: 'run the search query', label: 'Search', type: 'function_search' }, { label: 'value', type: 'value' }, { label: 'value2', type: 'value' }, { label: 'value3', type: 'value' }, { label: 'value4', type: 'value' }, { description: 'and', label: 'and', type: 'conjunction' }, { description: 'or', label: 'or', type: 'conjunction' }, { description: 'close group', label: ')', type: 'operator_group' }]} + ${'field=value and'} | ${[{ description: 'Field', label: 'field', type: 'field' }, { description: 'Field2', label: 'field2', type: 'field' }, { description: 'open group', label: '(', type: 'operator_group' }]} `('Get suggestion from the input: $input', async ({ input, suggestions }) => { expect( await getSuggestions(tokenizer(input), { @@ -176,267 +191,279 @@ describe('Query language - WQL', () => { // Transform specific query language to UQL (Unified Query Language) it.each` - WQL | UQL - ${'field'} | ${'field'} - ${'field='} | ${'field='} - ${'field=value'} | ${'field=value'} - ${'field=value()'} | ${'field=value()'} - ${'field=valueand'} | ${'field=valueand'} - ${'field=valueor'} | ${'field=valueor'} - ${'field=value='} | ${'field=value='} - ${'field=value!='} | ${'field=value!='} - ${'field=value>'} | ${'field=value>'} - ${'field=value<'} | ${'field=value<'} - ${'field=value~'} | ${'field=value~' /** ~ character is not supported as value in the q query parameter */} - ${'field="custom value"'} | ${'field=custom value'} - ${'field="custom value()"'} | ${'field=custom value()'} - ${'field="value and value2"'} | ${'field=value and value2'} - ${'field="value or value2"'} | ${'field=value or value2'} - ${'field="value = value2"'} | ${'field=value = value2'} - ${'field="value != value2"'} | ${'field=value != value2'} - ${'field="value > value2"'} | ${'field=value > value2'} - ${'field="value < value2"'} | ${'field=value < value2'} - ${'field="value ~ value2"'} | ${'field=value ~ value2' /** ~ character is not supported as value in the q query parameter */} - ${'field="custom \\"value"'} | ${'field=custom "value'} - ${'field="custom \\"value\\""'} | ${'field=custom "value"'} - ${'field=value and'} | ${'field=value;'} - ${'field="custom value" and'} | ${'field=custom value;'} - ${'(field=value'} | ${'(field=value'} - ${'(field=value)'} | ${'(field=value)'} - ${'(field=value) and'} | ${'(field=value);'} - ${'(field=value) and field2'} | ${'(field=value);field2'} - ${'(field=value) and field2>'} | ${'(field=value);field2>'} - ${'(field=value) and field2>"wrappedcommas"'} | ${'(field=value);field2>wrappedcommas'} - ${'(field=value) and field2>"value with spaces"'} | ${'(field=value);field2>value with spaces'} - ${'field ='} | ${'field='} - ${'field = value'} | ${'field=value'} - ${'field = value()'} | ${'field=value()'} - ${'field = valueand'} | ${'field=valueand'} - ${'field = valueor'} | ${'field=valueor'} - ${'field = value='} | ${'field=value='} - ${'field = value!='} | ${'field=value!='} - ${'field = value>'} | ${'field=value>'} - ${'field = value<'} | ${'field=value<'} - ${'field = value~'} | ${'field=value~' /** ~ character is not supported as value in the q query parameter */} - ${'field = "custom value"'} | ${'field=custom value'} - ${'field = "custom value()"'} | ${'field=custom value()'} - ${'field = "value and value2"'} | ${'field=value and value2'} - ${'field = "value or value2"'} | ${'field=value or value2'} - ${'field = "value = value2"'} | ${'field=value = value2'} - ${'field = "value != value2"'} | ${'field=value != value2'} - ${'field = "value > value2"'} | ${'field=value > value2'} - ${'field = "value < value2"'} | ${'field=value < value2'} - ${'field = "value ~ value2"'} | ${'field=value ~ value2' /** ~ character is not supported as value in the q query parameter */} - ${'field = value or'} | ${'field=value,'} - ${'field = value or field2'} | ${'field=value,field2'} - ${'field = value or field2 <'} | ${'field=value,field2<'} - ${'field = value or field2 < value2'} | ${'field=value,field2 "custom value" '} | ${'(field=value);field2>custom value'} - `('transformSpecificQLToUnifiedQL - WQL $WQL TO UQL $UQL', ({WQL, UQL}) => { + WQL | UQL + ${'field'} | ${'field'} + ${'field='} | ${'field='} + ${'field=value'} | ${'field=value'} + ${'field=value()'} | ${'field=value()'} + ${'field=valueand'} | ${'field=valueand'} + ${'field=valueor'} | ${'field=valueor'} + ${'field=value='} | ${'field=value='} + ${'field=value!='} | ${'field=value!='} + ${'field=value>'} | ${'field=value>'} + ${'field=value<'} | ${'field=value<'} + ${'field=value~'} | ${'field=value~' /** ~ character is not supported as value in the q query parameter */} + ${'field="custom value"'} | ${'field=custom value'} + ${'field="custom value()"'} | ${'field=custom value()'} + ${'field="value and value2"'} | ${'field=value and value2'} + ${'field="value or value2"'} | ${'field=value or value2'} + ${'field="value = value2"'} | ${'field=value = value2'} + ${'field="value != value2"'} | ${'field=value != value2'} + ${'field="value > value2"'} | ${'field=value > value2'} + ${'field="value < value2"'} | ${'field=value < value2'} + ${'field="value ~ value2"'} | ${'field=value ~ value2' /** ~ character is not supported as value in the q query parameter */} + ${'field="custom \\"value"'} | ${'field=custom "value'} + ${'field="custom \\"value\\""'} | ${'field=custom "value"'} + ${'field=value and'} | ${'field=value;'} + ${'field="custom value" and'} | ${'field=custom value;'} + ${'(field=value'} | ${'(field=value'} + ${'(field=value)'} | ${'(field=value)'} + ${'(field=value) and'} | ${'(field=value);'} + ${'(field=value) and field2'} | ${'(field=value);field2'} + ${'(field=value) and field2>'} | ${'(field=value);field2>'} + ${'(field=value) and field2>"wrappedcommas"'} | ${'(field=value);field2>wrappedcommas'} + ${'(field=value) and field2>"value with spaces"'} | ${'(field=value);field2>value with spaces'} + ${'field ='} | ${'field='} + ${'field = value'} | ${'field=value'} + ${'field = value()'} | ${'field=value()'} + ${'field = valueand'} | ${'field=valueand'} + ${'field = valueor'} | ${'field=valueor'} + ${'field = value='} | ${'field=value='} + ${'field = value!='} | ${'field=value!='} + ${'field = value>'} | ${'field=value>'} + ${'field = value<'} | ${'field=value<'} + ${'field = value~'} | ${'field=value~' /** ~ character is not supported as value in the q query parameter */} + ${'field = "custom value"'} | ${'field=custom value'} + ${'field = "custom value()"'} | ${'field=custom value()'} + ${'field = "value and value2"'} | ${'field=value and value2'} + ${'field = "value or value2"'} | ${'field=value or value2'} + ${'field = "value = value2"'} | ${'field=value = value2'} + ${'field = "value != value2"'} | ${'field=value != value2'} + ${'field = "value > value2"'} | ${'field=value > value2'} + ${'field = "value < value2"'} | ${'field=value < value2'} + ${'field = "value ~ value2"'} | ${'field=value ~ value2' /** ~ character is not supported as value in the q query parameter */} + ${'field = value or'} | ${'field=value,'} + ${'field = value or field2'} | ${'field=value,field2'} + ${'field = value or field2 <'} | ${'field=value,field2<'} + ${'field = value or field2 < value2'} | ${'field=value,field2 "custom value" '} | ${'(field=value);field2>custom value'} + `('transformSpecificQLToUnifiedQL - WQL $WQL TO UQL $UQL', ({ WQL, UQL }) => { expect(transformSpecificQLToUnifiedQL(WQL)).toEqual(UQL); }); // When a suggestion is clicked, change the input text it.each` - WQL | clikedSuggestion | changedInput - ${''} | ${{type: { iconType: 'kqlField', color: 'tint4' }, label: 'field'}} | ${'field'} - ${'field'} | ${{type: { iconType: 'kqlField', color: 'tint4' }, label: 'field2'}} | ${'field2'} - ${'field'} | ${{type: { iconType: 'kqlOperand', color: 'tint1' }, label: '='}} | ${'field='} - ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value'}} | ${'field=value'} - ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value()'}} | ${'field=value()'} - ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'valueand'}} | ${'field=valueand'} - ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'valueor'}} | ${'field=valueor'} - ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value='}} | ${'field=value='} - ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value!='}} | ${'field=value!='} - ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value>'}} | ${'field=value>'} - ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value<'}} | ${'field=value<'} - ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value~'}} | ${'field=value~' /** ~ character is not supported as value in the q query parameter */} - ${'field='} | ${{type: { iconType: 'kqlOperand', color: 'tint1' }, label: '!='}} | ${'field!='} - ${'field=value'} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value2'}} | ${'field=value2'} - ${'field=value'} | ${{type: { iconType: 'kqlSelector', color: 'tint3' }, label: 'and'}} | ${'field=value and '} - ${'field=value and'} | ${{type: { iconType: 'kqlSelector', color: 'tint3' }, label: 'or'}} | ${'field=value or'} - ${'field=value and'} | ${{type: { iconType: 'kqlField', color: 'tint4' }, label: 'field2'}} | ${'field=value and field2'} - ${'field=value and '} | ${{type: { iconType: 'kqlSelector', color: 'tint3' }, label: 'or'}} | ${'field=value or '} - ${'field=value and '} | ${{type: { iconType: 'kqlField', color: 'tint4' }, label: 'field2'}} | ${'field=value and field2'} - ${'field=value and field2'} | ${{type: { iconType: 'kqlOperand', color: 'tint1' }, label: '>'}} | ${'field=value and field2>'} - ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with spaces'}} | ${'field="with spaces"'} - ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with "spaces'}} | ${'field="with \\"spaces"'} - ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with value()'}} | ${'field="with value()"'} - ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with and value'}}| ${'field="with and value"'} - ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with or value'}} | ${'field="with or value"'} - ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with = value'}} | ${'field="with = value"'} - ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with != value'}} | ${'field="with != value"'} - ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with > value'}} | ${'field="with > value"'} - ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with < value'}} | ${'field="with < value"'} - ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with ~ value'}} | ${'field="with ~ value"' /** ~ character is not supported as value in the q query parameter */} - ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: '"value'}} | ${'field="\\"value"'} - ${'field="with spaces"'} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value'}} | ${'field=value'} - ${'field="with spaces"'} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'other spaces'}} | ${'field="other spaces"'} - ${''} | ${{type: { iconType: 'tokenDenseVector', color: 'tint3' }, label: '('}} | ${'('} - ${'('} | ${{type: { iconType: 'kqlField', color: 'tint4' }, label: 'field'}} | ${'(field'} - ${'(field'} | ${{type: { iconType: 'kqlField', color: 'tint4' }, label: 'field2'}} | ${'(field2'} - ${'(field'} | ${{type: { iconType: 'kqlOperand', color: 'tint1' }, label: '='}} | ${'(field='} - ${'(field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value'}} | ${'(field=value'} - ${'(field=value'} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value2'}} | ${'(field=value2'} - ${'(field=value'} | ${{type: { iconType: 'kqlSelector', color: 'tint3' }, label: 'or'}} | ${'(field=value or '} - ${'(field=value or'} | ${{type: { iconType: 'kqlSelector', color: 'tint3' }, label: 'and'}} | ${'(field=value and'} - ${'(field=value or'} | ${{type: { iconType: 'kqlField', color: 'tint4' }, label: 'field2'}} | ${'(field=value or field2'} - ${'(field=value or '} | ${{type: { iconType: 'kqlSelector', color: 'tint3' }, label: 'and'}} | ${'(field=value and '} - ${'(field=value or '} | ${{type: { iconType: 'kqlField', color: 'tint4' }, label: 'field2'}} | ${'(field=value or field2'} - ${'(field=value or field2'} | ${{type: { iconType: 'kqlOperand', color: 'tint1' }, label: '>'}} | ${'(field=value or field2>'} - ${'(field=value or field2>'} | ${{type: { iconType: 'kqlOperand', color: 'tint1' }, label: '>'}} | ${'(field=value or field2>'} - ${'(field=value or field2>'} | ${{type: { iconType: 'kqlOperand', color: 'tint1' }, label: '~'}} | ${'(field=value or field2~'} - ${'(field=value or field2>'} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value2'}} | ${'(field=value or field2>value2'} - ${'(field=value or field2>value2'}| ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value3'}} | ${'(field=value or field2>value3'} - ${'(field=value or field2>value2'}| ${{type: { iconType: 'tokenDenseVector', color: 'tint3' }, label: ')'}} | ${'(field=value or field2>value2)'} - `('click suggestion - WQL $WQL => $changedInput', async ({WQL: currentInput, clikedSuggestion, changedInput}) => { - // Mock input - let input = currentInput; + WQL | clikedSuggestion | changedInput + ${''} | ${{ type: { iconType: 'kqlField', color: 'tint4' }, label: 'field' }} | ${'field'} + ${'field'} | ${{ type: { iconType: 'kqlField', color: 'tint4' }, label: 'field2' }} | ${'field2'} + ${'field'} | ${{ type: { iconType: 'kqlOperand', color: 'tint1' }, label: '=' }} | ${'field='} + ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value' }} | ${'field=value'} + ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value()' }} | ${'field=value()'} + ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'valueand' }} | ${'field=valueand'} + ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'valueor' }} | ${'field=valueor'} + ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value=' }} | ${'field=value='} + ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value!=' }} | ${'field=value!='} + ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value>' }} | ${'field=value>'} + ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value<' }} | ${'field=value<'} + ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value~' }} | ${'field=value~' /** ~ character is not supported as value in the q query parameter */} + ${'field='} | ${{ type: { iconType: 'kqlOperand', color: 'tint1' }, label: '!=' }} | ${'field!='} + ${'field=value'} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value2' }} | ${'field=value2'} + ${'field=value'} | ${{ type: { iconType: 'kqlSelector', color: 'tint3' }, label: 'and' }} | ${'field=value and '} + ${'field=value and'} | ${{ type: { iconType: 'kqlSelector', color: 'tint3' }, label: 'or' }} | ${'field=value or'} + ${'field=value and'} | ${{ type: { iconType: 'kqlField', color: 'tint4' }, label: 'field2' }} | ${'field=value and field2'} + ${'field=value and '} | ${{ type: { iconType: 'kqlSelector', color: 'tint3' }, label: 'or' }} | ${'field=value or '} + ${'field=value and '} | ${{ type: { iconType: 'kqlField', color: 'tint4' }, label: 'field2' }} | ${'field=value and field2'} + ${'field=value and field2'} | ${{ type: { iconType: 'kqlOperand', color: 'tint1' }, label: '>' }} | ${'field=value and field2>'} + ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with spaces' }} | ${'field="with spaces"'} + ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with "spaces' }} | ${'field="with \\"spaces"'} + ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with value()' }} | ${'field="with value()"'} + ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with and value' }} | ${'field="with and value"'} + ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with or value' }} | ${'field="with or value"'} + ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with = value' }} | ${'field="with = value"'} + ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with != value' }} | ${'field="with != value"'} + ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with > value' }} | ${'field="with > value"'} + ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with < value' }} | ${'field="with < value"'} + ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with ~ value' }} | ${'field="with ~ value"' /** ~ character is not supported as value in the q query parameter */} + ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: '"value' }} | ${'field="\\"value"'} + ${'field="with spaces"'} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value' }} | ${'field=value'} + ${'field="with spaces"'} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'other spaces' }} | ${'field="other spaces"'} + ${''} | ${{ type: { iconType: 'tokenDenseVector', color: 'tint3' }, label: '(' }} | ${'('} + ${'('} | ${{ type: { iconType: 'kqlField', color: 'tint4' }, label: 'field' }} | ${'(field'} + ${'(field'} | ${{ type: { iconType: 'kqlField', color: 'tint4' }, label: 'field2' }} | ${'(field2'} + ${'(field'} | ${{ type: { iconType: 'kqlOperand', color: 'tint1' }, label: '=' }} | ${'(field='} + ${'(field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value' }} | ${'(field=value'} + ${'(field=value'} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value2' }} | ${'(field=value2'} + ${'(field=value'} | ${{ type: { iconType: 'kqlSelector', color: 'tint3' }, label: 'or' }} | ${'(field=value or '} + ${'(field=value or'} | ${{ type: { iconType: 'kqlSelector', color: 'tint3' }, label: 'and' }} | ${'(field=value and'} + ${'(field=value or'} | ${{ type: { iconType: 'kqlField', color: 'tint4' }, label: 'field2' }} | ${'(field=value or field2'} + ${'(field=value or '} | ${{ type: { iconType: 'kqlSelector', color: 'tint3' }, label: 'and' }} | ${'(field=value and '} + ${'(field=value or '} | ${{ type: { iconType: 'kqlField', color: 'tint4' }, label: 'field2' }} | ${'(field=value or field2'} + ${'(field=value or field2'} | ${{ type: { iconType: 'kqlOperand', color: 'tint1' }, label: '>' }} | ${'(field=value or field2>'} + ${'(field=value or field2>'} | ${{ type: { iconType: 'kqlOperand', color: 'tint1' }, label: '>' }} | ${'(field=value or field2>'} + ${'(field=value or field2>'} | ${{ type: { iconType: 'kqlOperand', color: 'tint1' }, label: '~' }} | ${'(field=value or field2~'} + ${'(field=value or field2>'} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value2' }} | ${'(field=value or field2>value2'} + ${'(field=value or field2>value2'} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value3' }} | ${'(field=value or field2>value3'} + ${'(field=value or field2>value2'} | ${{ type: { iconType: 'tokenDenseVector', color: 'tint3' }, label: ')' }} | ${'(field=value or field2>value2)'} + `( + 'click suggestion - WQL $WQL => $changedInput', + async ({ WQL: currentInput, clikedSuggestion, changedInput }) => { + // Mock input + let input = currentInput; - const qlOutput = await WQL.run(input, { - setInput: (value: string): void => { input = value; }, - queryLanguage: { - parameters: { - options: {}, - suggestions: { - field: () => ([]), - value: () => ([]) - } - } - } - }); - qlOutput.searchBarProps.onItemClick(clikedSuggestion); - expect(input).toEqual(changedInput); - }); + const qlOutput = await WQL.run(input, { + setInput: (value: string): void => { + input = value; + }, + queryLanguage: { + parameters: { + options: {}, + suggestions: { + field: () => [], + value: () => [], + }, + }, + }, + }); + qlOutput.searchBarProps.onItemClick(clikedSuggestion); + expect(input).toEqual(changedInput); + }, + ); // Transform the external input in UQL (Unified Query Language) to QL it.each` - UQL | WQL - ${''} | ${''} - ${'field'} | ${'field'} - ${'field='} | ${'field='} - ${'field=()'} | ${'field=()'} - ${'field=valueand'} | ${'field=valueand'} - ${'field=valueor'} | ${'field=valueor'} - ${'field=value='} | ${'field=value='} - ${'field=value!='} | ${'field=value!='} - ${'field=value>'} | ${'field=value>'} - ${'field=value<'} | ${'field=value<'} - ${'field=value~'} | ${'field=value~'} - ${'field!='} | ${'field!='} - ${'field>'} | ${'field>'} - ${'field<'} | ${'field<'} - ${'field~'} | ${'field~'} - ${'field=value'} | ${'field=value'} - ${'field=value;'} | ${'field=value and '} - ${'field=value;field2'} | ${'field=value and field2'} - ${'field="'} | ${'field="\\""'} - ${'field=with spaces'} | ${'field="with spaces"'} - ${'field=with "spaces'} | ${'field="with \\"spaces"'} - ${'field=value ()'} | ${'field="value ()"'} - ${'field=with and value'} | ${'field="with and value"'} - ${'field=with or value'} | ${'field="with or value"'} - ${'field=with = value'} | ${'field="with = value"'} - ${'field=with > value'} | ${'field="with > value"'} - ${'field=with < value'} | ${'field="with < value"'} - ${'('} | ${'('} - ${'(field'} | ${'(field'} - ${'(field='} | ${'(field='} - ${'(field=value'} | ${'(field=value'} - ${'(field=value,'} | ${'(field=value or '} - ${'(field=value,field2'} | ${'(field=value or field2'} - ${'(field=value,field2>'} | ${'(field=value or field2>'} - ${'(field=value,field2>value2'} | ${'(field=value or field2>value2'} - ${'(field=value,field2>value2)'} | ${'(field=value or field2>value2)'} - ${'implicit=value;'} | ${''} - ${'implicit=value;field'} | ${'field'} - `('Transform the external input UQL to QL - UQL $UQL => $WQL', async ({UQL, WQL: changedInput}) => { - expect(WQL.transformInput(UQL, { - parameters: { - options: { - implicitQuery: { - query: 'implicit=value', - conjunction: ';' - } - } - } - })).toEqual(changedInput); - }); + UQL | WQL + ${''} | ${''} + ${'field'} | ${'field'} + ${'field='} | ${'field='} + ${'field=()'} | ${'field=()'} + ${'field=valueand'} | ${'field=valueand'} + ${'field=valueor'} | ${'field=valueor'} + ${'field=value='} | ${'field=value='} + ${'field=value!='} | ${'field=value!='} + ${'field=value>'} | ${'field=value>'} + ${'field=value<'} | ${'field=value<'} + ${'field=value~'} | ${'field=value~'} + ${'field!='} | ${'field!='} + ${'field>'} | ${'field>'} + ${'field<'} | ${'field<'} + ${'field~'} | ${'field~'} + ${'field=value'} | ${'field=value'} + ${'field=value;'} | ${'field=value and '} + ${'field=value;field2'} | ${'field=value and field2'} + ${'field="'} | ${'field="\\""'} + ${'field=with spaces'} | ${'field="with spaces"'} + ${'field=with "spaces'} | ${'field="with \\"spaces"'} + ${'field=value ()'} | ${'field="value ()"'} + ${'field=with and value'} | ${'field="with and value"'} + ${'field=with or value'} | ${'field="with or value"'} + ${'field=with = value'} | ${'field="with = value"'} + ${'field=with > value'} | ${'field="with > value"'} + ${'field=with < value'} | ${'field="with < value"'} + ${'('} | ${'('} + ${'(field'} | ${'(field'} + ${'(field='} | ${'(field='} + ${'(field=value'} | ${'(field=value'} + ${'(field=value,'} | ${'(field=value or '} + ${'(field=value,field2'} | ${'(field=value or field2'} + ${'(field=value,field2>'} | ${'(field=value or field2>'} + ${'(field=value,field2>value2'} | ${'(field=value or field2>value2'} + ${'(field=value,field2>value2)'} | ${'(field=value or field2>value2)'} + ${'implicit=value;'} | ${''} + ${'implicit=value;field'} | ${'field'} + `( + 'Transform the external input UQL to QL - UQL $UQL => $WQL', + async ({ UQL, WQL: changedInput }) => { + expect( + WQL.transformInput(UQL, { + parameters: { + options: { + implicitQuery: { + query: 'implicit=value', + conjunction: ';', + }, + }, + }, + }), + ).toEqual(changedInput); + }, + ); /* The ! and ~ characters can't be part of a value that contains examples. The tests doesn't include these cases. - + Value examples: - with != value - with ~ value */ - + // Validate the tokens it.each` - WQL | validationError - ${''} | ${undefined} - ${'field1'} | ${undefined} - ${'field2'} | ${undefined} - ${'field1='} | ${['The value for field "field1" is missing.']} - ${'field2='} | ${['The value for field "field2" is missing.']} - ${'field='} | ${['"field" is not a valid field.']} - ${'custom='} | ${['"custom" is not a valid field.']} - ${'field1=value'} | ${undefined} - ${'field1=1'} | ${['Numbers are not valid for field1']} - ${'field1=value1'} | ${['Numbers are not valid for field1']} - ${'field2=value'} | ${undefined} - ${'field=value'} | ${['"field" is not a valid field.']} - ${'custom=value'} | ${['"custom" is not a valid field.']} - ${'field1=value!test'} | ${['"value!test" is not a valid value. Invalid characters found: !']} - ${'field1=value&test'} | ${['"value&test" is not a valid value. Invalid characters found: &']} - ${'field1=value!value&test'} | ${['"value!value&test" is not a valid value. Invalid characters found: !, &']} - ${'field1=value!value!test'} | ${['"value!value!test" is not a valid value. Invalid characters found: !']} - ${'field1=value!value!t$&st'} | ${['"value!value!t$&st" is not a valid value. Invalid characters found: !, $, &']} - ${'field1=value and'} | ${['There is no whitespace after conjunction "and".', 'There is no sentence after conjunction "and".']} - ${'field2=value and'} | ${['There is no whitespace after conjunction "and".', 'There is no sentence after conjunction "and".']} - ${'field=value and'} | ${['"field" is not a valid field.', 'There is no whitespace after conjunction "and".', 'There is no sentence after conjunction "and".']} - ${'custom=value and'} | ${['"custom" is not a valid field.', 'There is no whitespace after conjunction "and".', 'There is no sentence after conjunction "and".']} - ${'field1=value and '} | ${['There is no sentence after conjunction "and".']} - ${'field2=value and '} | ${['There is no sentence after conjunction "and".']} - ${'field=value and '} | ${['"field" is not a valid field.', 'There is no sentence after conjunction "and".']} - ${'custom=value and '} | ${['"custom" is not a valid field.', 'There is no sentence after conjunction "and".']} - ${'field1=value and field2'} | ${['The operator for field \"field2\" is missing.']} - ${'field2=value and field1'} | ${['The operator for field \"field1\" is missing.']} - ${'field1=value and field'} | ${['"field" is not a valid field.']} - ${'field2=value and field'} | ${['"field" is not a valid field.']} - ${'field=value and custom'} | ${['"field" is not a valid field.', '"custom" is not a valid field.']} - ${'('} | ${undefined} - ${'(field'} | ${undefined} - ${'(field='} | ${['"field" is not a valid field.']} - ${'(field=value'} | ${['"field" is not a valid field.']} - ${'(field=value or'} | ${['"field" is not a valid field.', 'There is no whitespace after conjunction "or".', 'There is no sentence after conjunction "or".']} - ${'(field=value or '} | ${['"field" is not a valid field.', 'There is no sentence after conjunction "or".']} - ${'(field=value or field2'} | ${['"field" is not a valid field.', 'The operator for field \"field2\" is missing.']} - ${'(field=value or field2>'} | ${['"field" is not a valid field.', 'The value for field "field2" is missing.']} - ${'(field=value or field2>value2'}| ${['"field" is not a valid field.']} - `('validate the tokens - WQL $WQL => $validationError', async ({WQL: currentInput, validationError}) => { - - const qlOutput = await WQL.run(currentInput, { - queryLanguage: { - parameters: { - options: {}, - suggestions: { - field: () => (['field1', 'field2'].map(label => ({label}))), - value: () => ([]) + WQL | validationError + ${''} | ${undefined} + ${'field1'} | ${undefined} + ${'field2'} | ${undefined} + ${'field1='} | ${['The value for field "field1" is missing.']} + ${'field2='} | ${['The value for field "field2" is missing.']} + ${'field='} | ${['"field" is not a valid field.']} + ${'custom='} | ${['"custom" is not a valid field.']} + ${'field1=value'} | ${undefined} + ${'field1=1'} | ${['Numbers are not valid for field1']} + ${'field1=value1'} | ${['Numbers are not valid for field1']} + ${'field2=value'} | ${undefined} + ${'field=value'} | ${['"field" is not a valid field.']} + ${'custom=value'} | ${['"custom" is not a valid field.']} + ${'field1=value!test'} | ${['"value!test" is not a valid value. Invalid characters found: !']} + ${'field1=value&test'} | ${['"value&test" is not a valid value. Invalid characters found: &']} + ${'field1=value!value&test'} | ${['"value!value&test" is not a valid value. Invalid characters found: !&']} + ${'field1=value!value!test'} | ${['"value!value!test" is not a valid value. Invalid characters found: !']} + ${'field1=value!value!t$&st'} | ${['"value!value!t$&st" is not a valid value. Invalid characters found: !$&']} + ${'field1=value and'} | ${['There is no whitespace after conjunction "and".', 'There is no sentence after conjunction "and".']} + ${'field2=value and'} | ${['There is no whitespace after conjunction "and".', 'There is no sentence after conjunction "and".']} + ${'field=value and'} | ${['"field" is not a valid field.', 'There is no whitespace after conjunction "and".', 'There is no sentence after conjunction "and".']} + ${'custom=value and'} | ${['"custom" is not a valid field.', 'There is no whitespace after conjunction "and".', 'There is no sentence after conjunction "and".']} + ${'field1=value and '} | ${['There is no sentence after conjunction "and".']} + ${'field2=value and '} | ${['There is no sentence after conjunction "and".']} + ${'field=value and '} | ${['"field" is not a valid field.', 'There is no sentence after conjunction "and".']} + ${'custom=value and '} | ${['"custom" is not a valid field.', 'There is no sentence after conjunction "and".']} + ${'field1=value and field2'} | ${['The operator for field "field2" is missing.']} + ${'field2=value and field1'} | ${['The operator for field "field1" is missing.']} + ${'field1=value and field'} | ${['"field" is not a valid field.']} + ${'field2=value and field'} | ${['"field" is not a valid field.']} + ${'field=value and custom'} | ${['"field" is not a valid field.', '"custom" is not a valid field.']} + ${'('} | ${undefined} + ${'(field'} | ${undefined} + ${'(field='} | ${['"field" is not a valid field.']} + ${'(field=value'} | ${['"field" is not a valid field.']} + ${'(field=value or'} | ${['"field" is not a valid field.', 'There is no whitespace after conjunction "or".', 'There is no sentence after conjunction "or".']} + ${'(field=value or '} | ${['"field" is not a valid field.', 'There is no sentence after conjunction "or".']} + ${'(field=value or field2'} | ${['"field" is not a valid field.', 'The operator for field "field2" is missing.']} + ${'(field=value or field2>'} | ${['"field" is not a valid field.', 'The value for field "field2" is missing.']} + ${'(field=value or field2>value2'} | ${['"field" is not a valid field.']} + `( + 'validate the tokens - WQL $WQL => $validationError', + async ({ WQL: currentInput, validationError }) => { + const qlOutput = await WQL.run(currentInput, { + queryLanguage: { + parameters: { + options: {}, + suggestions: { + field: () => ['field1', 'field2'].map(label => ({ label })), + value: () => [], + }, + validate: { + value: (token, { field, operator_compare }) => { + if (field === 'field1') { + const value = token.formattedValue || token.value; + return /\d/.test(value) + ? `Numbers are not valid for ${field}` + : undefined; + } + }, + }, }, - validate: { - value: (token, {field, operator_compare}) => { - if(field === 'field1'){ - const value = token.formattedValue || token.value; - return /\d/.test(value) - ? `Numbers are not valid for ${field}` - : undefined - } - } - } - } - } - }); - expect(qlOutput.output.error).toEqual(validationError); - }); + }, + }); + expect(qlOutput.output.error).toEqual(validationError); + }, + ); }); diff --git a/plugins/main/public/components/search-bar/query-language/wql.tsx b/plugins/main/public/components/search-bar/query-language/wql.tsx index db85ca55b2..7470587f45 100644 --- a/plugins/main/public/components/search-bar/query-language/wql.tsx +++ b/plugins/main/public/components/search-bar/query-language/wql.tsx @@ -22,7 +22,7 @@ type ITokenType = | 'value' | 'conjunction' | 'whitespace'; -type IToken = { type: ITokenType; value: string }; +type IToken = { type: ITokenType; value: string; formattedValue?: string }; type ITokens = IToken[]; /* API Query Language @@ -187,9 +187,10 @@ export function tokenizer(input: string): ITokens { ? 'whitespace' : key, value, - ...(key === 'value' && value && /^"(.+)"$/.test(value) - ? { formattedValue: value.match(/^"(.+)"$/)[1] } - : { formattedValue: value }), + ...(key === 'value' && + (value && /^"([\s\S]+)"$/.test(value) + ? { formattedValue: value.match(/^"([\s\S]+)"$/)[1] } + : { formattedValue: value })), })), ) .flat(); @@ -601,16 +602,20 @@ export function transformSpecificQLToUnifiedQL( } return tokens - .filter(({ type, value }) => type !== 'whitespace' && value) - .map(({ type, value }) => { + .filter( + ({ type, value, formattedValue }) => + type !== 'whitespace' && (formattedValue ?? value), + ) + .map(({ type, value, formattedValue }) => { switch (type) { case 'value': { - // Value is wrapped with " - let [_, extractedValue] = value.match(/^"(.+)"$/) || [null, null]; - // Replace the escaped comma (\") by comma (") + // If the value is wrapped with ", then replace the escaped double quotation mark (\") + // by double quotation marks (") // WARN: This could cause a problem with value that contains this sequence \" - extractedValue && - (extractedValue = extractedValue.replace(/\\"/g, '"')); + const extractedValue = + formattedValue !== value + ? formattedValue.replace(/\\"/g, '"') + : formattedValue; return extractedValue || value; break; } @@ -695,7 +700,7 @@ function validateTokenValue(token: IToken): string | undefined { : [ `"${token.value}" is not a valid value.`, ...(invalidCharacters.length - ? [`Invalid characters found: ${invalidCharacters.join(', ')}`] + ? [`Invalid characters found: ${invalidCharacters.join('')}`] : []), ].join(' '); } From 3831beee36c4ed8d89530c2c852fd25292c956f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Thu, 3 Aug 2023 12:54:23 +0200 Subject: [PATCH 56/76] fix(search-bar): update test snapshot --- .../__snapshots__/table-with-search-bar.test.tsx.snap | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/main/public/components/common/tables/__snapshots__/table-with-search-bar.test.tsx.snap b/plugins/main/public/components/common/tables/__snapshots__/table-with-search-bar.test.tsx.snap index d4a34ae9fc..7b13e589a4 100644 --- a/plugins/main/public/components/common/tables/__snapshots__/table-with-search-bar.test.tsx.snap +++ b/plugins/main/public/components/common/tables/__snapshots__/table-with-search-bar.test.tsx.snap @@ -109,7 +109,7 @@ exports[`Table With Search Bar component renders correctly to match the snapshot
@@ -178,7 +178,7 @@ exports[`Table With Search Bar component renders correctly to match the snapshot
@@ -258,7 +258,7 @@ exports[`Table With Search Bar component renders correctly to match the snapshot
@@ -368,7 +368,7 @@ exports[`Table With Search Bar component renders correctly to match the snapshot
@@ -434,7 +434,7 @@ exports[`Table With Search Bar component renders correctly to match the snapshot
From 449eeee5f4a2563e5083fd5a1ab1f14c5a42394d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Fri, 4 Aug 2023 09:05:03 +0200 Subject: [PATCH 57/76] fix(table-wz-api): avoid the double toast message when there is an error fetching data and replace the error.name to RequestError --- .../components/common/tables/table-wz-api.tsx | 116 +++++++++++------- 1 file changed, 72 insertions(+), 44 deletions(-) diff --git a/plugins/main/public/components/common/tables/table-wz-api.tsx b/plugins/main/public/components/common/tables/table-wz-api.tsx index b31be28dfc..fc11c05c42 100644 --- a/plugins/main/public/components/common/tables/table-wz-api.tsx +++ b/plugins/main/public/components/common/tables/table-wz-api.tsx @@ -65,8 +65,10 @@ export function TableWzAPI({ const [totalItems, setTotalItems] = useState(0); const [filters, setFilters] = useState({}); const [isLoading, setIsLoading] = useState(false); - const onFiltersChange = (filters) => - typeof rest.onFiltersChange === 'function' ? rest.onFiltersChange(filters) : null; + const onFiltersChange = filters => + typeof rest.onFiltersChange === 'function' + ? rest.onFiltersChange(filters) + : null; /** * Changing the reloadFootprint timestamp will trigger reloading the table @@ -74,15 +76,22 @@ export function TableWzAPI({ const [reloadFootprint, setReloadFootprint] = useState(rest.reload || 0); const [selectedFields, setSelectedFields] = useStateStorage( - rest.tableColumns.some(({show}) => show) - ? rest.tableColumns.filter(({show}) => show).map(({field}) => field) - : rest.tableColumns.map(({field}) => field), + rest.tableColumns.some(({ show }) => show) + ? rest.tableColumns.filter(({ show }) => show).map(({ field }) => field) + : rest.tableColumns.map(({ field }) => field), rest?.saveStateStorage?.system, - rest?.saveStateStorage?.key ? `${rest?.saveStateStorage?.key}-visible-fields` : undefined + rest?.saveStateStorage?.key + ? `${rest?.saveStateStorage?.key}-visible-fields` + : undefined, ); const [isOpenFieldSelector, setIsOpenFieldSelector] = useState(false); - const onSearch = useCallback(async function (endpoint, filters, pagination, sorting) { + const onSearch = useCallback(async function ( + endpoint, + filters, + pagination, + sorting, + ) { try { const { pageIndex, pageSize } = pagination; const { field, direction } = sorting.sort; @@ -103,23 +112,25 @@ export function TableWzAPI({ ).data; setIsLoading(false); setTotalItems(totalItems); - return { items: rest.mapResponseItem ? items.map(rest.mapResponseItem) : items, totalItems }; + return { + items: rest.mapResponseItem ? items.map(rest.mapResponseItem) : items, + totalItems, + }; } catch (error) { setIsLoading(false); setTotalItems(0); - const options = { - context: `${TableWithSearchBar.name}.useEffect`, - level: UI_LOGGER_LEVELS.ERROR, - severity: UI_ERROR_SEVERITIES.BUSINESS, - error: { - error: error, - message: error.message || error, - title: `${error.name}: Error searching items`, - }, - }; - getErrorOrchestrator().handleError(options); + if (error?.name) { + /* This replaces the error name. The intention is that an AxiosError + doesn't appear in the toast message. + TODO: This should be managed by the service that does the request instead of only changing + the name in this case. + */ + error.name = 'RequestError'; + } + throw error; } - }, []); + }, + []); const renderActionButtons = ( <> @@ -148,10 +159,7 @@ export function TableWzAPI({ const ReloadButton = ( - triggerReload()} - > + triggerReload()}> Refresh @@ -160,16 +168,22 @@ export function TableWzAPI({ const header = ( <> - + {rest.title && ( - +

{rest.title}{' '} - {isLoading ? : ({totalItems})} + {isLoading ? ( + + ) : ( + ({totalItems}) + )}

)} - {rest.description && {rest.description}} + {rest.description && ( + {rest.description} + )}
@@ -183,14 +197,20 @@ export function TableWzAPI({ endpoint={rest.endpoint} totalItems={totalItems} filters={getFilters(filters)} - title={typeof rest.downloadCsv === 'string' ? rest.downloadCsv : rest.title} + title={ + typeof rest.downloadCsv === 'string' + ? rest.downloadCsv + : rest.title + } /> )} {rest.showFieldSelector && ( - - setIsOpenFieldSelector(state => !state)}> - + + setIsOpenFieldSelector(state => !state)} + > + @@ -205,21 +225,20 @@ export function TableWzAPI({ options={rest.tableColumns.map(item => ({ id: item.field, label: item.name, - checked: selectedFields.includes(item.field) + checked: selectedFields.includes(item.field), }))} - onChange={(optionID) => { + onChange={optionID => { setSelectedFields(state => { - if(state.includes(optionID)){ - if(state.length > 1){ + if (state.includes(optionID)) { + if (state.length > 1) { return state.filter(field => field !== optionID); } return state; - }; + } return [...state, optionID]; - } - ) + }); }} - className="columnsSelectedCheckboxs" + className='columnsSelectedCheckboxs' idToSelectedMap={{}} /> @@ -228,13 +247,22 @@ export function TableWzAPI({ ); - const tableColumns = rest.tableColumns - .filter(({field}) => selectedFields.includes(field)); + const tableColumns = rest.tableColumns.filter(({ field }) => + selectedFields.includes(field), + ); const table = rest.searchTable ? ( - + ) : ( - + ); return ( From 579d30fe52c0f7d6ff08795fb2bc95f544b0313c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Fri, 4 Aug 2023 13:49:43 +0200 Subject: [PATCH 58/76] fix(search-bar): add validation for value token in WQL --- .../search-bar/query-language/wql.test.tsx | 93 ++++++++++--------- .../search-bar/query-language/wql.tsx | 28 ++++-- 2 files changed, 70 insertions(+), 51 deletions(-) diff --git a/plugins/main/public/components/search-bar/query-language/wql.test.tsx b/plugins/main/public/components/search-bar/query-language/wql.test.tsx index 5cdecb968b..4de5de790b 100644 --- a/plugins/main/public/components/search-bar/query-language/wql.test.tsx +++ b/plugins/main/public/components/search-bar/query-language/wql.test.tsx @@ -397,48 +397,52 @@ describe('Query language - WQL', () => { */ // Validate the tokens + // Some examples of value tokens are based on this API test: https://github.com/wazuh/wazuh/blob/813595cf58d753c1066c3e7c2018dbb4708df088/framework/wazuh/core/tests/test_utils.py#L987-L1050 it.each` - WQL | validationError - ${''} | ${undefined} - ${'field1'} | ${undefined} - ${'field2'} | ${undefined} - ${'field1='} | ${['The value for field "field1" is missing.']} - ${'field2='} | ${['The value for field "field2" is missing.']} - ${'field='} | ${['"field" is not a valid field.']} - ${'custom='} | ${['"custom" is not a valid field.']} - ${'field1=value'} | ${undefined} - ${'field1=1'} | ${['Numbers are not valid for field1']} - ${'field1=value1'} | ${['Numbers are not valid for field1']} - ${'field2=value'} | ${undefined} - ${'field=value'} | ${['"field" is not a valid field.']} - ${'custom=value'} | ${['"custom" is not a valid field.']} - ${'field1=value!test'} | ${['"value!test" is not a valid value. Invalid characters found: !']} - ${'field1=value&test'} | ${['"value&test" is not a valid value. Invalid characters found: &']} - ${'field1=value!value&test'} | ${['"value!value&test" is not a valid value. Invalid characters found: !&']} - ${'field1=value!value!test'} | ${['"value!value!test" is not a valid value. Invalid characters found: !']} - ${'field1=value!value!t$&st'} | ${['"value!value!t$&st" is not a valid value. Invalid characters found: !$&']} - ${'field1=value and'} | ${['There is no whitespace after conjunction "and".', 'There is no sentence after conjunction "and".']} - ${'field2=value and'} | ${['There is no whitespace after conjunction "and".', 'There is no sentence after conjunction "and".']} - ${'field=value and'} | ${['"field" is not a valid field.', 'There is no whitespace after conjunction "and".', 'There is no sentence after conjunction "and".']} - ${'custom=value and'} | ${['"custom" is not a valid field.', 'There is no whitespace after conjunction "and".', 'There is no sentence after conjunction "and".']} - ${'field1=value and '} | ${['There is no sentence after conjunction "and".']} - ${'field2=value and '} | ${['There is no sentence after conjunction "and".']} - ${'field=value and '} | ${['"field" is not a valid field.', 'There is no sentence after conjunction "and".']} - ${'custom=value and '} | ${['"custom" is not a valid field.', 'There is no sentence after conjunction "and".']} - ${'field1=value and field2'} | ${['The operator for field "field2" is missing.']} - ${'field2=value and field1'} | ${['The operator for field "field1" is missing.']} - ${'field1=value and field'} | ${['"field" is not a valid field.']} - ${'field2=value and field'} | ${['"field" is not a valid field.']} - ${'field=value and custom'} | ${['"field" is not a valid field.', '"custom" is not a valid field.']} - ${'('} | ${undefined} - ${'(field'} | ${undefined} - ${'(field='} | ${['"field" is not a valid field.']} - ${'(field=value'} | ${['"field" is not a valid field.']} - ${'(field=value or'} | ${['"field" is not a valid field.', 'There is no whitespace after conjunction "or".', 'There is no sentence after conjunction "or".']} - ${'(field=value or '} | ${['"field" is not a valid field.', 'There is no sentence after conjunction "or".']} - ${'(field=value or field2'} | ${['"field" is not a valid field.', 'The operator for field "field2" is missing.']} - ${'(field=value or field2>'} | ${['"field" is not a valid field.', 'The value for field "field2" is missing.']} - ${'(field=value or field2>value2'} | ${['"field" is not a valid field.']} + WQL | validationError + ${''} | ${undefined} + ${'field1'} | ${undefined} + ${'field2'} | ${undefined} + ${'field1='} | ${['The value for field "field1" is missing.']} + ${'field2='} | ${['The value for field "field2" is missing.']} + ${'field='} | ${['"field" is not a valid field.']} + ${'custom='} | ${['"custom" is not a valid field.']} + ${'field1=value'} | ${undefined} + ${'field_not_number=1'} | ${['Numbers are not valid for field_not_number']} + ${'field_not_number=value1'} | ${['Numbers are not valid for field_not_number']} + ${'field2=value'} | ${undefined} + ${'field=value'} | ${['"field" is not a valid field.']} + ${'custom=value'} | ${['"custom" is not a valid field.']} + ${'field1=value!test'} | ${['"value!test" is not a valid value. Invalid characters found: !']} + ${'field1=value&test'} | ${['"value&test" is not a valid value. Invalid characters found: &']} + ${'field1=value!value&test'} | ${['"value!value&test" is not a valid value. Invalid characters found: !&']} + ${'field1=value!value!test'} | ${['"value!value!test" is not a valid value. Invalid characters found: !']} + ${'field1=value!value!t$&st'} | ${['"value!value!t$&st" is not a valid value. Invalid characters found: !$&']} + ${'field1=value,'} | ${['"value," is not a valid value.']} + ${'field1="Mozilla Firefox 53.0 (x64 en-US)"'} | ${undefined} + ${'field1="[\\"https://example-link@<>=,%?\\"]"'} | ${undefined} + ${'field1=value and'} | ${['There is no whitespace after conjunction "and".', 'There is no sentence after conjunction "and".']} + ${'field2=value and'} | ${['There is no whitespace after conjunction "and".', 'There is no sentence after conjunction "and".']} + ${'field=value and'} | ${['"field" is not a valid field.', 'There is no whitespace after conjunction "and".', 'There is no sentence after conjunction "and".']} + ${'custom=value and'} | ${['"custom" is not a valid field.', 'There is no whitespace after conjunction "and".', 'There is no sentence after conjunction "and".']} + ${'field1=value and '} | ${['There is no sentence after conjunction "and".']} + ${'field2=value and '} | ${['There is no sentence after conjunction "and".']} + ${'field=value and '} | ${['"field" is not a valid field.', 'There is no sentence after conjunction "and".']} + ${'custom=value and '} | ${['"custom" is not a valid field.', 'There is no sentence after conjunction "and".']} + ${'field1=value and field2'} | ${['The operator for field "field2" is missing.']} + ${'field2=value and field1'} | ${['The operator for field "field1" is missing.']} + ${'field1=value and field'} | ${['"field" is not a valid field.']} + ${'field2=value and field'} | ${['"field" is not a valid field.']} + ${'field=value and custom'} | ${['"field" is not a valid field.', '"custom" is not a valid field.']} + ${'('} | ${undefined} + ${'(field'} | ${undefined} + ${'(field='} | ${['"field" is not a valid field.']} + ${'(field=value'} | ${['"field" is not a valid field.']} + ${'(field=value or'} | ${['"field" is not a valid field.', 'There is no whitespace after conjunction "or".', 'There is no sentence after conjunction "or".']} + ${'(field=value or '} | ${['"field" is not a valid field.', 'There is no sentence after conjunction "or".']} + ${'(field=value or field2'} | ${['"field" is not a valid field.', 'The operator for field "field2" is missing.']} + ${'(field=value or field2>'} | ${['"field" is not a valid field.', 'The value for field "field2" is missing.']} + ${'(field=value or field2>value2'} | ${['"field" is not a valid field.']} `( 'validate the tokens - WQL $WQL => $validationError', async ({ WQL: currentInput, validationError }) => { @@ -447,12 +451,15 @@ describe('Query language - WQL', () => { parameters: { options: {}, suggestions: { - field: () => ['field1', 'field2'].map(label => ({ label })), + field: () => + ['field1', 'field2', 'field_not_number'].map(label => ({ + label, + })), value: () => [], }, validate: { value: (token, { field, operator_compare }) => { - if (field === 'field1') { + if (field === 'field_not_number') { const value = token.formattedValue || token.value; return /\d/.test(value) ? `Numbers are not valid for ${field}` diff --git a/plugins/main/public/components/search-bar/query-language/wql.tsx b/plugins/main/public/components/search-bar/query-language/wql.tsx index 7470587f45..06bddbc1c5 100644 --- a/plugins/main/public/components/search-bar/query-language/wql.tsx +++ b/plugins/main/public/components/search-bar/query-language/wql.tsx @@ -685,6 +685,20 @@ function getOutput(input: string, options: OptionsQL) { * @returns */ function validateTokenValue(token: IToken): string | undefined { + const re = new RegExp( + // Value: A string. + '^(?(?:(?:\\((?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|[\\[\\]\\w _\\-.:?\\/\'"=@%<>{}]*)\\))*' + + '(?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|^[\\[\\]\\w _\\-.:?\\\\/\'"=@%<>{}]+)' + + '(?:\\((?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|[\\[\\]\\w _\\-.:?\\\\/\'"=@%<>{}]*)\\))*)+)$', + ); + + const value = token.formattedValue ?? token.value; + const match = value.match(re); + + if (match?.groups?.value === value) { + return undefined; + } + const invalidCharacters: string[] = token.value .split('') .filter((value, index, array) => array.indexOf(value) === index) @@ -695,14 +709,12 @@ function validateTokenValue(token: IToken): string | undefined { ), ); - return !invalidCharacters.length - ? undefined - : [ - `"${token.value}" is not a valid value.`, - ...(invalidCharacters.length - ? [`Invalid characters found: ${invalidCharacters.join('')}`] - : []), - ].join(' '); + return [ + `"${token.value}" is not a valid value.`, + ...(invalidCharacters.length + ? [`Invalid characters found: ${invalidCharacters.join('')}`] + : []), + ].join(' '); } type ITokenValidator = ( From f8b004484a0b43dbf54f317a000e405526a989b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Mon, 7 Aug 2023 16:15:27 +0200 Subject: [PATCH 59/76] fix(search-bar): value token in message related to this is invalid --- .../main/public/components/search-bar/query-language/wql.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/main/public/components/search-bar/query-language/wql.tsx b/plugins/main/public/components/search-bar/query-language/wql.tsx index 06bddbc1c5..9df7dbbf01 100644 --- a/plugins/main/public/components/search-bar/query-language/wql.tsx +++ b/plugins/main/public/components/search-bar/query-language/wql.tsx @@ -710,7 +710,7 @@ function validateTokenValue(token: IToken): string | undefined { ); return [ - `"${token.value}" is not a valid value.`, + `"${value}" is not a valid value.`, ...(invalidCharacters.length ? [`Invalid characters found: ${invalidCharacters.join('')}`] : []), From 885ad452f25129c8169e47c54d366a5a8f02ff9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Tue, 8 Aug 2023 09:32:09 +0200 Subject: [PATCH 60/76] fix(search-bar): fix error related to details.program_name suggestion in the Decoders section --- .../management/decoders/components/decoders-suggestions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/main/public/controllers/management/components/management/decoders/components/decoders-suggestions.ts b/plugins/main/public/controllers/management/components/management/decoders/components/decoders-suggestions.ts index 675b691685..8be17c92dd 100644 --- a/plugins/main/public/controllers/management/components/management/decoders/components/decoders-suggestions.ts +++ b/plugins/main/public/controllers/management/components/management/decoders/components/decoders-suggestions.ts @@ -44,7 +44,7 @@ const decodersItems = { }); // FIX: this breaks the search bar component because returns a non-string value. return result?.data?.data?.affected_items - ?.filter(item => item?.details?.program_name) + ?.filter(item => typeof item?.details?.program_name === 'string') .map(item => ({ label: item?.details?.program_name, })); From eee2bb69cb427cad5d1732fc9e0a658ca56864f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Tue, 8 Aug 2023 10:05:23 +0200 Subject: [PATCH 61/76] feat(search-bar): add constant to define the count of distinct values to get in the suggestions --- plugins/main/common/constants.ts | 1840 ++++++++++------- .../agents/sca/inventory/lib/api-request.ts | 3 +- .../agents/vuls/inventory/lib/api-requests.ts | 3 +- .../mitre_attack_intelligence/resources.tsx | 7 +- .../cdblists/components/cdblists-table.tsx | 7 +- .../components/decoders-suggestions.ts | 58 +- .../ruleset/components/ruleset-suggestions.ts | 71 +- 7 files changed, 1115 insertions(+), 874 deletions(-) diff --git a/plugins/main/common/constants.ts b/plugins/main/common/constants.ts index 403b9153e1..d9894ca0c2 100644 --- a/plugins/main/common/constants.ts +++ b/plugins/main/common/constants.ts @@ -24,10 +24,10 @@ export const WAZUH_ALERTS_PREFIX = 'wazuh-alerts-'; export const WAZUH_ALERTS_PATTERN = 'wazuh-alerts-*'; // Job - Wazuh monitoring -export const WAZUH_INDEX_TYPE_MONITORING = "monitoring"; -export const WAZUH_MONITORING_PREFIX = "wazuh-monitoring-"; -export const WAZUH_MONITORING_PATTERN = "wazuh-monitoring-*"; -export const WAZUH_MONITORING_TEMPLATE_NAME = "wazuh-agent"; +export const WAZUH_INDEX_TYPE_MONITORING = 'monitoring'; +export const WAZUH_MONITORING_PREFIX = 'wazuh-monitoring-'; +export const WAZUH_MONITORING_PATTERN = 'wazuh-monitoring-*'; +export const WAZUH_MONITORING_TEMPLATE_NAME = 'wazuh-agent'; export const WAZUH_MONITORING_DEFAULT_INDICES_SHARDS = 1; export const WAZUH_MONITORING_DEFAULT_INDICES_REPLICAS = 0; export const WAZUH_MONITORING_DEFAULT_CREATION = 'w'; @@ -36,9 +36,9 @@ export const WAZUH_MONITORING_DEFAULT_FREQUENCY = 900; export const WAZUH_MONITORING_DEFAULT_CRON_FREQ = '0 * * * * *'; // Job - Wazuh statistics -export const WAZUH_INDEX_TYPE_STATISTICS = "statistics"; -export const WAZUH_STATISTICS_DEFAULT_PREFIX = "wazuh"; -export const WAZUH_STATISTICS_DEFAULT_NAME = "statistics"; +export const WAZUH_INDEX_TYPE_STATISTICS = 'statistics'; +export const WAZUH_STATISTICS_DEFAULT_PREFIX = 'wazuh'; +export const WAZUH_STATISTICS_DEFAULT_NAME = 'statistics'; export const WAZUH_STATISTICS_PATTERN = `${WAZUH_STATISTICS_DEFAULT_PREFIX}-${WAZUH_STATISTICS_DEFAULT_NAME}-*`; export const WAZUH_STATISTICS_TEMPLATE_NAME = `${WAZUH_STATISTICS_DEFAULT_PREFIX}-${WAZUH_STATISTICS_DEFAULT_NAME}`; export const WAZUH_STATISTICS_DEFAULT_INDICES_SHARDS = 1; @@ -60,7 +60,8 @@ export const WAZUH_SAMPLE_ALERT_PREFIX = 'wazuh-alerts-4.x-'; export const WAZUH_SAMPLE_ALERTS_INDEX_SHARDS = 1; export const WAZUH_SAMPLE_ALERTS_INDEX_REPLICAS = 0; export const WAZUH_SAMPLE_ALERTS_CATEGORY_SECURITY = 'security'; -export const WAZUH_SAMPLE_ALERTS_CATEGORY_AUDITING_POLICY_MONITORING = 'auditing-policy-monitoring'; +export const WAZUH_SAMPLE_ALERTS_CATEGORY_AUDITING_POLICY_MONITORING = + 'auditing-policy-monitoring'; export const WAZUH_SAMPLE_ALERTS_CATEGORY_THREAT_DETECTION = 'threat-detection'; export const WAZUH_SAMPLE_ALERTS_DEFAULT_NUMBER_ALERTS = 3000; export const WAZUH_SAMPLE_ALERTS_CATEGORIES_TYPE_ALERTS = { @@ -74,7 +75,7 @@ export const WAZUH_SAMPLE_ALERTS_CATEGORIES_TYPE_ALERTS = { { apache: true, alerts: 2000 }, { web: true }, { windows: { service_control_manager: true }, alerts: 1000 }, - { github: true } + { github: true }, ], [WAZUH_SAMPLE_ALERTS_CATEGORY_AUDITING_POLICY_MONITORING]: [ { rootcheck: true }, @@ -92,7 +93,8 @@ export const WAZUH_SAMPLE_ALERTS_CATEGORIES_TYPE_ALERTS = { }; // Security -export const WAZUH_SECURITY_PLUGIN_OPENSEARCH_DASHBOARDS_SECURITY = 'OpenSearch Dashboards Security'; +export const WAZUH_SECURITY_PLUGIN_OPENSEARCH_DASHBOARDS_SECURITY = + 'OpenSearch Dashboards Security'; export const WAZUH_SECURITY_PLUGINS = [ WAZUH_SECURITY_PLUGIN_OPENSEARCH_DASHBOARDS_SECURITY, @@ -103,40 +105,49 @@ export const WAZUH_CONFIGURATION_CACHE_TIME = 10000; // time in ms; // Reserved ids for Users/Role mapping export const WAZUH_API_RESERVED_ID_LOWER_THAN = 100; -export const WAZUH_API_RESERVED_WUI_SECURITY_RULES = [ - 1, - 2 -]; +export const WAZUH_API_RESERVED_WUI_SECURITY_RULES = [1, 2]; // Wazuh data path const WAZUH_DATA_PLUGIN_PLATFORM_BASE_PATH = 'data'; export const WAZUH_DATA_PLUGIN_PLATFORM_BASE_ABSOLUTE_PATH = path.join( __dirname, '../../../', - WAZUH_DATA_PLUGIN_PLATFORM_BASE_PATH + WAZUH_DATA_PLUGIN_PLATFORM_BASE_PATH, +); +export const WAZUH_DATA_ABSOLUTE_PATH = path.join( + WAZUH_DATA_PLUGIN_PLATFORM_BASE_ABSOLUTE_PATH, + 'wazuh', ); -export const WAZUH_DATA_ABSOLUTE_PATH = path.join(WAZUH_DATA_PLUGIN_PLATFORM_BASE_ABSOLUTE_PATH, 'wazuh'); // Wazuh data path - config -export const WAZUH_DATA_CONFIG_DIRECTORY_PATH = path.join(WAZUH_DATA_ABSOLUTE_PATH, 'config'); -export const WAZUH_DATA_CONFIG_APP_PATH = path.join(WAZUH_DATA_CONFIG_DIRECTORY_PATH, 'wazuh.yml'); +export const WAZUH_DATA_CONFIG_DIRECTORY_PATH = path.join( + WAZUH_DATA_ABSOLUTE_PATH, + 'config', +); +export const WAZUH_DATA_CONFIG_APP_PATH = path.join( + WAZUH_DATA_CONFIG_DIRECTORY_PATH, + 'wazuh.yml', +); export const WAZUH_DATA_CONFIG_REGISTRY_PATH = path.join( WAZUH_DATA_CONFIG_DIRECTORY_PATH, - 'wazuh-registry.json' + 'wazuh-registry.json', ); // Wazuh data path - logs export const MAX_MB_LOG_FILES = 100; -export const WAZUH_DATA_LOGS_DIRECTORY_PATH = path.join(WAZUH_DATA_ABSOLUTE_PATH, 'logs'); +export const WAZUH_DATA_LOGS_DIRECTORY_PATH = path.join( + WAZUH_DATA_ABSOLUTE_PATH, + 'logs', +); export const WAZUH_DATA_LOGS_PLAIN_FILENAME = 'wazuhapp-plain.log'; export const WAZUH_DATA_LOGS_PLAIN_PATH = path.join( WAZUH_DATA_LOGS_DIRECTORY_PATH, - WAZUH_DATA_LOGS_PLAIN_FILENAME + WAZUH_DATA_LOGS_PLAIN_FILENAME, ); export const WAZUH_DATA_LOGS_RAW_FILENAME = 'wazuhapp.log'; export const WAZUH_DATA_LOGS_RAW_PATH = path.join( WAZUH_DATA_LOGS_DIRECTORY_PATH, - WAZUH_DATA_LOGS_RAW_FILENAME + WAZUH_DATA_LOGS_RAW_FILENAME, ); // Wazuh data path - UI logs @@ -144,15 +155,21 @@ export const WAZUH_UI_LOGS_PLAIN_FILENAME = 'wazuh-ui-plain.log'; export const WAZUH_UI_LOGS_RAW_FILENAME = 'wazuh-ui.log'; export const WAZUH_UI_LOGS_PLAIN_PATH = path.join( WAZUH_DATA_LOGS_DIRECTORY_PATH, - WAZUH_UI_LOGS_PLAIN_FILENAME + WAZUH_UI_LOGS_PLAIN_FILENAME, +); +export const WAZUH_UI_LOGS_RAW_PATH = path.join( + WAZUH_DATA_LOGS_DIRECTORY_PATH, + WAZUH_UI_LOGS_RAW_FILENAME, ); -export const WAZUH_UI_LOGS_RAW_PATH = path.join(WAZUH_DATA_LOGS_DIRECTORY_PATH, WAZUH_UI_LOGS_RAW_FILENAME); // Wazuh data path - downloads -export const WAZUH_DATA_DOWNLOADS_DIRECTORY_PATH = path.join(WAZUH_DATA_ABSOLUTE_PATH, 'downloads'); +export const WAZUH_DATA_DOWNLOADS_DIRECTORY_PATH = path.join( + WAZUH_DATA_ABSOLUTE_PATH, + 'downloads', +); export const WAZUH_DATA_DOWNLOADS_REPORTS_DIRECTORY_PATH = path.join( WAZUH_DATA_DOWNLOADS_DIRECTORY_PATH, - 'reports' + 'reports', ); // Queue @@ -191,8 +208,8 @@ export enum WAZUH_MODULES_ID { CIS_CAT = 'ciscat', VIRUSTOTAL = 'virustotal', GDPR = 'gdpr', - GITHUB = 'github' -}; + GITHUB = 'github', +} export enum WAZUH_MENU_MANAGEMENT_SECTIONS_ID { MANAGEMENT = 'management', @@ -209,19 +226,19 @@ export enum WAZUH_MENU_MANAGEMENT_SECTIONS_ID { LOGS = 'logs', REPORTING = 'reporting', STATISTICS = 'statistics', -}; +} export enum WAZUH_MENU_TOOLS_SECTIONS_ID { API_CONSOLE = 'devTools', RULESET_TEST = 'logtest', -}; +} export enum WAZUH_MENU_SECURITY_SECTIONS_ID { USERS = 'users', ROLES = 'roles', POLICIES = 'policies', ROLES_MAPPING = 'roleMapping', -}; +} export enum WAZUH_MENU_SETTINGS_SECTIONS_ID { SETTINGS = 'settings', @@ -232,13 +249,14 @@ export enum WAZUH_MENU_SETTINGS_SECTIONS_ID { LOGS = 'logs', MISCELLANEOUS = 'miscellaneous', ABOUT = 'about', -}; +} export const AUTHORIZED_AGENTS = 'authorized-agents'; // Wazuh links export const WAZUH_LINK_GITHUB = 'https://github.com/wazuh'; -export const WAZUH_LINK_GOOGLE_GROUPS = 'https://groups.google.com/forum/#!forum/wazuh'; +export const WAZUH_LINK_GOOGLE_GROUPS = + 'https://groups.google.com/forum/#!forum/wazuh'; export const WAZUH_LINK_SLACK = 'https://wazuh.com/community/join-us-on-slack'; export const HEALTH_CHECK = 'health-check'; @@ -252,7 +270,8 @@ export const WAZUH_PLUGIN_PLATFORM_SETTING_TIME_FILTER = { from: 'now-24h', to: 'now', }; -export const PLUGIN_PLATFORM_SETTING_NAME_TIME_FILTER = 'timepicker:timeDefaults'; +export const PLUGIN_PLATFORM_SETTING_NAME_TIME_FILTER = + 'timepicker:timeDefaults'; // Default maxBuckets set by the app export const WAZUH_PLUGIN_PLATFORM_SETTING_MAX_BUCKETS = 200000; @@ -280,24 +299,30 @@ export const ASSETS_BASE_URL_PREFIX = '/plugins/wazuh/assets/'; export const ASSETS_PUBLIC_URL = '/plugins/wazuh/public/assets/'; // Reports -export const REPORTS_LOGO_IMAGE_ASSETS_RELATIVE_PATH = 'images/logo_reports.png'; +export const REPORTS_LOGO_IMAGE_ASSETS_RELATIVE_PATH = + 'images/logo_reports.png'; export const REPORTS_PRIMARY_COLOR = '#256BD1'; export const REPORTS_PAGE_FOOTER_TEXT = 'Copyright © 2022 Wazuh, Inc.'; export const REPORTS_PAGE_HEADER_TEXT = 'info@wazuh.com\nhttps://wazuh.com'; // Plugin platform export const PLUGIN_PLATFORM_NAME = 'Wazuh dashboard'; -export const PLUGIN_PLATFORM_BASE_INSTALLATION_PATH = '/usr/share/wazuh-dashboard/data/wazuh/'; +export const PLUGIN_PLATFORM_BASE_INSTALLATION_PATH = + '/usr/share/wazuh-dashboard/data/wazuh/'; export const PLUGIN_PLATFORM_INSTALLATION_USER = 'wazuh-dashboard'; export const PLUGIN_PLATFORM_INSTALLATION_USER_GROUP = 'wazuh-dashboard'; -export const PLUGIN_PLATFORM_WAZUH_DOCUMENTATION_URL_PATH_UPGRADE_PLATFORM = 'upgrade-guide'; -export const PLUGIN_PLATFORM_WAZUH_DOCUMENTATION_URL_PATH_TROUBLESHOOTING = 'user-manual/wazuh-dashboard/troubleshooting.html'; -export const PLUGIN_PLATFORM_WAZUH_DOCUMENTATION_URL_PATH_APP_CONFIGURATION = 'user-manual/wazuh-dashboard/config-file.html'; -export const PLUGIN_PLATFORM_URL_GUIDE = 'https://opensearch.org/docs/1.2/opensearch/index/'; +export const PLUGIN_PLATFORM_WAZUH_DOCUMENTATION_URL_PATH_UPGRADE_PLATFORM = + 'upgrade-guide'; +export const PLUGIN_PLATFORM_WAZUH_DOCUMENTATION_URL_PATH_TROUBLESHOOTING = + 'user-manual/wazuh-dashboard/troubleshooting.html'; +export const PLUGIN_PLATFORM_WAZUH_DOCUMENTATION_URL_PATH_APP_CONFIGURATION = + 'user-manual/wazuh-dashboard/config-file.html'; +export const PLUGIN_PLATFORM_URL_GUIDE = + 'https://opensearch.org/docs/1.2/opensearch/index/'; export const PLUGIN_PLATFORM_URL_GUIDE_TITLE = 'OpenSearch guide'; export const PLUGIN_PLATFORM_REQUEST_HEADERS = { - 'osd-xsrf': 'kibana' + 'osd-xsrf': 'kibana', }; // Plugin app @@ -316,7 +341,7 @@ export const UI_COLOR_AGENT_STATUS = { [API_NAME_AGENT_STATUS.DISCONNECTED]: '#BD271E', [API_NAME_AGENT_STATUS.PENDING]: '#FEC514', [API_NAME_AGENT_STATUS.NEVER_CONNECTED]: '#646A77', - default: '#000000' + default: '#000000', } as const; export const UI_LABEL_NAME_AGENT_STATUS = { @@ -324,23 +349,23 @@ export const UI_LABEL_NAME_AGENT_STATUS = { [API_NAME_AGENT_STATUS.DISCONNECTED]: 'Disconnected', [API_NAME_AGENT_STATUS.PENDING]: 'Pending', [API_NAME_AGENT_STATUS.NEVER_CONNECTED]: 'Never connected', - default: 'Unknown' + default: 'Unknown', } as const; export const UI_ORDER_AGENT_STATUS = [ API_NAME_AGENT_STATUS.ACTIVE, API_NAME_AGENT_STATUS.DISCONNECTED, API_NAME_AGENT_STATUS.PENDING, - API_NAME_AGENT_STATUS.NEVER_CONNECTED -] + API_NAME_AGENT_STATUS.NEVER_CONNECTED, +]; export const AGENT_SYNCED_STATUS = { SYNCED: 'synced', NOT_SYNCED: 'not synced', -} +}; // Documentation -export const DOCUMENTATION_WEB_BASE_URL = "https://documentation.wazuh.com"; +export const DOCUMENTATION_WEB_BASE_URL = 'https://documentation.wazuh.com'; // Default Elasticsearch user name context export const ELASTIC_NAME = 'elastic'; @@ -360,62 +385,62 @@ export enum SettingCategory { STATISTICS, SECURITY, CUSTOMIZATION, -}; +} type TPluginSettingOptionsTextArea = { - maxRows?: number - minRows?: number - maxLength?: number + maxRows?: number; + minRows?: number; + maxLength?: number; }; type TPluginSettingOptionsSelect = { - select: { text: string, value: any }[] + select: { text: string; value: any }[]; }; type TPluginSettingOptionsEditor = { - editor: { - language: string - } + editor: { + language: string; + }; }; type TPluginSettingOptionsFile = { - file: { - type: 'image' - extensions?: string[] - size?: { - maxBytes?: number - minBytes?: number - } - recommended?: { - dimensions?: { - width: number, - height: number, - unit: string - } - } - store?: { - relativePathFileSystem: string - filename: string - resolveStaticURL: (filename: string) => string - } - } + file: { + type: 'image'; + extensions?: string[]; + size?: { + maxBytes?: number; + minBytes?: number; + }; + recommended?: { + dimensions?: { + width: number; + height: number; + unit: string; + }; + }; + store?: { + relativePathFileSystem: string; + filename: string; + resolveStaticURL: (filename: string) => string; + }; + }; }; type TPluginSettingOptionsNumber = { number: { - min?: number - max?: number - integer?: boolean - } + min?: number; + max?: number; + integer?: boolean; + }; }; type TPluginSettingOptionsSwitch = { switch: { values: { - disabled: { label?: string, value: any }, - enabled: { label?: string, value: any }, - } - } + disabled: { label?: string; value: any }; + enabled: { label?: string; value: any }; + }; + }; }; export enum EpluginSettingType { @@ -425,61 +450,63 @@ export enum EpluginSettingType { number = 'number', editor = 'editor', select = 'select', - filepicker = 'filepicker' -}; + filepicker = 'filepicker', +} export type TPluginSetting = { // Define the text displayed in the UI. - title: string + title: string; // Description. - description: string + description: string; // Category. - category: SettingCategory + category: SettingCategory; // Type. - type: EpluginSettingType + type: EpluginSettingType; // Default value. - defaultValue: any + defaultValue: any; // Default value if it is not set. It has preference over `default`. - defaultValueIfNotSet?: any + defaultValueIfNotSet?: any; // Configurable from the configuration file. - isConfigurableFromFile: boolean + isConfigurableFromFile: boolean; // Configurable from the UI (Settings/Configuration). - isConfigurableFromUI: boolean + isConfigurableFromUI: boolean; // Modify the setting requires running the plugin health check (frontend). - requiresRunningHealthCheck?: boolean + requiresRunningHealthCheck?: boolean; // Modify the setting requires reloading the browser tab (frontend). - requiresReloadingBrowserTab?: boolean + requiresReloadingBrowserTab?: boolean; // Modify the setting requires restarting the plugin platform to take effect. - requiresRestartingPluginPlatform?: boolean + requiresRestartingPluginPlatform?: boolean; // Define options related to the `type`. options?: - TPluginSettingOptionsEditor | - TPluginSettingOptionsFile | - TPluginSettingOptionsNumber | - TPluginSettingOptionsSelect | - TPluginSettingOptionsSwitch | - TPluginSettingOptionsTextArea + | TPluginSettingOptionsEditor + | TPluginSettingOptionsFile + | TPluginSettingOptionsNumber + | TPluginSettingOptionsSelect + | TPluginSettingOptionsSwitch + | TPluginSettingOptionsTextArea; // Transform the input value. The result is saved in the form global state of Settings/Configuration - uiFormTransformChangedInputValue?: (value: any) => any + uiFormTransformChangedInputValue?: (value: any) => any; // Transform the configuration value or default as initial value for the input in Settings/Configuration - uiFormTransformConfigurationValueToInputValue?: (value: any) => any + uiFormTransformConfigurationValueToInputValue?: (value: any) => any; // Transform the input value changed in the form of Settings/Configuration and returned in the `changed` property of the hook useForm - uiFormTransformInputValueToConfigurationValue?: (value: any) => any + uiFormTransformInputValueToConfigurationValue?: (value: any) => any; // Validate the value in the form of Settings/Configuration. It returns a string if there is some validation error. - validate?: (value: any) => string | undefined - // Validate function creator to validate the setting in the backend. It uses `schema` of the `@kbn/config-schema` package. - validateBackend?: (schema: any) => (value: unknown) => string | undefined + validate?: (value: any) => string | undefined; + // Validate function creator to validate the setting in the backend. It uses `schema` of the `@kbn/config-schema` package. + validateBackend?: (schema: any) => (value: unknown) => string | undefined; }; export type TPluginSettingWithKey = TPluginSetting & { key: TPluginSettingKey }; export type TPluginSettingCategory = { - title: string - description?: string - documentationLink?: string - renderOrder?: number + title: string; + description?: string; + documentationLink?: string; + renderOrder?: number; }; -export const PLUGIN_SETTINGS_CATEGORIES: { [category: number]: TPluginSettingCategory } = { +export const PLUGIN_SETTINGS_CATEGORIES: { + [category: number]: TPluginSettingCategory; +} = { [SettingCategory.HEALTH_CHECK]: { title: 'Health check', description: "Checks will be executed by the app's Healthcheck.", @@ -487,40 +514,45 @@ export const PLUGIN_SETTINGS_CATEGORIES: { [category: number]: TPluginSettingCat }, [SettingCategory.GENERAL]: { title: 'General', - description: "Basic app settings related to alerts index pattern, hide the manager alerts in the dashboards, logs level and more.", + description: + 'Basic app settings related to alerts index pattern, hide the manager alerts in the dashboards, logs level and more.', renderOrder: SettingCategory.GENERAL, }, [SettingCategory.EXTENSIONS]: { title: 'Initial display state of the modules of the new API host entries.', - description: "Extensions.", + description: 'Extensions.', }, [SettingCategory.SECURITY]: { title: 'Security', - description: "Application security options such as unauthorized roles.", + description: 'Application security options such as unauthorized roles.', renderOrder: SettingCategory.SECURITY, }, [SettingCategory.MONITORING]: { title: 'Task:Monitoring', - description: "Options related to the agent status monitoring job and its storage in indexes.", + description: + 'Options related to the agent status monitoring job and its storage in indexes.', renderOrder: SettingCategory.MONITORING, }, [SettingCategory.STATISTICS]: { title: 'Task:Statistics', - description: "Options related to the daemons manager monitoring job and their storage in indexes.", + description: + 'Options related to the daemons manager monitoring job and their storage in indexes.', renderOrder: SettingCategory.STATISTICS, }, [SettingCategory.CUSTOMIZATION]: { title: 'Custom branding', - description: "If you want to use custom branding elements such as logos, you can do so by editing the settings below.", + description: + 'If you want to use custom branding elements such as logos, you can do so by editing the settings below.', documentationLink: 'user-manual/wazuh-dashboard/white-labeling.html', renderOrder: SettingCategory.CUSTOMIZATION, - } + }, }; export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { - "alerts.sample.prefix": { - title: "Sample alerts prefix", - description: "Define the index name prefix of sample alerts. It must match the template used by the index pattern to avoid unknown fields in dashboards.", + 'alerts.sample.prefix': { + title: 'Sample alerts prefix', + description: + 'Define the index name prefix of sample alerts. It must match the template used by the index pattern to avoid unknown fields in dashboards.', category: SettingCategory.GENERAL, type: EpluginSettingType.text, defaultValue: WAZUH_SAMPLE_ALERT_PREFIX, @@ -532,15 +564,26 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { SettingsValidator.isNotEmptyString, SettingsValidator.hasNoSpaces, SettingsValidator.noStartsWithString('-', '_', '+', '.'), - SettingsValidator.hasNotInvalidCharacters('\\', '/', '?', '"', '<', '>', '|', ',', '#', '*') + SettingsValidator.hasNotInvalidCharacters( + '\\', + '/', + '?', + '"', + '<', + '>', + '|', + ',', + '#', + '*', + ), ), - validateBackend: function(schema){ - return schema.string({validate: this.validate}); - }, + validateBackend: function (schema) { + return schema.string({ validate: this.validate }); + }, }, - "checks.api": { - title: "API connection", - description: "Enable or disable the API health check when opening the app.", + 'checks.api': { + title: 'API connection', + description: 'Enable or disable the API health check when opening the app.', category: SettingCategory.HEALTH_CHECK, type: EpluginSettingType.switch, defaultValue: true, @@ -551,20 +594,23 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { values: { disabled: { label: 'false', value: false }, enabled: { label: 'true', value: true }, - } - } + }, + }, }, - uiFormTransformChangedInputValue: function (value: boolean | string): boolean { + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { return Boolean(value); }, validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, + validateBackend: function (schema) { + return schema.boolean(); + }, }, - "checks.fields": { - title: "Known fields", - description: "Enable or disable the known fields health check when opening the app.", + 'checks.fields': { + title: 'Known fields', + description: + 'Enable or disable the known fields health check when opening the app.', category: SettingCategory.HEALTH_CHECK, type: EpluginSettingType.switch, defaultValue: true, @@ -575,20 +621,23 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { values: { disabled: { label: 'false', value: false }, enabled: { label: 'true', value: true }, - } - } + }, + }, }, - uiFormTransformChangedInputValue: function (value: boolean | string): boolean { + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { return Boolean(value); }, validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, + validateBackend: function (schema) { + return schema.boolean(); + }, }, - "checks.maxBuckets": { - title: "Set max buckets to 200000", - description: "Change the default value of the plugin platform max buckets configuration.", + 'checks.maxBuckets': { + title: 'Set max buckets to 200000', + description: + 'Change the default value of the plugin platform max buckets configuration.', category: SettingCategory.HEALTH_CHECK, type: EpluginSettingType.switch, defaultValue: true, @@ -599,20 +648,23 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { values: { disabled: { label: 'false', value: false }, enabled: { label: 'true', value: true }, - } + }, }, }, - uiFormTransformChangedInputValue: function (value: boolean | string): boolean { + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { return Boolean(value); }, validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, + validateBackend: function (schema) { + return schema.boolean(); + }, }, - "checks.metaFields": { - title: "Remove meta fields", - description: "Change the default value of the plugin platform metaField configuration.", + 'checks.metaFields': { + title: 'Remove meta fields', + description: + 'Change the default value of the plugin platform metaField configuration.', category: SettingCategory.HEALTH_CHECK, type: EpluginSettingType.switch, defaultValue: true, @@ -623,20 +675,23 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { values: { disabled: { label: 'false', value: false }, enabled: { label: 'true', value: true }, - } - } + }, + }, }, - uiFormTransformChangedInputValue: function (value: boolean | string): boolean { + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { return Boolean(value); }, validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, + validateBackend: function (schema) { + return schema.boolean(); + }, }, - "checks.pattern": { - title: "Index pattern", - description: "Enable or disable the index pattern health check when opening the app.", + 'checks.pattern': { + title: 'Index pattern', + description: + 'Enable or disable the index pattern health check when opening the app.', category: SettingCategory.HEALTH_CHECK, type: EpluginSettingType.switch, defaultValue: true, @@ -647,20 +702,23 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { values: { disabled: { label: 'false', value: false }, enabled: { label: 'true', value: true }, - } - } + }, + }, }, - uiFormTransformChangedInputValue: function (value: boolean | string): boolean { + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { return Boolean(value); }, validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, + validateBackend: function (schema) { + return schema.boolean(); + }, }, - "checks.setup": { - title: "API version", - description: "Enable or disable the setup health check when opening the app.", + 'checks.setup': { + title: 'API version', + description: + 'Enable or disable the setup health check when opening the app.', category: SettingCategory.HEALTH_CHECK, type: EpluginSettingType.switch, defaultValue: true, @@ -671,20 +729,23 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { values: { disabled: { label: 'false', value: false }, enabled: { label: 'true', value: true }, - } - } + }, + }, }, - uiFormTransformChangedInputValue: function (value: boolean | string): boolean { + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { return Boolean(value); }, validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, + validateBackend: function (schema) { + return schema.boolean(); + }, }, - "checks.template": { - title: "Index template", - description: "Enable or disable the template health check when opening the app.", + 'checks.template': { + title: 'Index template', + description: + 'Enable or disable the template health check when opening the app.', category: SettingCategory.HEALTH_CHECK, type: EpluginSettingType.switch, defaultValue: true, @@ -695,20 +756,23 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { values: { disabled: { label: 'false', value: false }, enabled: { label: 'true', value: true }, - } - } + }, + }, }, - uiFormTransformChangedInputValue: function (value: boolean | string): boolean { + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { return Boolean(value); }, validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, + validateBackend: function (schema) { + return schema.boolean(); + }, }, - "checks.timeFilter": { - title: "Set time filter to 24h", - description: "Change the default value of the plugin platform timeFilter configuration.", + 'checks.timeFilter': { + title: 'Set time filter to 24h', + description: + 'Change the default value of the plugin platform timeFilter configuration.', category: SettingCategory.HEALTH_CHECK, type: EpluginSettingType.switch, defaultValue: true, @@ -719,20 +783,22 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { values: { disabled: { label: 'false', value: false }, enabled: { label: 'true', value: true }, - } - } + }, + }, }, - uiFormTransformChangedInputValue: function (value: boolean | string): boolean { + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { return Boolean(value); }, validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, + validateBackend: function (schema) { + return schema.boolean(); + }, }, - "cron.prefix": { - title: "Cron prefix", - description: "Define the index prefix of predefined jobs.", + 'cron.prefix': { + title: 'Cron prefix', + description: 'Define the index prefix of predefined jobs.', category: SettingCategory.GENERAL, type: EpluginSettingType.text, defaultValue: WAZUH_STATISTICS_DEFAULT_PREFIX, @@ -743,15 +809,27 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { SettingsValidator.isNotEmptyString, SettingsValidator.hasNoSpaces, SettingsValidator.noStartsWithString('-', '_', '+', '.'), - SettingsValidator.hasNotInvalidCharacters('\\', '/', '?', '"', '<', '>', '|', ',', '#', '*') + SettingsValidator.hasNotInvalidCharacters( + '\\', + '/', + '?', + '"', + '<', + '>', + '|', + ',', + '#', + '*', + ), ), - validateBackend: function(schema){ - return schema.string({validate: this.validate}); - }, + validateBackend: function (schema) { + return schema.string({ validate: this.validate }); + }, }, - "cron.statistics.apis": { - title: "Includes APIs", - description: "Enter the ID of the hosts you want to save data from, leave this empty to run the task on every host.", + 'cron.statistics.apis': { + title: 'Includes APIs', + description: + 'Enter the ID of the hosts you want to save data from, leave this empty to run the task on every host.', category: SettingCategory.STATISTICS, type: EpluginSettingType.editor, defaultValue: [], @@ -759,72 +837,87 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { isConfigurableFromUI: true, options: { editor: { - language: 'json' - } + language: 'json', + }, }, uiFormTransformConfigurationValueToInputValue: function (value: any): any { return JSON.stringify(value); }, - uiFormTransformInputValueToConfigurationValue: function (value: string): any { + uiFormTransformInputValueToConfigurationValue: function ( + value: string, + ): any { try { return JSON.parse(value); } catch (error) { return value; - }; + } + }, + validate: SettingsValidator.json( + SettingsValidator.compose( + SettingsValidator.array( + SettingsValidator.compose( + SettingsValidator.isString, + SettingsValidator.isNotEmptyString, + SettingsValidator.hasNoSpaces, + ), + ), + ), + ), + validateBackend: function (schema) { + return schema.arrayOf( + schema.string({ + validate: SettingsValidator.compose( + SettingsValidator.isNotEmptyString, + SettingsValidator.hasNoSpaces, + ), + }), + ); }, - validate: SettingsValidator.json(SettingsValidator.compose( - SettingsValidator.array(SettingsValidator.compose( - SettingsValidator.isString, - SettingsValidator.isNotEmptyString, - SettingsValidator.hasNoSpaces, - )), - )), - validateBackend: function(schema){ - return schema.arrayOf(schema.string({validate: SettingsValidator.compose( - SettingsValidator.isNotEmptyString, - SettingsValidator.hasNoSpaces, - )})); - }, }, - "cron.statistics.index.creation": { - title: "Index creation", - description: "Define the interval in which a new index will be created.", + 'cron.statistics.index.creation': { + title: 'Index creation', + description: 'Define the interval in which a new index will be created.', category: SettingCategory.STATISTICS, type: EpluginSettingType.select, options: { select: [ { - text: "Hourly", - value: "h" + text: 'Hourly', + value: 'h', }, { - text: "Daily", - value: "d" + text: 'Daily', + value: 'd', }, { - text: "Weekly", - value: "w" + text: 'Weekly', + value: 'w', }, { - text: "Monthly", - value: "m" - } - ] + text: 'Monthly', + value: 'm', + }, + ], }, defaultValue: WAZUH_STATISTICS_DEFAULT_CREATION, isConfigurableFromFile: true, isConfigurableFromUI: true, requiresRunningHealthCheck: true, - validate: function (value){ - return SettingsValidator.literal(this.options.select.map(({value}) => value))(value) - }, - validateBackend: function(schema){ - return schema.oneOf(this.options.select.map(({value}) => schema.literal(value))); - }, + validate: function (value) { + return SettingsValidator.literal( + this.options.select.map(({ value }) => value), + )(value); + }, + validateBackend: function (schema) { + return schema.oneOf( + this.options.select.map(({ value }) => schema.literal(value)), + ); + }, }, - "cron.statistics.index.name": { - title: "Index name", - description: "Define the name of the index in which the documents will be saved.", + 'cron.statistics.index.name': { + title: 'Index name', + description: + 'Define the name of the index in which the documents will be saved.', category: SettingCategory.STATISTICS, type: EpluginSettingType.text, defaultValue: WAZUH_STATISTICS_DEFAULT_NAME, @@ -836,15 +929,27 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { SettingsValidator.isNotEmptyString, SettingsValidator.hasNoSpaces, SettingsValidator.noStartsWithString('-', '_', '+', '.'), - SettingsValidator.hasNotInvalidCharacters('\\', '/', '?', '"', '<', '>', '|', ',', '#', '*') + SettingsValidator.hasNotInvalidCharacters( + '\\', + '/', + '?', + '"', + '<', + '>', + '|', + ',', + '#', + '*', + ), ), - validateBackend: function(schema){ - return schema.string({validate: this.validate}); - }, + validateBackend: function (schema) { + return schema.string({ validate: this.validate }); + }, }, - "cron.statistics.index.replicas": { - title: "Index replicas", - description: "Define the number of replicas to use for the statistics indices.", + 'cron.statistics.index.replicas': { + title: 'Index replicas', + description: + 'Define the number of replicas to use for the statistics indices.', category: SettingCategory.STATISTICS, type: EpluginSettingType.number, defaultValue: WAZUH_STATISTICS_DEFAULT_INDICES_REPLICAS, @@ -854,25 +959,30 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { options: { number: { min: 0, - integer: true - } + integer: true, + }, }, - uiFormTransformConfigurationValueToInputValue: function (value: number): string { + uiFormTransformConfigurationValueToInputValue: function ( + value: number, + ): string { return String(value); }, - uiFormTransformInputValueToConfigurationValue: function (value: string): number { + uiFormTransformInputValueToConfigurationValue: function ( + value: string, + ): number { return Number(value); }, - validate: function(value){ - return SettingsValidator.number(this.options.number)(value) - }, - validateBackend: function(schema){ - return schema.number({validate: this.validate.bind(this)}); - }, + validate: function (value) { + return SettingsValidator.number(this.options.number)(value); + }, + validateBackend: function (schema) { + return schema.number({ validate: this.validate.bind(this) }); + }, }, - "cron.statistics.index.shards": { - title: "Index shards", - description: "Define the number of shards to use for the statistics indices.", + 'cron.statistics.index.shards': { + title: 'Index shards', + description: + 'Define the number of shards to use for the statistics indices.', category: SettingCategory.STATISTICS, type: EpluginSettingType.number, defaultValue: WAZUH_STATISTICS_DEFAULT_INDICES_SHARDS, @@ -882,41 +992,46 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { options: { number: { min: 1, - integer: true - } + integer: true, + }, }, uiFormTransformConfigurationValueToInputValue: function (value: number) { - return String(value) + return String(value); }, - uiFormTransformInputValueToConfigurationValue: function (value: string): number { + uiFormTransformInputValueToConfigurationValue: function ( + value: string, + ): number { return Number(value); }, - validate: function(value){ - return SettingsValidator.number(this.options.number)(value) - }, - validateBackend: function(schema){ - return schema.number({validate: this.validate.bind(this)}); - }, + validate: function (value) { + return SettingsValidator.number(this.options.number)(value); + }, + validateBackend: function (schema) { + return schema.number({ validate: this.validate.bind(this) }); + }, }, - "cron.statistics.interval": { - title: "Interval", - description: "Define the frequency of task execution using cron schedule expressions.", + 'cron.statistics.interval': { + title: 'Interval', + description: + 'Define the frequency of task execution using cron schedule expressions.', category: SettingCategory.STATISTICS, type: EpluginSettingType.text, defaultValue: WAZUH_STATISTICS_DEFAULT_CRON_FREQ, isConfigurableFromFile: true, isConfigurableFromUI: true, requiresRestartingPluginPlatform: true, - validate: function(value: string){ - return validateNodeCronInterval(value) ? undefined : "Interval is not valid." - }, - validateBackend: function(schema){ - return schema.string({validate: this.validate}); - }, + validate: function (value: string) { + return validateNodeCronInterval(value) + ? undefined + : 'Interval is not valid.'; + }, + validateBackend: function (schema) { + return schema.string({ validate: this.validate }); + }, }, - "cron.statistics.status": { - title: "Status", - description: "Enable or disable the statistics tasks.", + 'cron.statistics.status': { + title: 'Status', + description: 'Enable or disable the statistics tasks.', category: SettingCategory.STATISTICS, type: EpluginSettingType.switch, defaultValue: WAZUH_STATISTICS_DEFAULT_STATUS, @@ -927,217 +1042,248 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { values: { disabled: { label: 'false', value: false }, enabled: { label: 'true', value: true }, - } - } + }, + }, }, - uiFormTransformChangedInputValue: function (value: boolean | string): boolean { + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { return Boolean(value); }, validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, + validateBackend: function (schema) { + return schema.boolean(); + }, }, - "customization.enabled": { - title: "Status", - description: "Enable or disable the customization.", - category: SettingCategory.CUSTOMIZATION, - type: EpluginSettingType.switch, - defaultValue: true, - isConfigurableFromFile: true, - isConfigurableFromUI: true, + 'customization.enabled': { + title: 'Status', + description: 'Enable or disable the customization.', + category: SettingCategory.CUSTOMIZATION, + type: EpluginSettingType.switch, + defaultValue: true, + isConfigurableFromFile: true, + isConfigurableFromUI: true, requiresReloadingBrowserTab: true, - options: { - switch: { - values: { - disabled: {label: 'false', value: false}, - enabled: {label: 'true', value: true}, - } - } - }, - uiFormTransformChangedInputValue: function(value: boolean | string): boolean{ - return Boolean(value); - }, - validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, - }, - "customization.logo.app": { - title: "App main logo", + options: { + switch: { + values: { + disabled: { label: 'false', value: false }, + enabled: { label: 'true', value: true }, + }, + }, + }, + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { + return Boolean(value); + }, + validate: SettingsValidator.isBoolean, + validateBackend: function (schema) { + return schema.boolean(); + }, + }, + 'customization.logo.app': { + title: 'App main logo', description: `This logo is used in the app main menu, at the top left corner.`, category: SettingCategory.CUSTOMIZATION, type: EpluginSettingType.filepicker, - defaultValue: "", + defaultValue: '', isConfigurableFromFile: true, isConfigurableFromUI: true, options: { - file: { - type: 'image', - extensions: ['.jpeg', '.jpg', '.png', '.svg'], - size: { - maxBytes: CUSTOMIZATION_ENDPOINT_PAYLOAD_UPLOAD_CUSTOM_FILE_MAXIMUM_BYTES, - }, - recommended: { - dimensions: { - width: 300, - height: 70, - unit: 'px' - } - }, - store: { - relativePathFileSystem: 'public/assets/custom/images', - filename: 'customization.logo.app', - resolveStaticURL: (filename: string) => `custom/images/${filename}?v=${Date.now()}` + file: { + type: 'image', + extensions: ['.jpeg', '.jpg', '.png', '.svg'], + size: { + maxBytes: + CUSTOMIZATION_ENDPOINT_PAYLOAD_UPLOAD_CUSTOM_FILE_MAXIMUM_BYTES, + }, + recommended: { + dimensions: { + width: 300, + height: 70, + unit: 'px', + }, + }, + store: { + relativePathFileSystem: 'public/assets/custom/images', + filename: 'customization.logo.app', + resolveStaticURL: (filename: string) => + `custom/images/${filename}?v=${Date.now()}`, // ?v=${Date.now()} is used to force the browser to reload the image when a new file is uploaded - } - } - }, - validate: function(value){ - return SettingsValidator.compose( - SettingsValidator.filePickerFileSize({...this.options.file.size, meaningfulUnit: true}), - SettingsValidator.filePickerSupportedExtensions(this.options.file.extensions) - )(value) - }, + }, + }, + }, + validate: function (value) { + return SettingsValidator.compose( + SettingsValidator.filePickerFileSize({ + ...this.options.file.size, + meaningfulUnit: true, + }), + SettingsValidator.filePickerSupportedExtensions( + this.options.file.extensions, + ), + )(value); + }, }, - "customization.logo.healthcheck": { - title: "Healthcheck logo", + 'customization.logo.healthcheck': { + title: 'Healthcheck logo', description: `This logo is displayed during the Healthcheck routine of the app.`, category: SettingCategory.CUSTOMIZATION, type: EpluginSettingType.filepicker, - defaultValue: "", + defaultValue: '', isConfigurableFromFile: true, isConfigurableFromUI: true, options: { - file: { - type: 'image', - extensions: ['.jpeg', '.jpg', '.png', '.svg'], - size: { - maxBytes: CUSTOMIZATION_ENDPOINT_PAYLOAD_UPLOAD_CUSTOM_FILE_MAXIMUM_BYTES, - }, - recommended: { - dimensions: { - width: 300, - height: 70, - unit: 'px' - } - }, - store: { - relativePathFileSystem: 'public/assets/custom/images', - filename: 'customization.logo.healthcheck', - resolveStaticURL: (filename: string) => `custom/images/${filename}?v=${Date.now()}` + file: { + type: 'image', + extensions: ['.jpeg', '.jpg', '.png', '.svg'], + size: { + maxBytes: + CUSTOMIZATION_ENDPOINT_PAYLOAD_UPLOAD_CUSTOM_FILE_MAXIMUM_BYTES, + }, + recommended: { + dimensions: { + width: 300, + height: 70, + unit: 'px', + }, + }, + store: { + relativePathFileSystem: 'public/assets/custom/images', + filename: 'customization.logo.healthcheck', + resolveStaticURL: (filename: string) => + `custom/images/${filename}?v=${Date.now()}`, // ?v=${Date.now()} is used to force the browser to reload the image when a new file is uploaded - } - } - }, - validate: function(value){ - return SettingsValidator.compose( - SettingsValidator.filePickerFileSize({...this.options.file.size, meaningfulUnit: true}), - SettingsValidator.filePickerSupportedExtensions(this.options.file.extensions) - )(value) - }, + }, + }, + }, + validate: function (value) { + return SettingsValidator.compose( + SettingsValidator.filePickerFileSize({ + ...this.options.file.size, + meaningfulUnit: true, + }), + SettingsValidator.filePickerSupportedExtensions( + this.options.file.extensions, + ), + )(value); + }, }, - "customization.logo.reports": { - title: "PDF reports logo", + 'customization.logo.reports': { + title: 'PDF reports logo', description: `This logo is used in the PDF reports generated by the app. It's placed at the top left corner of every page of the PDF.`, category: SettingCategory.CUSTOMIZATION, type: EpluginSettingType.filepicker, - defaultValue: "", + defaultValue: '', defaultValueIfNotSet: REPORTS_LOGO_IMAGE_ASSETS_RELATIVE_PATH, isConfigurableFromFile: true, isConfigurableFromUI: true, options: { - file: { - type: 'image', - extensions: ['.jpeg', '.jpg', '.png'], - size: { - maxBytes: CUSTOMIZATION_ENDPOINT_PAYLOAD_UPLOAD_CUSTOM_FILE_MAXIMUM_BYTES, - }, - recommended: { - dimensions: { - width: 190, - height: 40, - unit: 'px' - } - }, - store: { - relativePathFileSystem: 'public/assets/custom/images', - filename: 'customization.logo.reports', - resolveStaticURL: (filename: string) => `custom/images/${filename}` - } - } - }, - validate: function(value){ - return SettingsValidator.compose( - SettingsValidator.filePickerFileSize({...this.options.file.size, meaningfulUnit: true}), - SettingsValidator.filePickerSupportedExtensions(this.options.file.extensions) - )(value) - }, + file: { + type: 'image', + extensions: ['.jpeg', '.jpg', '.png'], + size: { + maxBytes: + CUSTOMIZATION_ENDPOINT_PAYLOAD_UPLOAD_CUSTOM_FILE_MAXIMUM_BYTES, + }, + recommended: { + dimensions: { + width: 190, + height: 40, + unit: 'px', + }, + }, + store: { + relativePathFileSystem: 'public/assets/custom/images', + filename: 'customization.logo.reports', + resolveStaticURL: (filename: string) => `custom/images/${filename}`, + }, + }, + }, + validate: function (value) { + return SettingsValidator.compose( + SettingsValidator.filePickerFileSize({ + ...this.options.file.size, + meaningfulUnit: true, + }), + SettingsValidator.filePickerSupportedExtensions( + this.options.file.extensions, + ), + )(value); + }, }, - "customization.logo.sidebar": { - title: "Navigation drawer logo", + 'customization.logo.sidebar': { + title: 'Navigation drawer logo', description: `This is the logo for the app to display in the platform's navigation drawer, this is, the main sidebar collapsible menu.`, category: SettingCategory.CUSTOMIZATION, type: EpluginSettingType.filepicker, - defaultValue: "", + defaultValue: '', isConfigurableFromFile: true, isConfigurableFromUI: true, requiresReloadingBrowserTab: true, options: { - file: { - type: 'image', - extensions: ['.jpeg', '.jpg', '.png', '.svg'], - size: { - maxBytes: CUSTOMIZATION_ENDPOINT_PAYLOAD_UPLOAD_CUSTOM_FILE_MAXIMUM_BYTES, - }, - recommended: { - dimensions: { - width: 80, - height: 80, - unit: 'px' - } - }, - store: { - relativePathFileSystem: 'public/assets/custom/images', - filename: 'customization.logo.sidebar', - resolveStaticURL: (filename: string) => `custom/images/${filename}?v=${Date.now()}` + file: { + type: 'image', + extensions: ['.jpeg', '.jpg', '.png', '.svg'], + size: { + maxBytes: + CUSTOMIZATION_ENDPOINT_PAYLOAD_UPLOAD_CUSTOM_FILE_MAXIMUM_BYTES, + }, + recommended: { + dimensions: { + width: 80, + height: 80, + unit: 'px', + }, + }, + store: { + relativePathFileSystem: 'public/assets/custom/images', + filename: 'customization.logo.sidebar', + resolveStaticURL: (filename: string) => + `custom/images/${filename}?v=${Date.now()}`, // ?v=${Date.now()} is used to force the browser to reload the image when a new file is uploaded - } - } - }, - validate: function(value){ - return SettingsValidator.compose( - SettingsValidator.filePickerFileSize({...this.options.file.size, meaningfulUnit: true}), - SettingsValidator.filePickerSupportedExtensions(this.options.file.extensions) - )(value) - }, + }, + }, + }, + validate: function (value) { + return SettingsValidator.compose( + SettingsValidator.filePickerFileSize({ + ...this.options.file.size, + meaningfulUnit: true, + }), + SettingsValidator.filePickerSupportedExtensions( + this.options.file.extensions, + ), + )(value); + }, }, - "customization.reports.footer": { - title: "Reports footer", - description: "Set the footer of the reports.", - category: SettingCategory.CUSTOMIZATION, - type: EpluginSettingType.textarea, - defaultValue: "", + 'customization.reports.footer': { + title: 'Reports footer', + description: 'Set the footer of the reports.', + category: SettingCategory.CUSTOMIZATION, + type: EpluginSettingType.textarea, + defaultValue: '', defaultValueIfNotSet: REPORTS_PAGE_FOOTER_TEXT, - isConfigurableFromFile: true, - isConfigurableFromUI: true, + isConfigurableFromFile: true, + isConfigurableFromUI: true, options: { maxRows: 2, maxLength: 50 }, validate: function (value) { return SettingsValidator.multipleLinesString({ maxRows: this.options?.maxRows, - maxLength: this.options?.maxLength - })(value) + maxLength: this.options?.maxLength, + })(value); }, validateBackend: function (schema) { return schema.string({ validate: this.validate.bind(this) }); }, }, - "customization.reports.header": { - title: "Reports header", - description: "Set the header of the reports.", + 'customization.reports.header': { + title: 'Reports header', + description: 'Set the header of the reports.', category: SettingCategory.CUSTOMIZATION, type: EpluginSettingType.textarea, - defaultValue: "", + defaultValue: '', defaultValueIfNotSet: REPORTS_PAGE_HEADER_TEXT, isConfigurableFromFile: true, isConfigurableFromUI: true, @@ -1145,16 +1291,16 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { validate: function (value) { return SettingsValidator.multipleLinesString({ maxRows: this.options?.maxRows, - maxLength: this.options?.maxLength - })(value) - }, - validateBackend: function(schema){ - return schema.string({validate: this.validate.bind(this)}); - }, - }, - "disabled_roles": { - title: "Disable roles", - description: "Disabled the plugin visibility for users with the roles.", + maxLength: this.options?.maxLength, + })(value); + }, + validateBackend: function (schema) { + return schema.string({ validate: this.validate.bind(this) }); + }, + }, + disabled_roles: { + title: 'Disable roles', + description: 'Disabled the plugin visibility for users with the roles.', category: SettingCategory.SECURITY, type: EpluginSettingType.editor, defaultValue: [], @@ -1162,62 +1308,74 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { isConfigurableFromUI: true, options: { editor: { - language: 'json' - } + language: 'json', + }, }, uiFormTransformConfigurationValueToInputValue: function (value: any): any { return JSON.stringify(value); }, - uiFormTransformInputValueToConfigurationValue: function (value: string): any { + uiFormTransformInputValueToConfigurationValue: function ( + value: string, + ): any { try { return JSON.parse(value); } catch (error) { return value; - }; + } + }, + validate: SettingsValidator.json( + SettingsValidator.compose( + SettingsValidator.array( + SettingsValidator.compose( + SettingsValidator.isString, + SettingsValidator.isNotEmptyString, + SettingsValidator.hasNoSpaces, + ), + ), + ), + ), + validateBackend: function (schema) { + return schema.arrayOf( + schema.string({ + validate: SettingsValidator.compose( + SettingsValidator.isNotEmptyString, + SettingsValidator.hasNoSpaces, + ), + }), + ); }, - validate: SettingsValidator.json(SettingsValidator.compose( - SettingsValidator.array(SettingsValidator.compose( - SettingsValidator.isString, - SettingsValidator.isNotEmptyString, - SettingsValidator.hasNoSpaces, - )), - )), - validateBackend: function(schema){ - return schema.arrayOf(schema.string({validate: SettingsValidator.compose( - SettingsValidator.isNotEmptyString, - SettingsValidator.hasNoSpaces, - )})); - }, }, - "enrollment.dns": { - title: "Enrollment DNS", - description: "Specifies the Wazuh registration server, used for the agent enrollment.", + 'enrollment.dns': { + title: 'Enrollment DNS', + description: + 'Specifies the Wazuh registration server, used for the agent enrollment.', category: SettingCategory.GENERAL, type: EpluginSettingType.text, - defaultValue: "", + defaultValue: '', isConfigurableFromFile: true, isConfigurableFromUI: true, validate: SettingsValidator.hasNoSpaces, - validateBackend: function(schema){ - return schema.string({validate: this.validate}); - }, + validateBackend: function (schema) { + return schema.string({ validate: this.validate }); + }, }, - "enrollment.password": { - title: "Enrollment password", - description: "Specifies the password used to authenticate during the agent enrollment.", + 'enrollment.password': { + title: 'Enrollment password', + description: + 'Specifies the password used to authenticate during the agent enrollment.', category: SettingCategory.GENERAL, type: EpluginSettingType.text, - defaultValue: "", + defaultValue: '', isConfigurableFromFile: true, isConfigurableFromUI: false, validate: SettingsValidator.isNotEmptyString, - validateBackend: function(schema){ - return schema.string({validate: this.validate}); - }, + validateBackend: function (schema) { + return schema.string({ validate: this.validate }); + }, }, - "extensions.audit": { - title: "System auditing", - description: "Enable or disable the Audit tab on Overview and Agents.", + 'extensions.audit': { + title: 'System auditing', + description: 'Enable or disable the Audit tab on Overview and Agents.', category: SettingCategory.EXTENSIONS, type: EpluginSettingType.switch, defaultValue: true, @@ -1228,20 +1386,22 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { values: { disabled: { label: 'false', value: false }, enabled: { label: 'true', value: true }, - } - } + }, + }, }, - uiFormTransformChangedInputValue: function (value: boolean | string): boolean { + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { return Boolean(value); }, validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, + validateBackend: function (schema) { + return schema.boolean(); + }, }, - "extensions.aws": { - title: "Amazon AWS", - description: "Enable or disable the Amazon (AWS) tab on Overview.", + 'extensions.aws': { + title: 'Amazon AWS', + description: 'Enable or disable the Amazon (AWS) tab on Overview.', category: SettingCategory.EXTENSIONS, type: EpluginSettingType.switch, defaultValue: false, @@ -1252,20 +1412,22 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { values: { disabled: { label: 'false', value: false }, enabled: { label: 'true', value: true }, - } - } + }, + }, }, - uiFormTransformChangedInputValue: function (value: boolean | string): boolean { + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { return Boolean(value); }, validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, + validateBackend: function (schema) { + return schema.boolean(); + }, }, - "extensions.ciscat": { - title: "CIS-CAT", - description: "Enable or disable the CIS-CAT tab on Overview and Agents.", + 'extensions.ciscat': { + title: 'CIS-CAT', + description: 'Enable or disable the CIS-CAT tab on Overview and Agents.', category: SettingCategory.EXTENSIONS, type: EpluginSettingType.switch, defaultValue: false, @@ -1276,20 +1438,23 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { values: { disabled: { label: 'false', value: false }, enabled: { label: 'true', value: true }, - } - } + }, + }, }, - uiFormTransformChangedInputValue: function (value: boolean | string): boolean { + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { return Boolean(value); }, validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, + validateBackend: function (schema) { + return schema.boolean(); + }, }, - "extensions.docker": { - title: "Docker listener", - description: "Enable or disable the Docker listener tab on Overview and Agents.", + 'extensions.docker': { + title: 'Docker listener', + description: + 'Enable or disable the Docker listener tab on Overview and Agents.', category: SettingCategory.EXTENSIONS, type: EpluginSettingType.switch, defaultValue: false, @@ -1300,20 +1465,22 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { values: { disabled: { label: 'false', value: false }, enabled: { label: 'true', value: true }, - } - } + }, + }, }, - uiFormTransformChangedInputValue: function (value: boolean | string): boolean { + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { return Boolean(value); }, validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, + validateBackend: function (schema) { + return schema.boolean(); + }, }, - "extensions.gcp": { - title: "Google Cloud platform", - description: "Enable or disable the Google Cloud Platform tab on Overview.", + 'extensions.gcp': { + title: 'Google Cloud platform', + description: 'Enable or disable the Google Cloud Platform tab on Overview.', category: SettingCategory.EXTENSIONS, type: EpluginSettingType.switch, defaultValue: false, @@ -1324,20 +1491,22 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { values: { disabled: { label: 'false', value: false }, enabled: { label: 'true', value: true }, - } - } + }, + }, }, - uiFormTransformChangedInputValue: function (value: boolean | string): boolean { + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { return Boolean(value); }, validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, + validateBackend: function (schema) { + return schema.boolean(); + }, }, - "extensions.gdpr": { - title: "GDPR", - description: "Enable or disable the GDPR tab on Overview and Agents.", + 'extensions.gdpr': { + title: 'GDPR', + description: 'Enable or disable the GDPR tab on Overview and Agents.', category: SettingCategory.EXTENSIONS, type: EpluginSettingType.switch, defaultValue: true, @@ -1348,20 +1517,22 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { values: { disabled: { label: 'false', value: false }, enabled: { label: 'true', value: true }, - } - } + }, + }, }, - uiFormTransformChangedInputValue: function (value: boolean | string): boolean { + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { return Boolean(value); }, validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, + validateBackend: function (schema) { + return schema.boolean(); + }, }, - "extensions.github": { - title: "GitHub", - description: "Enable or disable the GitHub tab on Overview and Agents.", + 'extensions.github': { + title: 'GitHub', + description: 'Enable or disable the GitHub tab on Overview and Agents.', category: SettingCategory.EXTENSIONS, type: EpluginSettingType.switch, defaultValue: false, @@ -1372,20 +1543,22 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { values: { disabled: { label: 'false', value: false }, enabled: { label: 'true', value: true }, - } - } + }, + }, }, - uiFormTransformChangedInputValue: function (value: boolean | string): boolean { + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { return Boolean(value); }, validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, + validateBackend: function (schema) { + return schema.boolean(); + }, }, - "extensions.hipaa": { - title: "HIPAA", - description: "Enable or disable the HIPAA tab on Overview and Agents.", + 'extensions.hipaa': { + title: 'HIPAA', + description: 'Enable or disable the HIPAA tab on Overview and Agents.', category: SettingCategory.EXTENSIONS, type: EpluginSettingType.switch, defaultValue: true, @@ -1396,20 +1569,23 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { values: { disabled: { label: 'false', value: false }, enabled: { label: 'true', value: true }, - } - } + }, + }, }, - uiFormTransformChangedInputValue: function (value: boolean | string): boolean { + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { return Boolean(value); }, validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, + validateBackend: function (schema) { + return schema.boolean(); + }, }, - "extensions.nist": { - title: "NIST", - description: "Enable or disable the NIST 800-53 tab on Overview and Agents.", + 'extensions.nist': { + title: 'NIST', + description: + 'Enable or disable the NIST 800-53 tab on Overview and Agents.', category: SettingCategory.EXTENSIONS, type: EpluginSettingType.switch, defaultValue: true, @@ -1420,20 +1596,22 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { values: { disabled: { label: 'false', value: false }, enabled: { label: 'true', value: true }, - } - } + }, + }, }, - uiFormTransformChangedInputValue: function (value: boolean | string): boolean { + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { return Boolean(value); }, validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, + validateBackend: function (schema) { + return schema.boolean(); + }, }, - "extensions.office": { - title: "Office 365", - description: "Enable or disable the Office 365 tab on Overview and Agents.", + 'extensions.office': { + title: 'Office 365', + description: 'Enable or disable the Office 365 tab on Overview and Agents.', category: SettingCategory.EXTENSIONS, type: EpluginSettingType.switch, defaultValue: false, @@ -1444,20 +1622,22 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { values: { disabled: { label: 'false', value: false }, enabled: { label: 'true', value: true }, - } - } + }, + }, }, - uiFormTransformChangedInputValue: function (value: boolean | string): boolean { + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { return Boolean(value); }, validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, + validateBackend: function (schema) { + return schema.boolean(); + }, }, - "extensions.oscap": { - title: "OSCAP", - description: "Enable or disable the Open SCAP tab on Overview and Agents.", + 'extensions.oscap': { + title: 'OSCAP', + description: 'Enable or disable the Open SCAP tab on Overview and Agents.', category: SettingCategory.EXTENSIONS, type: EpluginSettingType.switch, defaultValue: false, @@ -1468,20 +1648,22 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { values: { disabled: { label: 'false', value: false }, enabled: { label: 'true', value: true }, - } - } + }, + }, }, - uiFormTransformChangedInputValue: function (value: boolean | string): boolean { + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { return Boolean(value); }, validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, + validateBackend: function (schema) { + return schema.boolean(); + }, }, - "extensions.osquery": { - title: "Osquery", - description: "Enable or disable the Osquery tab on Overview and Agents.", + 'extensions.osquery': { + title: 'Osquery', + description: 'Enable or disable the Osquery tab on Overview and Agents.', category: SettingCategory.EXTENSIONS, type: EpluginSettingType.switch, defaultValue: false, @@ -1492,20 +1674,22 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { values: { disabled: { label: 'false', value: false }, enabled: { label: 'true', value: true }, - } - } + }, + }, }, - uiFormTransformChangedInputValue: function (value: boolean | string): boolean { + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { return Boolean(value); }, validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, + validateBackend: function (schema) { + return schema.boolean(); + }, }, - "extensions.pci": { - title: "PCI DSS", - description: "Enable or disable the PCI DSS tab on Overview and Agents.", + 'extensions.pci': { + title: 'PCI DSS', + description: 'Enable or disable the PCI DSS tab on Overview and Agents.', category: SettingCategory.EXTENSIONS, type: EpluginSettingType.switch, defaultValue: true, @@ -1516,20 +1700,22 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { values: { disabled: { label: 'false', value: false }, enabled: { label: 'true', value: true }, - } - } + }, + }, }, - uiFormTransformChangedInputValue: function (value: boolean | string): boolean { + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { return Boolean(value); }, validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, + validateBackend: function (schema) { + return schema.boolean(); + }, }, - "extensions.tsc": { - title: "TSC", - description: "Enable or disable the TSC tab on Overview and Agents.", + 'extensions.tsc': { + title: 'TSC', + description: 'Enable or disable the TSC tab on Overview and Agents.', category: SettingCategory.EXTENSIONS, type: EpluginSettingType.switch, defaultValue: true, @@ -1540,20 +1726,22 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { values: { disabled: { label: 'false', value: false }, enabled: { label: 'true', value: true }, - } - } + }, + }, }, - uiFormTransformChangedInputValue: function (value: boolean | string): boolean { + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { return Boolean(value); }, validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, + validateBackend: function (schema) { + return schema.boolean(); + }, }, - "extensions.virustotal": { - title: "Virustotal", - description: "Enable or disable the VirusTotal tab on Overview and Agents.", + 'extensions.virustotal': { + title: 'Virustotal', + description: 'Enable or disable the VirusTotal tab on Overview and Agents.', category: SettingCategory.EXTENSIONS, type: EpluginSettingType.switch, defaultValue: false, @@ -1564,20 +1752,22 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { values: { disabled: { label: 'false', value: false }, enabled: { label: 'true', value: true }, - } - } + }, + }, }, - uiFormTransformChangedInputValue: function (value: boolean | string): boolean { + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { return Boolean(value); }, validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, + validateBackend: function (schema) { + return schema.boolean(); + }, }, - "hideManagerAlerts": { - title: "Hide manager alerts", - description: "Hide the alerts of the manager in every dashboard.", + hideManagerAlerts: { + title: 'Hide manager alerts', + description: 'Hide the alerts of the manager in every dashboard.', category: SettingCategory.GENERAL, type: EpluginSettingType.switch, defaultValue: false, @@ -1589,20 +1779,23 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { values: { disabled: { label: 'false', value: false }, enabled: { label: 'true', value: true }, - } - } + }, + }, }, - uiFormTransformChangedInputValue: function (value: boolean | string): boolean { + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { return Boolean(value); }, validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, + validateBackend: function (schema) { + return schema.boolean(); + }, }, - "ip.ignore": { - title: "Index pattern ignore", - description: "Disable certain index pattern names from being available in index pattern selector.", + 'ip.ignore': { + title: 'Index pattern ignore', + description: + 'Disable certain index pattern names from being available in index pattern selector.', category: SettingCategory.GENERAL, type: EpluginSettingType.editor, defaultValue: [], @@ -1610,43 +1803,74 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { isConfigurableFromUI: true, options: { editor: { - language: 'json' - } + language: 'json', + }, }, uiFormTransformConfigurationValueToInputValue: function (value: any): any { return JSON.stringify(value); }, - uiFormTransformInputValueToConfigurationValue: function (value: string): any { + uiFormTransformInputValueToConfigurationValue: function ( + value: string, + ): any { try { return JSON.parse(value); } catch (error) { return value; - }; + } }, // Validation: https://github.com/elastic/elasticsearch/blob/v7.10.2/docs/reference/indices/create-index.asciidoc - validate: SettingsValidator.json(SettingsValidator.compose( - SettingsValidator.array(SettingsValidator.compose( - SettingsValidator.isString, - SettingsValidator.isNotEmptyString, - SettingsValidator.hasNoSpaces, - SettingsValidator.noLiteralString('.', '..'), - SettingsValidator.noStartsWithString('-', '_', '+', '.'), - SettingsValidator.hasNotInvalidCharacters('\\', '/', '?', '"', '<', '>', '|', ',', '#') - )), - )), - validateBackend: function(schema){ - return schema.arrayOf(schema.string({validate: SettingsValidator.compose( - SettingsValidator.isNotEmptyString, - SettingsValidator.hasNoSpaces, - SettingsValidator.noLiteralString('.', '..'), - SettingsValidator.noStartsWithString('-', '_', '+', '.'), - SettingsValidator.hasNotInvalidCharacters('\\', '/', '?', '"', '<', '>', '|', ',', '#') - )})); - }, + validate: SettingsValidator.json( + SettingsValidator.compose( + SettingsValidator.array( + SettingsValidator.compose( + SettingsValidator.isString, + SettingsValidator.isNotEmptyString, + SettingsValidator.hasNoSpaces, + SettingsValidator.noLiteralString('.', '..'), + SettingsValidator.noStartsWithString('-', '_', '+', '.'), + SettingsValidator.hasNotInvalidCharacters( + '\\', + '/', + '?', + '"', + '<', + '>', + '|', + ',', + '#', + ), + ), + ), + ), + ), + validateBackend: function (schema) { + return schema.arrayOf( + schema.string({ + validate: SettingsValidator.compose( + SettingsValidator.isNotEmptyString, + SettingsValidator.hasNoSpaces, + SettingsValidator.noLiteralString('.', '..'), + SettingsValidator.noStartsWithString('-', '_', '+', '.'), + SettingsValidator.hasNotInvalidCharacters( + '\\', + '/', + '?', + '"', + '<', + '>', + '|', + ',', + '#', + ), + ), + }), + ); + }, }, - "ip.selector": { - title: "IP selector", - description: "Define if the user is allowed to change the selected index pattern directly from the top menu bar.", + 'ip.selector': { + title: 'IP selector', + description: + 'Define if the user is allowed to change the selected index pattern directly from the top menu bar.', category: SettingCategory.GENERAL, type: EpluginSettingType.switch, defaultValue: true, @@ -1657,48 +1881,55 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { values: { disabled: { label: 'false', value: false }, enabled: { label: 'true', value: true }, - } - } + }, + }, }, - uiFormTransformChangedInputValue: function (value: boolean | string): boolean { + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { return Boolean(value); }, validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, + validateBackend: function (schema) { + return schema.boolean(); + }, }, - "logs.level": { - title: "Log level", - description: "Logging level of the App.", + 'logs.level': { + title: 'Log level', + description: 'Logging level of the App.', category: SettingCategory.GENERAL, type: EpluginSettingType.select, options: { select: [ { - text: "Info", - value: "info" + text: 'Info', + value: 'info', }, { - text: "Debug", - value: "debug" - } - ] + text: 'Debug', + value: 'debug', + }, + ], }, - defaultValue: "info", + defaultValue: 'info', isConfigurableFromFile: true, isConfigurableFromUI: true, requiresRestartingPluginPlatform: true, - validate: function (value){ - return SettingsValidator.literal(this.options.select.map(({value}) => value))(value) - }, - validateBackend: function(schema){ - return schema.oneOf(this.options.select.map(({value}) => schema.literal(value))); - }, + validate: function (value) { + return SettingsValidator.literal( + this.options.select.map(({ value }) => value), + )(value); + }, + validateBackend: function (schema) { + return schema.oneOf( + this.options.select.map(({ value }) => schema.literal(value)), + ); + }, }, - "pattern": { - title: "Index pattern", - description: "Default index pattern to use on the app. If there's no valid index pattern, the app will automatically create one with the name indicated in this option.", + pattern: { + title: 'Index pattern', + description: + "Default index pattern to use on the app. If there's no valid index pattern, the app will automatically create one with the name indicated in this option.", category: SettingCategory.GENERAL, type: EpluginSettingType.text, defaultValue: WAZUH_ALERTS_PATTERN, @@ -1711,15 +1942,26 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { SettingsValidator.hasNoSpaces, SettingsValidator.noLiteralString('.', '..'), SettingsValidator.noStartsWithString('-', '_', '+', '.'), - SettingsValidator.hasNotInvalidCharacters('\\', '/', '?', '"', '<', '>', '|', ',', '#') + SettingsValidator.hasNotInvalidCharacters( + '\\', + '/', + '?', + '"', + '<', + '>', + '|', + ',', + '#', + ), ), - validateBackend: function(schema){ - return schema.string({validate: this.validate}); - }, + validateBackend: function (schema) { + return schema.string({ validate: this.validate }); + }, }, - "timeout": { - title: "Request timeout", - description: "Maximum time, in milliseconds, the app will wait for an API response when making requests to it. It will be ignored if the value is set under 1500 milliseconds.", + timeout: { + title: 'Request timeout', + description: + 'Maximum time, in milliseconds, the app will wait for an API response when making requests to it. It will be ignored if the value is set under 1500 milliseconds.', category: SettingCategory.GENERAL, type: EpluginSettingType.number, defaultValue: 20000, @@ -1728,61 +1970,69 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { options: { number: { min: 1500, - integer: true - } + integer: true, + }, }, uiFormTransformConfigurationValueToInputValue: function (value: number) { - return String(value) + return String(value); }, - uiFormTransformInputValueToConfigurationValue: function (value: string): number { + uiFormTransformInputValueToConfigurationValue: function ( + value: string, + ): number { return Number(value); }, - validate: function(value){ - return SettingsValidator.number(this.options.number)(value); - }, - validateBackend: function(schema){ - return schema.number({validate: this.validate.bind(this)}); - }, + validate: function (value) { + return SettingsValidator.number(this.options.number)(value); + }, + validateBackend: function (schema) { + return schema.number({ validate: this.validate.bind(this) }); + }, }, - "wazuh.monitoring.creation": { - title: "Index creation", - description: "Define the interval in which a new wazuh-monitoring index will be created.", + 'wazuh.monitoring.creation': { + title: 'Index creation', + description: + 'Define the interval in which a new wazuh-monitoring index will be created.', category: SettingCategory.MONITORING, type: EpluginSettingType.select, options: { select: [ { - text: "Hourly", - value: "h" + text: 'Hourly', + value: 'h', }, { - text: "Daily", - value: "d" + text: 'Daily', + value: 'd', }, { - text: "Weekly", - value: "w" + text: 'Weekly', + value: 'w', }, { - text: "Monthly", - value: "m" - } - ] + text: 'Monthly', + value: 'm', + }, + ], }, defaultValue: WAZUH_MONITORING_DEFAULT_CREATION, isConfigurableFromFile: true, isConfigurableFromUI: true, requiresRunningHealthCheck: true, - validate: function (value){ - return SettingsValidator.literal(this.options.select.map(({value}) => value))(value) - }, - validateBackend: function(schema){ - return schema.oneOf(this.options.select.map(({value}) => schema.literal(value))); - }, + validate: function (value) { + return SettingsValidator.literal( + this.options.select.map(({ value }) => value), + )(value); + }, + validateBackend: function (schema) { + return schema.oneOf( + this.options.select.map(({ value }) => schema.literal(value)), + ); + }, }, - "wazuh.monitoring.enabled": { - title: "Status", - description: "Enable or disable the wazuh-monitoring index creation and/or visualization.", + 'wazuh.monitoring.enabled': { + title: 'Status', + description: + 'Enable or disable the wazuh-monitoring index creation and/or visualization.', category: SettingCategory.MONITORING, type: EpluginSettingType.switch, defaultValue: WAZUH_MONITORING_DEFAULT_ENABLED, @@ -1794,20 +2044,23 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { values: { disabled: { label: 'false', value: false }, enabled: { label: 'true', value: true }, - } - } + }, + }, }, - uiFormTransformChangedInputValue: function (value: boolean | string): boolean { + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { return Boolean(value); }, validate: SettingsValidator.isBoolean, - validateBackend: function(schema){ - return schema.boolean(); - }, + validateBackend: function (schema) { + return schema.boolean(); + }, }, - "wazuh.monitoring.frequency": { - title: "Frequency", - description: "Frequency, in seconds, of API requests to get the state of the agents and create a new document in the wazuh-monitoring index with this data.", + 'wazuh.monitoring.frequency': { + title: 'Frequency', + description: + 'Frequency, in seconds, of API requests to get the state of the agents and create a new document in the wazuh-monitoring index with this data.', category: SettingCategory.MONITORING, type: EpluginSettingType.number, defaultValue: WAZUH_MONITORING_DEFAULT_FREQUENCY, @@ -1817,25 +2070,27 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { options: { number: { min: 60, - integer: true - } + integer: true, + }, }, uiFormTransformConfigurationValueToInputValue: function (value: number) { - return String(value) + return String(value); }, - uiFormTransformInputValueToConfigurationValue: function (value: string): number { + uiFormTransformInputValueToConfigurationValue: function ( + value: string, + ): number { return Number(value); }, - validate: function(value){ - return SettingsValidator.number(this.options.number)(value); - }, - validateBackend: function(schema){ - return schema.number({validate: this.validate.bind(this)}); - }, + validate: function (value) { + return SettingsValidator.number(this.options.number)(value); + }, + validateBackend: function (schema) { + return schema.number({ validate: this.validate.bind(this) }); + }, }, - "wazuh.monitoring.pattern": { - title: "Index pattern", - description: "Default index pattern to use for Wazuh monitoring.", + 'wazuh.monitoring.pattern': { + title: 'Index pattern', + description: 'Default index pattern to use for Wazuh monitoring.', category: SettingCategory.MONITORING, type: EpluginSettingType.text, defaultValue: WAZUH_MONITORING_PATTERN, @@ -1847,15 +2102,26 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { SettingsValidator.hasNoSpaces, SettingsValidator.noLiteralString('.', '..'), SettingsValidator.noStartsWithString('-', '_', '+', '.'), - SettingsValidator.hasNotInvalidCharacters('\\', '/', '?', '"', '<', '>', '|', ',', '#') + SettingsValidator.hasNotInvalidCharacters( + '\\', + '/', + '?', + '"', + '<', + '>', + '|', + ',', + '#', + ), ), - validateBackend: function(schema){ - return schema.string({minLength: 1, validate: this.validate}); - }, + validateBackend: function (schema) { + return schema.string({ minLength: 1, validate: this.validate }); + }, }, - "wazuh.monitoring.replicas": { - title: "Index replicas", - description: "Define the number of replicas to use for the wazuh-monitoring-* indices.", + 'wazuh.monitoring.replicas': { + title: 'Index replicas', + description: + 'Define the number of replicas to use for the wazuh-monitoring-* indices.', category: SettingCategory.MONITORING, type: EpluginSettingType.number, defaultValue: WAZUH_MONITORING_DEFAULT_INDICES_REPLICAS, @@ -1865,25 +2131,28 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { options: { number: { min: 0, - integer: true - } + integer: true, + }, }, uiFormTransformConfigurationValueToInputValue: function (value: number) { - return String(value) + return String(value); }, - uiFormTransformInputValueToConfigurationValue: function (value: string): number { + uiFormTransformInputValueToConfigurationValue: function ( + value: string, + ): number { return Number(value); }, - validate: function(value){ - return SettingsValidator.number(this.options.number)(value); - }, - validateBackend: function(schema){ - return schema.number({validate: this.validate.bind(this)}); - }, + validate: function (value) { + return SettingsValidator.number(this.options.number)(value); + }, + validateBackend: function (schema) { + return schema.number({ validate: this.validate.bind(this) }); + }, }, - "wazuh.monitoring.shards": { - title: "Index shards", - description: "Define the number of shards to use for the wazuh-monitoring-* indices.", + 'wazuh.monitoring.shards': { + title: 'Index shards', + description: + 'Define the number of shards to use for the wazuh-monitoring-* indices.', category: SettingCategory.MONITORING, type: EpluginSettingType.number, defaultValue: WAZUH_MONITORING_DEFAULT_INDICES_SHARDS, @@ -1893,22 +2162,24 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { options: { number: { min: 1, - integer: true - } + integer: true, + }, }, uiFormTransformConfigurationValueToInputValue: function (value: number) { - return String(value) + return String(value); }, - uiFormTransformInputValueToConfigurationValue: function (value: string): number { + uiFormTransformInputValueToConfigurationValue: function ( + value: string, + ): number { return Number(value); }, - validate: function(value){ - return SettingsValidator.number(this.options.number)(value); - }, - validateBackend: function(schema){ - return schema.number({validate: this.validate.bind(this)}); - }, - } + validate: function (value) { + return SettingsValidator.number(this.options.number)(value); + }, + validateBackend: function (schema) { + return schema.number({ validate: this.validate.bind(this) }); + }, + }, }; export type TPluginSettingKey = keyof typeof PLUGIN_SETTINGS; @@ -1969,12 +2240,15 @@ export enum HTTP_STATUS_CODES { GATEWAY_TIMEOUT = 504, HTTP_VERSION_NOT_SUPPORTED = 505, INSUFFICIENT_STORAGE = 507, - NETWORK_AUTHENTICATION_REQUIRED = 511 + NETWORK_AUTHENTICATION_REQUIRED = 511, } // Module Security configuration assessment export const MODULE_SCA_CHECK_RESULT_LABEL = { passed: 'Passed', failed: 'Failed', - 'not applicable': 'Not applicable' -} + 'not applicable': 'Not applicable', +}; + +// Search bar +export const SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT = 30; diff --git a/plugins/main/public/components/agents/sca/inventory/lib/api-request.ts b/plugins/main/public/components/agents/sca/inventory/lib/api-request.ts index 0d3eacd92e..1804a72529 100644 --- a/plugins/main/public/components/agents/sca/inventory/lib/api-request.ts +++ b/plugins/main/public/components/agents/sca/inventory/lib/api-request.ts @@ -1,3 +1,4 @@ +import { SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT } from '../../../../../../common/constants'; import { WzRequest } from '../../../../../react-services/wz-request'; export async function getFilterValues( @@ -12,7 +13,7 @@ export async function getFilterValues( distinct: true, select: field, sort: `+${field}`, - limit: 30, + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, }; const result = await WzRequest.apiReq( 'GET', diff --git a/plugins/main/public/components/agents/vuls/inventory/lib/api-requests.ts b/plugins/main/public/components/agents/vuls/inventory/lib/api-requests.ts index c567b46f9b..f89279cbd5 100644 --- a/plugins/main/public/components/agents/vuls/inventory/lib/api-requests.ts +++ b/plugins/main/public/components/agents/vuls/inventory/lib/api-requests.ts @@ -9,6 +9,7 @@ * * Find more information about this on the LICENSE file. */ +import { SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT } from '../../../../../../common/constants'; import { WzRequest } from '../../../../../react-services/wz-request'; export async function getAggregation( @@ -35,7 +36,7 @@ export async function getFilterValues( distinct: true, select: field, sort: `+${field}`, - limit: 30, + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, }; const result = await WzRequest.apiReq('GET', `/vulnerability/${agentId}`, { params: filter, diff --git a/plugins/main/public/components/overview/mitre_attack_intelligence/resources.tsx b/plugins/main/public/components/overview/mitre_attack_intelligence/resources.tsx index 87749e9557..bf90d04a65 100644 --- a/plugins/main/public/components/overview/mitre_attack_intelligence/resources.tsx +++ b/plugins/main/public/components/overview/mitre_attack_intelligence/resources.tsx @@ -16,7 +16,10 @@ import { Markdown } from '../../common/util'; import { formatUIDate } from '../../../react-services'; import React from 'react'; import { EuiLink } from '@elastic/eui'; -import { UI_LOGGER_LEVELS } from '../../../../common/constants'; +import { + SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, + UI_LOGGER_LEVELS, +} from '../../../../common/constants'; import { UI_ERROR_SEVERITIES } from '../../../react-services/error-orchestrator/types'; import { getErrorOrchestrator } from '../../../react-services/common-services'; @@ -28,7 +31,7 @@ const getMitreAttackIntelligenceSuggestions = async ( try { const params = { distinct: true, - limit: 30, + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, select: field, sort: `+${field}`, ...(currentValue ? { q: `${field}~${currentValue}` } : {}), diff --git a/plugins/main/public/controllers/management/components/management/cdblists/components/cdblists-table.tsx b/plugins/main/public/controllers/management/components/management/cdblists/components/cdblists-table.tsx index 32aa651f09..11f4fc4854 100644 --- a/plugins/main/public/controllers/management/components/management/cdblists/components/cdblists-table.tsx +++ b/plugins/main/public/controllers/management/components/management/cdblists/components/cdblists-table.tsx @@ -20,7 +20,10 @@ import { } from '../../common/resources-handler'; import { getErrorOrchestrator } from '../../../../../../react-services/common-services'; import { UI_ERROR_SEVERITIES } from '../../../../../../react-services/error-orchestrator/types'; -import { UI_LOGGER_LEVELS } from '../../../../../../../common/constants'; +import { + SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, + UI_LOGGER_LEVELS, +} from '../../../../../../../common/constants'; import { SECTION_CDBLIST_SECTION, @@ -200,7 +203,7 @@ function CDBListsTable(props) { const response = await WzRequest.apiReq('GET', '/lists', { params: { distinct: true, - limit: 30, + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, select: field, sort: `+${field}`, ...(currentValue ? { q: `${field}~${currentValue}` } : {}), diff --git a/plugins/main/public/controllers/management/components/management/decoders/components/decoders-suggestions.ts b/plugins/main/public/controllers/management/components/management/decoders/components/decoders-suggestions.ts index 8be17c92dd..4a46eeb602 100644 --- a/plugins/main/public/controllers/management/components/management/decoders/components/decoders-suggestions.ts +++ b/plugins/main/public/controllers/management/components/management/decoders/components/decoders-suggestions.ts @@ -1,3 +1,4 @@ +import { SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT } from '../../../../../../../common/constants'; import { WzRequest } from '../../../../../../react-services/wz-request'; const decodersItems = { @@ -16,7 +17,7 @@ const decodersItems = { case 'details.order': { const filter = { distinct: true, - limit: 30, + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, select: field, ...(currentValue ? { q: `${field}~${currentValue}` } : {}), }; @@ -35,7 +36,7 @@ const decodersItems = { case 'details.program_name': { const filter = { distinct: true, - limit: 30, + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, select: field, ...(currentValue ? { q: `${field}~${currentValue}` } : {}), }; @@ -52,7 +53,7 @@ const decodersItems = { case 'filename': { const filter = { distinct: true, - limit: 30, + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, select: field, sort: `+${field}`, ...(currentValue ? { q: `${field}~${currentValue}` } : {}), @@ -67,7 +68,7 @@ const decodersItems = { case 'name': { const filter = { distinct: true, - limit: 30, + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, select: field, sort: `+${field}`, ...(currentValue ? { q: `${field}~${currentValue}` } : {}), @@ -82,7 +83,7 @@ const decodersItems = { case 'relative_dirname': { const filter = { distinct: true, - limit: 30, + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, select: field, sort: `+${field}`, ...(currentValue ? { q: `${field}~${currentValue}` } : {}), @@ -113,40 +114,19 @@ const decodersFiles = { }, value: async (currentValue, { field }) => { try { - switch (field) { - case 'filename': { - const filter = { - distinct: true, - limit: 30, - select: field, - sort: `+${field}`, - ...(currentValue ? { q: `${field}~${currentValue}` } : {}), - }; - const result = await WzRequest.apiReq('GET', '/decoders/files', { - params: filter, - }); - return result?.data?.data?.affected_items?.map(item => ({ - label: item[field], - })); - break; - } - case 'relative_dirname': { - const filter = { - distinct: true, - limit: 30, - select: field, - sort: `+${field}`, - ...(currentValue ? { q: `${field}~${currentValue}` } : {}), - }; - const result = await WzRequest.apiReq('GET', '/decoders', { - params: filter, - }); - return result?.data?.data?.affected_items.map(item => ({ - label: item[field], - })); - } - default: - return []; + const filter = { + distinct: true, + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, + select: field, + sort: `+${field}`, + ...(currentValue ? { q: `${field}~${currentValue}` } : {}), + }; + const result = await WzRequest.apiReq('GET', '/decoders/files', { + params: filter, + }); + return result?.data?.data?.affected_items?.map(item => ({ + label: item[field], + })); } } catch (error) { return []; diff --git a/plugins/main/public/controllers/management/components/management/ruleset/components/ruleset-suggestions.ts b/plugins/main/public/controllers/management/components/management/ruleset/components/ruleset-suggestions.ts index b63412b33e..aa6486bdb4 100644 --- a/plugins/main/public/controllers/management/components/management/ruleset/components/ruleset-suggestions.ts +++ b/plugins/main/public/controllers/management/components/management/ruleset/components/ruleset-suggestions.ts @@ -1,3 +1,4 @@ +import { SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT } from '../../../../../../../common/constants'; import { WzRequest } from '../../../../../../react-services/wz-request'; const rulesItems = { @@ -24,7 +25,7 @@ const rulesItems = { case 'id': { const filter = { distinct: true, - limit: 30, + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, select: field, sort: `+${field}`, ...(currentValue ? { q: `id~${currentValue}` } : {}), @@ -41,7 +42,7 @@ const rulesItems = { } case 'groups': { const filter = { - limit: 30, + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, ...(currentValue ? { search: currentValue } : {}), }; const result = await WzRequest.apiReq('GET', '/rules/groups', { @@ -55,7 +56,7 @@ const rulesItems = { case 'filename': { const filter = { distinct: true, - limit: 30, + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, select: field, sort: `+${field}`, ...(currentValue ? { q: `${field}~${currentValue}` } : {}), @@ -70,7 +71,7 @@ const rulesItems = { case 'relative_dirname': { const filter = { distinct: true, - limit: 30, + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, select: field, sort: `+${field}`, ...(currentValue ? { q: `${field}~${currentValue}` } : {}), @@ -84,7 +85,7 @@ const rulesItems = { } case 'hipaa': { const filter = { - limit: 30, + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, ...(currentValue ? { search: currentValue } : {}), }; const result = await WzRequest.apiReq( @@ -96,7 +97,7 @@ const rulesItems = { } case 'gdpr': { const filter = { - limit: 30, + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, ...(currentValue ? { search: currentValue } : {}), }; const result = await WzRequest.apiReq( @@ -108,7 +109,7 @@ const rulesItems = { } case 'nist_800_53': { const filter = { - limit: 30, + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, ...(currentValue ? { search: currentValue } : {}), }; const result = await WzRequest.apiReq( @@ -120,7 +121,7 @@ const rulesItems = { } case 'gpg13': { const filter = { - limit: 30, + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, ...(currentValue ? { search: currentValue } : {}), }; const result = await WzRequest.apiReq( @@ -132,7 +133,7 @@ const rulesItems = { } case 'pci_dss': { const filter = { - limit: 30, + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, ...(currentValue ? { search: currentValue } : {}), }; const result = await WzRequest.apiReq( @@ -144,7 +145,7 @@ const rulesItems = { } case 'tsc': { const filter = { - limit: 30, + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, ...(currentValue ? { search: currentValue } : {}), }; const result = await WzRequest.apiReq( @@ -156,7 +157,7 @@ const rulesItems = { } case 'mitre': { const filter = { - limit: 30, + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, ...(currentValue ? { search: currentValue } : {}), }; const result = await WzRequest.apiReq( @@ -184,41 +185,19 @@ const rulesFiles = { }, value: async (currentValue, { field }) => { try { - switch (field) { - case 'filename': { - const filter = { - distinct: true, - limit: 30, - select: field, - sort: `+${field}`, - ...(currentValue ? { q: `${field}~${currentValue}` } : {}), - }; - const result = await WzRequest.apiReq('GET', '/rules/files', { - params: filter, - }); - return result?.data?.data?.affected_items?.map(item => ({ - label: item[field], - })); - break; - } - case 'relative_dirname': { - const filter = { - distinct: true, - limit: 30, - select: field, - sort: `+${field}`, - ...(currentValue ? { q: `${field}~${currentValue}` } : {}), - }; - const result = await WzRequest.apiReq('GET', '/rules', { - params: filter, - }); - return result?.data?.data?.affected_items.map(item => ({ - label: item[field], - })); - } - default: - return []; - } + const filter = { + distinct: true, + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, + select: field, + sort: `+${field}`, + ...(currentValue ? { q: `${field}~${currentValue}` } : {}), + }; + const result = await WzRequest.apiReq('GET', '/rules/files', { + params: filter, + }); + return result?.data?.data?.affected_items?.map(item => ({ + label: item[field], + })); } catch (error) { return []; } From 904aa79f950c9ce5fb702c4a53ffd023b41f16f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Tue, 8 Aug 2023 10:20:03 +0200 Subject: [PATCH 62/76] feat(search-bar): use constant to define the count of distinct values to get in the suggestions --- .../components/overview-actions/agents-selection-table.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/main/public/controllers/overview/components/overview-actions/agents-selection-table.js b/plugins/main/public/controllers/overview/components/overview-actions/agents-selection-table.js index bceecb37fc..99cc5a3790 100644 --- a/plugins/main/public/controllers/overview/components/overview-actions/agents-selection-table.js +++ b/plugins/main/public/controllers/overview/components/overview-actions/agents-selection-table.js @@ -12,6 +12,7 @@ import store from '../../../../redux/store'; import { GroupTruncate } from '../../../../components/common/util/agent-group-truncate/'; import { get as getLodash } from 'lodash'; import { + SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, UI_LOGGER_LEVELS, UI_ORDER_AGENT_STATUS, } from '../../../../../common/constants'; @@ -261,7 +262,7 @@ export class AgentSelectionTable extends Component { { params: { distinct: true, - limit: 30, + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, select: field, sort: `+${field}`, ...(currentValue From dc9e55b2e589377c416cafd34878df1341ac357f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Tue, 8 Aug 2023 12:14:35 +0200 Subject: [PATCH 63/76] fix(search-bar): fix value suggestions in the Decoders section --- .../management/decoders/components/decoders-suggestions.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/main/public/controllers/management/components/management/decoders/components/decoders-suggestions.ts b/plugins/main/public/controllers/management/components/management/decoders/components/decoders-suggestions.ts index 4a46eeb602..d81f0e825c 100644 --- a/plugins/main/public/controllers/management/components/management/decoders/components/decoders-suggestions.ts +++ b/plugins/main/public/controllers/management/components/management/decoders/components/decoders-suggestions.ts @@ -127,7 +127,6 @@ const decodersFiles = { return result?.data?.data?.affected_items?.map(item => ({ label: item[field], })); - } } catch (error) { return []; } From 0b451ed68a46d406b91140437ba1b60ce26f98c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Tue, 8 Aug 2023 12:15:55 +0200 Subject: [PATCH 64/76] fix: add comment to constant --- plugins/main/common/constants.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/main/common/constants.ts b/plugins/main/common/constants.ts index d9894ca0c2..4b395b9283 100644 --- a/plugins/main/common/constants.ts +++ b/plugins/main/common/constants.ts @@ -2251,4 +2251,6 @@ export const MODULE_SCA_CHECK_RESULT_LABEL = { }; // Search bar + +// This limits the results in the API request export const SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT = 30; From 736d9583136f3b894d43553878a7d54a1645f634 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Tue, 8 Aug 2023 12:16:50 +0200 Subject: [PATCH 65/76] workaround(search-bar): add a filter to the value suggestions in WQL When getting the distinct values for some fields, the value could not match the regular expression that validates them, and this causes that the search can not be run. So, we filters the distinct values to ensure or reduce the suggestions can be used to search. This causes some possible values are not displayed in the suggestions. To undone this, then the API should allow these values. --- plugins/main/common/constants.ts | 2 + .../search-bar/query-language/wql.tsx | 65 +++++++++++++++---- 2 files changed, 56 insertions(+), 11 deletions(-) diff --git a/plugins/main/common/constants.ts b/plugins/main/common/constants.ts index 4b395b9283..7db2d10444 100644 --- a/plugins/main/common/constants.ts +++ b/plugins/main/common/constants.ts @@ -2254,3 +2254,5 @@ export const MODULE_SCA_CHECK_RESULT_LABEL = { // This limits the results in the API request export const SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT = 30; +// This limits the suggestions for the token of type value displayed in the search bar +export const SEARCH_BAR_WQL_VALUE_SUGGESTIONS_DISPLAY_COUNT = 10; diff --git a/plugins/main/public/components/search-bar/query-language/wql.tsx b/plugins/main/public/components/search-bar/query-language/wql.tsx index 9df7dbbf01..63216eaeaf 100644 --- a/plugins/main/public/components/search-bar/query-language/wql.tsx +++ b/plugins/main/public/components/search-bar/query-language/wql.tsx @@ -7,7 +7,10 @@ import { EuiCode, } from '@elastic/eui'; import { tokenizer as tokenizerUQL } from './aql'; -import { PLUGIN_VERSION } from '../../../../common/constants'; +import { + PLUGIN_VERSION, + SEARCH_BAR_WQL_VALUE_SUGGESTIONS_DISPLAY_COUNT, +} from '../../../../common/constants'; /* UI Query language https://documentation.wazuh.com/current/user-manual/api/queries.html @@ -314,6 +317,37 @@ function getTokenNearTo( ); } +/** + * It returns the regular expression that validate the token of type value + * @returns The regular expression + */ +function getTokenValueRegularExpression() { + return new RegExp( + // Value: A string. + '^(?(?:(?:\\((?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|[\\[\\]\\w _\\-.:?\\/\'"=@%<>{}]*)\\))*' + + '(?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|^[\\[\\]\\w _\\-.:?\\\\/\'"=@%<>{}]+)' + + '(?:\\((?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|[\\[\\]\\w _\\-.:?\\\\/\'"=@%<>{}]*)\\))*)+)$', + ); +} + +/** + * It filters the values that matche the validation regular expression and returns the first items + * defined by SEARCH_BAR_WQL_VALUE_SUGGESTIONS_DISPLAY_COUNT constant. + * @param suggestions Suggestions provided by the suggestions.value method of each instance of the + * search bar + * @returns + */ +function filterTokenValueSuggestion( + suggestions: QLOptionSuggestionEntityItemTyped[], +) { + return suggestions + .filter(({ label }: QLOptionSuggestionEntityItemTyped) => { + const re = getTokenValueRegularExpression(); + return re.test(label); + }) + .slice(0, SEARCH_BAR_WQL_VALUE_SUGGESTIONS_DISPLAY_COUNT); +} + /** * Get the suggestions from the tokens * @param tokens @@ -407,11 +441,18 @@ export async function getSuggestions( operator => operator === lastToken.value, ) ? [ - ...( + /* + WORKAROUND: When getting suggestions for the distinct values for any field, the API + could reply some values that doesn't match the expected regular expression. If the + value is invalid, a validation message is displayed and avoid the search can be run. + The goal of this filter is that the suggested values can be used to search. This + causes some values could not be displayed as suggestions. + */ + ...filterTokenValueSuggestion( await options.suggestions.value(undefined, { field, operatorCompare, - }) + }), ).map(mapSuggestionCreatorValue), ] : []), @@ -441,11 +482,18 @@ export async function getSuggestions( }, ] : []), - ...( + /* + WORKAROUND: When getting suggestions for the distinct values for any field, the API + could reply some values that doesn't match the expected regular expression. If the + value is invalid, a validation message is displayed and avoid the search can be run. + The goal of this filter is that the suggested values can be used to search. This + causes some values could not be displayed as suggestions. + */ + ...filterTokenValueSuggestion( await options.suggestions.value(lastToken.formattedValue, { field, operatorCompare, - }) + }), ).map(mapSuggestionCreatorValue), ...Object.entries(language.tokens.conjunction.literal).map( ([conjunction, description]) => ({ @@ -685,12 +733,7 @@ function getOutput(input: string, options: OptionsQL) { * @returns */ function validateTokenValue(token: IToken): string | undefined { - const re = new RegExp( - // Value: A string. - '^(?(?:(?:\\((?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|[\\[\\]\\w _\\-.:?\\/\'"=@%<>{}]*)\\))*' + - '(?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|^[\\[\\]\\w _\\-.:?\\\\/\'"=@%<>{}]+)' + - '(?:\\((?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|[\\[\\]\\w _\\-.:?\\\\/\'"=@%<>{}]*)\\))*)+)$', - ); + const re = getTokenValueRegularExpression(); const value = token.formattedValue ?? token.value; const match = value.match(re); From 3e1b9194afb3939c8fe98662e983dc91de005145 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Wed, 9 Aug 2023 08:48:02 +0200 Subject: [PATCH 66/76] changelog: add entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ae71b32b4..f8e5751cdf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Changed the query to search for an agent in `management/configuration`. [#5485](https://github.com/wazuh/wazuh-kibana-app/pull/5485) - Changed the search bar in management/log to the one used in the rest of the app. [#5476](https://github.com/wazuh/wazuh-kibana-app/pull/5476) - Changed the design of the wizard to add agents. [#5457](https://github.com/wazuh/wazuh-kibana-app/pull/5457) +- Changed the search bar in Management (Rules, Decoders, CDB List) and Modules (Vulnerabilities > Inventory, Security Configuration Assessment > Inventory > {Policy ID} > Checks, MITRE ATT&CK > Intelligence > {Resource}) [#5442](https://github.com/wazuh/wazuh-kibana-app/pull/5442) ### Fixed From d15889a64e478367e5f55ddc16fb1645897eef7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Wed, 9 Aug 2023 08:50:07 +0200 Subject: [PATCH 67/76] changelog: add entry --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f8e5751cdf..6e70fd264b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,7 +20,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Changed the query to search for an agent in `management/configuration`. [#5485](https://github.com/wazuh/wazuh-kibana-app/pull/5485) - Changed the search bar in management/log to the one used in the rest of the app. [#5476](https://github.com/wazuh/wazuh-kibana-app/pull/5476) - Changed the design of the wizard to add agents. [#5457](https://github.com/wazuh/wazuh-kibana-app/pull/5457) -- Changed the search bar in Management (Rules, Decoders, CDB List) and Modules (Vulnerabilities > Inventory, Security Configuration Assessment > Inventory > {Policy ID} > Checks, MITRE ATT&CK > Intelligence > {Resource}) [#5442](https://github.com/wazuh/wazuh-kibana-app/pull/5442) +- Changed the search bar in Management (Rules, Decoders, CDB List) and Modules (Vulnerabilities > Inventory, Security Configuration Assessment > Inventory > {Policy ID} > Checks, MITRE ATT&CK > Intelligence > {Resource}) [#5363](https://github.com/wazuh/wazuh-kibana-app/pull/5363) [#5442](https://github.com/wazuh/wazuh-kibana-app/pull/5442) ### Fixed From 0ceae84aa32d8e9c91205a0b296caebf00a2039d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Wed, 9 Aug 2023 11:51:55 +0200 Subject: [PATCH 68/76] changelog: add entry --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e70fd264b..8d8dc83551 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,7 +20,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Changed the query to search for an agent in `management/configuration`. [#5485](https://github.com/wazuh/wazuh-kibana-app/pull/5485) - Changed the search bar in management/log to the one used in the rest of the app. [#5476](https://github.com/wazuh/wazuh-kibana-app/pull/5476) - Changed the design of the wizard to add agents. [#5457](https://github.com/wazuh/wazuh-kibana-app/pull/5457) -- Changed the search bar in Management (Rules, Decoders, CDB List) and Modules (Vulnerabilities > Inventory, Security Configuration Assessment > Inventory > {Policy ID} > Checks, MITRE ATT&CK > Intelligence > {Resource}) [#5363](https://github.com/wazuh/wazuh-kibana-app/pull/5363) [#5442](https://github.com/wazuh/wazuh-kibana-app/pull/5442) +- Changed the search bar in Management (Rules, Decoders, CDB List) and Modules (Vulnerabilities > Inventory, Security Configuration Assessment > Inventory > {Policy ID} > Checks, MITRE ATT&CK > Intelligence > {Resource}), Explore agent modal [#5363](https://github.com/wazuh/wazuh-kibana-app/pull/5363) [#5442](https://github.com/wazuh/wazuh-kibana-app/pull/5442) [#5447](https://github.com/wazuh/wazuh-kibana-app/pull/5447) ### Fixed From 9e121ad1d351a092622f1c522b27e4aefb01effd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Wed, 9 Aug 2023 15:07:25 +0200 Subject: [PATCH 69/76] fix(wql): add whitespace before closing grouping operator ) when using the suggestions --- .../components/search-bar/query-language/wql.test.tsx | 2 +- .../public/components/search-bar/query-language/wql.tsx | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/plugins/main/public/components/search-bar/query-language/wql.test.tsx b/plugins/main/public/components/search-bar/query-language/wql.test.tsx index 4de5de790b..c911516610 100644 --- a/plugins/main/public/components/search-bar/query-language/wql.test.tsx +++ b/plugins/main/public/components/search-bar/query-language/wql.test.tsx @@ -303,7 +303,7 @@ describe('Query language - WQL', () => { ${'(field=value or field2>'} | ${{ type: { iconType: 'kqlOperand', color: 'tint1' }, label: '~' }} | ${'(field=value or field2~'} ${'(field=value or field2>'} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value2' }} | ${'(field=value or field2>value2'} ${'(field=value or field2>value2'} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value3' }} | ${'(field=value or field2>value3'} - ${'(field=value or field2>value2'} | ${{ type: { iconType: 'tokenDenseVector', color: 'tint3' }, label: ')' }} | ${'(field=value or field2>value2)'} + ${'(field=value or field2>value2'} | ${{ type: { iconType: 'tokenDenseVector', color: 'tint3' }, label: ')' }} | ${'(field=value or field2>value2 )'} `( 'click suggestion - WQL $WQL => $changedInput', async ({ WQL: currentInput, clikedSuggestion, changedInput }) => { diff --git a/plugins/main/public/components/search-bar/query-language/wql.tsx b/plugins/main/public/components/search-bar/query-language/wql.tsx index 63216eaeaf..cfdf1c1e58 100644 --- a/plugins/main/public/components/search-bar/query-language/wql.tsx +++ b/plugins/main/public/components/search-bar/query-language/wql.tsx @@ -1094,10 +1094,15 @@ export const WQL = { : item.label; } else { // add a whitespace for conjunction + // add a whitespace for grouping operator ) !/\s$/.test(input) && (item.type.iconType === suggestionMappingLanguageTokenType.conjunction.iconType || - lastToken?.type === 'conjunction') && + lastToken?.type === 'conjunction' || + (item.type.iconType === + suggestionMappingLanguageTokenType.operator_group + .iconType && + item.label === ')')) && tokens.push({ type: 'whitespace', value: ' ', From 8ed6ae4f5ba3212631b42c9dab38f9b8155f9810 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Thu, 10 Aug 2023 09:04:04 +0200 Subject: [PATCH 70/76] feat(search-bar): add a debounce time to update the search bar state --- plugins/main/common/constants.ts | 3 + .../public/components/search-bar/index.tsx | 134 +++++++++++------- 2 files changed, 84 insertions(+), 53 deletions(-) diff --git a/plugins/main/common/constants.ts b/plugins/main/common/constants.ts index 7db2d10444..7a75742a9c 100644 --- a/plugins/main/common/constants.ts +++ b/plugins/main/common/constants.ts @@ -2256,3 +2256,6 @@ export const MODULE_SCA_CHECK_RESULT_LABEL = { export const SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT = 30; // This limits the suggestions for the token of type value displayed in the search bar export const SEARCH_BAR_WQL_VALUE_SUGGESTIONS_DISPLAY_COUNT = 10; +/* Time in milliseconds to debounce the analysis of search bar. This mitigates some problems related +to changes running in parallel */ +export const SEARCH_BAR_DEBOUNCE_UPDATE_TIME = 400; diff --git a/plugins/main/public/components/search-bar/index.tsx b/plugins/main/public/components/search-bar/index.tsx index 4a82d5d360..f73c4a5207 100644 --- a/plugins/main/public/components/search-bar/index.tsx +++ b/plugins/main/public/components/search-bar/index.tsx @@ -9,21 +9,22 @@ import { EuiSelect, EuiText, EuiFlexGroup, - EuiFlexItem + EuiFlexItem, } from '@elastic/eui'; import { EuiSuggest } from '../eui-suggest'; import { searchBarQueryLanguages } from './query-language'; import _ from 'lodash'; import { ISearchBarModeWQL } from './query-language/wql'; +import { SEARCH_BAR_DEBOUNCE_UPDATE_TIME } from '../../../common/constants'; -export interface SearchBarProps{ +export interface SearchBarProps { defaultMode?: string; modes: ISearchBarModeWQL[]; onChange?: (params: any) => void; onSearch: (params: any) => void; - buttonsRender?: () => React.ReactNode + buttonsRender?: () => React.ReactNode; input?: string; -}; +} export const SearchBar = ({ defaultMode, @@ -54,12 +55,16 @@ export const SearchBar = ({ output: undefined, }); // Cache the previous output - const queryLanguageOutputRunPreviousOutput = useRef(queryLanguageOutputRun.output); + const queryLanguageOutputRunPreviousOutput = useRef( + queryLanguageOutputRun.output, + ); // Controls when the suggestion popover is open/close const [isOpenSuggestionPopover, setIsOpenSuggestionPopover] = useState(false); // Reference to the input const inputRef = useRef(); + // Debounce update timer + const debounceUpdateSearchBarTimer = useRef(); // Handler when searching const _onSearch = (output: any) => { @@ -79,55 +84,69 @@ export const SearchBar = ({ } }; - const selectedQueryLanguageParameters = modes.find(({ id }) => id === queryLanguage.id); + const selectedQueryLanguageParameters = modes.find( + ({ id }) => id === queryLanguage.id, + ); useEffect(() => { // React to external changes and set the internal input text. Use the `transformInput` of // the query language in use - rest.input && searchBarQueryLanguages[queryLanguage.id]?.transformInput && setInput( - searchBarQueryLanguages[queryLanguage.id]?.transformInput?.( - rest.input, - { - configuration: queryLanguage.configuration, - parameters: selectedQueryLanguageParameters, - } - ), - ); + rest.input && + searchBarQueryLanguages[queryLanguage.id]?.transformInput && + setInput( + searchBarQueryLanguages[queryLanguage.id]?.transformInput?.( + rest.input, + { + configuration: queryLanguage.configuration, + parameters: selectedQueryLanguageParameters, + }, + ), + ); }, [rest.input]); useEffect(() => { (async () => { // Set the query language output - const queryLanguageOutput = await searchBarQueryLanguages[queryLanguage.id].run(input, { - onSearch: _onSearch, - setInput, - closeSuggestionPopover: () => setIsOpenSuggestionPopover(false), - openSuggestionPopover: () => setIsOpenSuggestionPopover(true), - setQueryLanguageConfiguration: (configuration: any) => - setQueryLanguage(state => ({ - ...state, - configuration: - configuration?.(state.configuration) || configuration, - })), - setQueryLanguageOutput: setQueryLanguageOutputRun, - inputRef, - queryLanguage: { - configuration: queryLanguage.configuration, - parameters: selectedQueryLanguageParameters, - }, - }); - queryLanguageOutputRunPreviousOutput.current = { - ...queryLanguageOutputRun.output - }; - setQueryLanguageOutputRun(queryLanguageOutput); + debounceUpdateSearchBarTimer.current && + clearTimeout(debounceUpdateSearchBarTimer.current); + // Debounce the updating of the search bar state + debounceUpdateSearchBarTimer.current = setTimeout(async () => { + const queryLanguageOutput = await searchBarQueryLanguages[ + queryLanguage.id + ].run(input, { + onSearch: _onSearch, + setInput, + closeSuggestionPopover: () => setIsOpenSuggestionPopover(false), + openSuggestionPopover: () => setIsOpenSuggestionPopover(true), + setQueryLanguageConfiguration: (configuration: any) => + setQueryLanguage(state => ({ + ...state, + configuration: + configuration?.(state.configuration) || configuration, + })), + setQueryLanguageOutput: setQueryLanguageOutputRun, + inputRef, + queryLanguage: { + configuration: queryLanguage.configuration, + parameters: selectedQueryLanguageParameters, + }, + }); + queryLanguageOutputRunPreviousOutput.current = { + ...queryLanguageOutputRun.output, + }; + setQueryLanguageOutputRun(queryLanguageOutput); + }, SEARCH_BAR_DEBOUNCE_UPDATE_TIME); })(); }, [input, queryLanguage, selectedQueryLanguageParameters?.options]); useEffect(() => { - onChange - // Ensure the previous output is different to the new one - && !_.isEqual(queryLanguageOutputRun.output, queryLanguageOutputRunPreviousOutput.current) - && onChange(queryLanguageOutputRun.output); + onChange && + // Ensure the previous output is different to the new one + !_.isEqual( + queryLanguageOutputRun.output, + queryLanguageOutputRunPreviousOutput.current, + ) && + onChange(queryLanguageOutputRun.output); }, [queryLanguageOutputRun.output]); const onQueryLanguagePopoverSwitch = () => @@ -163,7 +182,7 @@ export const SearchBar = ({ closePopover={onQueryLanguagePopoverSwitch} > SYNTAX OPTIONS -
+
{searchBarQueryLanguages[queryLanguage.id].description} @@ -173,7 +192,8 @@ export const SearchBar = ({
) => { + onChange={( + event: React.ChangeEvent, + ) => { const queryLanguageID: string = event.target.value; setQueryLanguage({ id: queryLanguageID, @@ -217,13 +239,19 @@ export const SearchBar = ({ /> ); - return rest.buttonsRender || queryLanguageOutputRun.filterButtons - ? ( - - {searchBar} - {rest.buttonsRender && {rest.buttonsRender()}} - {queryLanguageOutputRun.filterButtons && {queryLanguageOutputRun.filterButtons}} - - ) - : searchBar; + return rest.buttonsRender || queryLanguageOutputRun.filterButtons ? ( + + {searchBar} + {rest.buttonsRender && ( + {rest.buttonsRender()} + )} + {queryLanguageOutputRun.filterButtons && ( + + {queryLanguageOutputRun.filterButtons} + + )} + + ) : ( + searchBar + ); }; From a2abff59235b53f0a9ae07b16ecd92bd9aa72e5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Wed, 9 Aug 2023 13:28:08 +0200 Subject: [PATCH 71/76] fix(search-bar): fix prop type error related to EuiSuggestItem --- .../public/components/search-bar/query-language/wql.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/plugins/main/public/components/search-bar/query-language/wql.tsx b/plugins/main/public/components/search-bar/query-language/wql.tsx index cfdf1c1e58..10b3e1b0ea 100644 --- a/plugins/main/public/components/search-bar/query-language/wql.tsx +++ b/plugins/main/public/components/search-bar/query-language/wql.tsx @@ -102,10 +102,14 @@ const suggestionMappingLanguageTokenType = { * @returns */ function mapSuggestionCreator(type: ITokenType) { - return function ({ ...params }) { + return function ({ label, ...params }) { return { type, ...params, + /* WORKAROUND: ensure the label is a string. If it is not a string, an warning is + displayed in the console related to prop types + */ + ...(typeof label !== 'undefined' ? { label: String(label) } : {}), }; }; } From 99aa426f61e48772e1f5004d8d1cf48d663c2b1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Thu, 10 Aug 2023 14:08:11 +0200 Subject: [PATCH 72/76] fix(search-bar-wql): problem related to execute the search before the input is analyzed due to this process is debounced --- .../public/components/search-bar/index.tsx | 6 + .../search-bar/query-language/aql.tsx | 240 ++++++++++-------- .../search-bar/query-language/wql.tsx | 162 +++++++----- 3 files changed, 230 insertions(+), 178 deletions(-) diff --git a/plugins/main/public/components/search-bar/index.tsx b/plugins/main/public/components/search-bar/index.tsx index f73c4a5207..d0538739de 100644 --- a/plugins/main/public/components/search-bar/index.tsx +++ b/plugins/main/public/components/search-bar/index.tsx @@ -236,6 +236,12 @@ export const SearchBar = ({ } {...queryLanguageOutputRun.searchBarProps} + {...(queryLanguageOutputRun.searchBarProps?.onItemClick + ? { + onItemClick: + queryLanguageOutputRun.searchBarProps?.onItemClick(input), + } + : {})} /> ); diff --git a/plugins/main/public/components/search-bar/query-language/aql.tsx b/plugins/main/public/components/search-bar/query-language/aql.tsx index 8c898af3e2..6c383d1dc4 100644 --- a/plugins/main/public/components/search-bar/query-language/aql.tsx +++ b/plugins/main/public/components/search-bar/query-language/aql.tsx @@ -71,28 +71,27 @@ const suggestionMappingLanguageTokenType = { /** * Creator of intermediate interface of EuiSuggestItem - * @param type - * @returns + * @param type + * @returns */ -function mapSuggestionCreator(type: ITokenType ){ - return function({...params}){ +function mapSuggestionCreator(type: ITokenType) { + return function ({ ...params }) { return { type, - ...params + ...params, }; }; -}; +} const mapSuggestionCreatorField = mapSuggestionCreator('field'); const mapSuggestionCreatorValue = mapSuggestionCreator('value'); - /** * Tokenize the input string. Returns an array with the tokens. * @param input * @returns */ -export function tokenizer(input: string): ITokens{ +export function tokenizer(input: string): ITokens { // API regular expression // https://github.com/wazuh/wazuh/blob/v4.4.0-rc1/framework/wazuh/core/utils.py#L1242-L1257 // self.query_regex = re.compile( @@ -118,44 +117,50 @@ export function tokenizer(input: string): ITokens{ // completed. This helps to tokenize the query and manage when the input is not completed. // A ( character. '(?\\()?' + - // Field name: name of the field to look on DB. - '(?[\\w.]+)?' + // Added an optional find - // Operator: looks for '=', '!=', '<', '>' or '~'. - // This seems to be a bug because is not searching the literal valid operators. - // I guess the operator is validated after the regular expression matches - `(?[${Object.keys(language.tokens.operator_compare.literal)}]{1,2})?` + // Added an optional find - // Value: A string. - '(?(?:(?:\\((?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|[\\[\\]\\w _\\-.:?\\/\'"=@%<>{}]*)\\))*' + - '(?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|[\\[\\]\\w _\\-.:?\\\\/\'"=@%<>{}]+)' + - '(?:\\((?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|[\\[\\]\\w _\\-.:?\\\\/\'"=@%<>{}]*)\\))*)+)?' + // Added an optional find - // A ) character. - '(?\\))?' + - `(?[${Object.keys(language.tokens.conjunction.literal)}])?`, - 'g' + // Field name: name of the field to look on DB. + '(?[\\w.]+)?' + // Added an optional find + // Operator: looks for '=', '!=', '<', '>' or '~'. + // This seems to be a bug because is not searching the literal valid operators. + // I guess the operator is validated after the regular expression matches + `(?[${Object.keys( + language.tokens.operator_compare.literal, + )}]{1,2})?` + // Added an optional find + // Value: A string. + '(?(?:(?:\\((?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|[\\[\\]\\w _\\-.:?\\/\'"=@%<>{}]*)\\))*' + + '(?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|[\\[\\]\\w _\\-.:?\\\\/\'"=@%<>{}]+)' + + '(?:\\((?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|[\\[\\]\\w _\\-.:?\\\\/\'"=@%<>{}]*)\\))*)+)?' + // Added an optional find + // A ) character. + '(?\\))?' + + `(?[${Object.keys(language.tokens.conjunction.literal)}])?`, + 'g', ); - return [ - ...input.matchAll(re)] - .map( - ({groups}) => Object.entries(groups) - .map(([key, value]) => ({ - type: key.startsWith('operator_group') ? 'operator_group' : key, - value}) - ) - ).flat(); -}; + return [...input.matchAll(re)] + .map(({ groups }) => + Object.entries(groups).map(([key, value]) => ({ + type: key.startsWith('operator_group') ? 'operator_group' : key, + value, + })), + ) + .flat(); +} type QLOptionSuggestionEntityItem = { - description?: string - label: string + description?: string; + label: string; }; -type QLOptionSuggestionEntityItemTyped = - QLOptionSuggestionEntityItem - & { type: 'operator_group'|'field'|'operator_compare'|'value'|'conjunction' }; +type QLOptionSuggestionEntityItemTyped = QLOptionSuggestionEntityItem & { + type: + | 'operator_group' + | 'field' + | 'operator_compare' + | 'value' + | 'conjunction'; +}; type SuggestItem = QLOptionSuggestionEntityItem & { - type: { iconType: string, color: string } + type: { iconType: string; color: string }; }; type QLOptionSuggestionHandler = ( @@ -179,15 +184,11 @@ type optionsQL = { * @param tokenType token type to search * @returns */ -function getLastTokenWithValue( - tokens: ITokens -): IToken | undefined { +function getLastTokenWithValue(tokens: ITokens): IToken | undefined { // Reverse the tokens array and use the Array.protorype.find method const shallowCopyArray = Array.from([...tokens]); const shallowCopyArrayReversed = shallowCopyArray.reverse(); - const tokenFound = shallowCopyArrayReversed.find( - ({ value }) => value, - ); + const tokenFound = shallowCopyArrayReversed.find(({ value }) => value); return tokenFound; } @@ -218,7 +219,10 @@ function getLastTokenWithValueByType( * @param options * @returns */ -export async function getSuggestions(tokens: ITokens, options: optionsQL): Promise { +export async function getSuggestions( + tokens: ITokens, + options: optionsQL, +): Promise { if (!tokens.length) { return []; } @@ -227,40 +231,42 @@ export async function getSuggestions(tokens: ITokens, options: optionsQL): Promi const lastToken = getLastTokenWithValue(tokens); // If it can't get a token with value, then returns fields and open operator group - if(!lastToken?.type){ - return [ + if (!lastToken?.type) { + return [ // fields ...(await options.suggestions.field()).map(mapSuggestionCreatorField), { type: 'operator_group', label: '(', description: language.tokens.operator_group.literal['('], - } + }, ]; - }; + } switch (lastToken.type) { case 'field': return [ // fields that starts with the input but is not equals - ...(await options.suggestions.field()).filter( - ({ label }) => - label.startsWith(lastToken.value) && label !== lastToken.value, - ).map(mapSuggestionCreatorField), + ...(await options.suggestions.field()) + .filter( + ({ label }) => + label.startsWith(lastToken.value) && label !== lastToken.value, + ) + .map(mapSuggestionCreatorField), // operators if the input field is exact ...((await options.suggestions.field()).some( ({ label }) => label === lastToken.value, ) ? [ - ...Object.keys(language.tokens.operator_compare.literal).map( - operator => ({ - type: 'operator_compare', - label: operator, - description: - language.tokens.operator_compare.literal[operator], - }), - ), - ] + ...Object.keys(language.tokens.operator_compare.literal).map( + operator => ({ + type: 'operator_compare', + label: operator, + description: + language.tokens.operator_compare.literal[operator], + }), + ), + ] : []), ]; break; @@ -281,14 +287,17 @@ export async function getSuggestions(tokens: ITokens, options: optionsQL): Promi operator => operator === lastToken.value, ) ? [ - ...(await options.suggestions.value(undefined, { - previousField: getLastTokenWithValueByType(tokens, 'field')!.value, - previousOperatorCompare: getLastTokenWithValueByType( - tokens, - 'operator_compare', - )!.value, - })).map(mapSuggestionCreatorValue), - ] + ...( + await options.suggestions.value(undefined, { + previousField: getLastTokenWithValueByType(tokens, 'field')! + .value, + previousOperatorCompare: getLastTokenWithValueByType( + tokens, + 'operator_compare', + )!.value, + }) + ).map(mapSuggestionCreatorValue), + ] : []), ]; break; @@ -296,22 +305,24 @@ export async function getSuggestions(tokens: ITokens, options: optionsQL): Promi return [ ...(lastToken.value ? [ - { - type: 'function_search', - label: 'Search', - description: 'run the search query', - }, - ] + { + type: 'function_search', + label: 'Search', + description: 'run the search query', + }, + ] : []), - ...(await options.suggestions.value(lastToken.value, { - previousField: getLastTokenWithValueByType(tokens, 'field')!.value, - previousOperatorCompare: getLastTokenWithValueByType( - tokens, - 'operator_compare', - )!.value, - })).map(mapSuggestionCreatorValue), + ...( + await options.suggestions.value(lastToken.value, { + previousField: getLastTokenWithValueByType(tokens, 'field')!.value, + previousOperatorCompare: getLastTokenWithValueByType( + tokens, + 'operator_compare', + )!.value, + }) + ).map(mapSuggestionCreatorValue), ...Object.entries(language.tokens.conjunction.literal).map( - ([ conjunction, description]) => ({ + ([conjunction, description]) => ({ type: 'conjunction', label: conjunction, description, @@ -342,8 +353,10 @@ export async function getSuggestions(tokens: ITokens, options: optionsQL): Promi conjunction => conjunction === lastToken.value, ) ? [ - ...(await options.suggestions.field()).map(mapSuggestionCreatorField), - ] + ...(await options.suggestions.field()).map( + mapSuggestionCreatorField, + ), + ] : []), { type: 'operator_group', @@ -381,16 +394,18 @@ export async function getSuggestions(tokens: ITokens, options: optionsQL): Promi /** * Transform the suggestion object to the expected object by EuiSuggestItem - * @param param0 - * @returns + * @param param0 + * @returns */ -export function transformSuggestionToEuiSuggestItem(suggestion: QLOptionSuggestionEntityItemTyped): SuggestItem{ - const { type, ...rest} = suggestion; +export function transformSuggestionToEuiSuggestItem( + suggestion: QLOptionSuggestionEntityItemTyped, +): SuggestItem { + const { type, ...rest } = suggestion; return { type: { ...suggestionMappingLanguageTokenType[type] }, - ...rest + ...rest, }; -}; +} /** * Transform the suggestion object to the expected object by EuiSuggestItem @@ -398,24 +413,26 @@ export function transformSuggestionToEuiSuggestItem(suggestion: QLOptionSuggesti * @returns */ function transformSuggestionsToEuiSuggestItem( - suggestions: QLOptionSuggestionEntityItemTyped[] + suggestions: QLOptionSuggestionEntityItemTyped[], ): SuggestItem[] { return suggestions.map(transformSuggestionToEuiSuggestItem); -}; +} /** * Get the output from the input * @param input * @returns */ -function getOutput(input: string, options: {implicitQuery?: string} = {}) { - const unifiedQuery = `${options?.implicitQuery ?? ''}${options?.implicitQuery ? `(${input})` : input}`; +function getOutput(input: string, options: { implicitQuery?: string } = {}) { + const unifiedQuery = `${options?.implicitQuery ?? ''}${ + options?.implicitQuery ? `(${input})` : input + }`; return { language: AQL.id, query: unifiedQuery, - unifiedQuery + unifiedQuery, }; -}; +} export const AQL = { id: 'aql', @@ -436,10 +453,10 @@ export const AQL = { // Props that will be used by the EuiSuggest component // Suggestions suggestions: transformSuggestionsToEuiSuggestItem( - await getSuggestions(tokens, params.queryLanguage.parameters) + await getSuggestions(tokens, params.queryLanguage.parameters), ), // Handler to manage when clicking in a suggestion item - onItemClick: item => { + onItemClick: input => item => { // When the clicked item has the `search` iconType, run the `onSearch` function if (item.type.iconType === 'search') { // Execute the search action @@ -449,8 +466,9 @@ export const AQL = { const lastToken: IToken = getLastTokenWithValue(tokens); // if the clicked suggestion is of same type of last token if ( - lastToken && suggestionMappingLanguageTokenType[lastToken.type].iconType === - item.type.iconType + lastToken && + suggestionMappingLanguageTokenType[lastToken.type].iconType === + item.type.iconType ) { // replace the value of last token lastToken.value = item.label; @@ -462,15 +480,17 @@ export const AQL = { )[0], value: item.label, }); - }; + } // Change the input - params.setInput(tokens - .filter(({ value }) => value) // Ensure the input is rebuilt using tokens with value. - // The input tokenization can contain tokens with no value due to the used - // regular expression. - .map(({ value }) => value) - .join('')); + params.setInput( + tokens + .filter(({ value }) => value) // Ensure the input is rebuilt using tokens with value. + // The input tokenization can contain tokens with no value due to the used + // regular expression. + .map(({ value }) => value) + .join(''), + ); } }, prepend: params.queryLanguage.parameters.implicitQuery ? ( @@ -512,7 +532,7 @@ export const AQL = { // This causes when using the Search suggestion, the suggestion popover can be closed. // If this is disabled, then the suggestion popover is open after a short time for this // use case. - disableFocusTrap: true + disableFocusTrap: true, }, output: getOutput(input, params.queryLanguage.parameters), }; diff --git a/plugins/main/public/components/search-bar/query-language/wql.tsx b/plugins/main/public/components/search-bar/query-language/wql.tsx index 10b3e1b0ea..b24cfe18a8 100644 --- a/plugins/main/public/components/search-bar/query-language/wql.tsx +++ b/plugins/main/public/components/search-bar/query-language/wql.tsx @@ -1006,7 +1006,7 @@ export const WQL = { error: validationStrict, }; - const onSearch = () => { + const onSearch = output => { if (output?.error) { params.setQueryLanguageOutput(state => ({ ...state, @@ -1019,6 +1019,7 @@ export const WQL = { description: error, })), ), + isInvalid: true, }, })); } else { @@ -1071,79 +1072,92 @@ export const WQL = { : await getSuggestions(tokens, params.queryLanguage.parameters), ), // Handler to manage when clicking in a suggestion item - onItemClick: item => { - // There is an error, clicking on the item does nothing - if (item.type.iconType === 'alert') { - return; - } - // When the clicked item has the `search` iconType, run the `onSearch` function - if (item.type.iconType === 'search') { - // Execute the search action - onSearch(); - } else { - // When the clicked item has another iconType - const lastToken: IToken | undefined = getLastTokenDefined(tokens); - // if the clicked suggestion is of same type of last token - if ( - lastToken && - suggestionMappingLanguageTokenType[lastToken.type].iconType === - item.type.iconType - ) { - // replace the value of last token with the current one. - // if the current token is a value, then transform it - lastToken.value = - item.type.iconType === - suggestionMappingLanguageTokenType.value.iconType - ? transformQLValue(item.label) - : item.label; - } else { - // add a whitespace for conjunction - // add a whitespace for grouping operator ) - !/\s$/.test(input) && - (item.type.iconType === - suggestionMappingLanguageTokenType.conjunction.iconType || - lastToken?.type === 'conjunction' || - (item.type.iconType === - suggestionMappingLanguageTokenType.operator_group - .iconType && - item.label === ')')) && - tokens.push({ - type: 'whitespace', - value: ' ', - }); + onItemClick: + input => + (item, ...rest) => { + // There is an error, clicking on the item does nothing + if (item.type.iconType === 'alert') { + return; + } + // When the clicked item has the `search` iconType, run the `onSearch` function + if (item.type.iconType === 'search') { + // Execute the search action + // Get the tokens from the input + const tokens: ITokens = tokenizer(input); - // add a new token of the selected type and value - tokens.push({ - type: Object.entries(suggestionMappingLanguageTokenType).find( - ([, { iconType }]) => iconType === item.type.iconType, - )[0], - value: + const validationStrict = validate(tokens, validators); + + // Get the output of query language + const output = { + ...getOutput(input, params.queryLanguage.parameters), + error: validationStrict, + }; + + onSearch(output); + } else { + // When the clicked item has another iconType + const lastToken: IToken | undefined = getLastTokenDefined(tokens); + // if the clicked suggestion is of same type of last token + if ( + lastToken && + suggestionMappingLanguageTokenType[lastToken.type].iconType === + item.type.iconType + ) { + // replace the value of last token with the current one. + // if the current token is a value, then transform it + lastToken.value = item.type.iconType === suggestionMappingLanguageTokenType.value.iconType ? transformQLValue(item.label) - : item.label, - }); - - // add a whitespace for conjunction - item.type.iconType === - suggestionMappingLanguageTokenType.conjunction.iconType && + : item.label; + } else { + // add a whitespace for conjunction + // add a whitespace for grouping operator ) + !/\s$/.test(input) && + (item.type.iconType === + suggestionMappingLanguageTokenType.conjunction.iconType || + lastToken?.type === 'conjunction' || + (item.type.iconType === + suggestionMappingLanguageTokenType.operator_group + .iconType && + item.label === ')')) && + tokens.push({ + type: 'whitespace', + value: ' ', + }); + + // add a new token of the selected type and value tokens.push({ - type: 'whitespace', - value: ' ', + type: Object.entries(suggestionMappingLanguageTokenType).find( + ([, { iconType }]) => iconType === item.type.iconType, + )[0], + value: + item.type.iconType === + suggestionMappingLanguageTokenType.value.iconType + ? transformQLValue(item.label) + : item.label, }); - } - // Change the input - params.setInput( - tokens - .filter(value => value) // Ensure the input is rebuilt using tokens with value. - // The input tokenization can contain tokens with no value due to the used - // regular expression. - .map(({ value }) => value) - .join(''), - ); - } - }, + // add a whitespace for conjunction + item.type.iconType === + suggestionMappingLanguageTokenType.conjunction.iconType && + tokens.push({ + type: 'whitespace', + value: ' ', + }); + } + + // Change the input + params.setInput( + tokens + .filter(value => value) // Ensure the input is rebuilt using tokens with value. + // The input tokenization can contain tokens with no value due to the used + // regular expression. + .map(({ value }) => value) + .join(''), + ); + } + }, prepend: implicitQueryAsQL ? ( { if (event.key === 'Enter') { - onSearch(); + // Get the tokens from the input + const input = event.currentTarget.value; + const tokens: ITokens = tokenizer(input); + + const validationStrict = validate(tokens, validators); + + // Get the output of query language + const output = { + ...getOutput(input, params.queryLanguage.parameters), + error: validationStrict, + }; + + onSearch(output); } }, }, From 4ff265348362e753758f863fd9dfc0b328b883cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Thu, 10 Aug 2023 14:16:28 +0200 Subject: [PATCH 73/76] fix(search-bar-wql): remove unnued parameter in function --- .../search-bar/query-language/wql.tsx | 150 +++++++++--------- 1 file changed, 74 insertions(+), 76 deletions(-) diff --git a/plugins/main/public/components/search-bar/query-language/wql.tsx b/plugins/main/public/components/search-bar/query-language/wql.tsx index b24cfe18a8..94af42be42 100644 --- a/plugins/main/public/components/search-bar/query-language/wql.tsx +++ b/plugins/main/public/components/search-bar/query-language/wql.tsx @@ -1072,92 +1072,90 @@ export const WQL = { : await getSuggestions(tokens, params.queryLanguage.parameters), ), // Handler to manage when clicking in a suggestion item - onItemClick: - input => - (item, ...rest) => { - // There is an error, clicking on the item does nothing - if (item.type.iconType === 'alert') { - return; - } - // When the clicked item has the `search` iconType, run the `onSearch` function - if (item.type.iconType === 'search') { - // Execute the search action - // Get the tokens from the input - const tokens: ITokens = tokenizer(input); + onItemClick: input => item => { + // There is an error, clicking on the item does nothing + if (item.type.iconType === 'alert') { + return; + } + // When the clicked item has the `search` iconType, run the `onSearch` function + if (item.type.iconType === 'search') { + // Execute the search action + // Get the tokens from the input + const tokens: ITokens = tokenizer(input); - const validationStrict = validate(tokens, validators); + const validationStrict = validate(tokens, validators); - // Get the output of query language - const output = { - ...getOutput(input, params.queryLanguage.parameters), - error: validationStrict, - }; + // Get the output of query language + const output = { + ...getOutput(input, params.queryLanguage.parameters), + error: validationStrict, + }; - onSearch(output); + onSearch(output); + } else { + // When the clicked item has another iconType + const lastToken: IToken | undefined = getLastTokenDefined(tokens); + // if the clicked suggestion is of same type of last token + if ( + lastToken && + suggestionMappingLanguageTokenType[lastToken.type].iconType === + item.type.iconType + ) { + // replace the value of last token with the current one. + // if the current token is a value, then transform it + lastToken.value = + item.type.iconType === + suggestionMappingLanguageTokenType.value.iconType + ? transformQLValue(item.label) + : item.label; } else { - // When the clicked item has another iconType - const lastToken: IToken | undefined = getLastTokenDefined(tokens); - // if the clicked suggestion is of same type of last token - if ( - lastToken && - suggestionMappingLanguageTokenType[lastToken.type].iconType === - item.type.iconType - ) { - // replace the value of last token with the current one. - // if the current token is a value, then transform it - lastToken.value = + // add a whitespace for conjunction + // add a whitespace for grouping operator ) + !/\s$/.test(input) && + (item.type.iconType === + suggestionMappingLanguageTokenType.conjunction.iconType || + lastToken?.type === 'conjunction' || + (item.type.iconType === + suggestionMappingLanguageTokenType.operator_group + .iconType && + item.label === ')')) && + tokens.push({ + type: 'whitespace', + value: ' ', + }); + + // add a new token of the selected type and value + tokens.push({ + type: Object.entries(suggestionMappingLanguageTokenType).find( + ([, { iconType }]) => iconType === item.type.iconType, + )[0], + value: item.type.iconType === suggestionMappingLanguageTokenType.value.iconType ? transformQLValue(item.label) - : item.label; - } else { - // add a whitespace for conjunction - // add a whitespace for grouping operator ) - !/\s$/.test(input) && - (item.type.iconType === - suggestionMappingLanguageTokenType.conjunction.iconType || - lastToken?.type === 'conjunction' || - (item.type.iconType === - suggestionMappingLanguageTokenType.operator_group - .iconType && - item.label === ')')) && - tokens.push({ - type: 'whitespace', - value: ' ', - }); - - // add a new token of the selected type and value + : item.label, + }); + + // add a whitespace for conjunction + item.type.iconType === + suggestionMappingLanguageTokenType.conjunction.iconType && tokens.push({ - type: Object.entries(suggestionMappingLanguageTokenType).find( - ([, { iconType }]) => iconType === item.type.iconType, - )[0], - value: - item.type.iconType === - suggestionMappingLanguageTokenType.value.iconType - ? transformQLValue(item.label) - : item.label, + type: 'whitespace', + value: ' ', }); - - // add a whitespace for conjunction - item.type.iconType === - suggestionMappingLanguageTokenType.conjunction.iconType && - tokens.push({ - type: 'whitespace', - value: ' ', - }); - } - - // Change the input - params.setInput( - tokens - .filter(value => value) // Ensure the input is rebuilt using tokens with value. - // The input tokenization can contain tokens with no value due to the used - // regular expression. - .map(({ value }) => value) - .join(''), - ); } - }, + + // Change the input + params.setInput( + tokens + .filter(value => value) // Ensure the input is rebuilt using tokens with value. + // The input tokenization can contain tokens with no value due to the used + // regular expression. + .map(({ value }) => value) + .join(''), + ); + } + }, prepend: implicitQueryAsQL ? ( Date: Thu, 10 Aug 2023 16:07:31 +0200 Subject: [PATCH 74/76] fix(search-bar): fix tests --- .../search-bar/query-language/aql.test.tsx | 218 +++++++++--------- .../search-bar/query-language/aql.tsx | 6 +- .../search-bar/query-language/wql.test.tsx | 4 +- .../search-bar/query-language/wql.tsx | 6 +- 4 files changed, 121 insertions(+), 113 deletions(-) diff --git a/plugins/main/public/components/search-bar/query-language/aql.test.tsx b/plugins/main/public/components/search-bar/query-language/aql.test.tsx index a5f7c7d36c..3c6a57caf3 100644 --- a/plugins/main/public/components/search-bar/query-language/aql.test.tsx +++ b/plugins/main/public/components/search-bar/query-language/aql.test.tsx @@ -15,27 +15,25 @@ describe('SearchBar component', () => { field(currentValue) { return []; }, - value(currentValue, { previousField }){ + value(currentValue, { previousField }) { return []; }, }, - } + }, ], /* eslint-disable @typescript-eslint/no-empty-function */ onChange: () => {}, - onSearch: () => {} + onSearch: () => {}, /* eslint-enable @typescript-eslint/no-empty-function */ }; it('Renders correctly to match the snapshot of query language', async () => { - const wrapper = render( - - ); + const wrapper = render(); await waitFor(() => { - const elementImplicitQuery = wrapper.container.querySelector('.euiCodeBlock__code'); + const elementImplicitQuery = wrapper.container.querySelector( + '.euiCodeBlock__code', + ); expect(elementImplicitQuery?.innerHTML).toEqual('id!=000;'); expect(wrapper.container).toMatchSnapshot(); }); @@ -45,32 +43,32 @@ describe('SearchBar component', () => { describe('Query language - AQL', () => { // Tokenize the input it.each` - input | tokens - ${''} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} - ${'f'} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'f' }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} - ${'field'} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} - ${'field='} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} - ${'field=value'} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} - ${'field=value with spaces'} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value with spaces' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} - ${'field=value with spaces<'} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value with spaces<' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} - ${'field=value with (parenthesis)'} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value with (parenthesis)' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} - ${'field=value;'} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} - ${'field=value;field2'} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: 'field2' }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} - ${'field=value;field2!='} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: 'field2' }, { type: 'operator_compare', value: '!=' }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} - ${'field=value;field2!=value2'} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: 'field2' }, { type: 'operator_compare', value: '!=' }, { type: 'value', value: 'value2' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} - ${'('} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} - ${'(field'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} - ${'(field>'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} - ${'(field>2'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: '2' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} - ${'(field>2)'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: '2' }, { type: 'operator_group', value: ')' }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} - ${'(field>2);'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: '2' }, { type: 'operator_group', value: ')' }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} - ${'(field>2;'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: '2' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} - ${'(field>2;field2'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: '2' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: 'field2' }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} - ${'(field>2;field2='} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: '2' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: 'field2' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} - ${'(field>2;field2=value2'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: '2' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: 'field2' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value2' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} - ${'(field>2;field2=value2)'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: '2' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: 'field2' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value2' }, { type: 'operator_group', value: ')' }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} - ${'(field>2;field2=custom value())'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: '2' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: 'field2' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'custom value()' }, { type: 'operator_group', value: ')' }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} - `(`Tokenizer API input $input`, ({input, tokens}) => { + input | tokens + ${''} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'f'} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'f' }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'field'} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'field='} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'field=value'} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'field=value with spaces'} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value with spaces' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'field=value with spaces<'} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value with spaces<' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'field=value with (parenthesis)'} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value with (parenthesis)' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'field=value;'} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'field=value;field2'} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: 'field2' }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'field=value;field2!='} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: 'field2' }, { type: 'operator_compare', value: '!=' }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'field=value;field2!=value2'} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: 'field2' }, { type: 'operator_compare', value: '!=' }, { type: 'value', value: 'value2' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'('} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'(field'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'(field>'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'(field>2'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: '2' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'(field>2)'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: '2' }, { type: 'operator_group', value: ')' }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'(field>2);'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: '2' }, { type: 'operator_group', value: ')' }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'(field>2;'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: '2' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'(field>2;field2'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: '2' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: 'field2' }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'(field>2;field2='} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: '2' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: 'field2' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'(field>2;field2=value2'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: '2' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: 'field2' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value2' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'(field>2;field2=value2)'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: '2' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: 'field2' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value2' }, { type: 'operator_group', value: ')' }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'(field>2;field2=custom value())'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: '2' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: 'field2' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'custom value()' }, { type: 'operator_group', value: ')' }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + `(`Tokenizer API input $input`, ({ input, tokens }) => { expect(tokenizer(input)).toEqual(tokens); }); @@ -127,79 +125,87 @@ describe('Query language - AQL', () => { // When a suggestion is clicked, change the input text it.each` - AQL | clikedSuggestion | changedInput - ${''} | ${{type: { iconType: 'kqlField', color: 'tint4' }, label: 'field'}} | ${'field'} - ${'field'} | ${{type: { iconType: 'kqlField', color: 'tint4' }, label: 'field2'}} | ${'field2'} - ${'field'} | ${{type: { iconType: 'kqlOperand', color: 'tint1' }, label: '='}} | ${'field='} - ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value'}} | ${'field=value'} - ${'field='} | ${{type: { iconType: 'kqlOperand', color: 'tint1' }, label: '!='}} | ${'field!='} - ${'field=value'} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value2'}} | ${'field=value2'} - ${'field=value'} | ${{type: { iconType: 'kqlSelector', color: 'tint3' }, label: ';'}} | ${'field=value;'} - ${'field=value;'} | ${{type: { iconType: 'kqlField', color: 'tint4' }, label: 'field2'}} | ${'field=value;field2'} - ${'field=value;field2'} | ${{type: { iconType: 'kqlOperand', color: 'tint1' }, label: '>'}} | ${'field=value;field2>'} - ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with spaces'}} | ${'field=with spaces'} - ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with "spaces'}} | ${'field=with "spaces'} - ${'field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: '"value'}} | ${'field="value'} - ${''} | ${{type: { iconType: 'tokenDenseVector', color: 'tint3' }, label: '('}} | ${'('} - ${'('} | ${{type: { iconType: 'kqlField', color: 'tint4' }, label: 'field'}} | ${'(field'} - ${'(field'} | ${{type: { iconType: 'kqlField', color: 'tint4' }, label: 'field2'}} | ${'(field2'} - ${'(field'} | ${{type: { iconType: 'kqlOperand', color: 'tint1' }, label: '='}} | ${'(field='} - ${'(field='} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value'}} | ${'(field=value'} - ${'(field=value'} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value2'}} | ${'(field=value2'} - ${'(field=value'} | ${{type: { iconType: 'kqlSelector', color: 'tint3' }, label: ','}} | ${'(field=value,'} - ${'(field=value,'} | ${{type: { iconType: 'kqlField', color: 'tint4' }, label: 'field2'}} | ${'(field=value,field2'} - ${'(field=value,field2'} | ${{type: { iconType: 'kqlOperand', color: 'tint1' }, label: '>'}} | ${'(field=value,field2>'} - ${'(field=value,field2>'} | ${{type: { iconType: 'kqlOperand', color: 'tint1' }, label: '>'}} | ${'(field=value,field2>'} - ${'(field=value,field2>'} | ${{type: { iconType: 'kqlOperand', color: 'tint1' }, label: '~'}} | ${'(field=value,field2~'} - ${'(field=value,field2>'} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value2'}} | ${'(field=value,field2>value2'} - ${'(field=value,field2>value2'} | ${{type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value3'}} | ${'(field=value,field2>value3'} - ${'(field=value,field2>value2'} | ${{type: { iconType: 'tokenDenseVector', color: 'tint3' }, label: ')'}} | ${'(field=value,field2>value2)'} - `('click suggestion - AQL $AQL => $changedInput', async ({AQL: currentInput, clikedSuggestion, changedInput}) => { - // Mock input - let input = currentInput; + AQL | clikedSuggestion | changedInput + ${''} | ${{ type: { iconType: 'kqlField', color: 'tint4' }, label: 'field' }} | ${'field'} + ${'field'} | ${{ type: { iconType: 'kqlField', color: 'tint4' }, label: 'field2' }} | ${'field2'} + ${'field'} | ${{ type: { iconType: 'kqlOperand', color: 'tint1' }, label: '=' }} | ${'field='} + ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value' }} | ${'field=value'} + ${'field='} | ${{ type: { iconType: 'kqlOperand', color: 'tint1' }, label: '!=' }} | ${'field!='} + ${'field=value'} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value2' }} | ${'field=value2'} + ${'field=value'} | ${{ type: { iconType: 'kqlSelector', color: 'tint3' }, label: ';' }} | ${'field=value;'} + ${'field=value;'} | ${{ type: { iconType: 'kqlField', color: 'tint4' }, label: 'field2' }} | ${'field=value;field2'} + ${'field=value;field2'} | ${{ type: { iconType: 'kqlOperand', color: 'tint1' }, label: '>' }} | ${'field=value;field2>'} + ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with spaces' }} | ${'field=with spaces'} + ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with "spaces' }} | ${'field=with "spaces'} + ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: '"value' }} | ${'field="value'} + ${''} | ${{ type: { iconType: 'tokenDenseVector', color: 'tint3' }, label: '(' }} | ${'('} + ${'('} | ${{ type: { iconType: 'kqlField', color: 'tint4' }, label: 'field' }} | ${'(field'} + ${'(field'} | ${{ type: { iconType: 'kqlField', color: 'tint4' }, label: 'field2' }} | ${'(field2'} + ${'(field'} | ${{ type: { iconType: 'kqlOperand', color: 'tint1' }, label: '=' }} | ${'(field='} + ${'(field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value' }} | ${'(field=value'} + ${'(field=value'} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value2' }} | ${'(field=value2'} + ${'(field=value'} | ${{ type: { iconType: 'kqlSelector', color: 'tint3' }, label: ',' }} | ${'(field=value,'} + ${'(field=value,'} | ${{ type: { iconType: 'kqlField', color: 'tint4' }, label: 'field2' }} | ${'(field=value,field2'} + ${'(field=value,field2'} | ${{ type: { iconType: 'kqlOperand', color: 'tint1' }, label: '>' }} | ${'(field=value,field2>'} + ${'(field=value,field2>'} | ${{ type: { iconType: 'kqlOperand', color: 'tint1' }, label: '>' }} | ${'(field=value,field2>'} + ${'(field=value,field2>'} | ${{ type: { iconType: 'kqlOperand', color: 'tint1' }, label: '~' }} | ${'(field=value,field2~'} + ${'(field=value,field2>'} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value2' }} | ${'(field=value,field2>value2'} + ${'(field=value,field2>value2'} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value3' }} | ${'(field=value,field2>value3'} + ${'(field=value,field2>value2'} | ${{ type: { iconType: 'tokenDenseVector', color: 'tint3' }, label: ')' }} | ${'(field=value,field2>value2)'} + `( + 'click suggestion - AQL "$AQL" => "$changedInput"', + async ({ AQL: currentInput, clikedSuggestion, changedInput }) => { + // Mock input + let input = currentInput; - const qlOutput = await AQL.run(input, { - setInput: (value: string): void => { input = value; }, - queryLanguage: { - parameters: { - implicitQuery: '', - suggestions: { - field: () => ([]), - value: () => ([]) - } - } - } - }); - qlOutput.searchBarProps.onItemClick(clikedSuggestion); - expect(input).toEqual(changedInput); - }); + const qlOutput = await AQL.run(input, { + setInput: (value: string): void => { + input = value; + }, + queryLanguage: { + parameters: { + implicitQuery: '', + suggestions: { + field: () => [], + value: () => [], + }, + }, + }, + }); + qlOutput.searchBarProps.onItemClick('')(clikedSuggestion); + expect(input).toEqual(changedInput); + }, + ); // Transform the external input in UQL (Unified Query Language) to QL it.each` - UQL | AQL - ${''} | ${''} - ${'field'} | ${'field'} - ${'field='} | ${'field='} - ${'field!='} | ${'field!='} - ${'field>'} | ${'field>'} - ${'field<'} | ${'field<'} - ${'field~'} | ${'field~'} - ${'field=value'} | ${'field=value'} - ${'field=value;'} | ${'field=value;'} - ${'field=value;field2'} | ${'field=value;field2'} - ${'field="'} | ${'field="'} - ${'field=with spaces'} | ${'field=with spaces'} - ${'field=with "spaces'} | ${'field=with "spaces'} - ${'('} | ${'('} - ${'(field'} | ${'(field'} - ${'(field='} | ${'(field='} - ${'(field=value'} | ${'(field=value'} - ${'(field=value,'} | ${'(field=value,'} - ${'(field=value,field2'} | ${'(field=value,field2'} - ${'(field=value,field2>'} | ${'(field=value,field2>'} - ${'(field=value,field2>value2'} | ${'(field=value,field2>value2'} - ${'(field=value,field2>value2)'} | ${'(field=value,field2>value2)'} - `('Transform the external input UQL to QL - UQL $UQL => $AQL', async ({UQL, AQL: changedInput}) => { - expect(AQL.transformUQLToQL(UQL)).toEqual(changedInput); - }); + UQL | AQL + ${''} | ${''} + ${'field'} | ${'field'} + ${'field='} | ${'field='} + ${'field!='} | ${'field!='} + ${'field>'} | ${'field>'} + ${'field<'} | ${'field<'} + ${'field~'} | ${'field~'} + ${'field=value'} | ${'field=value'} + ${'field=value;'} | ${'field=value;'} + ${'field=value;field2'} | ${'field=value;field2'} + ${'field="'} | ${'field="'} + ${'field=with spaces'} | ${'field=with spaces'} + ${'field=with "spaces'} | ${'field=with "spaces'} + ${'('} | ${'('} + ${'(field'} | ${'(field'} + ${'(field='} | ${'(field='} + ${'(field=value'} | ${'(field=value'} + ${'(field=value,'} | ${'(field=value,'} + ${'(field=value,field2'} | ${'(field=value,field2'} + ${'(field=value,field2>'} | ${'(field=value,field2>'} + ${'(field=value,field2>value2'} | ${'(field=value,field2>value2'} + ${'(field=value,field2>value2)'} | ${'(field=value,field2>value2)'} + `( + 'Transform the external input UQL to QL - UQL $UQL => $AQL', + async ({ UQL, AQL: changedInput }) => { + expect(AQL.transformUQLToQL(UQL)).toEqual(changedInput); + }, + ); }); diff --git a/plugins/main/public/components/search-bar/query-language/aql.tsx b/plugins/main/public/components/search-bar/query-language/aql.tsx index 6c383d1dc4..68d1292a23 100644 --- a/plugins/main/public/components/search-bar/query-language/aql.tsx +++ b/plugins/main/public/components/search-bar/query-language/aql.tsx @@ -456,11 +456,13 @@ export const AQL = { await getSuggestions(tokens, params.queryLanguage.parameters), ), // Handler to manage when clicking in a suggestion item - onItemClick: input => item => { + onItemClick: currentInput => item => { // When the clicked item has the `search` iconType, run the `onSearch` function if (item.type.iconType === 'search') { // Execute the search action - params.onSearch(getOutput(input, params.queryLanguage.parameters)); + params.onSearch( + getOutput(currentInput, params.queryLanguage.parameters), + ); } else { // When the clicked item has another iconType const lastToken: IToken = getLastTokenWithValue(tokens); diff --git a/plugins/main/public/components/search-bar/query-language/wql.test.tsx b/plugins/main/public/components/search-bar/query-language/wql.test.tsx index c911516610..2d2a1b3171 100644 --- a/plugins/main/public/components/search-bar/query-language/wql.test.tsx +++ b/plugins/main/public/components/search-bar/query-language/wql.test.tsx @@ -305,7 +305,7 @@ describe('Query language - WQL', () => { ${'(field=value or field2>value2'} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value3' }} | ${'(field=value or field2>value3'} ${'(field=value or field2>value2'} | ${{ type: { iconType: 'tokenDenseVector', color: 'tint3' }, label: ')' }} | ${'(field=value or field2>value2 )'} `( - 'click suggestion - WQL $WQL => $changedInput', + 'click suggestion - WQL "$WQL" => "$changedInput"', async ({ WQL: currentInput, clikedSuggestion, changedInput }) => { // Mock input let input = currentInput; @@ -324,7 +324,7 @@ describe('Query language - WQL', () => { }, }, }); - qlOutput.searchBarProps.onItemClick(clikedSuggestion); + qlOutput.searchBarProps.onItemClick('')(clikedSuggestion); expect(input).toEqual(changedInput); }, ); diff --git a/plugins/main/public/components/search-bar/query-language/wql.tsx b/plugins/main/public/components/search-bar/query-language/wql.tsx index 94af42be42..539f90a076 100644 --- a/plugins/main/public/components/search-bar/query-language/wql.tsx +++ b/plugins/main/public/components/search-bar/query-language/wql.tsx @@ -1072,7 +1072,7 @@ export const WQL = { : await getSuggestions(tokens, params.queryLanguage.parameters), ), // Handler to manage when clicking in a suggestion item - onItemClick: input => item => { + onItemClick: currentInput => item => { // There is an error, clicking on the item does nothing if (item.type.iconType === 'alert') { return; @@ -1081,13 +1081,13 @@ export const WQL = { if (item.type.iconType === 'search') { // Execute the search action // Get the tokens from the input - const tokens: ITokens = tokenizer(input); + const tokens: ITokens = tokenizer(currentInput); const validationStrict = validate(tokens, validators); // Get the output of query language const output = { - ...getOutput(input, params.queryLanguage.parameters), + ...getOutput(currentInput, params.queryLanguage.parameters), error: validationStrict, }; From df8a8d3d9bf7a8796c7dfe24053ce94d893c1eb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Thu, 10 Aug 2023 16:23:24 +0200 Subject: [PATCH 75/76] fix(search-bar): suggestions in Modules > Vulenerabilities > Inventory --- plugins/main/public/components/agents/vuls/inventory/table.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/main/public/components/agents/vuls/inventory/table.tsx b/plugins/main/public/components/agents/vuls/inventory/table.tsx index 345f012675..d3bb882ed3 100644 --- a/plugins/main/public/components/agents/vuls/inventory/table.tsx +++ b/plugins/main/public/components/agents/vuls/inventory/table.tsx @@ -211,7 +211,6 @@ export class InventoryTable extends Component { try { return await getFilterValues( field, - currentValue, agentID, { ...(currentValue ? { q: `${field}~${currentValue}` } : {}), From 0f379aab79901527d80f0e98f1042101a20a1056 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Fri, 11 Aug 2023 14:36:36 +0200 Subject: [PATCH 76/76] fix: type --- .../common/tables/table-with-search-bar.tsx | 148 +++++++++++------- 1 file changed, 90 insertions(+), 58 deletions(-) diff --git a/plugins/main/public/components/common/tables/table-with-search-bar.tsx b/plugins/main/public/components/common/tables/table-with-search-bar.tsx index d8f0df3e7a..1f60167d3a 100644 --- a/plugins/main/public/components/common/tables/table-with-search-bar.tsx +++ b/plugins/main/public/components/common/tables/table-with-search-bar.tsx @@ -18,68 +18,80 @@ import { UI_LOGGER_LEVELS } from '../../../../common/constants'; import { getErrorOrchestrator } from '../../../react-services/common-services'; import { SearchBar, SearchBarProps } from '../../search-bar'; -export interface ITableWithSearcHBarProps{ +export interface ITableWithSearcHBarProps { /** * Function to fetch the data */ onSearch: ( endpoint: string, filters: Record, - pagination: {pageIndex: number, pageSize: number}, - sorting: {sort: {field: string, direction: string}} - ) => Promise<{items: any[], totalItems: number}> + pagination: { pageIndex: number; pageSize: number }, + sorting: { sort: { field: string; direction: string } }, + ) => Promise<{ items: any[]; totalItems: number }>; /** * Properties for the search bar */ - searchBarProps?: Omit + searchBarProps?: Omit< + SearchBarProps, + 'defaultMode' | 'modes' | 'onSearch' | 'input' + >; /** * Columns for the table */ tableColumns: EuiBasicTableProps['columns'] & { - composeField?: string[], - searchable?: string - show?: boolean, - } + composeField?: string[]; + searchable?: string; + show?: boolean; + }; /** * Table row properties for the table */ - rowProps?: EuiBasicTableProps['rowProps'] + rowProps?: EuiBasicTableProps['rowProps']; /** * Table page size options */ - tablePageSizeOptions?: number[] + tablePageSizeOptions?: number[]; /** * Table initial sorting direction */ - tableInitialSortingDirection?: 'asc' | 'dsc' + tableInitialSortingDirection?: 'asc' | 'desc'; /** * Table initial sorting field */ - tableInitialSortingField?: string + tableInitialSortingField?: string; /** * Table properties */ - tableProps?: Omit, 'columns' | 'items' | 'loading' | 'pagination' | 'sorting' | 'onChange' | 'rowProps'> + tableProps?: Omit< + EuiBasicTableProps, + | 'columns' + | 'items' + | 'loading' + | 'pagination' + | 'sorting' + | 'onChange' + | 'rowProps' + >; /** * Refresh the fetch of data */ - reload?: number + reload?: number; /** * API endpoint */ - endpoint: string + endpoint: string; /** * Search bar properties for WQL */ - searchBarWQL?: any + searchBarWQL?: any; /** * Visible fields */ - selectedFields: string[] + selectedFields: string[]; /** * API request filters */ - filters?: any + filters?: any; } export function TableWithSearchBar({ @@ -113,18 +125,24 @@ export function TableWithSearchBar({ const isMounted = useRef(false); - const searchBarWQLOptions = useMemo(() => ({ - searchTermFields: tableColumns - .filter(({field, searchable}) => searchable && rest.selectedFields.includes(field)) - .map(({field, composeField}) => ([composeField || field].flat())) - .flat(), - ...(rest?.searchBarWQL?.options || {}) - }), [rest?.searchBarWQL?.options, rest?.selectedFields]); + const searchBarWQLOptions = useMemo( + () => ({ + searchTermFields: tableColumns + .filter( + ({ field, searchable }) => + searchable && rest.selectedFields.includes(field), + ) + .map(({ field, composeField }) => [composeField || field].flat()) + .flat(), + ...(rest?.searchBarWQL?.options || {}), + }), + [rest?.searchBarWQL?.options, rest?.selectedFields], + ); function updateRefresh() { setPagination({ pageIndex: 0, pageSize: pagination.pageSize }); setRefresh(Date.now()); - }; + } function tableOnChange({ page = {}, sort = {} }) { if (isMounted.current) { @@ -154,31 +172,39 @@ export function TableWithSearchBar({ } }, [endpoint, reload]); - useEffect(function () { - (async () => { - try { - setLoading(true); - const { items, totalItems } = await onSearch(endpoint, filters, pagination, sorting); - setItems(items); - setTotalItems(totalItems); - } catch (error) { - setItems([]); - setTotalItems(0); - const options = { - context: `${TableWithSearchBar.name}.useEffect`, - level: UI_LOGGER_LEVELS.ERROR, - severity: UI_ERROR_SEVERITIES.BUSINESS, - error: { - error: error, - message: error.message || error, - title: `${error.name}: Error fetching items`, - }, - }; - getErrorOrchestrator().handleError(options); - } - setLoading(false); - })(); - }, [filters, pagination, sorting, refresh]); + useEffect( + function () { + (async () => { + try { + setLoading(true); + const { items, totalItems } = await onSearch( + endpoint, + filters, + pagination, + sorting, + ); + setItems(items); + setTotalItems(totalItems); + } catch (error) { + setItems([]); + setTotalItems(0); + const options = { + context: `${TableWithSearchBar.name}.useEffect`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: error.message || error, + title: `${error.name}: Error fetching items`, + }, + }; + getErrorOrchestrator().handleError(options); + } + setLoading(false); + })(); + }, + [filters, pagination, sorting, refresh], + ); useEffect(() => { // This effect is triggered when the component is mounted because of how to the useEffect hook works. @@ -211,20 +237,26 @@ export function TableWithSearchBar({ { id: 'wql', options: searchBarWQLOptions, - ...( rest?.searchBarWQL?.suggestions ? {suggestions: rest.searchBarWQL.suggestions} : {}), - ...( rest?.searchBarWQL?.validate ? {validate: rest.searchBarWQL.validate} : {}) - } + ...(rest?.searchBarWQL?.suggestions + ? { suggestions: rest.searchBarWQL.suggestions } + : {}), + ...(rest?.searchBarWQL?.validate + ? { validate: rest.searchBarWQL.validate } + : {}), + }, ]} input={rest?.filters?.q || ''} - onSearch={({apiQuery}) => { + onSearch={({ apiQuery }) => { // Set the query, reset the page index and update the refresh setFilters(apiQuery); updateRefresh(); }} /> - + ({...rest}))} + columns={tableColumns.map( + ({ searchable, show, composeField, ...rest }) => ({ ...rest }), + )} items={items} loading={loading} pagination={tablePagination}