mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Security Solution] Add validations to investigation query in markdown editor (#160574)
### Summary
This PR adds validations for filters in investigation guide query.
Currently validations are not sufficient on the form:
- User can save query without filter field, operator or value
(https://github.com/elastic/kibana/issues/153092)
- User can save query with values in invalid format (i.e. string in date
time field)
Validations added reference checks in unified search
`kibana/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_editor_utils.ts`
**How to test**
The `Save changes` button should be disabled if:
- Label of form is empty
- Filter is missing field name, operator type and/or value
- Filter value is of incorrect format, possible types and example
fields:
- date (`@timestamp`)
- string (`host.name`)
- number (`event.risk_score`).
- boolean (`Target.dll.Ext.code_signature.exists`)
- ip (`host.ip`)
- For `is one of` and `is not one of` operators, it should not allow
user to save if any value specified in the array is invalid
- For range operators, it should not allow user to save if `from` or
`to` has invalid value
### After
f0424e10
-0099-4e02-be00-d8247ab44389
### Checklist
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
This commit is contained in:
parent
5a294c6e88
commit
ff4559cb77
3 changed files with 361 additions and 3 deletions
|
@ -0,0 +1,266 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import isSemverValid from 'semver/functions/valid';
|
||||
import { DataViewField } from '@kbn/data-views-plugin/public';
|
||||
import { validateProvider, isProviderValid } from './helpers';
|
||||
import type { Provider } from './use_insight_data_providers';
|
||||
|
||||
const mockDataViewFieldSpec = {
|
||||
name: 'data view',
|
||||
type: 'string',
|
||||
searchable: true,
|
||||
aggregatable: false,
|
||||
};
|
||||
|
||||
const mockProvider: Provider = {
|
||||
field: 'field',
|
||||
excluded: false,
|
||||
queryType: 'phrase',
|
||||
value: 'value',
|
||||
valueType: 'string',
|
||||
};
|
||||
|
||||
describe('Filter validation utils', () => {
|
||||
describe('validateProvider', () => {
|
||||
it('should return false if value or value type is null', () => {
|
||||
const dataViewField = new DataViewField(mockDataViewFieldSpec);
|
||||
expect(validateProvider(dataViewField, '', 'string')).toBe(false);
|
||||
expect(validateProvider(dataViewField, 'text', 'undefined')).toBe(false);
|
||||
expect(validateProvider(dataViewField, undefined)).toBe(false);
|
||||
});
|
||||
|
||||
it('should validate value correctly when type is date', () => {
|
||||
const dataViewField = new DataViewField({ ...mockDataViewFieldSpec, type: 'date' });
|
||||
expect(validateProvider(dataViewField, '45621', 'string')).toBe(true);
|
||||
expect(validateProvider(dataViewField, 'Jan 2022', 'string')).toBe(true);
|
||||
expect(validateProvider(dataViewField, 'Jun 28, 2023 @ 00:00:00.000', 'string')).toBe(true);
|
||||
expect(validateProvider(dataViewField, '4562100000', 'string')).toBe(false);
|
||||
expect(validateProvider(dataViewField, 'date text', 'string')).toBe(false);
|
||||
expect(validateProvider(dataViewField, true, 'boolean')).toBe(false);
|
||||
});
|
||||
|
||||
it('should validate value correctly when type is ip', () => {
|
||||
const dataViewField = new DataViewField({ ...mockDataViewFieldSpec, type: 'ip' });
|
||||
expect(validateProvider(dataViewField, '123.000.000', 'string')).toBe(true);
|
||||
expect(validateProvider(dataViewField, '123215672', 'string')).toBe(true);
|
||||
expect(validateProvider(dataViewField, '123.000', 'string')).toBe(true);
|
||||
expect(validateProvider(dataViewField, 123215672, 'number')).toBe(true);
|
||||
expect(validateProvider(dataViewField, '12345678910', 'string')).toBe(false);
|
||||
expect(validateProvider(dataViewField, 'text', 'string')).toBe(false);
|
||||
expect(validateProvider(dataViewField, true, 'boolean')).toBe(false);
|
||||
});
|
||||
|
||||
it('should validate value correctly when type is string and esType available', () => {
|
||||
const dataViewField = new DataViewField({
|
||||
...mockDataViewFieldSpec,
|
||||
type: 'string',
|
||||
});
|
||||
expect(validateProvider(dataViewField, 'host.name', 'string')).toBe(true);
|
||||
expect(validateProvider(dataViewField, '123', 'string')).toBe(true);
|
||||
expect(validateProvider(dataViewField, true, 'boolean')).toBe(false);
|
||||
expect(validateProvider(dataViewField, 123, 'number')).toBe(false);
|
||||
});
|
||||
|
||||
it('should validate value correctly when type is string and esType has ES type versions', () => {
|
||||
const dataViewField = new DataViewField({
|
||||
...mockDataViewFieldSpec,
|
||||
type: 'string',
|
||||
esTypes: ['keyword', 'version'],
|
||||
});
|
||||
expect(validateProvider(dataViewField, 'host.name', 'string')).toBe(
|
||||
Boolean(isSemverValid('host.name'))
|
||||
);
|
||||
expect(validateProvider(dataViewField, '123', 'string')).toBe(Boolean(isSemverValid('123')));
|
||||
expect(validateProvider(dataViewField, true, 'boolean')).toBe(false);
|
||||
expect(validateProvider(dataViewField, 123, 'number')).toBe(false);
|
||||
});
|
||||
|
||||
it('should validate value correctly when type is boolean', () => {
|
||||
const dataViewField = new DataViewField({
|
||||
...mockDataViewFieldSpec,
|
||||
type: 'boolean',
|
||||
});
|
||||
expect(validateProvider(dataViewField, true, 'boolean')).toBe(true);
|
||||
expect(validateProvider(dataViewField, false, 'boolean')).toBe(true);
|
||||
expect(validateProvider(dataViewField, 'host.name', 'string')).toBe(false);
|
||||
expect(validateProvider(dataViewField, '123', 'string')).toBe(false);
|
||||
expect(validateProvider(dataViewField, 123, 'number')).toBe(false);
|
||||
});
|
||||
|
||||
it('should validate value correctly when type is number', () => {
|
||||
const dataViewField = new DataViewField({
|
||||
...mockDataViewFieldSpec,
|
||||
type: 'number',
|
||||
});
|
||||
expect(validateProvider(dataViewField, 123, 'number')).toBe(true);
|
||||
expect(validateProvider(dataViewField, '123', 'number')).toBe(true);
|
||||
expect(validateProvider(dataViewField, 'host.name', 'string')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isProviderValid', () => {
|
||||
const dataViewField = new DataViewField(mockDataViewFieldSpec);
|
||||
const dataViewFieldHostName = new DataViewField({
|
||||
...mockDataViewFieldSpec,
|
||||
name: 'host.name',
|
||||
type: 'string',
|
||||
});
|
||||
const dataViewFieldHostIP = new DataViewField({
|
||||
...mockDataViewFieldSpec,
|
||||
name: 'host.ip',
|
||||
type: 'ip',
|
||||
});
|
||||
const dataViewFieldCount = new DataViewField({
|
||||
...mockDataViewFieldSpec,
|
||||
name: 'count',
|
||||
type: 'number',
|
||||
});
|
||||
|
||||
const mockPhrasesProvider = { ...mockProvider, queryType: 'phrases' };
|
||||
const mockRangeProvider = { ...mockProvider, queryType: 'range' };
|
||||
const mockExistProvider = { ...mockProvider, queryType: 'exists' };
|
||||
|
||||
it('should return false if dataViewField or field name is empty', () => {
|
||||
expect(isProviderValid(mockProvider, undefined)).toBe(false);
|
||||
expect(isProviderValid({ ...mockProvider, field: '' }, dataViewField)).toBe(false);
|
||||
});
|
||||
|
||||
describe('should validate phrases query correctly', () => {
|
||||
it('should validate string field type correctly', () => {
|
||||
expect(
|
||||
isProviderValid(
|
||||
{
|
||||
...mockPhrasesProvider,
|
||||
field: 'host.name',
|
||||
value: JSON.stringify(['host1', 'host2']),
|
||||
},
|
||||
dataViewFieldHostName
|
||||
)
|
||||
).toBe(true);
|
||||
expect(
|
||||
isProviderValid(
|
||||
{ ...mockPhrasesProvider, field: 'host.name', value: JSON.stringify([]) },
|
||||
dataViewFieldHostName
|
||||
)
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should validate ip field type correctly', () => {
|
||||
expect(
|
||||
isProviderValid(
|
||||
{
|
||||
...mockPhrasesProvider,
|
||||
field: 'host.ip',
|
||||
value: JSON.stringify([123, '123.000.000']),
|
||||
},
|
||||
dataViewFieldHostIP
|
||||
)
|
||||
).toBe(true);
|
||||
expect(
|
||||
isProviderValid(
|
||||
{
|
||||
...mockPhrasesProvider,
|
||||
field: 'host.ip',
|
||||
value: JSON.stringify([123, 'random text']),
|
||||
},
|
||||
dataViewFieldHostIP
|
||||
)
|
||||
).toBe(false);
|
||||
expect(
|
||||
isProviderValid(
|
||||
{ ...mockPhrasesProvider, field: 'host.ip', value: JSON.stringify('123, random text') },
|
||||
dataViewFieldHostIP
|
||||
)
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should validate number field type correctly', () => {
|
||||
expect(
|
||||
isProviderValid(
|
||||
{ ...mockPhrasesProvider, field: 'count', value: JSON.stringify([123, 456]) },
|
||||
dataViewFieldCount
|
||||
)
|
||||
).toBe(true);
|
||||
expect(
|
||||
isProviderValid(
|
||||
{ ...mockPhrasesProvider, field: 'count', value: JSON.stringify('123, 456') },
|
||||
dataViewFieldCount
|
||||
)
|
||||
).toBe(false);
|
||||
expect(
|
||||
isProviderValid(
|
||||
{ ...mockPhrasesProvider, field: 'count', value: JSON.stringify([123, true]) },
|
||||
dataViewFieldCount
|
||||
)
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('should validate range query correctly', () => {
|
||||
it('should return false if value is not object type', () => {
|
||||
expect(
|
||||
isProviderValid(
|
||||
{ ...mockRangeProvider, field: 'host.ip', value: JSON.stringify('gte:120.000') },
|
||||
dataViewFieldHostIP
|
||||
)
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should validate ip field type correctly', () => {
|
||||
expect(
|
||||
isProviderValid(
|
||||
{ ...mockRangeProvider, field: 'host.ip', value: JSON.stringify({ gte: 120.0 }) },
|
||||
dataViewFieldHostIP
|
||||
)
|
||||
).toBe(true);
|
||||
expect(
|
||||
isProviderValid(
|
||||
{
|
||||
...mockRangeProvider,
|
||||
field: 'host.ip',
|
||||
value: JSON.stringify({ gte: 120.0, lt: 'text' }),
|
||||
},
|
||||
dataViewFieldHostIP
|
||||
)
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should validate number field type correctly', () => {
|
||||
expect(
|
||||
isProviderValid(
|
||||
{ ...mockRangeProvider, field: 'count', value: JSON.stringify({ gte: 12, lt: 40 }) },
|
||||
dataViewFieldCount
|
||||
)
|
||||
).toBe(true);
|
||||
expect(
|
||||
isProviderValid(
|
||||
{ ...mockRangeProvider, field: 'count', value: JSON.stringify({}) },
|
||||
dataViewFieldCount
|
||||
)
|
||||
).toBe(true);
|
||||
expect(
|
||||
isProviderValid(
|
||||
{
|
||||
...mockRangeProvider,
|
||||
field: 'count',
|
||||
value: JSON.stringify({ gte: 120, lt: 'text' }),
|
||||
},
|
||||
dataViewFieldCount
|
||||
)
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('should validate exist query correctly', () => {
|
||||
expect(isProviderValid({ ...mockExistProvider, field: 'host.ip' }, dataViewFieldHostIP)).toBe(
|
||||
true
|
||||
);
|
||||
expect(isProviderValid({ ...mockExistProvider, field: '' }, dataViewFieldHostIP)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import dateMath from '@kbn/datemath';
|
||||
import isSemverValid from 'semver/functions/valid';
|
||||
import { IpAddress } from '@kbn/data-plugin/common';
|
||||
import { ES_FIELD_TYPES } from '@kbn/field-types';
|
||||
import type { DataViewField } from '@kbn/data-views-plugin/public';
|
||||
import type { Provider } from './use_insight_data_providers';
|
||||
|
||||
export function validateProvider(
|
||||
dataViewField: DataViewField,
|
||||
value?: string | number | boolean,
|
||||
valueType?: string
|
||||
): boolean {
|
||||
if (value === undefined || valueType === 'undefined') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const fieldType = dataViewField.type;
|
||||
switch (fieldType) {
|
||||
case 'date':
|
||||
const moment = typeof value === 'string' ? dateMath.parse(value) : null;
|
||||
return Boolean(typeof value === 'string' && moment && moment.isValid());
|
||||
case 'ip':
|
||||
if (typeof value === 'string' || typeof value === 'number') {
|
||||
try {
|
||||
return Boolean(new IpAddress(value));
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
case 'string':
|
||||
if (typeof value === 'string' && dataViewField.esTypes?.includes(ES_FIELD_TYPES.VERSION)) {
|
||||
return Boolean(isSemverValid(value));
|
||||
}
|
||||
return typeof value === 'string' && value.trim().length > 0;
|
||||
case 'boolean':
|
||||
return typeof value === 'boolean';
|
||||
case 'number':
|
||||
return typeof value === 'number' || (typeof value === 'string' && !isNaN(parseFloat(value)));
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export function isProviderValid(provider: Provider, dataViewField?: DataViewField): boolean {
|
||||
if (!dataViewField || !provider.field) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (provider.queryType) {
|
||||
case 'phrase':
|
||||
return validateProvider(dataViewField, provider.value, provider.valueType);
|
||||
case 'phrases':
|
||||
const phraseArray =
|
||||
typeof provider.value === 'string' ? JSON.parse(`${provider.value}`) : null;
|
||||
if (!Array.isArray(phraseArray) || !phraseArray.length) {
|
||||
return false;
|
||||
}
|
||||
return phraseArray.every((phrase) =>
|
||||
validateProvider(dataViewField, phrase, provider.valueType)
|
||||
);
|
||||
case 'range':
|
||||
const rangeObject = JSON.parse(typeof provider.value === 'string' ? provider.value : '');
|
||||
if (typeof rangeObject !== 'object') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
(!rangeObject.gte ||
|
||||
validateProvider(dataViewField, rangeObject.gte, provider.valueType)) &&
|
||||
(!rangeObject.lt || validateProvider(dataViewField, rangeObject.lt, provider.valueType))
|
||||
);
|
||||
case 'exists':
|
||||
return true;
|
||||
default:
|
||||
throw new Error(`Unknown operator type: ${provider.queryType}`);
|
||||
}
|
||||
}
|
|
@ -55,6 +55,7 @@ import { useSourcererDataView } from '../../../../containers/sourcerer';
|
|||
import { SourcererScopeName } from '../../../../store/sourcerer/model';
|
||||
import { filtersToInsightProviders } from './provider';
|
||||
import { useLicense } from '../../../../hooks/use_license';
|
||||
import { isProviderValid } from './helpers';
|
||||
import * as i18n from './translations';
|
||||
|
||||
interface InsightComponentProps {
|
||||
|
@ -388,9 +389,16 @@ const InsightEditorComponent = ({
|
|||
[relativeTimerangeController.field]
|
||||
);
|
||||
const disableSubmit = useMemo(() => {
|
||||
const labelOrEmpty = labelController.field.value ? labelController.field.value : '';
|
||||
return labelOrEmpty.trim() === '' || providers.length === 0;
|
||||
}, [labelController.field.value, providers]);
|
||||
const labelOrEmpty = labelController.field.value ?? '';
|
||||
const flattenedProviders = providers.flat();
|
||||
return (
|
||||
labelOrEmpty.trim() === '' ||
|
||||
flattenedProviders.length === 0 ||
|
||||
flattenedProviders.some(
|
||||
(provider) => !isProviderValid(provider, dataView?.getFieldByName(provider.field))
|
||||
)
|
||||
);
|
||||
}, [labelController.field.value, providers, dataView]);
|
||||
const filtersStub = useMemo(() => {
|
||||
const index = indexPattern && indexPattern.getName ? indexPattern.getName() : '*';
|
||||
return [
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue