mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[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:
parent
9d78849941
commit
9474116570
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