[Security Solution][Admin][Event Filters] Wildcard warning with IS operator for event filters creation/editing (#178440)

## Summary

- [x] Adds updated warning messaging for trusted apps entries that use
wildcards `*?` with the "IS" operator
- [x] Three different warnings: callout, individual entry item warnings
and a final confirmation modal when the user tries to add an event
filter with ineffective IS / wildcard combination entry.
- [x] Unit tests
- [x] Fixes bug in endpoint exceptions where there is a missing tooltip
icon for the wildcard performance warning

# Screenshots

![image](fd960261-66cc-44f2-b437-bbd9bd3809ab)

![image](da580273-740c-49db-bef9-d79fe0f0ca35)

Bug Fix

![image](8ebbfd83-4286-4ac1-a858-0d390fbfd4b6)
This commit is contained in:
Candace Park 2024-04-16 12:59:02 -04:00 committed by GitHub
parent 0cbf527f08
commit 47c7174bff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 186 additions and 56 deletions

View file

@ -39,6 +39,7 @@ const BOOLEAN_OPTIONS = [
];
const SINGLE_SELECTION = { asPlainText: true };
type Warning = string | React.ReactNode;
interface AutocompleteFieldMatchProps {
placeholder: string;
@ -54,6 +55,8 @@ interface AutocompleteFieldMatchProps {
autocompleteService: AutocompleteStart;
onChange: (arg: string) => void;
onError?: (arg: boolean) => void;
onWarning?: (arg: boolean) => void;
warning?: Warning;
'aria-label'?: string;
}
@ -71,6 +74,8 @@ export const AutocompleteFieldMatchComponent: React.FC<AutocompleteFieldMatchPro
autocompleteService,
onChange,
onError,
onWarning,
warning,
'aria-label': ariaLabel,
}): JSX.Element => {
const [searchQuery, setSearchQuery] = useState('');
@ -122,6 +127,15 @@ export const AutocompleteFieldMatchComponent: React.FC<AutocompleteFieldMatchPro
[setError, onError]
);
const handleWarning = useCallback(
(warn: Warning | undefined): void => {
if (onWarning) {
onWarning(warn !== undefined);
}
},
[onWarning]
);
const { comboOptions, labels, selectedComboOptions } = useMemo(
(): GetGenericComboBoxPropsReturn =>
getGenericComboBoxProps<string>({
@ -148,18 +162,20 @@ export const AutocompleteFieldMatchComponent: React.FC<AutocompleteFieldMatchPro
if (searchVal !== '' && selectedField != null) {
const err = paramIsValid(searchVal, selectedField, isRequired, touched);
handleError(err);
handleWarning(warning);
if (!err) handleSpacesWarning(searchVal);
setSearchQuery(searchVal);
}
},
[handleError, handleSpacesWarning, isRequired, selectedField, touched]
[handleError, handleSpacesWarning, isRequired, selectedField, touched, handleWarning, warning]
);
const handleCreateOption = useCallback(
(option: string): boolean | undefined => {
const err = paramIsValid(option, selectedField, isRequired, touched);
handleError(err);
handleWarning(warning);
if (err != null) {
// Explicitly reject the user's input
@ -171,7 +187,16 @@ export const AutocompleteFieldMatchComponent: React.FC<AutocompleteFieldMatchPro
onChange(option);
return undefined;
},
[isRequired, onChange, selectedField, touched, handleError, handleSpacesWarning]
[
isRequired,
onChange,
selectedField,
touched,
handleError,
handleSpacesWarning,
handleWarning,
warning,
]
);
const handleNonComboBoxInputChange = useCallback(
@ -194,7 +219,8 @@ export const AutocompleteFieldMatchComponent: React.FC<AutocompleteFieldMatchPro
const err = paramIsValid(selectedValue, selectedField, isRequired, true);
handleError(err);
}, [setIsTouched, handleError, selectedValue, selectedField, isRequired]);
handleWarning(warning);
}, [setIsTouched, handleError, selectedValue, selectedField, isRequired, warning, handleWarning]);
const inputPlaceholder = useMemo((): string => {
if (isLoading || isLoadingSuggestions) {
@ -229,7 +255,7 @@ export const AutocompleteFieldMatchComponent: React.FC<AutocompleteFieldMatchPro
isInvalid={selectedField != null && error != null}
data-test-subj="valuesAutocompleteMatchLabel"
fullWidth
helpText={showSpacesWarning && i18n.FIELD_SPACE_WARNING}
helpText={warning || (showSpacesWarning && i18n.FIELD_SPACE_WARNING)}
>
<EuiComboBox
placeholder={inputPlaceholder}
@ -268,6 +294,7 @@ export const AutocompleteFieldMatchComponent: React.FC<AutocompleteFieldMatchPro
handleSearchChange,
handleCreateOption,
setIsTouchedValue,
warning,
fieldInputWidth,
ariaLabel,
]);

View file

@ -11,7 +11,7 @@ import {
hasSimpleExecutableName,
OperatingSystem,
ConditionEntryField,
hasWildcardAndInvalidOperator,
validateHasWildcardWithWrongOperator,
validatePotentialWildcardInput,
validateFilePathInput,
validateWildcardInput,
@ -131,16 +131,24 @@ describe('validateFilePathInput', () => {
describe('Wildcard and invalid operator', () => {
it('should return TRUE when operator is not "WILDCARD" and value contains a wildcard', () => {
expect(hasWildcardAndInvalidOperator({ operator: 'match', value: 'asdf*' })).toEqual(true);
expect(validateHasWildcardWithWrongOperator({ operator: 'match', value: 'asdf*' })).toEqual(
true
);
});
it('should return FALSE when operator is not "WILDCARD" and value does not contain a wildcard', () => {
expect(hasWildcardAndInvalidOperator({ operator: 'match', value: 'asdf' })).toEqual(false);
expect(validateHasWildcardWithWrongOperator({ operator: 'match', value: 'asdf' })).toEqual(
false
);
});
it('should return FALSE when operator is "WILDCARD" and value contains a wildcard', () => {
expect(hasWildcardAndInvalidOperator({ operator: 'wildcard', value: 'asdf*' })).toEqual(false);
expect(validateHasWildcardWithWrongOperator({ operator: 'wildcard', value: 'asdf*' })).toEqual(
false
);
});
it('should return FALSE when operator is "WILDCARD" and value does not contain a wildcard', () => {
expect(hasWildcardAndInvalidOperator({ operator: 'wildcard', value: 'asdf' })).toEqual(false);
expect(validateHasWildcardWithWrongOperator({ operator: 'wildcard', value: 'asdf' })).toEqual(
false
);
});
});

View file

@ -51,6 +51,7 @@ export enum OperatingSystem {
export type EntryTypes = 'match' | 'wildcard' | 'match_any';
export type TrustedAppEntryTypes = Extract<EntryTypes, 'match' | 'wildcard'>;
export type EventFiltersTypes = EntryTypes | 'exists' | 'nested';
export const validatePotentialWildcardInput = ({
field = '',
@ -106,11 +107,11 @@ export const validateWildcardInput = (value?: string): string | undefined => {
}
};
export const hasWildcardAndInvalidOperator = ({
export const validateHasWildcardWithWrongOperator = ({
operator,
value,
}: {
operator: EntryTypes | TrustedAppEntryTypes;
operator: TrustedAppEntryTypes | EventFiltersTypes;
value: string;
}): boolean => {
if (operator !== 'wildcard' && validateWildcardInput(value)) {

View file

@ -15,6 +15,7 @@ import {
EuiIcon,
EuiIconTip,
EuiSpacer,
EuiText,
useEuiPaddingSize,
} from '@elastic/eui';
import styled from 'styled-components';
@ -374,8 +375,21 @@ export const BuilderEntryItem: React.FC<EntryItemProps> = ({
}
};
// show warning when a wildcard is detected with the IS operator
const getWildcardWithIsOperatorWarning = (): React.ReactNode => {
return (
<EuiText size="xs">
<FormattedMessage
id="xpack.lists.exceptions.builder.exceptionIsOperator.warningMessage.incorrectWildCardUsage"
defaultMessage="Change the operator to 'matches' to ensure wildcards run properly."
/>{' '}
<EuiIconTip type="iInCircle" content={i18n.WILDCARD_WITH_IS_OPERATOR_TOOLTIP} />
</EuiText>
);
};
// show this when wildcard with matches operator
const getEventFilterWildcardWarningInfo = (precedingWarning: string): React.ReactNode => {
const getWildcardPerformanceWarningInfo = (precedingWarning: string): React.ReactNode => {
return (
<p>
{precedingWarning}{' '}
@ -404,6 +418,9 @@ export const BuilderEntryItem: React.FC<EntryItemProps> = ({
switch (type) {
case OperatorTypeEnum.MATCH:
const value = typeof entry.value === 'string' ? entry.value : undefined;
const fieldMatchWarning = /[*?]/.test(value ?? '')
? getWildcardWithIsOperatorWarning()
: '';
return (
<AutocompleteFieldMatchComponent
autocompleteService={autocompleteService}
@ -416,6 +433,8 @@ export const BuilderEntryItem: React.FC<EntryItemProps> = ({
isClearable={false}
indexPattern={indexPattern}
onError={handleError}
onWarning={handleWarning}
warning={fieldMatchWarning}
onChange={handleFieldMatchValueChange}
isRequired
data-test-subj="exceptionBuilderEntryFieldMatch"
@ -460,9 +479,7 @@ export const BuilderEntryItem: React.FC<EntryItemProps> = ({
value: wildcardValue,
});
actualWarning =
warning === WILDCARD_WARNING && listType === 'endpoint_events'
? getEventFilterWildcardWarningInfo(warning)
: warning;
warning === WILDCARD_WARNING ? getWildcardPerformanceWarningInfo(warning) : warning;
}
return (

View file

@ -107,3 +107,11 @@ export const CONFLICT_MULTIPLE_INDEX_DESCRIPTION = (name: string, count: number)
defaultMessage: '{name} ({count} indices)',
values: { count, name },
});
export const WILDCARD_WITH_IS_OPERATOR_TOOLTIP = i18n.translate(
'xpack.lists.exceptions.builder.exceptionIsOperator.warningmessage.tooltip',
{
defaultMessage:
'Using a wildcard with the "IS" operator can make an entry ineffective. Change the operator to "MATCHES" to ensure wildcards run properly.',
}
);

View file

@ -193,3 +193,25 @@ export const CONSOLE_COMMANDS = {
},
},
};
export const CONFIRM_WARNING_MODAL_LABELS = {
title: i18n.translate('xpack.securitySolution.artifacts.confirmWarningModal.title', {
defaultMessage: 'Confirm trusted application',
}),
body: i18n.translate('xpack.securitySolution.artifacts.confirmWarningModal.body', {
defaultMessage:
'Using a "*" or a "?" in the value with the "IS" operator can make the entry ineffective. Change the operator to matches to ensure wildcards run properly. Select “cancel” to revise your entry, or "add" to continue with the entry in its current state.',
}),
confirmButton: i18n.translate(
'xpack.securitySolution.artifacts.confirmWarningModal.confirmButtonText',
{
defaultMessage: 'Add',
}
),
cancelButton: i18n.translate(
'xpack.securitySolution.trustedapps.confirmWarningModal.cancelButtonText',
{
defaultMessage: 'Cancel',
}
),
};

View file

@ -28,7 +28,9 @@ import { useWithArtifactSubmitData } from '../../../../components/artifact_list_
import type {
ArtifactFormComponentOnChangeCallbackProps,
ArtifactFormComponentProps,
ArtifactConfirmModalLabelProps,
} from '../../../../components/artifact_list_page/types';
import { ArtifactConfirmModal } from '../../../../components/artifact_list_page/components/artifact_confirm_modal';
import { EventFiltersForm } from './form';
import { getInitialExceptionFromEvent } from '../utils';
@ -72,6 +74,12 @@ export const EventFiltersFlyout: React.FC<EventFiltersFlyoutProps> = memo(
getInitialExceptionFromEvent(data)
);
const [confirmModalLabels, setConfirmModalLabels] = useState<
ArtifactConfirmModalLabelProps | undefined
>();
const [showConfirmModal, setShowConfirmModal] = useState<boolean>(false);
const policiesIsLoading = useMemo<boolean>(
() => policiesRequest.isLoading || policiesRequest.isRefetching,
[policiesRequest]
@ -121,7 +129,7 @@ export const EventFiltersFlyout: React.FC<EventFiltersFlyoutProps> = memo(
onClose();
}, [isSubmittingData, policiesIsLoading, onClose]);
const handleOnSubmit = useCallback(() => {
const submitEventFilter = useCallback(() => {
return submitData(exception, {
onSuccess: (result) => {
toasts.addSuccess(getCreationSuccessMessage(result));
@ -133,6 +141,14 @@ export const EventFiltersFlyout: React.FC<EventFiltersFlyoutProps> = memo(
});
}, [exception, onClose, submitData, toasts]);
const handleOnSubmit = useCallback(() => {
if (confirmModalLabels) {
setShowConfirmModal(true);
} else {
return submitEventFilter();
}
}, [confirmModalLabels, submitEventFilter]);
const confirmButtonMemo = useMemo(
() => (
<EuiButton
@ -165,8 +181,26 @@ export const EventFiltersFlyout: React.FC<EventFiltersFlyoutProps> = memo(
if (!formState) return;
setIsFormValid(formState.isValid);
setException(formState.item);
setConfirmModalLabels(formState.confirmModalLabels);
}, []);
const confirmModal = useMemo(() => {
if (confirmModalLabels) {
const { title, body, confirmButton, cancelButton } = confirmModalLabels;
return (
<ArtifactConfirmModal
title={title}
body={body}
confirmButton={confirmButton}
cancelButton={cancelButton}
onSuccess={submitEventFilter}
onCancel={() => setShowConfirmModal(false)}
data-test-subj="artifactConfirmModal"
/>
);
}
}, [confirmModalLabels, submitEventFilter]);
return (
<EuiFlyout
size="l"
@ -230,6 +264,7 @@ export const EventFiltersFlyout: React.FC<EventFiltersFlyoutProps> = memo(
<EuiFlexItem grow={false}>{confirmButtonMemo}</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutFooter>
{showConfirmModal && confirmModal}
</EuiFlyout>
);
}

View file

@ -474,6 +474,31 @@ describe('Event filter form', () => {
rerender();
expect(renderResult.findByTestId('duplicate-fields-warning-message')).not.toBeNull();
});
it('should not show warning callout when wildcard is used with the "MATCHES" operator', async () => {
formProps.item.entries = [
{
field: 'event.category',
operator: 'included',
type: 'wildcard',
value: 'valuewithwildcard*',
},
];
rerender();
expect(renderResult.queryByTestId('wildcardWithWrongOperatorCallout')).toBeNull();
});
it('should show warning callout when wildcard is used with the "IS" operator', async () => {
formProps.item.entries = [
{
field: 'event.category',
operator: 'included',
type: 'match',
value: 'valuewithwildcard*',
},
];
rerender();
await expect(renderResult.findByTestId('wildcardWithWrongOperatorCallout')).not.toBeNull();
});
});
describe('Errors', () => {

View file

@ -23,7 +23,8 @@ import { FormattedMessage } from '@kbn/i18n-react';
import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types';
import { EVENT_FILTERS_OPERATORS } from '@kbn/securitysolution-list-utils';
import { OperatingSystem } from '@kbn/securitysolution-utils';
import { WildCardWithWrongOperatorCallout } from '@kbn/securitysolution-exception-list-components';
import { OperatingSystem, validateHasWildcardWithWrongOperator } from '@kbn/securitysolution-utils';
import { getExceptionBuilderComponentLazy } from '@kbn/lists-plugin/public';
import type { OnChangeProps } from '@kbn/lists-plugin/public';
@ -55,7 +56,7 @@ import {
OS_LABEL,
RULE_NAME,
} from '../event_filters_list';
import { OS_TITLES } from '../../../../common/translations';
import { OS_TITLES, CONFIRM_WARNING_MODAL_LABELS } from '../../../../common/translations';
import { ENDPOINT_EVENT_FILTERS_LIST_ID, EVENT_FILTER_LIST_TYPE } from '../../constants';
import type { EffectedPolicySelection } from '../../../../components/effected_policy_select';
@ -143,8 +144,9 @@ export const EventFiltersForm: React.FC<ArtifactFormComponentProps & { allowSele
[exception]
);
const [wasByPolicy, setWasByPolicy] = useState(!isGlobalPolicyEffected(exception?.tags));
const [hasDuplicateFields, setHasDuplicateFields] = useState<boolean>(false);
const [hasWildcardWithWrongOperator, setHasWildcardWithWrongOperator] =
useState<boolean>(false);
// This value has to be memoized to avoid infinite useEffect loop on useFetchIndex
const indexNames = useMemo(() => [eventsIndexPattern], []);
const [isIndexPatternLoading, { indexPatterns }] = useFetchIndex(
@ -193,9 +195,12 @@ export const EventFiltersForm: React.FC<ArtifactFormComponentProps & { allowSele
onChange({
item,
isValid: isFormValid && areConditionsValid,
confirmModalLabels: hasWildcardWithWrongOperator
? CONFIRM_WARNING_MODAL_LABELS
: undefined,
});
},
[areConditionsValid, exception, isFormValid, onChange]
[areConditionsValid, exception, isFormValid, onChange, hasWildcardWithWrongOperator]
);
// set initial state of `wasByPolicy` that checks
@ -413,6 +418,19 @@ export const EventFiltersForm: React.FC<ArtifactFormComponentProps & { allowSele
if (!hasFormChanged) setHasFormChanged(true);
return;
}
// handle wildcard with wrong operator case
arg.exceptionItems[0]?.entries.forEach((e) => {
if (
validateHasWildcardWithWrongOperator({
operator: (e as EventFilterItemEntries[number]).type,
value: (e as EventFilterItemEntries[number]).value,
})
) {
setHasWildcardWithWrongOperator(true);
}
});
const updatedItem: Partial<ArtifactFormComponentProps['item']> =
arg.exceptionItems[0] !== undefined
? {
@ -563,13 +581,14 @@ export const EventFiltersForm: React.FC<ArtifactFormComponentProps & { allowSele
{detailsSection}
<EuiHorizontalRule />
{criteriaSection}
{hasWildcardWithWrongOperator && <WildCardWithWrongOperatorCallout />}
{hasDuplicateFields && (
<>
<EuiSpacer size="xs" />
<EuiText color="subdued" size="xs" data-test-subj="duplicate-fields-warning-message">
<FormattedMessage
id="xpack.securitySolution.eventFilters.warningMessage.duplicateFields"
defaultMessage="Using multiples of the same filed values can degrade Endpoint performance and/or create ineffective rules"
defaultMessage="Using multiples of the same field values can degrade Endpoint performance and/or create ineffective rules"
/>
</EuiText>
</>

View file

@ -22,7 +22,7 @@ import {
import type { AllConditionEntryFields, EntryTypes } from '@kbn/securitysolution-utils';
import {
hasSimpleExecutableName,
hasWildcardAndInvalidOperator,
validateHasWildcardWithWrongOperator,
isPathValid,
ConditionEntryField,
OperatingSystem,
@ -58,9 +58,8 @@ import {
NAME_LABEL,
POLICY_SELECT_DESCRIPTION,
SELECT_OS_LABEL,
CONFIRM_WARNING_MODAL_LABELS,
} from '../translations';
import { OS_TITLES } from '../../../../common/translations';
import { OS_TITLES, CONFIRM_WARNING_MODAL_LABELS } from '../../../../common/translations';
import type { LogicalConditionBuilderProps } from './logical_condition';
import { LogicalConditionBuilder } from './logical_condition';
import { useTestIdGenerator } from '../../../../hooks/use_test_id_generator';
@ -166,7 +165,7 @@ const validateValues = (values: ArtifactFormComponentProps['item']): ValidationR
});
if (
hasWildcardAndInvalidOperator({
validateHasWildcardWithWrongOperator({
operator: entry.type as EntryTypes,
value: (entry as TrustedAppConditionEntry).value,
})

View file

@ -165,25 +165,3 @@ export const INPUT_ERRORS = {
values: { row: index + 1 },
}),
};
export const CONFIRM_WARNING_MODAL_LABELS = {
title: i18n.translate('xpack.securitySolution.trustedapps.confirmWarningModal.title', {
defaultMessage: 'Confirm trusted application',
}),
body: i18n.translate('xpack.securitySolution.trustedapps.confirmWarningModal.body', {
defaultMessage:
'Using a "*" or a "?" in the value and with the "IS" operator can make the entry ineffective. Change the operator to matches to ensure wildcards run properly. Select “cancel” to revise your entry, or "add" to continue with entry in its current state.',
}),
confirmButton: i18n.translate(
'xpack.securitySolution.trustedapps.confirmWarningModal.confirmButtonText',
{
defaultMessage: 'Add',
}
),
cancelButton: i18n.translate(
'xpack.securitySolution.trustedapps.confirmWarningModal.cancelButtonText',
{
defaultMessage: 'Cancel',
}
),
};

View file

@ -38122,10 +38122,7 @@
"xpack.securitySolution.trustedApps.cardActionEditLabel": "Modifier une application de confiance",
"xpack.securitySolution.trustedApps.conditions.header": "Conditions",
"xpack.securitySolution.trustedApps.conditions.header.description": "Sélectionnez un système d'exploitation et ajoutez des conditions. La disponibilité des conditions peut dépendre de votre système d'exploitation.",
"xpack.securitySolution.trustedapps.confirmWarningModal.body": "L'utilisation de \"*\" ou de \"?\" dans la valeur avec l'opérateur \"IS\" peut rendre l'entrée inefficace. Remplacez l'opérateur par \"matches\" (correspondances) pour que les caractères génériques soient correctement exécutés. Sélectionnez \"annuler\" pour modifier votre entrée, ou \"ajouter\" pour continuer avec l'entrée dans son état actuel.",
"xpack.securitySolution.trustedapps.confirmWarningModal.cancelButtonText": "Annuler",
"xpack.securitySolution.trustedapps.confirmWarningModal.confirmButtonText": "Ajouter",
"xpack.securitySolution.trustedapps.confirmWarningModal.title": "Confirmer qu'il s'agit d'une application de confiance",
"xpack.securitySolution.trustedapps.create.conditionRequiredMsg": "Au moins une définition de champ est requise",
"xpack.securitySolution.trustedapps.create.description": "Description",
"xpack.securitySolution.trustedapps.create.nameRequiredMsg": "Le nom est obligatoire",

View file

@ -38091,10 +38091,7 @@
"xpack.securitySolution.trustedApps.cardActionEditLabel": "信頼できるアプリケーションを編集",
"xpack.securitySolution.trustedApps.conditions.header": "条件",
"xpack.securitySolution.trustedApps.conditions.header.description": "オペレーティングシステムを選択して、条件を追加します。条件の可用性は選択したOSによって異なる場合があります。",
"xpack.securitySolution.trustedapps.confirmWarningModal.body": "値で*または?を使用して、IS演算子と結合すると、エントリが無効になる場合があります。演算子を「一致」に変更すると、ワイルドカードが確実に正常に動作します。エントリを修正するには、キャンセルを選択します。現在の状態でエントリを続行するには、追加を選択します。",
"xpack.securitySolution.trustedapps.confirmWarningModal.cancelButtonText": "キャンセル",
"xpack.securitySolution.trustedapps.confirmWarningModal.confirmButtonText": "追加",
"xpack.securitySolution.trustedapps.confirmWarningModal.title": "信頼できるアプリケーションを確認",
"xpack.securitySolution.trustedapps.create.conditionRequiredMsg": "1つ以上のフィールド定義が必要です",
"xpack.securitySolution.trustedapps.create.description": "説明",
"xpack.securitySolution.trustedapps.create.nameRequiredMsg": "名前が必要です",

View file

@ -38134,10 +38134,7 @@
"xpack.securitySolution.trustedApps.cardActionEditLabel": "编辑受信任的应用程序",
"xpack.securitySolution.trustedApps.conditions.header": "条件",
"xpack.securitySolution.trustedApps.conditions.header.description": "选择操作系统,然后添加条件。条件的可用性可能取决于您选定的 OS。",
"xpack.securitySolution.trustedapps.confirmWarningModal.body": "在带“IS”运算符的值中使用“*”或“?”可使该条目失效。将运算符更改为“matches”可确保通配符正常运行。选择“取消”可修正您的条目或选择“添加”以继续处理处于其当前状态的条目。",
"xpack.securitySolution.trustedapps.confirmWarningModal.cancelButtonText": "取消",
"xpack.securitySolution.trustedapps.confirmWarningModal.confirmButtonText": "添加",
"xpack.securitySolution.trustedapps.confirmWarningModal.title": "确认受信任的应用程序",
"xpack.securitySolution.trustedapps.create.conditionRequiredMsg": "至少需要一个字段定义",
"xpack.securitySolution.trustedapps.create.description": "描述",
"xpack.securitySolution.trustedapps.create.nameRequiredMsg": "“名称”必填",