mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Case Observables] Implement server side validation (#209706)
## Summary This PR introduces validation rules for Case Observables, shared between client and the server. ### Testing - Create a case - Add on observable to it, picking up the ipv4 as an observable type (for instance) - Verify that only the valid values are allowed. - Try updating the observable after it is created, same validation rules apply. - Do the same thing using API routes.
This commit is contained in:
parent
197a281bf9
commit
529a8573fa
9 changed files with 453 additions and 292 deletions
|
@ -0,0 +1,124 @@
|
|||
/*
|
||||
* 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 { validateDomain, validateEmail, validateGenericValue, validateIp } from './validators';
|
||||
|
||||
describe('validateEmail', () => {
|
||||
it('should return an error if the value is not a string', () => {
|
||||
const result = validateEmail(undefined);
|
||||
|
||||
expect(result).toEqual({
|
||||
code: 'ERR_NOT_STRING',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return an error if the value is not a valid email', () => {
|
||||
const result = validateEmail('invalid-email');
|
||||
|
||||
expect(result).toEqual({
|
||||
code: 'ERR_NOT_EMAIL',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return undefined if the value is a valid email', () => {
|
||||
const result = validateEmail('test@example.com');
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('genericValidator', () => {
|
||||
it('should return an error if the value is not a string', () => {
|
||||
const result = validateGenericValue(123);
|
||||
|
||||
expect(result).toEqual({
|
||||
code: 'ERR_NOT_STRING',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return an error if the value is not valid', () => {
|
||||
const result = validateGenericValue('invalid value!');
|
||||
|
||||
expect(result).toEqual({
|
||||
code: 'ERR_NOT_VALID',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return an error if the value is a json', () => {
|
||||
const result = validateGenericValue('{}');
|
||||
|
||||
expect(result).toEqual({
|
||||
code: 'ERR_NOT_VALID',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return undefined if the value is valid', () => {
|
||||
const result = validateGenericValue('valid_value');
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateDomain', () => {
|
||||
it('should return undefined for a valid domain', () => {
|
||||
const result = validateDomain('example.com');
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return an error for an invalid domain', () => {
|
||||
const result = validateDomain('-invalid.com');
|
||||
|
||||
expect(result).toEqual({
|
||||
code: 'ERR_NOT_VALID',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return an error for hyphen-spaced strings', () => {
|
||||
const result = validateDomain('test-test');
|
||||
|
||||
expect(result).toEqual({
|
||||
code: 'ERR_NOT_VALID',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return an error for a non-string value', () => {
|
||||
const result = validateDomain(12345);
|
||||
|
||||
expect(result).toEqual({
|
||||
code: 'ERR_NOT_STRING',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateIp', () => {
|
||||
it('should return undefined for a valid ipv4', () => {
|
||||
const result = validateIp('ipv4')('127.0.0.1');
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return an error for invalid ipv4', () => {
|
||||
const result = validateIp('ipv4')('invalid ip');
|
||||
|
||||
expect(result).toEqual({
|
||||
code: 'ERR_NOT_VALID',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return undefined for a valid ipv6', () => {
|
||||
const result = validateIp('ipv6')('::1');
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return an error for an invalid ipv6', () => {
|
||||
const result = validateIp('ipv6')('invalid ipv6');
|
||||
expect(result).toEqual({
|
||||
code: 'ERR_NOT_VALID',
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,113 @@
|
|||
/*
|
||||
* 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 ipaddr from 'ipaddr.js';
|
||||
import { parseAddressList } from 'email-addresses';
|
||||
import isString from 'lodash/isString';
|
||||
|
||||
import {
|
||||
OBSERVABLE_TYPE_DOMAIN,
|
||||
OBSERVABLE_TYPE_EMAIL,
|
||||
OBSERVABLE_TYPE_IPV4,
|
||||
OBSERVABLE_TYPE_IPV6,
|
||||
OBSERVABLE_TYPE_URL,
|
||||
} from '../constants';
|
||||
|
||||
const DOMAIN_REGEX = /^(?!-)[A-Za-z0-9-]{1,63}(?<!-)\.[A-Za-z]{2,}$/;
|
||||
const GENERIC_REGEX = /^[a-zA-Z0-9._:/\\]+$/;
|
||||
|
||||
export interface ValidationError {
|
||||
code: string;
|
||||
}
|
||||
|
||||
export type SharedValidationFunction = (value: unknown) => ValidationError | undefined;
|
||||
|
||||
export const createStringValidationFunction =
|
||||
(stringValidator: (value: string) => ValidationError | undefined): SharedValidationFunction =>
|
||||
(value: unknown): ValidationError | undefined => {
|
||||
if (!isString(value)) {
|
||||
return { code: 'ERR_NOT_STRING' };
|
||||
}
|
||||
|
||||
if (!value.length) {
|
||||
return { code: 'ERR_EMPTY' };
|
||||
}
|
||||
|
||||
return stringValidator(value);
|
||||
};
|
||||
|
||||
export const validateDomain = createStringValidationFunction((value) => {
|
||||
return DOMAIN_REGEX.test(value) ? undefined : { code: 'ERR_NOT_VALID' };
|
||||
});
|
||||
export const validateGenericValue = createStringValidationFunction((value) => {
|
||||
return GENERIC_REGEX.test(value) ? undefined : { code: 'ERR_NOT_VALID' };
|
||||
});
|
||||
|
||||
export const validateIp = (kind: 'ipv6' | 'ipv4') =>
|
||||
createStringValidationFunction((value: string) => {
|
||||
try {
|
||||
const parsed = ipaddr.parse(value);
|
||||
|
||||
if (parsed.kind() !== kind) {
|
||||
return {
|
||||
code: 'ERR_NOT_VALID',
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
code: 'ERR_NOT_VALID',
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
export const validateUrl = createStringValidationFunction((value) => {
|
||||
try {
|
||||
new URL(value);
|
||||
} catch (error) {
|
||||
return {
|
||||
code: 'ERR_NOT_VALID',
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
export const validateEmail = createStringValidationFunction((value: string) => {
|
||||
if (parseAddressList(value) === null) {
|
||||
return {
|
||||
code: 'ERR_NOT_EMAIL',
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
export const getValidatorForObservableType = (
|
||||
observableTypeKey: string | undefined
|
||||
): SharedValidationFunction => {
|
||||
switch (observableTypeKey) {
|
||||
case OBSERVABLE_TYPE_URL.key: {
|
||||
return validateUrl;
|
||||
}
|
||||
|
||||
case OBSERVABLE_TYPE_DOMAIN.key: {
|
||||
return validateDomain;
|
||||
}
|
||||
|
||||
case OBSERVABLE_TYPE_EMAIL.key: {
|
||||
return validateEmail;
|
||||
}
|
||||
|
||||
case OBSERVABLE_TYPE_IPV4.key: {
|
||||
return validateIp('ipv4');
|
||||
}
|
||||
|
||||
case OBSERVABLE_TYPE_IPV6.key: {
|
||||
return validateIp('ipv6');
|
||||
}
|
||||
|
||||
default: {
|
||||
return validateGenericValue;
|
||||
}
|
||||
}
|
||||
};
|
|
@ -5,142 +5,32 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { ValidationFunc } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib/types';
|
||||
import { domainValidator, emailValidator, genericValidator, ipv4Validator } from './fields_config';
|
||||
import { normalizeValueType, fieldsConfig } from './fields_config';
|
||||
import { OBSERVABLE_TYPE_DOMAIN } from '../../../common/constants';
|
||||
|
||||
describe('emailValidator', () => {
|
||||
it('should return an error if the value is not a string', () => {
|
||||
const result = emailValidator({
|
||||
value: undefined,
|
||||
path: 'email',
|
||||
} as Parameters<ValidationFunc>[0]);
|
||||
describe('fields_config.ts', () => {
|
||||
describe('normalizeValueType', () => {
|
||||
it('should return the correct value type if it exists', () => {
|
||||
expect(normalizeValueType(OBSERVABLE_TYPE_DOMAIN.key)).toEqual(OBSERVABLE_TYPE_DOMAIN.key);
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
code: 'ERR_NOT_STRING',
|
||||
message: 'Value should be a string',
|
||||
path: 'email',
|
||||
it('should return "generic" if value type does not exist', () => {
|
||||
expect(normalizeValueType('nonExistentKey')).toEqual('generic');
|
||||
});
|
||||
});
|
||||
|
||||
it('should return an error if the value is not a valid email', () => {
|
||||
const result = emailValidator({
|
||||
value: 'invalid-email',
|
||||
path: 'email',
|
||||
} as Parameters<ValidationFunc>[0]);
|
||||
expect(result).toEqual({
|
||||
code: 'ERR_NOT_EMAIL',
|
||||
message: 'Value should be a valid email',
|
||||
path: 'email',
|
||||
describe('fieldsConfig', () => {
|
||||
it('should have correct default values for type key validation', () => {
|
||||
const typeKeyValidations = fieldsConfig.typeKey.validations;
|
||||
|
||||
expect(typeKeyValidations.length).toBe(1);
|
||||
expect(typeKeyValidations[0].validator).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return undefined if the value is a valid email', () => {
|
||||
const result = emailValidator({
|
||||
value: 'test@example.com',
|
||||
path: 'email',
|
||||
} as Parameters<ValidationFunc>[0]);
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
it('should have observable value field types defined', () => {
|
||||
const valueConfigs = fieldsConfig.value;
|
||||
|
||||
describe('genericValidator', () => {
|
||||
it('should return an error if the value is not a string', () => {
|
||||
const result = genericValidator({
|
||||
value: 123,
|
||||
path: 'generic',
|
||||
} as Parameters<ValidationFunc>[0]);
|
||||
expect(result).toEqual({
|
||||
code: 'ERR_NOT_STRING',
|
||||
message: 'Value should be a string',
|
||||
path: 'generic',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return an error if the value is not valid', () => {
|
||||
const result = genericValidator({
|
||||
value: 'invalid value!',
|
||||
path: 'generic',
|
||||
} as Parameters<ValidationFunc>[0]);
|
||||
expect(result).toEqual({
|
||||
code: 'ERR_NOT_VALID',
|
||||
message: 'Value is invalid',
|
||||
path: 'generic',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return undefined if the value is valid', () => {
|
||||
const result = genericValidator({
|
||||
value: 'valid_value',
|
||||
path: 'generic',
|
||||
} as Parameters<ValidationFunc>[0]);
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('domainValidator', () => {
|
||||
it('should return undefined for a valid domain', () => {
|
||||
const result = domainValidator({
|
||||
value: 'example.com',
|
||||
path: 'domain',
|
||||
} as Parameters<ValidationFunc>[0]);
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return an error for an invalid domain', () => {
|
||||
const result = domainValidator({
|
||||
value: '-invalid.com',
|
||||
path: 'domain',
|
||||
} as Parameters<ValidationFunc>[0]);
|
||||
expect(result).toEqual({
|
||||
code: 'ERR_NOT_VALID',
|
||||
message: 'Value is invalid',
|
||||
path: 'domain',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return an error for hyphen-spaced strings', () => {
|
||||
const result = domainValidator({
|
||||
value: 'test-test',
|
||||
path: 'domain',
|
||||
} as Parameters<ValidationFunc>[0]);
|
||||
expect(result).toEqual({
|
||||
code: 'ERR_NOT_VALID',
|
||||
message: 'Value is invalid',
|
||||
path: 'domain',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return an error for a non-string value', () => {
|
||||
const result = domainValidator({
|
||||
value: 12345,
|
||||
path: 'domain',
|
||||
} as Parameters<ValidationFunc>[0]);
|
||||
expect(result).toEqual({
|
||||
code: 'ERR_NOT_STRING',
|
||||
message: 'Value should be a string',
|
||||
path: 'domain',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('ipv4Validator', () => {
|
||||
it('should return undefined for a valid ipv4', () => {
|
||||
const result = ipv4Validator({
|
||||
value: '127.0.0.1',
|
||||
path: 'ipv4',
|
||||
} as Parameters<ValidationFunc>[0]);
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return an error for invalid ipv4', () => {
|
||||
const result = domainValidator({
|
||||
value: 'invalid ip',
|
||||
path: 'ipv4',
|
||||
} as Parameters<ValidationFunc>[0]);
|
||||
expect(result).toEqual({
|
||||
code: 'ERR_NOT_VALID',
|
||||
message: 'Value is invalid',
|
||||
path: 'ipv4',
|
||||
expect(Object.keys(valueConfigs).length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,10 +6,10 @@
|
|||
*/
|
||||
|
||||
import { type ValidationFunc } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
|
||||
import { parseAddressList } from 'email-addresses';
|
||||
import ipaddr from 'ipaddr.js';
|
||||
import { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers';
|
||||
|
||||
import { getValidatorForObservableType } from '../../../common/observables/validators';
|
||||
|
||||
import {
|
||||
OBSERVABLE_TYPE_DOMAIN,
|
||||
OBSERVABLE_TYPE_EMAIL,
|
||||
|
@ -19,182 +19,89 @@ import {
|
|||
} from '../../../common/constants';
|
||||
import * as i18n from './translations';
|
||||
|
||||
export const normalizeValueType = (value: string): keyof typeof fieldsConfig.value | 'generic' => {
|
||||
if (value in fieldsConfig.value) {
|
||||
return value as keyof typeof fieldsConfig.value;
|
||||
const GENERIC_OBSERVABLE_VALUE_TYPE = 'generic' as const;
|
||||
|
||||
export const normalizeValueType = (
|
||||
observableTypeKey: string
|
||||
): keyof typeof fieldsConfig.value | typeof GENERIC_OBSERVABLE_VALUE_TYPE => {
|
||||
if (observableTypeKey in fieldsConfig.value) {
|
||||
return observableTypeKey as keyof typeof fieldsConfig.value;
|
||||
}
|
||||
|
||||
return 'generic';
|
||||
return GENERIC_OBSERVABLE_VALUE_TYPE;
|
||||
};
|
||||
|
||||
const DOMAIN_REGEX = /^(?!-)[A-Za-z0-9-]{1,63}(?<!-)\.[A-Za-z]{2,}$/;
|
||||
const GENERIC_REGEX = /^[a-zA-Z0-9._:/\\]+$/;
|
||||
|
||||
const notStringError = (path: string) => ({
|
||||
code: 'ERR_NOT_STRING',
|
||||
message: 'Value should be a string',
|
||||
path,
|
||||
});
|
||||
|
||||
const { emptyField } = fieldValidators;
|
||||
|
||||
const validatorFactory =
|
||||
(
|
||||
regex: RegExp,
|
||||
message: string = i18n.INVALID_VALUE,
|
||||
code: string = 'ERR_NOT_VALID'
|
||||
): ValidationFunc =>
|
||||
(...args: Parameters<ValidationFunc>) => {
|
||||
const [{ value, path }] = args;
|
||||
interface FieldValidationConfig {
|
||||
validator: ValidationFunc;
|
||||
}
|
||||
|
||||
if (typeof value !== 'string') {
|
||||
return notStringError(path);
|
||||
}
|
||||
const validationsFactory = (
|
||||
observableTypeKey: string | undefined,
|
||||
message: string = i18n.INVALID_VALUE
|
||||
): FieldValidationConfig[] => [
|
||||
{
|
||||
validator: emptyField(i18n.REQUIRED_VALUE),
|
||||
},
|
||||
{
|
||||
validator: (...args: Parameters<ValidationFunc>) => {
|
||||
const [{ value, path }] = args;
|
||||
|
||||
if (!regex.test(value)) {
|
||||
return {
|
||||
code,
|
||||
message,
|
||||
path,
|
||||
};
|
||||
}
|
||||
};
|
||||
const validationResult = getValidatorForObservableType(observableTypeKey)(value);
|
||||
|
||||
export const genericValidator = validatorFactory(GENERIC_REGEX);
|
||||
export const domainValidator = validatorFactory(DOMAIN_REGEX);
|
||||
|
||||
const ipValidatorFactory =
|
||||
(kind: 'ipv6' | 'ipv4') =>
|
||||
(...args: Parameters<ValidationFunc>) => {
|
||||
const [{ value, path }] = args;
|
||||
|
||||
if (typeof value !== 'string') {
|
||||
return notStringError(path);
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = ipaddr.parse(value);
|
||||
|
||||
if (parsed.kind() !== kind) {
|
||||
if (validationResult) {
|
||||
return {
|
||||
code: 'ERR_NOT_VALID',
|
||||
message: i18n.INVALID_VALUE,
|
||||
...validationResult,
|
||||
message,
|
||||
path,
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
code: 'ERR_NOT_VALID',
|
||||
message: i18n.INVALID_VALUE,
|
||||
path,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const ipv6Validator = ipValidatorFactory('ipv6');
|
||||
export const ipv4Validator = ipValidatorFactory('ipv4');
|
||||
|
||||
export const urlValidator = (...args: Parameters<ValidationFunc>) => {
|
||||
const [{ value, path }] = args;
|
||||
|
||||
if (typeof value !== 'string') {
|
||||
return notStringError(path);
|
||||
}
|
||||
|
||||
try {
|
||||
new URL(value);
|
||||
} catch (error) {
|
||||
return {
|
||||
code: 'ERR_NOT_VALID',
|
||||
message: i18n.INVALID_VALUE,
|
||||
path,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const emailValidator = (...args: Parameters<ValidationFunc>) => {
|
||||
const [{ value, path }] = args;
|
||||
|
||||
if (typeof value !== 'string') {
|
||||
return notStringError(path);
|
||||
}
|
||||
|
||||
const emailAddresses = parseAddressList(value);
|
||||
|
||||
if (emailAddresses == null) {
|
||||
return { message: i18n.INVALID_EMAIL, code: 'ERR_NOT_EMAIL', path };
|
||||
}
|
||||
};
|
||||
|
||||
export const fieldsConfig = {
|
||||
value: {
|
||||
generic: {
|
||||
validations: [
|
||||
{
|
||||
validator: emptyField(i18n.REQUIRED_VALUE),
|
||||
},
|
||||
{
|
||||
validator: genericValidator,
|
||||
},
|
||||
],
|
||||
label: i18n.FIELD_LABEL_VALUE,
|
||||
},
|
||||
[OBSERVABLE_TYPE_EMAIL.key]: {
|
||||
validations: [
|
||||
{
|
||||
validator: emptyField(i18n.REQUIRED_VALUE),
|
||||
},
|
||||
{
|
||||
validator: emailValidator,
|
||||
},
|
||||
],
|
||||
label: 'Email',
|
||||
},
|
||||
[OBSERVABLE_TYPE_DOMAIN.key]: {
|
||||
validations: [
|
||||
{
|
||||
validator: emptyField(i18n.REQUIRED_VALUE),
|
||||
},
|
||||
{
|
||||
validator: domainValidator,
|
||||
},
|
||||
],
|
||||
label: 'Domain',
|
||||
},
|
||||
[OBSERVABLE_TYPE_IPV4.key]: {
|
||||
validations: [
|
||||
{
|
||||
validator: emptyField(i18n.REQUIRED_VALUE),
|
||||
},
|
||||
{
|
||||
validator: ipv4Validator,
|
||||
},
|
||||
],
|
||||
label: 'IPv4',
|
||||
},
|
||||
[OBSERVABLE_TYPE_IPV6.key]: {
|
||||
validations: [
|
||||
{
|
||||
validator: emptyField(i18n.REQUIRED_VALUE),
|
||||
},
|
||||
{
|
||||
validator: ipv6Validator,
|
||||
},
|
||||
],
|
||||
label: 'IPv6',
|
||||
},
|
||||
[OBSERVABLE_TYPE_URL.key]: {
|
||||
validations: [
|
||||
{
|
||||
validator: emptyField(i18n.REQUIRED_VALUE),
|
||||
},
|
||||
{
|
||||
validator: urlValidator,
|
||||
},
|
||||
],
|
||||
label: 'URL',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const observableValueFieldTypes = [
|
||||
{
|
||||
key: undefined,
|
||||
label: i18n.FIELD_LABEL_VALUE,
|
||||
},
|
||||
{
|
||||
key: OBSERVABLE_TYPE_EMAIL.key,
|
||||
label: 'Email',
|
||||
},
|
||||
{
|
||||
key: OBSERVABLE_TYPE_DOMAIN.key,
|
||||
label: 'Domain',
|
||||
},
|
||||
{
|
||||
key: OBSERVABLE_TYPE_IPV4.key,
|
||||
label: 'IPv4',
|
||||
},
|
||||
{
|
||||
key: OBSERVABLE_TYPE_IPV6.key,
|
||||
label: 'IPv6',
|
||||
},
|
||||
{
|
||||
key: OBSERVABLE_TYPE_URL.key,
|
||||
label: 'URL',
|
||||
},
|
||||
];
|
||||
|
||||
const fieldsValueConfigsPerObservableType = observableValueFieldTypes.reduce(
|
||||
(fieldsConfig, valueFieldConfig) => {
|
||||
fieldsConfig[valueFieldConfig.key ?? GENERIC_OBSERVABLE_VALUE_TYPE] = {
|
||||
label: valueFieldConfig.label,
|
||||
validations: validationsFactory(valueFieldConfig.key),
|
||||
};
|
||||
|
||||
return fieldsConfig;
|
||||
},
|
||||
{} as Record<string, { label: string; validations: FieldValidationConfig[] }>
|
||||
);
|
||||
|
||||
export const fieldsConfig = {
|
||||
value: fieldsValueConfigsPerObservableType,
|
||||
typeKey: {
|
||||
validations: [
|
||||
{
|
||||
|
|
|
@ -73,6 +73,23 @@ describe('addObservable', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('should throw an error if the value is not valid', async () => {
|
||||
mockLicensingService.isAtLeastPlatinum.mockResolvedValue(true);
|
||||
|
||||
await expect(
|
||||
addObservable(
|
||||
'case-id',
|
||||
{ observable: { typeKey: OBSERVABLE_TYPE_IPV4.key, value: 'not an ip', description: '' } },
|
||||
mockClientArgs,
|
||||
mockCasesClient
|
||||
)
|
||||
).rejects.toThrow(
|
||||
Boom.forbidden(
|
||||
'Failed to add observable: Error: Observable value "not an ip" is not valid for selected observable type observable-type-ipv4.'
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error if observable type is invalid', async () => {
|
||||
mockLicensingService.isAtLeastPlatinum.mockResolvedValue(true);
|
||||
|
||||
|
@ -150,6 +167,28 @@ describe('updateObservable', () => {
|
|||
expect(result).toBeDefined();
|
||||
});
|
||||
|
||||
it('should not update an observable when the provided value is not valid', async () => {
|
||||
mockLicensingService.isAtLeastPlatinum.mockResolvedValue(true);
|
||||
await expect(
|
||||
updateObservable(
|
||||
'case-id',
|
||||
mockObservable.id,
|
||||
{
|
||||
observable: {
|
||||
value: 'not an ip',
|
||||
description: 'Updated description',
|
||||
},
|
||||
},
|
||||
mockClientArgs,
|
||||
mockCasesClient
|
||||
)
|
||||
).rejects.toThrow(
|
||||
Boom.forbidden(
|
||||
'Failed to update observable: Error: Observable value "not an ip" is not valid for selected observable type observable-type-ipv4.'
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error if license is not platinum', async () => {
|
||||
mockLicensingService.isAtLeastPlatinum.mockResolvedValue(false);
|
||||
|
||||
|
|
|
@ -28,6 +28,7 @@ import { LICENSING_CASE_OBSERVABLES_FEATURE } from '../../common/constants';
|
|||
import {
|
||||
validateDuplicatedObservablesInRequest,
|
||||
validateObservableTypeKeyExists,
|
||||
validateObservableValue,
|
||||
} from '../validators';
|
||||
|
||||
const ensureUpdateAuthorized = async (
|
||||
|
@ -76,6 +77,8 @@ export const addObservable = async (
|
|||
observableTypeKey: params.observable.typeKey,
|
||||
});
|
||||
|
||||
validateObservableValue(paramArgs.observable.typeKey, paramArgs.observable.value);
|
||||
|
||||
const currentObservables = retrievedCase.attributes.observables ?? [];
|
||||
|
||||
if (currentObservables.length === MAX_OBSERVABLES_PER_CASE) {
|
||||
|
@ -156,6 +159,11 @@ export const updateObservable = async (
|
|||
throw Boom.notFound(`Failed to update observable: observable id ${observableId} not found`);
|
||||
}
|
||||
|
||||
validateObservableValue(
|
||||
currentObservables[observableIndex].typeKey,
|
||||
paramArgs.observable.value
|
||||
);
|
||||
|
||||
const updatedObservables = [...currentObservables];
|
||||
updatedObservables[observableIndex] = {
|
||||
...updatedObservables[observableIndex],
|
||||
|
|
|
@ -5,13 +5,14 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { OBSERVABLE_TYPE_IPV4 } from '../../common/constants';
|
||||
import { OBSERVABLE_TYPE_EMAIL, OBSERVABLE_TYPE_IPV4 } from '../../common/constants';
|
||||
import { createCasesClientMock } from './mocks';
|
||||
import {
|
||||
validateDuplicatedKeysInRequest,
|
||||
validateDuplicatedObservableTypesInRequest,
|
||||
validateDuplicatedObservablesInRequest,
|
||||
validateObservableTypeKeyExists,
|
||||
validateObservableValue,
|
||||
} from './validators';
|
||||
|
||||
describe('validators', () => {
|
||||
|
@ -220,4 +221,20 @@ describe('validators', () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateObservableValue', () => {
|
||||
it('throws an error if any observable value is not valid', async () => {
|
||||
expect(() =>
|
||||
validateObservableValue(OBSERVABLE_TYPE_EMAIL.key, 'test')
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Observable value \\"test\\" is not valid for selected observable type observable-type-email."`
|
||||
);
|
||||
});
|
||||
|
||||
it('does not throw when obserable value is valid', async () => {
|
||||
expect(() =>
|
||||
validateObservableValue(OBSERVABLE_TYPE_EMAIL.key, 'test@test.com')
|
||||
).not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import Boom from '@hapi/boom';
|
||||
import { getValidatorForObservableType } from '../../common/observables/validators';
|
||||
import { OBSERVABLE_TYPES_BUILTIN } from '../../common/constants';
|
||||
import { type CasesClient } from './client';
|
||||
import { getAvailableObservableTypesMap } from './observable_types';
|
||||
|
@ -127,3 +128,17 @@ export const validateObservableTypeKeyExists = async (
|
|||
throw Boom.badRequest(`Invalid observable type, key does not exist: ${observableTypeKey}`);
|
||||
}
|
||||
};
|
||||
|
||||
export const validateObservableValue = (
|
||||
observableTypeKey: string | undefined,
|
||||
observableValue: unknown
|
||||
) => {
|
||||
const validator = getValidatorForObservableType(observableTypeKey);
|
||||
const validationError = validator(observableValue);
|
||||
|
||||
if (validationError) {
|
||||
throw Boom.badRequest(
|
||||
`Observable value "${observableValue}" is not valid for selected observable type ${observableTypeKey}.`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -53,6 +53,26 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
expect(updatedCase.observables.length).to.be.greaterThan(0);
|
||||
});
|
||||
|
||||
it('can returns bad request when observable value does not pass validation', async () => {
|
||||
const postedCase = await createCase(supertest, getPostCaseRequest());
|
||||
expect(postedCase.observables).to.eql([]);
|
||||
|
||||
const newObservableData = {
|
||||
value: 'not ip actually',
|
||||
typeKey: OBSERVABLE_TYPE_IPV4.key,
|
||||
description: '',
|
||||
};
|
||||
|
||||
await addObservable({
|
||||
supertest,
|
||||
caseId: postedCase.id,
|
||||
params: {
|
||||
observable: newObservableData,
|
||||
},
|
||||
expectedHttpCode: 400,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns bad request when using unknown observable type', async () => {
|
||||
const postedCase = await createCase(supertest, getPostCaseRequest());
|
||||
expect(postedCase.observables).to.eql([]);
|
||||
|
@ -103,6 +123,34 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
|
||||
expect(updatedObservable.observables[0].value).to.be('192.168.68.1');
|
||||
});
|
||||
|
||||
it('returns bad request when observable value does not pass validation', async () => {
|
||||
const postedCase = await createCase(supertest, getPostCaseRequest());
|
||||
|
||||
const newObservableData = {
|
||||
value: '127.0.0.1',
|
||||
typeKey: OBSERVABLE_TYPE_IPV4.key,
|
||||
description: '',
|
||||
};
|
||||
|
||||
const {
|
||||
observables: [observable],
|
||||
} = await addObservable({
|
||||
supertest,
|
||||
caseId: postedCase.id,
|
||||
params: {
|
||||
observable: newObservableData,
|
||||
},
|
||||
});
|
||||
|
||||
await updateObservable({
|
||||
supertest,
|
||||
params: { observable: { description: '', value: 'not ip' } },
|
||||
caseId: postedCase.id,
|
||||
observableId: observable.id as string,
|
||||
expectedHttpCode: 400,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete observable', () => {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue