[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:
christineweng 2023-06-29 13:47:21 -05:00 committed by GitHub
parent 5a294c6e88
commit ff4559cb77
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 361 additions and 3 deletions

View file

@ -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);
});
});
});

View file

@ -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}`);
}
}

View file

@ -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 [