From 752a2bd943a8478ad5cd2fc6446f101cfadd2133 Mon Sep 17 00:00:00 2001 From: Bohdan Tsymbala Date: Tue, 12 Jan 2021 16:31:35 +0100 Subject: [PATCH] Extracted some parts of server side validation from schema into shared space. Changed the schema structure a bit to avoid casting inside validation code. (#87523) --- .../common/endpoint/schema/trusted_apps.ts | 88 ++++++++----------- .../endpoint/validation/trusted_apps.ts | 29 ++++++ 2 files changed, 67 insertions(+), 50 deletions(-) create mode 100644 x-pack/plugins/security_solution/common/endpoint/validation/trusted_apps.ts diff --git a/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.ts b/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.ts index 59c83242d4a9b..920109e8be7b5 100644 --- a/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.ts +++ b/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.ts @@ -6,13 +6,7 @@ import { schema, Type } from '@kbn/config-schema'; import { ConditionEntry, ConditionEntryField, OperatingSystem } from '../types'; - -const HASH_LENGTHS: readonly number[] = [ - 32, // MD5 - 40, // SHA1 - 64, // SHA256 -]; -const INVALID_CHARACTERS_PATTERN = /[^0-9a-f]/i; +import { getDuplicateFields, isValidHash } from '../validation/trusted_apps'; const entryFieldLabels: { [k in ConditionEntryField]: string } = { [ConditionEntryField.HASH]: 'Hash', @@ -20,9 +14,6 @@ const entryFieldLabels: { [k in ConditionEntryField]: string } = { [ConditionEntryField.SIGNER]: 'Signer', }; -const isValidHash = (value: string) => - HASH_LENGTHS.includes(value.length) && !INVALID_CHARACTERS_PATTERN.test(value); - export const DeleteTrustedAppsRequestSchema = { params: schema.object({ id: schema.string(), @@ -36,61 +27,58 @@ export const GetTrustedAppsRequestSchema = { }), }; -const createNewTrustedAppForOsScheme = ( +const ConditionEntryTypeSchema = schema.literal('match'); +const ConditionEntryOperatorSchema = schema.literal('included'); +const HashConditionEntrySchema = schema.object({ + field: schema.literal(ConditionEntryField.HASH), + type: ConditionEntryTypeSchema, + operator: ConditionEntryOperatorSchema, + value: schema.string({ + validate: (hash) => (isValidHash(hash) ? undefined : `Invalid hash value [${hash}]`), + }), +}); +const PathConditionEntrySchema = schema.object({ + field: schema.literal(ConditionEntryField.PATH), + type: ConditionEntryTypeSchema, + operator: ConditionEntryOperatorSchema, + value: schema.string({ minLength: 1 }), +}); +const SignerConditionEntrySchema = schema.object({ + field: schema.literal(ConditionEntryField.SIGNER), + type: ConditionEntryTypeSchema, + operator: ConditionEntryOperatorSchema, + value: schema.string({ minLength: 1 }), +}); + +const createNewTrustedAppForOsScheme = ( osSchema: Type, - fieldSchema: Type + entriesSchema: Type ) => schema.object({ name: schema.string({ minLength: 1, maxLength: 256 }), description: schema.maybe(schema.string({ minLength: 0, maxLength: 256, defaultValue: '' })), os: osSchema, - entries: schema.arrayOf( - schema.object({ - field: fieldSchema, - type: schema.literal('match'), - operator: schema.literal('included'), - value: schema.string({ minLength: 1 }), - }), - { - minSize: 1, - validate(entries) { - const usedFields = new Set(); - - for (const entry of entries) { - // unfortunately combination of generics and Type<...> for "field" causes type errors - const { field, value } = entry as ConditionEntry; - - if (usedFields.has(field)) { - return `[${entryFieldLabels[field]}] field can only be used once`; - } - - usedFields.add(field); - - if (field === ConditionEntryField.HASH && !isValidHash(value)) { - return `Invalid hash value [${value}]`; - } - } - }, - } - ), + entries: schema.arrayOf(entriesSchema, { + minSize: 1, + validate(entries) { + return ( + getDuplicateFields(entries) + .map((field) => `[${entryFieldLabels[field]}] field can only be used once`) + .join(', ') || undefined + ); + }, + }), }); export const PostTrustedAppCreateRequestSchema = { body: schema.oneOf([ createNewTrustedAppForOsScheme( schema.oneOf([schema.literal(OperatingSystem.LINUX), schema.literal(OperatingSystem.MAC)]), - schema.oneOf([ - schema.literal(ConditionEntryField.HASH), - schema.literal(ConditionEntryField.PATH), - ]) + schema.oneOf([HashConditionEntrySchema, PathConditionEntrySchema]) ), createNewTrustedAppForOsScheme( schema.literal(OperatingSystem.WINDOWS), - schema.oneOf([ - schema.literal(ConditionEntryField.HASH), - schema.literal(ConditionEntryField.PATH), - schema.literal(ConditionEntryField.SIGNER), - ]) + schema.oneOf([HashConditionEntrySchema, PathConditionEntrySchema, SignerConditionEntrySchema]) ), ]), }; diff --git a/x-pack/plugins/security_solution/common/endpoint/validation/trusted_apps.ts b/x-pack/plugins/security_solution/common/endpoint/validation/trusted_apps.ts new file mode 100644 index 0000000000000..55b2355249c22 --- /dev/null +++ b/x-pack/plugins/security_solution/common/endpoint/validation/trusted_apps.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ConditionEntry, ConditionEntryField } from '../types'; + +const HASH_LENGTHS: readonly number[] = [ + 32, // MD5 + 40, // SHA1 + 64, // SHA256 +]; +const INVALID_CHARACTERS_PATTERN = /[^0-9a-f]/i; + +export const isValidHash = (value: string) => + HASH_LENGTHS.includes(value.length) && !INVALID_CHARACTERS_PATTERN.test(value); + +export const getDuplicateFields = (entries: ConditionEntry[]) => { + const groupedFields = new Map(); + + entries.forEach((entry) => { + groupedFields.set(entry.field, [...(groupedFields.get(entry.field) || []), entry]); + }); + + return [...groupedFields.entries()] + .filter((entry) => entry[1].length > 1) + .map((entry) => entry[0]); +};