[SECURITY_SOLUTION] Better BE validation message for multiple same fields (#94935) (#95047)

* WIP: Use schema.conditional instead of schema.oneOf to ensure the right schema validation from an specific field type

* Adds some comments on new schema definition

* Use validate functions to set custom messages

* Fixes type issue after schema changes. An overwrite of the schema inferred type is needed to match with the NewTrustedApp custom type

* Updates schema test after schema changes

* Changes error key by type. Updates related unit test

* WIP: Parse BE message into an user friendly one. Waiting for final texts

* Updates text messages for create trusted app errors

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
David Sánchez 2021-03-22 17:09:18 +01:00 committed by GitHub
parent 0eaaade078
commit f101d67639
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 149 additions and 61 deletions

View file

@ -250,7 +250,9 @@ describe('When invoking Trusted Apps Schema', () => {
const bodyMsg = createNewTrustedApp({
entries: [createConditionEntry(), createConditionEntry()],
});
expect(() => body.validate(bodyMsg)).toThrow('[Path] field can only be used once');
expect(() => body.validate(bodyMsg)).toThrow(
'[entries]: duplicatedEntry.process.executable.caseless'
);
});
it('should validate that `entry.field` hash field value can only be used once', () => {
@ -266,7 +268,7 @@ describe('When invoking Trusted Apps Schema', () => {
}),
],
});
expect(() => body.validate(bodyMsg)).toThrow('[Hash] field can only be used once');
expect(() => body.validate(bodyMsg)).toThrow('[entries]: duplicatedEntry.process.hash.*');
});
it('should validate that `entry.field` signer field value can only be used once', () => {
@ -282,7 +284,9 @@ describe('When invoking Trusted Apps Schema', () => {
}),
],
});
expect(() => body.validate(bodyMsg)).toThrow('[Signer] field can only be used once');
expect(() => body.validate(bodyMsg)).toThrow(
'[entries]: duplicatedEntry.process.Ext.code_signature'
);
});
it('should validate Hash field valid value', () => {

View file

@ -5,16 +5,10 @@
* 2.0.
*/
import { schema, Type } from '@kbn/config-schema';
import { ConditionEntry, ConditionEntryField, OperatingSystem } from '../types';
import { schema } from '@kbn/config-schema';
import { ConditionEntryField, OperatingSystem } from '../types';
import { getDuplicateFields, isValidHash } from '../validation/trusted_apps';
const entryFieldLabels: { [k in ConditionEntryField]: string } = {
[ConditionEntryField.HASH]: 'Hash',
[ConditionEntryField.PATH]: 'Path',
[ConditionEntryField.SIGNER]: 'Signer',
};
export const DeleteTrustedAppsRequestSchema = {
params: schema.object({
id: schema.string(),
@ -30,56 +24,99 @@ export const GetTrustedAppsRequestSchema = {
const ConditionEntryTypeSchema = schema.literal('match');
const ConditionEntryOperatorSchema = schema.literal('included');
const HashConditionEntrySchema = schema.object({
field: schema.literal(ConditionEntryField.HASH),
/*
* A generic Entry schema to be used for a specific entry schema depending on the OS
*/
const CommonEntrySchema = {
field: schema.oneOf([
schema.literal(ConditionEntryField.HASH),
schema.literal(ConditionEntryField.PATH),
]),
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 }),
// If field === HASH then validate hash with custom method, else validate string with minLength = 1
value: schema.conditional(
schema.siblingRef('field'),
ConditionEntryField.HASH,
schema.string({
validate: (hash) =>
isValidHash(hash) ? undefined : `invalidField.${ConditionEntryField.HASH}`,
}),
schema.conditional(
schema.siblingRef('field'),
ConditionEntryField.PATH,
schema.string({
validate: (field) =>
field.length > 0 ? undefined : `invalidField.${ConditionEntryField.PATH}`,
}),
schema.string({
validate: (field) =>
field.length > 0 ? undefined : `invalidField.${ConditionEntryField.SIGNER}`,
})
)
),
};
const WindowsEntrySchema = schema.object({
...CommonEntrySchema,
field: schema.oneOf([
schema.literal(ConditionEntryField.HASH),
schema.literal(ConditionEntryField.PATH),
schema.literal(ConditionEntryField.SIGNER),
]),
});
const createNewTrustedAppForOsScheme = <O extends OperatingSystem, E extends ConditionEntry>(
osSchema: Type<O>,
entriesSchema: Type<E>
) =>
schema.object({
name: schema.string({ minLength: 1, maxLength: 256 }),
description: schema.maybe(schema.string({ minLength: 0, maxLength: 256, defaultValue: '' })),
os: osSchema,
entries: schema.arrayOf(entriesSchema, {
minSize: 1,
validate(entries) {
return (
getDuplicateFields(entries)
.map((field) => `[${entryFieldLabels[field]}] field can only be used once`)
.join(', ') || undefined
);
},
}),
});
const LinuxEntrySchema = schema.object({
...CommonEntrySchema,
});
const MacEntrySchema = schema.object({
...CommonEntrySchema,
});
/*
* Entry Schema depending on Os type using schema.conditional.
* If OS === WINDOWS then use Windows schema,
* else if OS === LINUX then use Linux schema,
* else use Mac schema
*/
const EntrySchemaDependingOnOS = schema.conditional(
schema.siblingRef('os'),
OperatingSystem.WINDOWS,
WindowsEntrySchema,
schema.conditional(
schema.siblingRef('os'),
OperatingSystem.LINUX,
LinuxEntrySchema,
MacEntrySchema
)
);
/*
* Entities array schema.
* The validate function checks there is no duplicated entry inside the array
*/
const EntriesSchema = schema.arrayOf(EntrySchemaDependingOnOS, {
minSize: 1,
validate(entries) {
return (
getDuplicateFields(entries)
.map((field) => `duplicatedEntry.${field}`)
.join(', ') || undefined
);
},
});
export const PostTrustedAppCreateRequestSchema = {
body: schema.oneOf([
createNewTrustedAppForOsScheme(
schema.oneOf([schema.literal(OperatingSystem.LINUX), schema.literal(OperatingSystem.MAC)]),
schema.oneOf([HashConditionEntrySchema, PathConditionEntrySchema])
),
createNewTrustedAppForOsScheme(
body: schema.object({
name: schema.string({ minLength: 1, maxLength: 256 }),
description: schema.maybe(schema.string({ minLength: 0, maxLength: 256, defaultValue: '' })),
os: schema.oneOf([
schema.literal(OperatingSystem.WINDOWS),
schema.oneOf([HashConditionEntrySchema, PathConditionEntrySchema, SignerConditionEntrySchema])
),
]),
schema.literal(OperatingSystem.LINUX),
schema.literal(OperatingSystem.MAC),
]),
entries: EntriesSchema,
}),
};

View file

@ -27,8 +27,13 @@ export interface GetTrustedListAppsResponse {
data: TrustedApp[];
}
/** API Request body for creating a new Trusted App entry */
export type PostTrustedAppCreateRequest = TypeOf<typeof PostTrustedAppCreateRequestSchema.body>;
/*
* API Request body for creating a new Trusted App entry
* As this is an inferred type and the schema type doesn't match at all with the
* NewTrustedApp type it needs and overwrite from the MacosLinux/Windows custom types
*/
export type PostTrustedAppCreateRequest = TypeOf<typeof PostTrustedAppCreateRequestSchema.body> &
(MacosLinuxConditionEntries | WindowsConditionEntries);
export interface PostTrustedAppCreateResponse {
data: TrustedApp;

View file

@ -18,7 +18,7 @@ import {
EuiText,
EuiTitle,
} from '@elastic/eui';
import React, { memo, useCallback, useEffect } from 'react';
import React, { memo, useCallback, useEffect, useMemo } from 'react';
import { EuiFlyoutProps } from '@elastic/eui/src/components/flyout/flyout';
import { FormattedMessage } from '@kbn/i18n/react';
import { useDispatch } from 'react-redux';
@ -31,7 +31,7 @@ import {
} from '../../store/selectors';
import { AppAction } from '../../../../../common/store/actions';
import { useTrustedAppsSelector } from '../hooks';
import { ABOUT_TRUSTED_APPS } from '../translations';
import { ABOUT_TRUSTED_APPS, CREATE_TRUSTED_APP_ERROR } from '../translations';
type CreateTrustedAppFlyoutProps = Omit<EuiFlyoutProps, 'hideCloseButton'>;
export const CreateTrustedAppFlyout = memo<CreateTrustedAppFlyoutProps>(
@ -45,6 +45,15 @@ export const CreateTrustedAppFlyout = memo<CreateTrustedAppFlyoutProps>(
const dataTestSubj = flyoutProps['data-test-subj'];
const creationErrorsMessage = useMemo<string | undefined>(
() =>
creationErrors
? CREATE_TRUSTED_APP_ERROR[creationErrors.message.replace(/(\[(.*)\]\: )/, '')] ||
creationErrors.message
: undefined,
[creationErrors]
);
const getTestId = useCallback(
(suffix: string): string | undefined => {
if (dataTestSubj) {
@ -102,7 +111,7 @@ export const CreateTrustedAppFlyout = memo<CreateTrustedAppFlyoutProps>(
fullWidth
onChange={handleFormOnChange}
isInvalid={!!creationErrors}
error={creationErrors?.message}
error={creationErrorsMessage}
data-test-subj={getTestId('createForm')}
/>
</EuiFlyoutBody>

View file

@ -137,3 +137,36 @@ export const LIST_VIEW_TOGGLE_LABEL = i18n.translate(
export const NO_RESULTS_MESSAGE = i18n.translate('xpack.securitySolution.trustedapps.noResults', {
defaultMessage: 'No items found',
});
export const CREATE_TRUSTED_APP_ERROR: { [K in string]: string } = {
[`duplicatedEntry.${ConditionEntryField.HASH}`]: i18n.translate(
'xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.field.error.duplicated.hash',
{ defaultMessage: 'Hash value can only be used once. Please enter a single valid hash.' }
),
[`duplicatedEntry.${ConditionEntryField.PATH}`]: i18n.translate(
'xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.field.error.duplicated.path',
{ defaultMessage: 'Path value can only be used once. Please enter a single valid path.' }
),
[`duplicatedEntry.${ConditionEntryField.SIGNER}`]: i18n.translate(
'xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.field.error.duplicated.signature',
{
defaultMessage:
'Signature value can only be used once. Please enter a single valid signature.',
}
),
[`invalidField.${ConditionEntryField.HASH}`]: i18n.translate(
'xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.field.error.invalid.hash',
{
defaultMessage:
'An invalid Hash was entered. Please enter in a valid Hash (md5, sha1, or sha256).',
}
),
[`invalidField.${ConditionEntryField.PATH}`]: i18n.translate(
'xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.field.error.invalid.path',
{ defaultMessage: 'An invalid Path was entered. Please enter in a valid Path.' }
),
[`invalidField.${ConditionEntryField.SIGNER}`]: i18n.translate(
'xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.field.error.invalid.signature',
{ defaultMessage: 'An invalid Signature was entered. Please enter in a valid Signature.' }
),
};