[Security Solution] Update prebuilt rule customization UI copy (#210817)

**Resolves: https://github.com/elastic/security-docs/issues/6238**
**Deployed here:
[link](https://nikitaindik-pr-210817-prebuilt-rule-customization-update-ui.kbndev.co/app/security/rules/updates?rulesTable=(field:name,order:asc,searchTerm:'Shared%20Object%20Created%20or%20Changed%20by%20Previously%20Unknown%20Process')&sourcerer=(default:(id:security-solution-default,selectedPatterns:!()))&timerange=(global:(linkTo:!(timeline),timerange:(from:'2025-02-11T23:00:00.000Z',fromStr:now/d,kind:absolute,to:'2025-02-12T22:59:59.999Z',toStr:now/d)),timeline:(linkTo:!(global),timerange:(from:'2025-02-11T23:00:00.000Z',fromStr:now/d,kind:absolute,to:'2025-02-12T22:59:59.999Z',toStr:now/d)))&timeline=(activeTab:query,graphEventId:'',isOpen:!f))**

> ⚠️ CI fails are caused by an issue unrelated to this PR

## Summary

Changes in this PR:
- UI copy is updated in accordance with
[recommendations](https://docs.google.com/document/d/1Yl6DyN9pertqgB-iIKIEN3xdvlDM50oscJ00G-WwtyA/edit?tab=t.0)
(internal link) from Security Documentation team
- Text color for "No update" fields in upgrade flyout changed from green
to default.
- Fixed a minor bug with placeholder not displaying for "Setup guide"
and "Investigation guide" fields on Rule Creation/Editing page


<details>
<summary><strong>A few screenshots taken in Serverless</strong> (click
to expand)</summary>

<img width="523" alt="serverless_rep_tooltip"
src="https://github.com/user-attachments/assets/825e1514-a191-45c2-90ca-0f15a8c9da7b"
/>
  
<img width="836" alt="serverless_bulk_action_error"
src="https://github.com/user-attachments/assets/8aa38c77-5aaa-49cf-9b4e-8c992382a1d2"
/>
  
<img width="1102" alt="serverless_upgrade_callout"
src="https://github.com/user-attachments/assets/cf947c73-d52d-4c85-abd6-369f616b8421"
/>
  
<img width="1004" alt="no_update_white"
src="https://github.com/user-attachments/assets/f720f24c-0c97-432f-b2d5-7ff7e5919ba0"
/>



</details>

## Testing
You can use [this
deployment](https://nikitaindik-pr-210817-prebuilt-rule-customization-update-ui.kbndev.co)
(default credentials) test to changes on ESS Enterprise license.
Here's a couple rules that has field updates of different kinds:
 - Unusual User Privilege Enumeration via id
 - Shared Object Created or Changed by Previously Unknown Process

To test on Serverless or with other licenses, you'll need to run it
locally. Reach out to me if you need help with this.

Work started: 11-Feb-2025
This commit is contained in:
Nikita Indik 2025-02-20 11:17:57 +01:00 committed by GitHub
parent 1f35d7ac7f
commit 994201ce87
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
34 changed files with 225 additions and 153 deletions

View file

@ -34935,8 +34935,6 @@
"xpack.securitySolution.detectionEngine.createRule.stepAboutRule.guideLabel": "Guide d'investigation",
"xpack.securitySolution.detectionEngine.createRule.stepAboutRule.maxAlertsFieldLessThanWarning": "Kibana ne permet qu'un maximum de {maxNumber} {maxNumber, plural, =1 {alerte} other {alertes}} par exécution de règle.",
"xpack.securitySolution.detectionEngine.createRule.stepAboutRule.nameFieldRequiredError": "Nom obligatoire.",
"xpack.securitySolution.detectionEngine.createRule.stepAboutrule.noteHelpText": "Ajouter un guide d'investigation sur les règles...",
"xpack.securitySolution.detectionEngine.createRule.stepAboutrule.setupHelpText": "Ajouter le guide de configuration de règle...",
"xpack.securitySolution.detectionEngine.createRule.stepAboutRule.setupHelpText": "Fournissez des instructions sur les conditions préalables à la règle, telles que les intégrations requises, les étapes de configuration et tout ce qui est nécessaire au bon fonctionnement de la règle.",
"xpack.securitySolution.detectionEngine.createRule.stepAboutRule.setupLabel": "Guide de configuration",
"xpack.securitySolution.detectionEngine.createRule.stepAboutRule.tagFieldEmptyError": "Une balise ne doit pas être vide",

View file

@ -34796,8 +34796,6 @@
"xpack.securitySolution.detectionEngine.createRule.stepAboutRule.guideLabel": "調査ガイド",
"xpack.securitySolution.detectionEngine.createRule.stepAboutRule.maxAlertsFieldLessThanWarning": "Kibanaで許可される最大数は、1回の実行につき、{maxNumber} {maxNumber, plural, other {アラート}}です。",
"xpack.securitySolution.detectionEngine.createRule.stepAboutRule.nameFieldRequiredError": "名前が必要です。",
"xpack.securitySolution.detectionEngine.createRule.stepAboutrule.noteHelpText": "ルール調査ガイドを追加...",
"xpack.securitySolution.detectionEngine.createRule.stepAboutrule.setupHelpText": "ルールセットアップガイドを追加...",
"xpack.securitySolution.detectionEngine.createRule.stepAboutRule.setupHelpText": "必要な統合、構成ステップ、ルールが正常に動作するために必要な他のすべての項目といった、ルール前提条件に関する指示を入力します。",
"xpack.securitySolution.detectionEngine.createRule.stepAboutRule.setupLabel": "セットアップガイド",
"xpack.securitySolution.detectionEngine.createRule.stepAboutRule.tagFieldEmptyError": "タグを空にすることはできません",

View file

@ -34266,8 +34266,6 @@
"xpack.securitySolution.detectionEngine.createRule.stepAboutRule.guideLabel": "调查指南",
"xpack.securitySolution.detectionEngine.createRule.stepAboutRule.maxAlertsFieldLessThanWarning": "每次规则运行时Kibana 最多只允许 {maxNumber} 个{maxNumber, plural, other {告警}}。",
"xpack.securitySolution.detectionEngine.createRule.stepAboutRule.nameFieldRequiredError": "名称必填。",
"xpack.securitySolution.detectionEngine.createRule.stepAboutrule.noteHelpText": "添加规则调查指南......",
"xpack.securitySolution.detectionEngine.createRule.stepAboutrule.setupHelpText": "添加规则设置指南......",
"xpack.securitySolution.detectionEngine.createRule.stepAboutRule.setupHelpText": "提供有关规则先决条件的说明,如所需集成、配置步骤,以及规则正常运行所需的任何其他内容。",
"xpack.securitySolution.detectionEngine.createRule.stepAboutRule.setupLabel": "设置指南",
"xpack.securitySolution.detectionEngine.createRule.stepAboutRule.tagFieldEmptyError": "标签不得为空",

View file

@ -55,10 +55,27 @@ export const UPGRADE_NOTES_MANAGEMENT_USER_FILTER = (requiredLicense: string) =>
},
});
export const UPGRADE_PREBUILT_RULE_CUSTOMIZATION = (requiredLicense: string) =>
export const PREBUILT_RULE_CUSTOMIZATION = (requiredLicense: string, licenseKind: string) =>
i18n.translate('securitySolutionPackages.ruleManagement.prebuiltRuleCustomization.upsell', {
defaultMessage: 'Upgrade to {requiredLicense} to enable prebuilt rule customization',
defaultMessage: '{requiredLicense} {licenseKind} is required to customize prebuilt rules',
values: {
requiredLicense,
licenseKind,
},
});
export const PREBUILT_RULE_CUSTOMIZATION_DESCRIPTION = (
requiredLicense: string,
licenseKind: string
) =>
i18n.translate(
'securitySolutionPackages.ruleManagement.prebuiltRuleCustomization.descriptionUpsell',
{
defaultMessage:
"Without the {requiredLicense} {licenseKind}, prebuilt rules can't be customized. To access this feature, upgrade your {licenseKind} or contact your admin for assistance.",
values: {
requiredLicense,
licenseKind,
},
}
);

View file

@ -29,4 +29,5 @@ export type UpsellingMessageId =
| 'alert_suppression_rule_form'
| 'alert_suppression_rule_details'
| 'note_management_user_filter'
| 'prebuilt_rule_customization';
| 'prebuilt_rule_customization'
| 'prebuilt_rule_customization_description';

View file

@ -34,6 +34,7 @@ interface MarkdownEditorProps {
autoFocusDisabled?: boolean;
setIsMarkdownInvalid: (value: boolean) => void;
includePlugins?: boolean;
placeholder?: string;
}
type EuiMarkdownEditorRef = ElementRef<typeof EuiMarkdownEditor>;
@ -56,6 +57,7 @@ const MarkdownEditorComponent = forwardRef<MarkdownEditorRef, MarkdownEditorProp
autoFocusDisabled,
setIsMarkdownInvalid,
includePlugins = true,
placeholder,
},
ref
) => {
@ -118,6 +120,7 @@ const MarkdownEditorComponent = forwardRef<MarkdownEditorRef, MarkdownEditorProp
errors={markdownErrorMessages}
data-test-subj={dataTestSubj}
height={height}
placeholder={placeholder}
/>
);
}

