[8.9] [Security Solution] Add validations to investigation query in markdown editor (#160574) (#160941)

# Backport

This will backport the following commits from `main` to `8.9`:
- [[Security Solution] Add validations to investigation query in
markdown editor
(#160574)](https://github.com/elastic/kibana/pull/160574)

<!--- Backport version: 8.9.7 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT
[{"author":{"name":"christineweng","email":"18648970+christineweng@users.noreply.github.com"},"sourceCommit":{"committedDate":"2023-06-29T18:47:21Z","message":"[Security
Solution] Add validations to investigation query in markdown editor
(#160574)\n\n### Summary\r\n\r\nThis PR adds validations for filters in
investigation guide query.\r\nCurrently validations are not sufficient
on the form:\r\n- User can save query without filter field, operator or
value\r\n(https://github.com/elastic/kibana/issues/153092)\r\n- User can
save query with values in invalid format (i.e. string in date\r\ntime
field)\r\n\r\nValidations added reference checks in unified
search\r\n`kibana/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_editor_utils.ts`\r\n\r\n**How
to test**\r\nThe `Save changes` button should be disabled if:\r\n- Label
of form is empty\r\n- Filter is missing field name, operator type and/or
value\r\n- Filter value is of incorrect format, possible types and
example\r\nfields:\r\n - date (`@timestamp`)\r\n - string
(`host.name`)\r\n - number (`event.risk_score`). \r\n - boolean
(`Target.dll.Ext.code_signature.exists`)\r\n - ip (`host.ip`)\r\n- For
`is one of` and `is not one of` operators, it should not allow\r\nuser
to save if any value specified in the array is invalid\r\n- For range
operators, it should not allow user to save if `from` or\r\n`to` has
invalid value\r\n\r\n###
After\r\n\r\n\r\n\r\nf0424e10-0099-4e02-be00-d8247ab44389\r\n\r\n\r\n\r\n###
Checklist\r\n\r\n- [x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common
scenarios","sha":"ff4559cb7700fdaed7568a1909bab29cb99ba226","branchLabelMapping":{"^v8.10.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:fix","Team:Threat
Hunting","Team: SecuritySolution","Team:Threat
Hunting:Investigations","v8.9.0","v8.10.0"],"number":160574,"url":"https://github.com/elastic/kibana/pull/160574","mergeCommit":{"message":"[Security
Solution] Add validations to investigation query in markdown editor
(#160574)\n\n### Summary\r\n\r\nThis PR adds validations for filters in
investigation guide query.\r\nCurrently validations are not sufficient
on the form:\r\n- User can save query without filter field, operator or
value\r\n(https://github.com/elastic/kibana/issues/153092)\r\n- User can
save query with values in invalid format (i.e. string in date\r\ntime
field)\r\n\r\nValidations added reference checks in unified
search\r\n`kibana/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_editor_utils.ts`\r\n\r\n**How
to test**\r\nThe `Save changes` button should be disabled if:\r\n- Label
of form is empty\r\n- Filter is missing field name, operator type and/or
value\r\n- Filter value is of incorrect format, possible types and
example\r\nfields:\r\n - date (`@timestamp`)\r\n - string
(`host.name`)\r\n - number (`event.risk_score`). \r\n - boolean
(`Target.dll.Ext.code_signature.exists`)\r\n - ip (`host.ip`)\r\n- For
`is one of` and `is not one of` operators, it should not allow\r\nuser
to save if any value specified in the array is invalid\r\n- For range
operators, it should not allow user to save if `from` or\r\n`to` has
invalid value\r\n\r\n###
After\r\n\r\n\r\n\r\nf0424e10-0099-4e02-be00-d8247ab44389\r\n\r\n\r\n\r\n###
Checklist\r\n\r\n- [x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common
scenarios","sha":"ff4559cb7700fdaed7568a1909bab29cb99ba226"}},"sourceBranch":"main","suggestedTargetBranches":["8.9"],"targetPullRequestStates":[{"branch":"8.9","label":"v8.9.0","labelRegex":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v8.10.0","labelRegex":"^v8.10.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/160574","number":160574,"mergeCommit":{"message":"[Security
Solution] Add validations to investigation query in markdown editor
(#160574)\n\n### Summary\r\n\r\nThis PR adds validations for filters in
investigation guide query.\r\nCurrently validations are not sufficient
on the form:\r\n- User can save query without filter field, operator or
value\r\n(https://github.com/elastic/kibana/issues/153092)\r\n- User can
save query with values in invalid format (i.e. string in date\r\ntime
field)\r\n\r\nValidations added reference checks in unified
search\r\n`kibana/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_editor_utils.ts`\r\n\r\n**How
to test**\r\nThe `Save changes` button should be disabled if:\r\n- Label
of form is empty\r\n- Filter is missing field name, operator type and/or
value\r\n- Filter value is of incorrect format, possible types and
example\r\nfields:\r\n - date (`@timestamp`)\r\n - string
(`host.name`)\r\n - number (`event.risk_score`). \r\n - boolean
(`Target.dll.Ext.code_signature.exists`)\r\n - ip (`host.ip`)\r\n- For
`is one of` and `is not one of` operators, it should not allow\r\nuser
to save if any value specified in the array is invalid\r\n- For range
operators, it should not allow user to save if `from` or\r\n`to` has
invalid value\r\n\r\n###
After\r\n\r\n\r\n\r\nf0424e10-0099-4e02-be00-d8247ab44389\r\n\r\n\r\n\r\n###
Checklist\r\n\r\n- [x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common
scenarios","sha":"ff4559cb7700fdaed7568a1909bab29cb99ba226"}}]}]
BACKPORT-->

Co-authored-by: christineweng <18648970+christineweng@users.noreply.github.com>
This commit is contained in:
Kibana Machine 2023-06-29 18:39:46 -04:00 committed by GitHub
parent 9d78849941
commit 9474116570
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 [