View file

@ -25,7 +25,7 @@ type MarkdownEditorFormProps = EuiMarkdownEditorProps & {
export const MarkdownEditorForm = React.memo(
forwardRef<MarkdownEditorRef, MarkdownEditorFormProps>(
({ field, dataTestSubj, idAria, includePlugins }, ref) => {
({ field, dataTestSubj, idAria, includePlugins, placeholder }, ref) => {
const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field);
const [isMarkdownInvalid, setIsMarkdownInvalid] = useState(false);
@ -48,6 +48,7 @@ export const MarkdownEditorForm = React.memo(
data-test-subj={`${dataTestSubj}-markdown-editor`}
setIsMarkdownInvalid={setIsMarkdownInvalid}
includePlugins={includePlugins}
placeholder={placeholder}
/>
</EuiFormRow>
);

View file

@ -196,7 +196,7 @@ export const BUILDING_BLOCK_LABEL = i18n.translate(
export const BUILDING_BLOCK_DESCRIPTION = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleDescription.buildingBlockDescription',
{
defaultMessage: 'All generated alerts will be marked as "building block" alerts',
defaultMessage: 'All generated alerts will be marked as building block alerts',
}
);

View file

@ -284,7 +284,7 @@ const StepAboutRuleComponent: FC<StepAboutRuleProps> = ({
<EuiToolTip
content={
ruleSource?.type === 'external'
? I18n.AUTHOR_IMMUTABLE_FIELD_TOOLTIP_TEXT
? I18n.FIELD_NOT_EDITABLE_TOOLTIP_TEXT(I18n.AUTHOR_FIELD_LABEL)
: undefined
}
display="block"
@ -307,7 +307,7 @@ const StepAboutRuleComponent: FC<StepAboutRuleProps> = ({
<EuiToolTip
content={
ruleSource?.type === 'external'
? I18n.LICENSE_IMMUTABLE_FIELD_TOOLTIP_TEXT
? I18n.FIELD_NOT_EDITABLE_TOOLTIP_TEXT(I18n.LICENSE_FIELD_LABEL)
: undefined
}
display="block"

View file

@ -29,12 +29,7 @@ const { emptyField } = fieldValidators;
export const schema: FormSchema<AboutStepRule> = {
author: {
type: FIELD_TYPES.COMBO_BOX,
label: i18n.translate(
'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldAuthorLabel',
{
defaultMessage: 'Author',
}
),
label: I18n.AUTHOR_FIELD_LABEL,
helpText: i18n.translate(
'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldAuthorHelpText',
{
@ -209,12 +204,7 @@ export const schema: FormSchema<AboutStepRule> = {
},
license: {
type: FIELD_TYPES.TEXT,
label: i18n.translate(
'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldLicenseLabel',
{
defaultMessage: 'License',
}
),
label: I18n.LICENSE_FIELD_LABEL,
helpText: i18n.translate(
'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldLicenseHelpText',
{

View file

@ -85,29 +85,40 @@ export const URL_FORMAT_INVALID = i18n.translate(
);
export const ADD_RULE_NOTE_HELP_TEXT = i18n.translate(
'xpack.securitySolution.detectionEngine.createRule.stepAboutrule.noteHelpText',
'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.noteHelpText',
{
defaultMessage: 'Add rule investigation guide...',
}
);
export const ADD_RULE_SETUP_HELP_TEXT = i18n.translate(
'xpack.securitySolution.detectionEngine.createRule.stepAboutrule.setupHelpText',
'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.addRuleSetupHelpText',
{
defaultMessage: 'Add rule setup guide...',
}
);
export const AUTHOR_IMMUTABLE_FIELD_TOOLTIP_TEXT = i18n.translate(
'xpack.securitySolution.detectionEngine.createRule.stepAboutrule.authorImmutableFieldTooltipText',
export const AUTHOR_FIELD_LABEL = i18n.translate(
'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldAuthorLabel',
{
defaultMessage: 'Author is not editable for Elastic rules',
defaultMessage: 'Author',
}
);
export const LICENSE_IMMUTABLE_FIELD_TOOLTIP_TEXT = i18n.translate(
'xpack.securitySolution.detectionEngine.createRule.stepAboutrule.licenseImmutableFieldTooltipText',
export const LICENSE_FIELD_LABEL = i18n.translate(
'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldLicenseLabel',
{
defaultMessage: 'License is not editable for Elastic rules',
defaultMessage: 'License',
}
);
export const FIELD_NOT_EDITABLE_TOOLTIP_TEXT = (fieldName: string) =>
i18n.translate(
'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldNotEditableTooltipText',
{
defaultMessage: "{fieldName} can't be edited for Elastic rules.",
values: {
fieldName,
},
}
);

View file

@ -95,7 +95,9 @@ const EditRulePageComponent: FC<{ rule: RuleResponse }> = ({ rule }) => {
usePrebuiltRulesCustomizationStatus();
const canEditRule = isRulesCustomizationEnabled || !rule.immutable;
const prebuiltCustomizationUpsellingMessage = usePrebuiltRuleCustomizationUpsellingMessage();
const prebuiltCustomizationUpsellingMessage = usePrebuiltRuleCustomizationUpsellingMessage(
'prebuilt_rule_customization_description'
);
const { detailName: ruleId } = useParams<{ detailName: string }>();

View file

@ -9,9 +9,8 @@ import React from 'react';
import useToggle from 'react-use/lib/useToggle';
import { EuiPopover, EuiText, EuiButtonIcon } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { TITLE } from './translations';
import * as i18n from './translations';
import type { VersionsPickerOptionEnum } from './versions_picker/versions_picker';
import { useFieldUpgradeContext } from '../rule_upgrade/field_upgrade_context';
import { getOptionDetails } from './utils';
/**
@ -30,10 +29,7 @@ interface ComparisonSideHelpInfoProps {
export function ComparisonSideHelpInfo({ options }: ComparisonSideHelpInfoProps): JSX.Element {
const [isPopoverOpen, togglePopover] = useToggle(false);
const { hasResolvedValueDifferentFromSuggested } = useFieldUpgradeContext();
const optionsWithDescriptions = options.map((option) =>
getOptionDetails(option, hasResolvedValueDifferentFromSuggested)
);
const optionsWithDescriptions = options.map((option) => getOptionDetails(option));
const button = (
<EuiButtonIcon
@ -48,25 +44,20 @@ export function ComparisonSideHelpInfo({ options }: ComparisonSideHelpInfoProps)
<EuiText style={{ width: POPOVER_WIDTH }} size="s">
<FormattedMessage
id="xpack.securitySolution.detectionEngine.rules.upgradeRules.comparisonSide.upgradeHelpText"
defaultMessage="The {title} lets you compare the values of a field across different versions of a rule: {versions} Differences are shown as JSON, with red lines showing what was removed, green lines showing additions, and bold text highlighting changes. Use {title} to review and understand changes across versions."
defaultMessage="The {title} lets you compare the values of a field across different versions of a rule:"
values={{
title: <strong>{TITLE}</strong>,
versions: (
<>
<br />
<ul>
{optionsWithDescriptions.map(
({ title: displayName, description: explanation }) => (
<li key={displayName}>
<strong>{displayName}</strong> {'-'} {explanation}
</li>
)
)}
</ul>
</>
),
title: <strong>{i18n.TITLE}</strong>,
}}
/>
<ul>
{optionsWithDescriptions.map(({ title: displayName, description: explanation }) => (
<li key={displayName}>
<strong>{displayName}</strong>
{':'} {explanation}
</li>
))}
</ul>
<p>{i18n.DIFF_FORMAT_AND_COLORS_EXPLANATION}</p>
</EuiText>
</EuiPopover>
);

View file

@ -54,7 +54,7 @@ export function FieldComparisonSide(): JSX.Element {
) {
setSelectedOption(VersionsPickerOptionEnum.MyChanges);
}
}, [hasResolvedValueDifferentFromSuggested, selectedOption, prevResolvedValue, resolvedValue]);
}, [selectedOption, prevResolvedValue, resolvedValue]);
return (
<>
@ -73,7 +73,6 @@ export function FieldComparisonSide(): JSX.Element {
options={options}
selectedOption={selectedOption}
onChange={setSelectedOption}
hasResolvedValueDifferentFromSuggested={hasResolvedValueDifferentFromSuggested}
/>
</EuiFlexItem>
</EuiFlexGroup>

View file

@ -25,28 +25,30 @@ export const NO_CHANGES = i18n.translate(
export const UPDATE_FROM_ELASTIC_TITLE = i18n.translate(
'xpack.securitySolution.detectionEngine.rules.upgradeRules.versions.updateFromElasticTitle',
{
defaultMessage: 'Update from Elastic',
defaultMessage: 'Changes from Elastic',
}
);
export const UPDATE_FROM_ELASTIC_EXPLANATION = i18n.translate(
'xpack.securitySolution.detectionEngine.rules.upgradeRules.versions.updateFromElasticExplanation',
{
defaultMessage: 'view the changes in Elastics latest update',
defaultMessage:
"Compare the field's original value with changes from the Elastic update. Your changes aren't displayed.",
}
);
export const MY_CHANGES_TITLE = i18n.translate(
'xpack.securitySolution.detectionEngine.rules.upgradeRules.versions.myChangesTitle',
export const MY_CHANGES_AND_FINAL_UPDATES_TITLE = i18n.translate(
'xpack.securitySolution.detectionEngine.rules.upgradeRules.versions.myChangesAndFinalUpdatesTitle',
{
defaultMessage: 'My changes',
defaultMessage: 'My changes and final updates',
}
);
export const MY_CHANGES_EXPLANATION = i18n.translate(
'xpack.securitySolution.detectionEngine.rules.upgradeRules.versions.myChangesExplanation',
export const MY_CHANGES_AND_FINAL_UPDATES_EXPLANATION = i18n.translate(
'xpack.securitySolution.detectionEngine.rules.upgradeRules.versions.myChangesAndFinalUpdatesExplanation',
{
defaultMessage: `view what you have changed in your installed rule and in the {finalUpdateSectionLabel} section`,
defaultMessage:
"Compare the field's original value with your changes or changes made in the {finalUpdateSectionLabel} section.",
values: {
finalUpdateSectionLabel: FINAL_UPDATE,
},
@ -54,9 +56,9 @@ export const MY_CHANGES_EXPLANATION = i18n.translate(
);
export const MY_CHANGES_IN_RULE_UPGRADE_WORKFLOW_EXPLANATION = i18n.translate(
'xpack.securitySolution.detectionEngine.rules.upgradeRules.versions.myChangesFinalUpdateOnlyExplanation',
'xpack.securitySolution.detectionEngine.rules.upgradeRules.versions.myChangesInRuleUpdateWorkflowOnlyExplanation',
{
defaultMessage: `view the changes you made in the {finalUpdateSectionLabel} section`,
defaultMessage: 'View the changes you made in the {finalUpdateSectionLabel} section.',
values: {
finalUpdateSectionLabel: FINAL_UPDATE,
},
@ -66,30 +68,40 @@ export const MY_CHANGES_IN_RULE_UPGRADE_WORKFLOW_EXPLANATION = i18n.translate(
export const MERGED_CHANGES_TITLE = i18n.translate(
'xpack.securitySolution.detectionEngine.rules.upgradeRules.versions.mergedChangesTitle',
{
defaultMessage: 'My changes merged with Elastics',
defaultMessage: "My changes merged with Elastic's",
}
);
export const MERGED_CHANGES_EXPLANATION = i18n.translate(
'xpack.securitySolution.detectionEngine.rules.upgradeRules.versions.mergedChangesExplanation',
{
defaultMessage: 'view an update suggestion that combines your changes with Elastics',
defaultMessage:
"Compare the field's original value with a version that combines your changes with those in the Elastic update. This version is only a suggestion.",
}
);
export const MY_ORIGINAL_CHANGES_TITLE = i18n.translate(
'xpack.securitySolution.detectionEngine.rules.upgradeRules.versions.myOriginalChangesTitle',
{
defaultMessage: 'My original changes',
defaultMessage: 'My changes only',
}
);
export const MY_ORIGINAL_CHANGES_EXPLANATION = i18n.translate(
'xpack.securitySolution.detectionEngine.rules.upgradeRules.versions.myCustomizationExplanation',
'xpack.securitySolution.detectionEngine.rules.upgradeRules.versions.myOriginalChangesExplanation',
{
defaultMessage: `view what you have changed in your installed rule. Doesnt include changes made in the {finalUpdateSectionLabel} section.`,
defaultMessage:
"Compare the field's original value with your changes. Modifications in the {finalUpdateSectionLabel} section aren't displayed.",
values: {
finalUpdateSectionLabel: FINAL_UPDATE,
},
}
);
export const DIFF_FORMAT_AND_COLORS_EXPLANATION = i18n.translate(
'xpack.securitySolution.detectionEngine.rules.upgradeRules.versions.diffFormatAndColorsExplanation',
{
defaultMessage:
"Differences are shown in JSON and color-coded or bolded. Lines that are highlighted in green were added. Lines that are highlighted in red were removed. Text that's bolded was changed.",
}
);

View file

@ -60,21 +60,13 @@ interface OptionDetails {
/**
* Returns the title and description for a given versions picker option.
*/
export function getOptionDetails(
option: VersionsPickerOptionEnum,
hasResolvedValueDifferentFromSuggested: boolean
): OptionDetails {
export function getOptionDetails(option: VersionsPickerOptionEnum): OptionDetails {
switch (option) {
case VersionsPickerOptionEnum.MyChanges:
return hasResolvedValueDifferentFromSuggested
? {
title: i18n.MY_CHANGES_TITLE,
description: i18n.MY_CHANGES_IN_RULE_UPGRADE_WORKFLOW_EXPLANATION,
}
: {
title: i18n.MY_CHANGES_TITLE,
description: i18n.MY_CHANGES_EXPLANATION,
};
return {
title: i18n.MY_CHANGES_AND_FINAL_UPDATES_TITLE,
description: i18n.MY_CHANGES_AND_FINAL_UPDATES_EXPLANATION,
};
case VersionsPickerOptionEnum.MyOriginalChanges:
return {
title: i18n.MY_ORIGINAL_CHANGES_TITLE,

View file

@ -22,7 +22,6 @@ export const Default = () => {
options={options}
selectedOption={selectedOption}
onChange={setSelectedOption}
hasResolvedValueDifferentFromSuggested={false}
/>
);
};

View file

@ -22,20 +22,11 @@ interface VersionsPickerProps {
options: VersionsPickerOptionEnum[];
selectedOption: VersionsPickerOptionEnum;
onChange: (selectedOption: VersionsPickerOptionEnum) => void;
hasResolvedValueDifferentFromSuggested: boolean;
}
export function VersionsPicker({
options,
selectedOption,
onChange,
hasResolvedValueDifferentFromSuggested,
}: VersionsPickerProps) {
export function VersionsPicker({ options, selectedOption, onChange }: VersionsPickerProps) {
const euiSelectOptions = options.map((option) => {
const { title: displayName, description: explanation } = getOptionDetails(
option,
hasResolvedValueDifferentFromSuggested
);
const { title: displayName, description: explanation } = getOptionDetails(option);
return {
value: option,

View file

@ -9,7 +9,7 @@ import React from 'react';
import useToggle from 'react-use/lib/useToggle';
import { EuiPopover, EuiText, EuiButtonIcon } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import * as i18n from '../../../../../../rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/translations';
import * as i18n from './translations';
/**
* Theme doesn't expose width variables. Using provided size variables will require
@ -36,9 +36,9 @@ export function FieldFinalSideHelpInfo(): JSX.Element {
<EuiText style={{ width: POPOVER_WIDTH }} size="s">
<FormattedMessage
id="xpack.securitySolution.detectionEngine.rules.upgradeRules.upgradeHelpText"
defaultMessage="The Final Update section lets you preview and edit the final value of a field. This is the value the rule will have after you click {updateButtonLabel}."
defaultMessage="The {finalUpdateSectionLabel} section lets you preview and edit the final value of a field. This value is fully applied when you update the rule."
values={{
updateButtonLabel: <strong>{i18n.UPDATE_BUTTON_LABEL}</strong>,
finalUpdateSectionLabel: <strong>{i18n.FINAL_UPDATE}</strong>,
}}
/>
</EuiText>

View file

@ -23,7 +23,7 @@ export function FieldUpgradeStateInfo({ state }: FieldUpgradeStateInfoProps): JS
switch (state) {
case FieldUpgradeStateEnum.NoUpdate:
return {
color: 'success',
color: 'default',
title: i18n.NO_UPDATE,
description: i18n.NO_UPDATE_DESCRIPTION,
};

View file

@ -17,8 +17,7 @@ export const NO_UPDATE = i18n.translate(
export const NO_UPDATE_DESCRIPTION = i18n.translate(
'xpack.securitySolution.detectionEngine.rules.upgradeRules.fieldUpgradeState.noUpdateDescription',
{
defaultMessage:
'The field was modified after rule installation but does not have Elastic update.',
defaultMessage: 'This field has no Elastic update but can still be edited if needed.',
}
);
@ -47,7 +46,8 @@ export const NO_CONFLICT = i18n.translate(
export const NO_CONFLICT_DESCRIPTION = i18n.translate(
'xpack.securitySolution.detectionEngine.rules.upgradeRules.fieldUpgradeState.noConflictDescription',
{
defaultMessage: 'The update has no conflicts and has been applied to the final update.',
defaultMessage:
"There are no conflicts with the field's current value and incoming Elastic update.",
}
);
@ -61,7 +61,7 @@ export const REVIEWED_AND_ACCEPTED = i18n.translate(
export const SOLVABLE_CONFLICT = i18n.translate(
'xpack.securitySolution.detectionEngine.rules.upgradeRules.fieldUpgradeState.solvableConflict',
{
defaultMessage: 'Solved conflict',
defaultMessage: 'Auto-resolved conflict',
}
);
@ -69,14 +69,14 @@ export const SOLVABLE_CONFLICT_DESCRIPTION = i18n.translate(
'xpack.securitySolution.detectionEngine.rules.upgradeRules.fieldUpgradeState.solvableConflictDescription',
{
defaultMessage:
'We have suggested an update for this modified field, please review before accepting.',
"We combined your changes with changes from the Elastic update. Review the suggested changes to ensure they're correct.",
}
);
export const NON_SOLVABLE_CONFLICT = i18n.translate(
'xpack.securitySolution.detectionEngine.rules.upgradeRules.fieldUpgradeState.nonSolvableConflict',
{
defaultMessage: 'Unsolved conflict',
defaultMessage: 'Unresolved conflict',
}
);
@ -84,7 +84,7 @@ export const NON_SOLVABLE_CONFLICT_DESCRIPTION = i18n.translate(
'xpack.securitySolution.detectionEngine.rules.upgradeRules.fieldUpgradeState.nonSolvableConflictDescription',
{
defaultMessage:
'Unable to suggest a merged version for the update. Current version is provided for you to edit.',
"We couldn't resolve this conflict. Edit the provided current version before saving and accepting the final update.",
}
);

View file

@ -36,7 +36,12 @@ export function RuleUpgradeCallout({
color="danger"
size="s"
>
<p>{i18n.RULE_HAS_HARD_CONFLICTS_DESCRIPTION}</p>
<span>{i18n.RULE_HAS_HARD_CONFLICTS_DESCRIPTION}</span>
<ul>
<li>{i18n.RULE_HAS_HARD_CONFLICTS_KEEP_YOUR_CHANGES}</li>
<li>{i18n.RULE_HAS_HARD_CONFLICTS_ACCEPT_ELASTIC_UPDATE}</li>
<li>{i18n.RULE_HAS_HARD_CONFLICTS_EDIT_FINAL_VERSION}</li>
</ul>
</EuiCallOut>
);
}
@ -56,7 +61,11 @@ export function RuleUpgradeCallout({
color="warning"
size="s"
>
<p>{i18n.RULE_HAS_SOFT_CONFLICTS_DESCRIPTION}</p>
<span>{i18n.RULE_HAS_SOFT_CONFLICTS_DESCRIPTION}</span>
<ul>
<li>{i18n.RULE_HAS_SOFT_CONFLICTS_ACCEPT_SUGGESTED_UPDATE}</li>
<li>{i18n.RULE_HAS_SOFT_CONFLICTS_EDIT_FINAL_VERSION}</li>
</ul>
</EuiCallOut>
);
}

View file

@ -14,7 +14,7 @@ import { useKibana } from '../../../../../../common/lib/kibana/kibana_react';
export const TOTAL_NUM_OF_FIELDS = (count: number) => (
<FormattedMessage
id="xpack.securitySolution.detectionEngine.rules.upgradeRules.diffTab.totalNumOfFieldsWithUpdates"
defaultMessage="{countValue} {count, plural, one {field} other {fields}} for review"
defaultMessage="{countValue} {count, plural, one {field} other {fields}} require review"
values={{ countValue: <strong>{count}</strong>, count }}
/>
);
@ -26,7 +26,7 @@ export const VERSION_UPDATE_INFO = (
) => (
<FormattedMessage
id="xpack.securitySolution.detectionEngine.rules.upgradeRules.diffTab.versionUpdateInfo"
defaultMessage="{numOfFieldsWithUpdatesValue} {numOfFieldsWithUpdates, plural, one {field} other {fields}} changed in Elastic update from version {currentVersionNumber} to {targetVersionNumber}"
defaultMessage="{numOfFieldsWithUpdatesValue} {numOfFieldsWithUpdates, plural, one {field} other {fields}} being changed in this Elastic update from version {currentVersionNumber} to {targetVersionNumber}"
values={{
numOfFieldsWithUpdatesValue: <strong>{numOfFieldsWithUpdates}</strong>,
numOfFieldsWithUpdates,
@ -79,7 +79,7 @@ export function RuleUpgradeHelper(): JSX.Element {
export const UPGRADE_STATUS = i18n.translate(
'xpack.securitySolution.detectionEngine.rules.upgradeRules.fieldUpgradeState.upgradeStatusTitle',
{
defaultMessage: 'Update status:',
defaultMessage: 'Status:',
}
);
@ -89,7 +89,7 @@ export const RULE_HAS_CONFLICTS = (count: number) =>
{
values: { count },
defaultMessage:
'{count} {count, plural, one {field has a conflict} other {fields have conflicts}}. Please review and provide a final update.',
'{count} {count, plural, one {field has a conflict} other {fields have conflicts}}. Review and provide a final update.',
}
);
@ -97,7 +97,21 @@ export const RULE_HAS_SOFT_CONFLICTS_DESCRIPTION = i18n.translate(
'xpack.securitySolution.detectionEngine.rules.upgradeRules.fieldUpgradeState.ruleHasSoftConflictsDescription',
{
defaultMessage:
'Please review and accept conflicts. You can also keep the current version without the updates, or accept the Elastic update but lose your modifications.',
'We auto-resolved the conflicts between your changes and the Elastic update. Review them and do one of the following:',
}
);
export const RULE_HAS_SOFT_CONFLICTS_ACCEPT_SUGGESTED_UPDATE = i18n.translate(
'xpack.securitySolution.detectionEngine.rules.upgradeRules.fieldUpgradeState.ruleHasSoftConflictsAcceptSuggestedUpdate',
{
defaultMessage: 'Accept the suggested update.',
}
);
export const RULE_HAS_SOFT_CONFLICTS_EDIT_FINAL_VERSION = i18n.translate(
'xpack.securitySolution.detectionEngine.rules.upgradeRules.fieldUpgradeState.ruleHasSoftConflictsEditFinalVersion',
{
defaultMessage: 'Edit the final version and choose a more appropriate field value.',
}
);
@ -105,14 +119,36 @@ export const RULE_HAS_HARD_CONFLICTS_DESCRIPTION = i18n.translate(
'xpack.securitySolution.detectionEngine.rules.upgradeRules.fieldUpgradeState.ruleHasHardConflictsDescription',
{
defaultMessage:
'Please provide an input for the conflicts. You can also keep the current version without the updates, or accept the Elastic update but lose your modifications.',
"We couldn't auto-resolve the conflicts between your changes and the Elastic update. To resolve them, do one of the following:",
}
);
export const RULE_HAS_HARD_CONFLICTS_KEEP_YOUR_CHANGES = i18n.translate(
'xpack.securitySolution.detectionEngine.rules.upgradeRules.fieldUpgradeState.ruleHasHardConflictsKeepYourChanges',
{
defaultMessage: 'Keep your changes and reject the Elastic update.',
}
);
export const RULE_HAS_HARD_CONFLICTS_ACCEPT_ELASTIC_UPDATE = i18n.translate(
'xpack.securitySolution.detectionEngine.rules.upgradeRules.fieldUpgradeState.ruleHasHardConflictsAcceptElasticUpdate',
{
defaultMessage: 'Accept the Elastic update and overwrite your changes.',
}
);
export const RULE_HAS_HARD_CONFLICTS_EDIT_FINAL_VERSION = i18n.translate(
'xpack.securitySolution.detectionEngine.rules.upgradeRules.fieldUpgradeState.ruleHasHardConflictsEditFinalVersion',
{
defaultMessage:
'Edit the final version by combining your changes with the Elastic update or choosing a more appropriate field value.',
}
);
export const RULE_IS_READY_FOR_UPGRADE_DESCRIPTION = i18n.translate(
'xpack.securitySolution.detectionEngine.rules.upgradeRules.fieldUpgradeState.ruleIsReadyForUpgradeDescription',
{
defaultMessage: 'There are no conflicts and the update is ready to be applied.',
defaultMessage: 'There are no conflicts. The rule is ready to be updated.',
}
);
@ -120,6 +156,6 @@ export const FIELD_MODIFIED_BADGE_DESCRIPTION = i18n.translate(
'xpack.securitySolution.detectionEngine.upgradeFlyout.fieldModifiedBadgeDescription',
{
defaultMessage:
"The field value was edited after rule's installation and differs from the value upon installation",
'This field value differs from the one provided in the original version of the rule.',
}
);

View file

@ -33,7 +33,7 @@ export const INVESTIGATION_GUIDE_TAB_LABEL = i18n.translate(
export const UPDATES_TAB_LABEL = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleDetails.updatesTabLabel',
{
defaultMessage: 'Updates',
defaultMessage: 'Elastic update overview',
}
);
@ -117,14 +117,14 @@ export const BUILDING_BLOCK_FIELD_LABEL = i18n.translate(
export const BUILDING_BLOCK_ENABLED_FIELD_DESCRIPTION = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleDetails.buildingBlockFieldDescription',
{
defaultMessage: 'All generated alerts will be marked as "building block" alerts',
defaultMessage: 'All generated alerts will be marked as building block alerts',
}
);
export const BUILDING_BLOCK_DISABLED_FIELD_DESCRIPTION = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleDetails.buildingBlockDisabledFieldDescription',
{
defaultMessage: 'Will not mark alerts as "building block" alerts',
defaultMessage: 'Will not mark alerts as building block alerts',
}
);

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import type { UpsellingMessageId } from '@kbn/security-solution-upselling/service';
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
import { useUpsellingMessage } from '../../../../common/hooks/use_upselling';
@ -13,10 +14,10 @@ import { useUpsellingMessage } from '../../../../common/hooks/use_upselling';
* for prebuilt rule customization. If the license level is sufficient, it
* returns `undefined`.
*/
export const usePrebuiltRuleCustomizationUpsellingMessage = () => {
export const usePrebuiltRuleCustomizationUpsellingMessage = (messageId: UpsellingMessageId) => {
// Upselling message is returned when the license level is insufficient,
// otherwise it's undefined
const upsellingMessage = useUpsellingMessage('prebuilt_rule_customization');
const upsellingMessage = useUpsellingMessage(messageId);
// We show the upselling message only if the feature flag is enabled
const isFeatureFlagEnabled = useIsExperimentalFeatureEnabled('prebuiltRulesCustomizationEnabled');

View file

@ -20,7 +20,9 @@ export const usePrebuiltRulesCustomizationStatus = (): PrebuiltRulesCustomizatio
const isFeatureFlagEnabled = useIsExperimentalFeatureEnabled('prebuiltRulesCustomizationEnabled');
// Upselling message is returned when the license level is insufficient,
// otherwise it's undefined
const upsellingMessage = usePrebuiltRuleCustomizationUpsellingMessage();
const upsellingMessage = usePrebuiltRuleCustomizationUpsellingMessage(
'prebuilt_rule_customization'
);
const isRulesCustomizationEnabled = isFeatureFlagEnabled && !upsellingMessage;
let customizationDisabledReason;

View file

@ -29,7 +29,9 @@ const BulkEditRuleErrorItem = ({
message,
rulesCount,
}: BulkActionRuleErrorItemProps) => {
const upsellingMessage = usePrebuiltRuleCustomizationUpsellingMessage();
const upsellingMessage = usePrebuiltRuleCustomizationUpsellingMessage(
'prebuilt_rule_customization'
);
switch (errorCode) {
case BulkActionsDryRunErrCodeEnum.IMMUTABLE:

View file

@ -7,20 +7,24 @@
import React from 'react';
import { EuiCallOut } from '@elastic/eui';
import { CUSTOMIZATION_DISABLED_CALLOUT_DESCRIPTION } from './translations';
import { usePrebuiltRuleCustomizationUpsellingMessage } from '../../../../rule_management/logic/prebuilt_rules/use_prebuilt_rule_customization_upselling_message';
import * as i18n from './translations';
export function CustomizationDisabledCallout() {
const upsellingMessage = usePrebuiltRuleCustomizationUpsellingMessage();
const title = usePrebuiltRuleCustomizationUpsellingMessage('prebuilt_rule_customization');
const description = usePrebuiltRuleCustomizationUpsellingMessage(
'prebuilt_rule_customization_description'
);
// Upselling message is returned only when the license level is insufficient
if (!upsellingMessage) {
if (!title) {
return null;
}
return (
<EuiCallOut title={upsellingMessage} size="s" color="primary">
<p>{CUSTOMIZATION_DISABLED_CALLOUT_DESCRIPTION}</p>
<EuiCallOut title={title} size="s" color="primary">
<p>{i18n.MODIFIED_RULE_UPGRADE_LICENSE_INSUFFICIENT_CALLOUT_DESCRIPTION}</p>
<p>{description}</p>
</EuiCallOut>
);
}

View file

@ -44,7 +44,7 @@ export const BULK_UPDATE_ALL_RULES_BUTTON_TOOLTIP_CONFLICTS = i18n.translate(
export const BULK_UPDATE_SELECTED_RULES_BUTTON_TOOLTIP_CONFLICTS = i18n.translate(
'xpack.securitySolution.detectionEngine.rules.upgradeRules.bulkButtons.selectedRules.conflicts',
{
defaultMessage: 'All selected rules have conflicts. Update them individually.',
defaultMessage: 'The selected rules have conflicts that must be manually resolved.',
}
);
@ -58,7 +58,7 @@ export const SEARCH_PLACEHOLDER = i18n.translate(
export const UPDATE_BUTTON_LABEL = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleDetails.updateButtonLabel',
{
defaultMessage: 'Update',
defaultMessage: 'Update rule',
}
);
@ -86,7 +86,14 @@ export const RULE_TYPE_CHANGE_CALLOUT_TITLE = i18n.translate(
export const RULE_TYPE_CHANGE_CALLOUT_DESCRIPTION = i18n.translate(
'xpack.securitySolution.detectionEngine.upgradeRules.ruleTypeChangeCalloutDescription',
{
defaultMessage: 'Elastic update has rule type changed.',
defaultMessage: 'The rule type will change if you update this rule.',
}
);
export const MODIFIED_RULE_UPGRADE_LICENSE_INSUFFICIENT_CALLOUT_DESCRIPTION = i18n.translate(
'xpack.securitySolution.detectionEngine.upgradeRules.ruleUpgradeLicenseInsufficientCalloutDescription',
{
defaultMessage: 'Updating the rule will erase your changes.',
}
);
@ -94,14 +101,14 @@ export const RULE_TYPE_CHANGE_WITH_CUSTOMIZATIONS_CALLOUT_DESCRIPTION = i18n.tra
'xpack.securitySolution.detectionEngine.upgradeRules.ruleTypeChangeWithCustomizationCalloutDescription',
{
defaultMessage:
'Your customization will be lost at update. Please take note of your customization or clone this rule before updating.',
'Updating the rule will erase your changes. To save them, first duplicate the rule, then update it.',
}
);
export const LAST_UPDATE = i18n.translate(
'xpack.securitySolution.detectionEngine.upgradeFlyout.header.lastUpdate',
{
defaultMessage: 'Last update',
defaultMessage: 'Last updated',
}
);
@ -168,11 +175,3 @@ export const RULE_NEW_VERSION_DETECTED_WARNING_DESCRIPTION = (ruleName: string)
values: { ruleName },
}
);
export const CUSTOMIZATION_DISABLED_CALLOUT_DESCRIPTION = i18n.translate(
'xpack.securitySolution.detectionEngine.upgradeRules.customizationDisabledCalloutDescription',
{
defaultMessage:
'Prebuilt rule customization is disabled. Only updates to Elastic version are available.',
}
);

View file

@ -831,7 +831,7 @@ export const NO_TAGS_AVAILABLE = i18n.translate(
export const RULE_SOURCE = i18n.translate(
'xpack.securitySolution.detectionEngine.rules.allRules.filters.ruleSourceLabel',
{
defaultMessage: 'Modifications',
defaultMessage: 'Modified/Unmodified',
}
);
@ -1436,7 +1436,7 @@ export const UPDATE_RULE_BUTTON = i18n.translate(
export const UPDATE_RULE_BUTTON_TOOLTIP_CONFLICTS = i18n.translate(
'xpack.securitySolution.detectionEngine.rules.upgradeRules.button.conflicts',
{
defaultMessage: 'Rule has conflicts. Resolve them manually.',
defaultMessage: 'This rule has conflicts that must be manually resolved.',
}
);

View file

@ -10,10 +10,11 @@ import { SecurityPageName } from '@kbn/security-solution-plugin/common';
import {
ALERT_SUPPRESSION_RULE_DETAILS,
ALERT_SUPPRESSION_RULE_FORM,
PREBUILT_RULE_CUSTOMIZATION_DESCRIPTION,
UPGRADE_ALERT_ASSIGNMENTS,
UPGRADE_INVESTIGATION_GUIDE,
UPGRADE_NOTES_MANAGEMENT_USER_FILTER,
UPGRADE_PREBUILT_RULE_CUSTOMIZATION,
PREBUILT_RULE_CUSTOMIZATION,
} from '@kbn/security-solution-upselling/messages';
import type {
MessageUpsellings,
@ -142,6 +143,11 @@ export const upsellingMessages: UpsellingMessages = [
{
id: 'prebuilt_rule_customization',
minimumLicenseRequired: 'enterprise',
message: UPGRADE_PREBUILT_RULE_CUSTOMIZATION('Enterprise'),
message: PREBUILT_RULE_CUSTOMIZATION('Enterprise', 'subscription'),
},
{
id: 'prebuilt_rule_customization_description',
minimumLicenseRequired: 'enterprise',
message: PREBUILT_RULE_CUSTOMIZATION_DESCRIPTION('Enterprise', 'subscription'),
},
];

View file

@ -10,7 +10,8 @@ import { SecurityPageName } from '@kbn/security-solution-plugin/common';
import {
UPGRADE_INVESTIGATION_GUIDE,
UPGRADE_INVESTIGATION_GUIDE_INTERACTIONS,
UPGRADE_PREBUILT_RULE_CUSTOMIZATION,
PREBUILT_RULE_CUSTOMIZATION,
PREBUILT_RULE_CUSTOMIZATION_DESCRIPTION,
} from '@kbn/security-solution-upselling/messages';
import type {
UpsellingMessageId,
@ -166,8 +167,17 @@ export const upsellingMessages: UpsellingMessages = [
{
id: 'prebuilt_rule_customization',
pli: ProductFeatureKey.prebuiltRuleCustomization,
message: UPGRADE_PREBUILT_RULE_CUSTOMIZATION(
getProductTypeByPLI(ProductFeatureKey.prebuiltRuleCustomization) ?? ''
message: PREBUILT_RULE_CUSTOMIZATION(
getProductTypeByPLI(ProductFeatureKey.prebuiltRuleCustomization) ?? '',
'feature tier'
),
},
{
id: 'prebuilt_rule_customization_description',
pli: ProductFeatureKey.prebuiltRuleCustomization,
message: PREBUILT_RULE_CUSTOMIZATION_DESCRIPTION(
getProductTypeByPLI(ProductFeatureKey.prebuiltRuleCustomization) ?? '',
'feature tier'
),
},
];

View file

@ -73,7 +73,7 @@ import { enableRules, waitForRulesToFinishExecution } from '../../../../tasks/ap
const PREVIEW_TABS = {
OVERVIEW: 'Overview',
JSON_VIEW: 'JSON view',
UPDATES: 'Updates', // Currently open by default on upgrade
UPDATES: 'Elastic update overview', // Currently open by default on upgrade
};
describe(

View file

@ -142,7 +142,7 @@ export const assertCommonPropertiesShown = (properties: Partial<PrebuiltRuleAsse
cy.get(BUILDING_BLOCK_TITLE).should('have.text', 'Building block');
cy.get(BUILDING_BLOCK_VALUE).should(
'have.text',
'All generated alerts will be marked as "building block" alerts'
'All generated alerts will be marked as building block alerts'
);
cy.get(SEVERITY_TITLE).should('have.text', 'Severity');