mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
# Backport This will backport the following commits from `main` to `8.18`: - [[Security Solution] Add UI incentivizers to upgrade prebuilt rules (#211862)](https://github.com/elastic/kibana/pull/211862) <!--- Backport version: 9.6.6 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sorenlouv/backport) <!--BACKPORT [{"author":{"name":"Davis Plumlee","email":"56367316+dplumlee@users.noreply.github.com"},"sourceCommit":{"committedDate":"2025-03-05T12:14:31Z","message":"[Security Solution] Add UI incentivizers to upgrade prebuilt rules (#211862)\n\n## Summary\n\nPartially addresses https://github.com/elastic/kibana/issues/210358\n\nAdds all callouts and logic to incentivize users to upgrade their rules asap. These include:\n\n- [x] Showing a callout on the Rule Management page\n- [x] Showing a callout on the Rule Details page\n - [x] Letting users open the Rule Upgrade flyout from the Rule Details page\n- [x] Showing a callout on the Rule Editing page\n- [x] Showing a callout in the Rule Upgrade flyout if rule has missing base version\n\nThis PR also adds related updates to the rule diff algorithms in order to facilitate an easier upgrade experience when rules have missing base versions. These include:\n\n- [x] When the rule has a missing base version and is NOT marked as customized:\n - [x] We should return all the target fields from the diff algorithm as NO_CONFLICT\n- [x] When the rule has a missing base version and is marked as customized:\n - [x] We should attempt to merge all non-functional mergeable fields (any field that doesn't have consequences with how the rule runs e.g. tags) and return them as `SOLVABLE_CONFLICT`.\n - **NOTE**: When base versions are missing and the rule is customized, we attempt to merge all mergable, non-functional rule fields. These include all fields covered by the scalar diff array (`tags`, `references`, `new_terms_fields`, `threat_index`). We typically also consider multi-line string fields as mergeable but without three versions of the string, we are currently unable to merge the strings together, so we just return target version.\n - [x] We should pick the target version for all functional mergeable fields (e.g. `index`) and non-mergeable fields and return them as `SOLVABLE_CONFLICT`.\n\n\n### Screenshots\n\n\n**Callout on Rule details page w/ flyout button**\n\n\n---\n\n**Upgrade flyout now accessible from rule details page**\n\n\n---\n\n**Callout on rule editing page**\n\n\n---\n\n**Dismissible callout on rule management page**\n\n\n---\n\n**Callout in rule upgrade flyout when rule has missing base version**\n\n\n### Checklist\n\nCheck the PR satisfies following conditions. \n\nReviewers should verify this PR satisfies this list as well.\n\n- [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md)\n- [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios","sha":"461787bea6a48cc2c55514843adedc9ca5bb5032","branchLabelMapping":{"^v9.1.0$":"main","^v8.19.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["bug","release_note:skip","impact:high","v9.0.0","Team:Detections and Resp","Team: SecuritySolution","Team:Detection Rule Management","Feature:Prebuilt Detection Rules","backport:version","v8.18.0","v9.1.0","v8.19.0"],"title":"[Security Solution] Add UI incentivizers to upgrade prebuilt rules","number":211862,"url":"https://github.com/elastic/kibana/pull/211862","mergeCommit":{"message":"[Security Solution] Add UI incentivizers to upgrade prebuilt rules (#211862)\n\n## Summary\n\nPartially addresses https://github.com/elastic/kibana/issues/210358\n\nAdds all callouts and logic to incentivize users to upgrade their rules asap. These include:\n\n- [x] Showing a callout on the Rule Management page\n- [x] Showing a callout on the Rule Details page\n - [x] Letting users open the Rule Upgrade flyout from the Rule Details page\n- [x] Showing a callout on the Rule Editing page\n- [x] Showing a callout in the Rule Upgrade flyout if rule has missing base version\n\nThis PR also adds related updates to the rule diff algorithms in order to facilitate an easier upgrade experience when rules have missing base versions. These include:\n\n- [x] When the rule has a missing base version and is NOT marked as customized:\n - [x] We should return all the target fields from the diff algorithm as NO_CONFLICT\n- [x] When the rule has a missing base version and is marked as customized:\n - [x] We should attempt to merge all non-functional mergeable fields (any field that doesn't have consequences with how the rule runs e.g. tags) and return them as `SOLVABLE_CONFLICT`.\n - **NOTE**: When base versions are missing and the rule is customized, we attempt to merge all mergable, non-functional rule fields. These include all fields covered by the scalar diff array (`tags`, `references`, `new_terms_fields`, `threat_index`). We typically also consider multi-line string fields as mergeable but without three versions of the string, we are currently unable to merge the strings together, so we just return target version.\n - [x] We should pick the target version for all functional mergeable fields (e.g. `index`) and non-mergeable fields and return them as `SOLVABLE_CONFLICT`.\n\n\n### Screenshots\n\n\n**Callout on Rule details page w/ flyout button**\n\n\n---\n\n**Upgrade flyout now accessible from rule details page**\n\n\n---\n\n**Callout on rule editing page**\n\n\n---\n\n**Dismissible callout on rule management page**\n\n\n---\n\n**Callout in rule upgrade flyout when rule has missing base version**\n\n\n### Checklist\n\nCheck the PR satisfies following conditions. \n\nReviewers should verify this PR satisfies this list as well.\n\n- [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md)\n- [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios","sha":"461787bea6a48cc2c55514843adedc9ca5bb5032"}},"sourceBranch":"main","suggestedTargetBranches":["9.0","8.18","8.x"],"targetPullRequestStates":[{"branch":"9.0","label":"v9.0.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"8.18","label":"v8.18.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v9.1.0","branchLabelMappingKey":"^v9.1.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/211862","number":211862,"mergeCommit":{"message":"[Security Solution] Add UI incentivizers to upgrade prebuilt rules (#211862)\n\n## Summary\n\nPartially addresses https://github.com/elastic/kibana/issues/210358\n\nAdds all callouts and logic to incentivize users to upgrade their rules asap. These include:\n\n- [x] Showing a callout on the Rule Management page\n- [x] Showing a callout on the Rule Details page\n - [x] Letting users open the Rule Upgrade flyout from the Rule Details page\n- [x] Showing a callout on the Rule Editing page\n- [x] Showing a callout in the Rule Upgrade flyout if rule has missing base version\n\nThis PR also adds related updates to the rule diff algorithms in order to facilitate an easier upgrade experience when rules have missing base versions. These include:\n\n- [x] When the rule has a missing base version and is NOT marked as customized:\n - [x] We should return all the target fields from the diff algorithm as NO_CONFLICT\n- [x] When the rule has a missing base version and is marked as customized:\n - [x] We should attempt to merge all non-functional mergeable fields (any field that doesn't have consequences with how the rule runs e.g. tags) and return them as `SOLVABLE_CONFLICT`.\n - **NOTE**: When base versions are missing and the rule is customized, we attempt to merge all mergable, non-functional rule fields. These include all fields covered by the scalar diff array (`tags`, `references`, `new_terms_fields`, `threat_index`). We typically also consider multi-line string fields as mergeable but without three versions of the string, we are currently unable to merge the strings together, so we just return target version.\n - [x] We should pick the target version for all functional mergeable fields (e.g. `index`) and non-mergeable fields and return them as `SOLVABLE_CONFLICT`.\n\n\n### Screenshots\n\n\n**Callout on Rule details page w/ flyout button**\n\n\n---\n\n**Upgrade flyout now accessible from rule details page**\n\n\n---\n\n**Callout on rule editing page**\n\n\n---\n\n**Dismissible callout on rule management page**\n\n\n---\n\n**Callout in rule upgrade flyout when rule has missing base version**\n\n\n### Checklist\n\nCheck the PR satisfies following conditions. \n\nReviewers should verify this PR satisfies this list as well.\n\n- [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md)\n- [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios","sha":"461787bea6a48cc2c55514843adedc9ca5bb5032"}},{"branch":"8.x","label":"v8.19.0","branchLabelMappingKey":"^v8.19.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}] BACKPORT--> Co-authored-by: Davis Plumlee <56367316+dplumlee@users.noreply.github.com>
This commit is contained in:
parent
a2d89b0a90
commit
894b47b6f0
56 changed files with 2029 additions and 1040 deletions
|
@ -145,5 +145,6 @@ export interface ThreeWayDiff<TValue> {
|
|||
* Given the three versions of a value, calculates a three-way diff for it.
|
||||
*/
|
||||
export type ThreeWayDiffAlgorithm<TValue> = (
|
||||
versions: ThreeVersionsOf<TValue>
|
||||
versions: ThreeVersionsOf<TValue>,
|
||||
isRuleCustomized: boolean
|
||||
) => ThreeWayDiff<TValue>;
|
||||
|
|
|
@ -10,7 +10,7 @@ import { SortOrder, type RuleObjectId, type RuleSignatureId, type RuleTagArray }
|
|||
import type { PartialRuleDiff } from '../model';
|
||||
import type { RuleResponse, RuleVersion } from '../../model/rule_schema';
|
||||
import { FindRulesSortField } from '../../rule_management';
|
||||
import { PrebuiltRulesFilter } from '../common/prebuilt_rules_filter';
|
||||
import { ReviewPrebuiltRuleUpgradeFilter } from '../common/review_prebuilt_rules_upgrade_filter';
|
||||
|
||||
export type ReviewRuleUpgradeSort = z.infer<typeof ReviewRuleUpgradeSort>;
|
||||
export const ReviewRuleUpgradeSort = z.object({
|
||||
|
@ -27,7 +27,7 @@ export const ReviewRuleUpgradeSort = z.object({
|
|||
export type ReviewRuleUpgradeRequestBody = z.infer<typeof ReviewRuleUpgradeRequestBody>;
|
||||
export const ReviewRuleUpgradeRequestBody = z
|
||||
.object({
|
||||
filter: PrebuiltRulesFilter.optional(),
|
||||
filter: ReviewPrebuiltRuleUpgradeFilter.optional(),
|
||||
sort: ReviewRuleUpgradeSort.optional(),
|
||||
|
||||
page: z.coerce.number().int().min(1).optional().default(1),
|
||||
|
@ -89,4 +89,5 @@ export interface RuleUpgradeInfoForReview {
|
|||
target_rule: RuleResponse;
|
||||
diff: PartialRuleDiff;
|
||||
revision: number;
|
||||
has_base_version: boolean;
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import { ecsFieldMap } from '@kbn/alerts-as-data-utils';
|
||||
import type { RequiredField, RequiredFieldInput } from '../../api/detection_engine';
|
||||
import type { RequiredField, RequiredFieldInput, RuleResponse } from '../../api/detection_engine';
|
||||
|
||||
/*
|
||||
Computes the boolean "ecs" property value for each required field based on the ECS field map.
|
||||
|
@ -23,3 +23,6 @@ export const addEcsToRequiredFields = (requiredFields?: RequiredFieldInput[]): R
|
|||
ecs: isEcsField,
|
||||
};
|
||||
});
|
||||
|
||||
export const isRuleCustomized = (rule: RuleResponse) =>
|
||||
rule.rule_source.type === 'external' && rule.rule_source.is_customized === true;
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
EuiCallOut,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiLink,
|
||||
EuiResizableContainer,
|
||||
EuiSpacer,
|
||||
EuiTab,
|
||||
|
@ -75,6 +76,7 @@ import { usePrebuiltRulesCustomizationStatus } from '../../../rule_management/lo
|
|||
import { PrebuiltRulesCustomizationDisabledReason } from '../../../../../common/detection_engine/prebuilt_rules/prebuilt_rule_customization_status';
|
||||
import { ALERT_SUPPRESSION_FIELDS_FIELD_NAME } from '../../../rule_creation/components/alert_suppression_edit';
|
||||
import { usePrebuiltRuleCustomizationUpsellingMessage } from '../../../rule_management/logic/prebuilt_rules/use_prebuilt_rule_customization_upselling_message';
|
||||
import { useRuleUpdateCallout } from '../../../rule_management/hooks/use_rule_update_callout';
|
||||
|
||||
const EditRulePageComponent: FC<{ rule: RuleResponse }> = ({ rule }) => {
|
||||
const { addSuccess } = useAppToasts();
|
||||
|
@ -509,6 +511,16 @@ const EditRulePageComponent: FC<{ rule: RuleResponse }> = ({ rule }) => {
|
|||
[navigateToApp, ruleId]
|
||||
);
|
||||
|
||||
const upgradeCallout = useRuleUpdateCallout({
|
||||
rule,
|
||||
message: ruleI18n.HAS_RULE_UPDATE_EDITING_CALLOUT_MESSAGE,
|
||||
actionButton: (
|
||||
<EuiLink onClick={goToDetailsRule} data-test-subj="ruleEditingUpdateRuleCalloutButton">
|
||||
{ruleI18n.HAS_RULE_UPDATE_EDITING_CALLOUT_BUTTON}
|
||||
</EuiLink>
|
||||
),
|
||||
});
|
||||
|
||||
if (
|
||||
redirectToDetections(
|
||||
isSignalIndexExists,
|
||||
|
@ -550,6 +562,7 @@ const EditRulePageComponent: FC<{ rule: RuleResponse }> = ({ rule }) => {
|
|||
setIsRulePreviewVisible={setIsRulePreviewVisible}
|
||||
togglePanel={togglePanel}
|
||||
/>
|
||||
{upgradeCallout}
|
||||
{invalidSteps.length > 0 && (
|
||||
<EuiCallOut title={i18n.SORRY_ERRORS} color="danger" iconType="warning">
|
||||
<FormattedMessage
|
||||
|
|
|
@ -148,6 +148,7 @@ import { useManualRuleRunConfirmation } from '../../../rule_gaps/components/manu
|
|||
import { useLegacyUrlRedirect } from './use_redirect_legacy_url';
|
||||
import { RuleDetailTabs, useRuleDetailsTabs } from './use_rule_details_tabs';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
|
||||
import { useRuleUpdateCallout } from '../../../rule_management/hooks/use_rule_update_callout';
|
||||
|
||||
const RULE_EXCEPTION_LIST_TYPES = [
|
||||
ExceptionListTypeEnum.DETECTION,
|
||||
|
@ -254,7 +255,7 @@ const RuleDetailsPageComponent: React.FC<DetectionEngineComponentProps> = ({
|
|||
|
||||
const { pollForSignalIndex } = useSignalHelpers();
|
||||
const [rule, setRule] = useState<RuleResponse | null>(null);
|
||||
const isLoading = ruleLoading && rule == null;
|
||||
const isLoading = useMemo(() => ruleLoading && rule == null, [rule, ruleLoading]);
|
||||
|
||||
const { starting: isStartingJobs, startMlJobs } = useStartMlJobs();
|
||||
const startMlJobsIfNeeded = useCallback(async () => {
|
||||
|
@ -394,6 +395,12 @@ const RuleDetailsPageComponent: React.FC<DetectionEngineComponentProps> = ({
|
|||
const lastExecutionDate = lastExecution?.date ?? '';
|
||||
const lastExecutionMessage = lastExecution?.message ?? '';
|
||||
|
||||
const upgradeCallout = useRuleUpdateCallout({
|
||||
rule,
|
||||
message: ruleI18n.HAS_RULE_UPDATE_DETAILS_CALLOUT_MESSAGE,
|
||||
onUpgrade: refreshRule,
|
||||
});
|
||||
|
||||
const ruleStatusInfo = useMemo(() => {
|
||||
return (
|
||||
<>
|
||||
|
@ -556,6 +563,7 @@ const RuleDetailsPageComponent: React.FC<DetectionEngineComponentProps> = ({
|
|||
<>
|
||||
<NeedAdminForUpdateRulesCallOut />
|
||||
<MissingPrivilegesCallOut />
|
||||
{upgradeCallout}
|
||||
{isBulkDuplicateConfirmationVisible && (
|
||||
<BulkActionDuplicateExceptionsConfirmation
|
||||
onCancel={cancelRuleDuplication}
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* 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 { EuiCallOut, EuiSpacer, EuiLink } from '@elastic/eui';
|
||||
import React, { useMemo } from 'react';
|
||||
import type { RuleResponse } from '../../../../../common/api/detection_engine';
|
||||
import * as i18n from './translations';
|
||||
import { usePrebuiltRulesUpgrade } from '../../hooks/use_prebuilt_rules_upgrade';
|
||||
|
||||
interface RuleUpdateCalloutProps {
|
||||
rule: RuleResponse;
|
||||
message: string;
|
||||
actionButton?: JSX.Element;
|
||||
onUpgrade?: () => void;
|
||||
}
|
||||
|
||||
const RuleUpdateCalloutComponent = ({
|
||||
rule,
|
||||
message,
|
||||
actionButton,
|
||||
onUpgrade,
|
||||
}: RuleUpdateCalloutProps): JSX.Element | null => {
|
||||
const { upgradeReviewResponse, rulePreviewFlyout, openRulePreview } = usePrebuiltRulesUpgrade({
|
||||
pagination: {
|
||||
page: 1, // we only want to fetch one result
|
||||
perPage: 1,
|
||||
},
|
||||
filter: { rule_ids: [rule.id] },
|
||||
onUpgrade,
|
||||
});
|
||||
|
||||
const isRuleUpgradeable = useMemo(
|
||||
() => upgradeReviewResponse !== undefined && upgradeReviewResponse.total > 0,
|
||||
[upgradeReviewResponse]
|
||||
);
|
||||
|
||||
const updateCallToActionButton = useMemo(() => {
|
||||
if (actionButton) {
|
||||
return actionButton;
|
||||
}
|
||||
return (
|
||||
<EuiLink
|
||||
onClick={() => openRulePreview(rule.rule_id)}
|
||||
data-test-subj="ruleDetailsUpdateRuleCalloutButton"
|
||||
>
|
||||
{i18n.HAS_RULE_UPDATE_CALLOUT_BUTTON}
|
||||
</EuiLink>
|
||||
);
|
||||
}, [actionButton, openRulePreview, rule.rule_id]);
|
||||
|
||||
if (!isRuleUpgradeable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiCallOut title={i18n.HAS_RULE_UPDATE_CALLOUT_TITLE} color="primary" iconType="gear">
|
||||
<p>{message}</p>
|
||||
{updateCallToActionButton}
|
||||
</EuiCallOut>
|
||||
<EuiSpacer size="l" />
|
||||
{rulePreviewFlyout}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const RuleUpdateCallout = React.memo(RuleUpdateCalloutComponent);
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { EuiCallOut } from '@elastic/eui';
|
||||
import * as i18n from './translations';
|
||||
|
||||
export const RuleHasMissingBaseVersionCallout = () => (
|
||||
<EuiCallOut color="warning" size="s">
|
||||
<p>{i18n.RULE_BASE_VERSION_IS_MISSING_DESCRIPTION}</p>
|
||||
</EuiCallOut>
|
||||
);
|
|
@ -18,6 +18,7 @@ import { RuleUpgradeInfoBar } from './rule_upgrade_info_bar';
|
|||
import { RuleUpgradeCallout } from './rule_upgrade_callout';
|
||||
import { FieldUpgrade } from './field_upgrade';
|
||||
import { FieldUpgradeContextProvider } from './field_upgrade_context';
|
||||
import { RuleHasMissingBaseVersionCallout } from './missing_base_version_callout';
|
||||
|
||||
interface RuleUpgradeProps {
|
||||
ruleUpgradeState: RuleUpgradeState;
|
||||
|
@ -45,6 +46,12 @@ export const RuleUpgrade = memo(function RuleUpgrade({
|
|||
targetVersionNumber={ruleUpgradeState.target_rule.version}
|
||||
/>
|
||||
<EuiSpacer size="s" />
|
||||
{!ruleUpgradeState.has_base_version && (
|
||||
<>
|
||||
<RuleHasMissingBaseVersionCallout />
|
||||
<EuiSpacer size="s" />
|
||||
</>
|
||||
)}
|
||||
<RuleUpgradeCallout
|
||||
numOfSolvableConflicts={numOfSolvableConflicts}
|
||||
numOfNonSolvableConflicts={numOfNonSolvableConflicts}
|
||||
|
|
|
@ -23,66 +23,72 @@ export function RuleUpgradeCallout({
|
|||
}: RuleUpgradeCalloutProps): JSX.Element {
|
||||
if (numOfNonSolvableConflicts > 0) {
|
||||
return (
|
||||
<EuiCallOut
|
||||
title={
|
||||
<>
|
||||
<strong>{i18n.UPGRADE_STATUS}</strong>
|
||||
|
||||
<ActionRequiredBadge />
|
||||
|
||||
{i18n.RULE_HAS_CONFLICTS(numOfNonSolvableConflicts + numOfSolvableConflicts)}
|
||||
</>
|
||||
}
|
||||
color="danger"
|
||||
size="s"
|
||||
>
|
||||
<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>
|
||||
<>
|
||||
<EuiCallOut
|
||||
title={
|
||||
<>
|
||||
<strong>{i18n.UPGRADE_STATUS}</strong>
|
||||
|
||||
<ActionRequiredBadge />
|
||||
|
||||
{i18n.RULE_HAS_CONFLICTS(numOfNonSolvableConflicts + numOfSolvableConflicts)}
|
||||
</>
|
||||
}
|
||||
color="danger"
|
||||
size="s"
|
||||
>
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (numOfSolvableConflicts > 0) {
|
||||
return (
|
||||
<>
|
||||
<EuiCallOut
|
||||
title={
|
||||
<>
|
||||
<strong>{i18n.UPGRADE_STATUS}</strong>
|
||||
|
||||
<ReviewRequiredBadge />
|
||||
|
||||
{i18n.RULE_HAS_CONFLICTS(numOfSolvableConflicts)}
|
||||
</>
|
||||
}
|
||||
color="warning"
|
||||
size="s"
|
||||
>
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiCallOut
|
||||
title={
|
||||
<>
|
||||
<strong>{i18n.UPGRADE_STATUS}</strong>
|
||||
|
||||
<ReviewRequiredBadge />
|
||||
|
||||
{i18n.RULE_HAS_CONFLICTS(numOfSolvableConflicts)}
|
||||
<ReadyForUpgradeBadge />
|
||||
</>
|
||||
}
|
||||
color="warning"
|
||||
color="success"
|
||||
size="s"
|
||||
>
|
||||
<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>
|
||||
<p>{i18n.RULE_IS_READY_FOR_UPGRADE_DESCRIPTION}</p>
|
||||
</EuiCallOut>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiCallOut
|
||||
title={
|
||||
<>
|
||||
<strong>{i18n.UPGRADE_STATUS}</strong>
|
||||
|
||||
<ReadyForUpgradeBadge />
|
||||
</>
|
||||
}
|
||||
color="success"
|
||||
size="s"
|
||||
>
|
||||
<p>{i18n.RULE_IS_READY_FOR_UPGRADE_DESCRIPTION}</p>
|
||||
</EuiCallOut>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -158,3 +158,11 @@ export const FIELD_MODIFIED_BADGE_DESCRIPTION = i18n.translate(
|
|||
'This field value differs from the one provided in the original version of the rule.',
|
||||
}
|
||||
);
|
||||
|
||||
export const RULE_BASE_VERSION_IS_MISSING_DESCRIPTION = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.upgradeFlyout.baseVersionMissingDescription',
|
||||
{
|
||||
defaultMessage:
|
||||
"The original, unedited version of this Elastic rule couldn't be found. This sometimes happens when a rule hasn't been updated in a while. You can still update this rule, but will only have access to its current version and the incoming Elastic update. Updating Elastic rules more often can help you avoid this in the future. We encourage you to review this update carefully and ensure your changes are not accidentally overwritten.",
|
||||
}
|
||||
);
|
||||
|
|
|
@ -461,3 +461,17 @@ export const LUCENE_LANGUAGE_LABEL = i18n.translate(
|
|||
defaultMessage: 'Lucene',
|
||||
}
|
||||
);
|
||||
|
||||
export const HAS_RULE_UPDATE_CALLOUT_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.ruleDetails.updateCalloutTitle',
|
||||
{
|
||||
defaultMessage: 'Elastic rule update available',
|
||||
}
|
||||
);
|
||||
|
||||
export const HAS_RULE_UPDATE_CALLOUT_BUTTON = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.ruleDetailsUpdate.calloutButton',
|
||||
{
|
||||
defaultMessage: 'Review update',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -0,0 +1,423 @@
|
|||
/*
|
||||
* 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 React, { useCallback, useMemo, useState } from 'react';
|
||||
import { EuiButton, EuiToolTip } from '@elastic/eui';
|
||||
import type { ReviewPrebuiltRuleUpgradeFilter } from '../../../../common/api/detection_engine/prebuilt_rules/common/review_prebuilt_rules_upgrade_filter';
|
||||
import { FieldUpgradeStateEnum, type RuleUpgradeState } from '../model/prebuilt_rule_upgrade';
|
||||
import { PerFieldRuleDiffTab } from '../components/rule_details/per_field_rule_diff_tab';
|
||||
import { PrebuiltRulesCustomizationDisabledReason } from '../../../../common/detection_engine/prebuilt_rules/prebuilt_rule_customization_status';
|
||||
import { useIsUpgradingSecurityPackages } from '../logic/use_upgrade_security_packages';
|
||||
import { usePrebuiltRulesCustomizationStatus } from '../logic/prebuilt_rules/use_prebuilt_rules_customization_status';
|
||||
import { usePerformUpgradeRules } from '../logic/prebuilt_rules/use_perform_rule_upgrade';
|
||||
import { usePrebuiltRulesUpgradeReview } from '../logic/prebuilt_rules/use_prebuilt_rules_upgrade_review';
|
||||
import type {
|
||||
FindRulesSortField,
|
||||
RuleFieldsToUpgrade,
|
||||
RuleResponse,
|
||||
RuleSignatureId,
|
||||
RuleUpgradeSpecifier,
|
||||
} from '../../../../common/api/detection_engine';
|
||||
import { usePrebuiltRulesUpgradeState } from '../../rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade_state';
|
||||
import { useOutdatedMlJobsUpgradeModal } from '../../rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_ml_jobs_upgrade_modal';
|
||||
import { useUpgradeWithConflictsModal } from '../../rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_upgrade_with_conflicts_modal';
|
||||
import * as ruleDetailsI18n from '../components/rule_details/translations';
|
||||
import * as i18n from '../../rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/translations';
|
||||
import { UpgradeFlyoutSubHeader } from '../../rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_flyout_subheader';
|
||||
import { CustomizationDisabledCallout } from '../../rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/customization_disabled_callout';
|
||||
import { RuleUpgradeTab } from '../components/rule_details/three_way_diff';
|
||||
import { TabContentPadding } from '../../../siem_migrations/rules/components/rule_details_flyout';
|
||||
import { RuleTypeChangeCallout } from '../../rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/rule_type_change_callout';
|
||||
import { RuleDiffTab } from '../components/rule_details/rule_diff_tab';
|
||||
import { useRulePreviewFlyout } from '../../rule_management_ui/components/rules_table/use_rule_preview_flyout';
|
||||
import type { UpgradePrebuiltRulesSortingOptions } from '../../rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_context';
|
||||
import { RULES_TABLE_INITIAL_PAGE_SIZE } from '../../rule_management_ui/components/rules_table/constants';
|
||||
|
||||
const REVIEW_PREBUILT_RULES_UPGRADE_REFRESH_INTERVAL = 5 * 60 * 1000;
|
||||
|
||||
export const PREBUILT_RULE_UPDATE_FLYOUT_ANCHOR = 'updatePrebuiltRulePreview';
|
||||
|
||||
export interface UsePrebuiltRulesUpgradeParams {
|
||||
pagination?: {
|
||||
page: number;
|
||||
perPage: number;
|
||||
};
|
||||
sort?: { order: UpgradePrebuiltRulesSortingOptions['order']; field: FindRulesSortField };
|
||||
filter: ReviewPrebuiltRuleUpgradeFilter;
|
||||
onUpgrade?: () => void;
|
||||
}
|
||||
|
||||
export function usePrebuiltRulesUpgrade({
|
||||
pagination = { page: 1, perPage: RULES_TABLE_INITIAL_PAGE_SIZE },
|
||||
sort,
|
||||
filter,
|
||||
onUpgrade,
|
||||
}: UsePrebuiltRulesUpgradeParams) {
|
||||
const { isRulesCustomizationEnabled, customizationDisabledReason } =
|
||||
usePrebuiltRulesCustomizationStatus();
|
||||
const isUpgradingSecurityPackages = useIsUpgradingSecurityPackages();
|
||||
const [loadingRules, setLoadingRules] = useState<RuleSignatureId[]>([]);
|
||||
|
||||
const {
|
||||
data: upgradeReviewResponse,
|
||||
refetch,
|
||||
dataUpdatedAt,
|
||||
isFetched,
|
||||
isLoading,
|
||||
isFetching,
|
||||
isRefetching,
|
||||
} = usePrebuiltRulesUpgradeReview(
|
||||
{
|
||||
page: pagination.page,
|
||||
per_page: pagination.perPage,
|
||||
sort,
|
||||
filter,
|
||||
},
|
||||
{
|
||||
refetchInterval: REVIEW_PREBUILT_RULES_UPGRADE_REFRESH_INTERVAL,
|
||||
keepPreviousData: true, // Use this option so that the state doesn't jump between "success" and "loading" on page change
|
||||
}
|
||||
);
|
||||
|
||||
const upgradeableRules = useMemo(
|
||||
() => upgradeReviewResponse?.rules ?? [],
|
||||
[upgradeReviewResponse]
|
||||
);
|
||||
|
||||
const { rulesUpgradeState, setRuleFieldResolvedValue } =
|
||||
usePrebuiltRulesUpgradeState(upgradeableRules);
|
||||
const ruleUpgradeStates = useMemo(() => Object.values(rulesUpgradeState), [rulesUpgradeState]);
|
||||
|
||||
const {
|
||||
modal: confirmLegacyMlJobsUpgradeModal,
|
||||
confirmLegacyMLJobs,
|
||||
isLoading: areMlJobsLoading,
|
||||
} = useOutdatedMlJobsUpgradeModal();
|
||||
const { modal: upgradeConflictsModal, confirmConflictsUpgrade } = useUpgradeWithConflictsModal();
|
||||
|
||||
const { mutateAsync: upgradeRulesRequest } = usePerformUpgradeRules();
|
||||
|
||||
const upgradeRulesToResolved = useCallback(
|
||||
async (ruleIds: RuleSignatureId[]) => {
|
||||
const conflictRuleIdsSet = new Set(
|
||||
ruleIds.filter(
|
||||
(ruleId) =>
|
||||
rulesUpgradeState[ruleId].diff.num_fields_with_conflicts > 0 &&
|
||||
rulesUpgradeState[ruleId].hasUnresolvedConflicts
|
||||
)
|
||||
);
|
||||
|
||||
const upgradingRuleIds = ruleIds.filter((ruleId) => !conflictRuleIdsSet.has(ruleId));
|
||||
const ruleUpgradeSpecifiers: RuleUpgradeSpecifier[] = upgradingRuleIds.map((ruleId) => ({
|
||||
rule_id: ruleId,
|
||||
version: rulesUpgradeState[ruleId].target_rule.version,
|
||||
revision: rulesUpgradeState[ruleId].revision,
|
||||
fields: constructRuleFieldsToUpgrade(rulesUpgradeState[ruleId]),
|
||||
}));
|
||||
|
||||
setLoadingRules((prev) => [...prev, ...upgradingRuleIds]);
|
||||
|
||||
try {
|
||||
// Handle MLJobs modal
|
||||
if (!(await confirmLegacyMLJobs())) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (conflictRuleIdsSet.size > 0 && !(await confirmConflictsUpgrade())) {
|
||||
return;
|
||||
}
|
||||
|
||||
await upgradeRulesRequest({
|
||||
mode: 'SPECIFIC_RULES',
|
||||
pick_version: 'MERGED',
|
||||
rules: ruleUpgradeSpecifiers,
|
||||
});
|
||||
} catch {
|
||||
// Error is handled by the mutation's onError callback, so no need to do anything here
|
||||
} finally {
|
||||
const upgradedRuleIdsSet = new Set(upgradingRuleIds);
|
||||
|
||||
if (onUpgrade) {
|
||||
onUpgrade();
|
||||
}
|
||||
|
||||
setLoadingRules((prev) => prev.filter((id) => !upgradedRuleIdsSet.has(id)));
|
||||
}
|
||||
},
|
||||
[
|
||||
rulesUpgradeState,
|
||||
confirmLegacyMLJobs,
|
||||
confirmConflictsUpgrade,
|
||||
upgradeRulesRequest,
|
||||
onUpgrade,
|
||||
]
|
||||
);
|
||||
|
||||
const upgradeRulesToTarget = useCallback(
|
||||
async (ruleIds: RuleSignatureId[]) => {
|
||||
const ruleUpgradeSpecifiers: RuleUpgradeSpecifier[] = ruleIds.map((ruleId) => ({
|
||||
rule_id: ruleId,
|
||||
version: rulesUpgradeState[ruleId].target_rule.version,
|
||||
revision: rulesUpgradeState[ruleId].revision,
|
||||
}));
|
||||
|
||||
setLoadingRules((prev) => [...prev, ...ruleIds]);
|
||||
|
||||
try {
|
||||
// Handle MLJobs modal
|
||||
if (!(await confirmLegacyMLJobs())) {
|
||||
return;
|
||||
}
|
||||
|
||||
await upgradeRulesRequest({
|
||||
mode: 'SPECIFIC_RULES',
|
||||
pick_version: 'TARGET',
|
||||
rules: ruleUpgradeSpecifiers,
|
||||
});
|
||||
} catch {
|
||||
// Error is handled by the mutation's onError callback, so no need to do anything here
|
||||
} finally {
|
||||
const upgradedRuleIdsSet = new Set(ruleIds);
|
||||
|
||||
if (onUpgrade) {
|
||||
onUpgrade();
|
||||
}
|
||||
|
||||
setLoadingRules((prev) => prev.filter((id) => !upgradedRuleIdsSet.has(id)));
|
||||
}
|
||||
},
|
||||
[confirmLegacyMLJobs, onUpgrade, rulesUpgradeState, upgradeRulesRequest]
|
||||
);
|
||||
|
||||
const upgradeRules = useCallback(
|
||||
async (ruleIds: RuleSignatureId[]) => {
|
||||
if (isRulesCustomizationEnabled) {
|
||||
await upgradeRulesToResolved(ruleIds);
|
||||
} else {
|
||||
await upgradeRulesToTarget(ruleIds);
|
||||
}
|
||||
},
|
||||
[isRulesCustomizationEnabled, upgradeRulesToResolved, upgradeRulesToTarget]
|
||||
);
|
||||
|
||||
const upgradeAllRules = useCallback(async () => {
|
||||
setLoadingRules((prev) => [...prev, ...upgradeableRules.map((rule) => rule.rule_id)]);
|
||||
|
||||
try {
|
||||
// Handle MLJobs modal
|
||||
if (!(await confirmLegacyMLJobs())) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dryRunResults = await upgradeRulesRequest({
|
||||
mode: 'ALL_RULES',
|
||||
pick_version: isRulesCustomizationEnabled ? 'MERGED' : 'TARGET',
|
||||
filter,
|
||||
dry_run: true,
|
||||
on_conflict: 'SKIP',
|
||||
});
|
||||
|
||||
const hasConflicts = dryRunResults.results.skipped.some(
|
||||
(skippedRule) => skippedRule.reason === 'CONFLICT'
|
||||
);
|
||||
|
||||
if (hasConflicts && !(await confirmConflictsUpgrade())) {
|
||||
return;
|
||||
}
|
||||
|
||||
await upgradeRulesRequest({
|
||||
mode: 'ALL_RULES',
|
||||
pick_version: isRulesCustomizationEnabled ? 'MERGED' : 'TARGET',
|
||||
filter,
|
||||
on_conflict: 'SKIP',
|
||||
});
|
||||
} catch {
|
||||
// Error is handled by the mutation's onError callback, so no need to do anything here
|
||||
} finally {
|
||||
setLoadingRules([]);
|
||||
}
|
||||
}, [
|
||||
upgradeableRules,
|
||||
confirmLegacyMLJobs,
|
||||
upgradeRulesRequest,
|
||||
isRulesCustomizationEnabled,
|
||||
filter,
|
||||
confirmConflictsUpgrade,
|
||||
]);
|
||||
|
||||
const subHeaderFactory = useCallback(
|
||||
(rule: RuleResponse) =>
|
||||
rulesUpgradeState[rule.rule_id] ? (
|
||||
<UpgradeFlyoutSubHeader ruleUpgradeState={rulesUpgradeState[rule.rule_id]} />
|
||||
) : null,
|
||||
[rulesUpgradeState]
|
||||
);
|
||||
const ruleActionsFactory = useCallback(
|
||||
(rule: RuleResponse, closeRulePreview: () => void, isEditingRule: boolean) => {
|
||||
const ruleUpgradeState = rulesUpgradeState[rule.rule_id];
|
||||
if (!ruleUpgradeState) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hasRuleTypeChange = ruleUpgradeState.diff.fields.type?.has_update ?? false;
|
||||
return (
|
||||
<EuiButton
|
||||
disabled={
|
||||
loadingRules.includes(rule.rule_id) ||
|
||||
isRefetching ||
|
||||
isUpgradingSecurityPackages ||
|
||||
(ruleUpgradeState.hasUnresolvedConflicts && !hasRuleTypeChange) ||
|
||||
isEditingRule
|
||||
}
|
||||
onClick={() => {
|
||||
if (hasRuleTypeChange || isRulesCustomizationEnabled === false) {
|
||||
// If there is a rule type change, we can't resolve conflicts, only accept the target rule
|
||||
upgradeRulesToTarget([rule.rule_id]);
|
||||
} else {
|
||||
upgradeRulesToResolved([rule.rule_id]);
|
||||
}
|
||||
closeRulePreview();
|
||||
}}
|
||||
fill
|
||||
data-test-subj="updatePrebuiltRuleFromFlyoutButton"
|
||||
>
|
||||
{i18n.UPDATE_BUTTON_LABEL}
|
||||
</EuiButton>
|
||||
);
|
||||
},
|
||||
[
|
||||
rulesUpgradeState,
|
||||
loadingRules,
|
||||
isRefetching,
|
||||
isUpgradingSecurityPackages,
|
||||
isRulesCustomizationEnabled,
|
||||
upgradeRulesToTarget,
|
||||
upgradeRulesToResolved,
|
||||
]
|
||||
);
|
||||
const extraTabsFactory = useCallback(
|
||||
(rule: RuleResponse) => {
|
||||
const ruleUpgradeState = rulesUpgradeState[rule.rule_id];
|
||||
|
||||
if (!ruleUpgradeState) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const hasRuleTypeChange = ruleUpgradeState.diff.fields.type?.has_update ?? false;
|
||||
const hasCustomizations =
|
||||
ruleUpgradeState.current_rule.rule_source.type === 'external' &&
|
||||
ruleUpgradeState.current_rule.rule_source.is_customized;
|
||||
|
||||
let headerCallout = null;
|
||||
if (
|
||||
hasCustomizations &&
|
||||
customizationDisabledReason === PrebuiltRulesCustomizationDisabledReason.License
|
||||
) {
|
||||
headerCallout = <CustomizationDisabledCallout />;
|
||||
} else if (hasRuleTypeChange && isRulesCustomizationEnabled) {
|
||||
headerCallout = <RuleTypeChangeCallout hasCustomizations={hasCustomizations} />;
|
||||
}
|
||||
|
||||
let updateTabContent = (
|
||||
<PerFieldRuleDiffTab header={headerCallout} ruleDiff={ruleUpgradeState.diff} />
|
||||
);
|
||||
|
||||
// Show the resolver tab only if rule customization is enabled and there
|
||||
// is no rule type change. In case of rule type change users can't resolve
|
||||
// conflicts, only accept the target rule.
|
||||
if (isRulesCustomizationEnabled && !hasRuleTypeChange) {
|
||||
updateTabContent = (
|
||||
<RuleUpgradeTab
|
||||
ruleUpgradeState={ruleUpgradeState}
|
||||
setRuleFieldResolvedValue={setRuleFieldResolvedValue}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const updatesTab = {
|
||||
id: 'updates',
|
||||
name: (
|
||||
<EuiToolTip position="top" content={i18n.UPDATE_FLYOUT_PER_FIELD_TOOLTIP_DESCRIPTION}>
|
||||
<>{ruleDetailsI18n.UPDATES_TAB_LABEL}</>
|
||||
</EuiToolTip>
|
||||
),
|
||||
content: <TabContentPadding>{updateTabContent}</TabContentPadding>,
|
||||
};
|
||||
|
||||
const jsonViewTab = {
|
||||
id: 'jsonViewUpdates',
|
||||
name: (
|
||||
<EuiToolTip position="top" content={i18n.UPDATE_FLYOUT_JSON_VIEW_TOOLTIP_DESCRIPTION}>
|
||||
<>{ruleDetailsI18n.JSON_VIEW_UPDATES_TAB_LABEL}</>
|
||||
</EuiToolTip>
|
||||
),
|
||||
content: (
|
||||
<div>
|
||||
<RuleDiffTab
|
||||
oldRule={ruleUpgradeState.current_rule}
|
||||
newRule={ruleUpgradeState.target_rule}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
return [updatesTab, jsonViewTab];
|
||||
},
|
||||
[
|
||||
rulesUpgradeState,
|
||||
customizationDisabledReason,
|
||||
isRulesCustomizationEnabled,
|
||||
setRuleFieldResolvedValue,
|
||||
]
|
||||
);
|
||||
const { rulePreviewFlyout, openRulePreview } = useRulePreviewFlyout({
|
||||
rules: ruleUpgradeStates.map(({ target_rule: targetRule }) => targetRule),
|
||||
subHeaderFactory,
|
||||
ruleActionsFactory,
|
||||
extraTabsFactory,
|
||||
flyoutProps: {
|
||||
id: PREBUILT_RULE_UPDATE_FLYOUT_ANCHOR,
|
||||
dataTestSubj: PREBUILT_RULE_UPDATE_FLYOUT_ANCHOR,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
ruleUpgradeStates,
|
||||
upgradeReviewResponse,
|
||||
isFetched,
|
||||
isLoading: isLoading || areMlJobsLoading,
|
||||
isFetching,
|
||||
isRefetching,
|
||||
isUpgradingSecurityPackages,
|
||||
loadingRules,
|
||||
lastUpdated: dataUpdatedAt,
|
||||
rulePreviewFlyout,
|
||||
confirmLegacyMlJobsUpgradeModal,
|
||||
upgradeConflictsModal,
|
||||
openRulePreview,
|
||||
reFetchRules: refetch,
|
||||
upgradeRules,
|
||||
upgradeAllRules,
|
||||
};
|
||||
}
|
||||
|
||||
function constructRuleFieldsToUpgrade(ruleUpgradeState: RuleUpgradeState): RuleFieldsToUpgrade {
|
||||
const ruleFieldsToUpgrade: Record<string, unknown> = {};
|
||||
|
||||
for (const [fieldName, fieldUpgradeState] of Object.entries(
|
||||
ruleUpgradeState.fieldsUpgradeState
|
||||
)) {
|
||||
if (fieldUpgradeState.state === FieldUpgradeStateEnum.Accepted) {
|
||||
ruleFieldsToUpgrade[fieldName] = {
|
||||
pick_version: 'RESOLVED',
|
||||
resolved_value: fieldUpgradeState.resolvedValue,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return ruleFieldsToUpgrade;
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import type { RuleResponse } from '../../../../common/api/detection_engine';
|
||||
import { RuleUpdateCallout } from '../components/rule_details/rule_update_callout';
|
||||
|
||||
interface UseRuleUpdateCalloutProps {
|
||||
rule: RuleResponse | null;
|
||||
message: string;
|
||||
actionButton?: JSX.Element;
|
||||
onUpgrade?: () => void;
|
||||
}
|
||||
|
||||
export const useRuleUpdateCallout = ({
|
||||
rule,
|
||||
message,
|
||||
actionButton,
|
||||
onUpgrade,
|
||||
}: UseRuleUpdateCalloutProps): JSX.Element | null =>
|
||||
!rule || rule.rule_source.type !== 'external' ? null : (
|
||||
<RuleUpdateCallout
|
||||
actionButton={actionButton}
|
||||
message={message}
|
||||
rule={rule}
|
||||
onUpgrade={onUpgrade}
|
||||
/>
|
||||
);
|
|
@ -17,4 +17,8 @@ export interface RuleUpgradeState extends RuleUpgradeInfoForReview {
|
|||
* Indicates whether there are conflicts blocking rule upgrading.
|
||||
*/
|
||||
hasUnresolvedConflicts: boolean;
|
||||
/**
|
||||
* Indicates whether there are non-solvable conflicts blocking rule upgrading.
|
||||
*/
|
||||
hasNonSolvableUnresolvedConflicts: boolean;
|
||||
}
|
||||
|
|
|
@ -34,7 +34,7 @@ type OnClick = () => void;
|
|||
export const getUpdateRulesCalloutTitle = (onClick: OnClick) => (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.detectionEngine.rules.updatePrebuiltRulesCalloutTitle"
|
||||
defaultMessage="Updates available for installed rules. Review and update in {link}."
|
||||
defaultMessage="Some Elastic rules have updates available. Update them to ensure you get the best detection experience. Review and update in {link}."
|
||||
values={{
|
||||
link: (
|
||||
<EuiLink
|
||||
|
|
|
@ -18,15 +18,23 @@ import {
|
|||
} from '../mini_callout/translations';
|
||||
import { AllRulesTabs } from '../rules_table/rules_table_toolbar';
|
||||
|
||||
export const RuleUpdateCallouts = () => {
|
||||
interface RuleUpdateCalloutsProps {
|
||||
shouldShowNewRulesCallout?: boolean;
|
||||
shouldShowUpdateRulesCallout?: boolean;
|
||||
}
|
||||
|
||||
export const RuleUpdateCallouts = ({
|
||||
shouldShowNewRulesCallout = false,
|
||||
shouldShowUpdateRulesCallout = false,
|
||||
}: RuleUpdateCalloutsProps) => {
|
||||
const { data: prebuiltRulesStatus } = usePrebuiltRulesStatus();
|
||||
|
||||
const rulesToInstallCount = prebuiltRulesStatus?.stats.num_prebuilt_rules_to_install ?? 0;
|
||||
const rulesToUpgradeCount = prebuiltRulesStatus?.stats.num_prebuilt_rules_to_upgrade ?? 0;
|
||||
|
||||
// Check against rulesInstalledCount since we don't want to show banners if we're showing the empty prompt
|
||||
const shouldDisplayNewRulesCallout = rulesToInstallCount > 0;
|
||||
const shouldDisplayUpdateRulesCallout = rulesToUpgradeCount > 0;
|
||||
const shouldDisplayNewRulesCallout = shouldShowNewRulesCallout && rulesToInstallCount > 0;
|
||||
const shouldDisplayUpdateRulesCallout = shouldShowUpdateRulesCallout && rulesToUpgradeCount > 0;
|
||||
|
||||
const getSecuritySolutionLinkProps = useGetSecuritySolutionLinkProps();
|
||||
const { href } = getSecuritySolutionLinkProps({
|
||||
|
|
|
@ -78,7 +78,7 @@ export const AddPrebuiltRulesTable = React.memo(() => {
|
|||
) : (
|
||||
<>
|
||||
<EuiFlexGroup direction="column">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexItem grow={false} css={{ alignSelf: 'start' }}>
|
||||
<RulesChangelogLink />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
|
|
|
@ -26,8 +26,8 @@ import React, { useEffect, useMemo } from 'react';
|
|||
import { NEW_FEATURES_TOUR_STORAGE_KEYS } from '../../../../../../common/constants';
|
||||
import { useKibana } from '../../../../../common/lib/kibana';
|
||||
import { useIsElementMounted } from '../rules_table/guided_onboarding/use_is_element_mounted';
|
||||
import { PREBUILT_RULE_UPDATE_FLYOUT_ANCHOR } from '../upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_context';
|
||||
import * as i18n from './translations';
|
||||
import { PREBUILT_RULE_UPDATE_FLYOUT_ANCHOR } from '../../../../rule_management/hooks/use_prebuilt_rules_upgrade';
|
||||
|
||||
export interface RulesFeatureTourContextType {
|
||||
steps: EuiTourStepProps[];
|
||||
|
|
|
@ -97,7 +97,7 @@ export const UpgradePrebuiltRulesTable = React.memo(() => {
|
|||
) : (
|
||||
<>
|
||||
<EuiFlexGroup direction="column">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexItem grow={false} css={{ alignSelf: 'start' }}>
|
||||
<RulesChangelogLink />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
|
|
|
@ -5,46 +5,29 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiButton, EuiToolTip } from '@elastic/eui';
|
||||
import type { Dispatch, SetStateAction } from 'react';
|
||||
import React, { createContext, useCallback, useContext, useMemo, useState } from 'react';
|
||||
import { PrebuiltRulesCustomizationDisabledReason } from '../../../../../../common/detection_engine/prebuilt_rules/prebuilt_rule_customization_status';
|
||||
import React, { createContext, useContext, useMemo, useState } from 'react';
|
||||
import type {
|
||||
FindRulesSortField,
|
||||
PrebuiltRulesFilter,
|
||||
RuleFieldsToUpgrade,
|
||||
RuleUpgradeSpecifier,
|
||||
SortOrder,
|
||||
} from '../../../../../../common/api/detection_engine';
|
||||
import { usePrebuiltRulesCustomizationStatus } from '../../../../rule_management/logic/prebuilt_rules/use_prebuilt_rules_customization_status';
|
||||
import type { RuleUpgradeState } from '../../../../rule_management/model/prebuilt_rule_upgrade';
|
||||
import { RuleUpgradeTab } from '../../../../rule_management/components/rule_details/three_way_diff';
|
||||
import { PerFieldRuleDiffTab } from '../../../../rule_management/components/rule_details/per_field_rule_diff_tab';
|
||||
import { useIsUpgradingSecurityPackages } from '../../../../rule_management/logic/use_upgrade_security_packages';
|
||||
import type {
|
||||
RuleResponse,
|
||||
RuleSignatureId,
|
||||
} from '../../../../../../common/api/detection_engine/model/rule_schema';
|
||||
import type { RuleSignatureId } from '../../../../../../common/api/detection_engine/model/rule_schema';
|
||||
import { invariant } from '../../../../../../common/utils/invariant';
|
||||
import { TabContentPadding } from '../../../../rule_management/components/rule_details/rule_details_flyout';
|
||||
import { usePerformUpgradeRules } from '../../../../rule_management/logic/prebuilt_rules/use_perform_rule_upgrade';
|
||||
import { usePrebuiltRulesUpgradeReview } from '../../../../rule_management/logic/prebuilt_rules/use_prebuilt_rules_upgrade_review';
|
||||
import { RuleDiffTab } from '../../../../rule_management/components/rule_details/rule_diff_tab';
|
||||
import { FieldUpgradeStateEnum } from '../../../../rule_management/model/prebuilt_rule_upgrade/field_upgrade_state_enum';
|
||||
import { useRulePreviewFlyout } from '../use_rule_preview_flyout';
|
||||
import { usePrebuiltRulesUpgradeState } from './use_prebuilt_rules_upgrade_state';
|
||||
import { useOutdatedMlJobsUpgradeModal } from './use_ml_jobs_upgrade_modal';
|
||||
import { useUpgradeWithConflictsModal } from './use_upgrade_with_conflicts_modal';
|
||||
import { RuleTypeChangeCallout } from './rule_type_change_callout';
|
||||
import { UpgradeFlyoutSubHeader } from './upgrade_flyout_subheader';
|
||||
import * as ruleDetailsI18n from '../../../../rule_management/components/rule_details/translations';
|
||||
import * as i18n from './translations';
|
||||
import { CustomizationDisabledCallout } from './customization_disabled_callout';
|
||||
import { RULES_TABLE_INITIAL_PAGE_SIZE } from '../constants';
|
||||
import type { PaginationOptions } from '../../../../rule_management/logic';
|
||||
import { usePrebuiltRulesStatus } from '../../../../rule_management/logic/prebuilt_rules/use_prebuilt_rules_status';
|
||||
import { usePrebuiltRulesUpgrade } from '../../../../rule_management/hooks/use_prebuilt_rules_upgrade';
|
||||
|
||||
const REVIEW_PREBUILT_RULES_UPGRADE_REFRESH_INTERVAL = 5 * 60 * 1000;
|
||||
export interface UpgradePrebuiltRulesSortingOptions {
|
||||
field:
|
||||
| 'current_rule.name'
|
||||
| 'current_rule.risk_score'
|
||||
| 'current_rule.severity'
|
||||
| 'current_rule.last_updated';
|
||||
order: SortOrder;
|
||||
}
|
||||
|
||||
export interface UpgradePrebuiltRulesSortingOptions {
|
||||
field:
|
||||
|
@ -111,8 +94,6 @@ export interface UpgradePrebuiltRulesTableState {
|
|||
sortingOptions: UpgradePrebuiltRulesSortingOptions;
|
||||
}
|
||||
|
||||
export const PREBUILT_RULE_UPDATE_FLYOUT_ANCHOR = 'updatePrebuiltRulePreview';
|
||||
|
||||
export interface UpgradePrebuiltRulesTableActions {
|
||||
reFetchRules: () => void;
|
||||
upgradeRules: (ruleIds: RuleSignatureId[]) => void;
|
||||
|
@ -146,9 +127,6 @@ interface UpgradePrebuiltRulesTableContextProviderProps {
|
|||
export const UpgradePrebuiltRulesTableContextProvider = ({
|
||||
children,
|
||||
}: UpgradePrebuiltRulesTableContextProviderProps) => {
|
||||
const { isRulesCustomizationEnabled, customizationDisabledReason } =
|
||||
usePrebuiltRulesCustomizationStatus();
|
||||
|
||||
// Use the data from the prebuilt rules status API to determine if there are
|
||||
// rules to upgrade because it returns information about all rules without filters
|
||||
const { data: prebuiltRulesStatusResponse } = usePrebuiltRulesStatus();
|
||||
|
@ -156,9 +134,7 @@ export const UpgradePrebuiltRulesTableContextProvider = ({
|
|||
(prebuiltRulesStatusResponse?.stats.num_prebuilt_rules_to_upgrade ?? 0) > 0;
|
||||
const tags = prebuiltRulesStatusResponse?.aggregated_fields?.upgradeable_rules.tags;
|
||||
|
||||
const [loadingRules, setLoadingRules] = useState<RuleSignatureId[]>([]);
|
||||
const [filterOptions, setFilterOptions] = useState<PrebuiltRulesFilter>({});
|
||||
const isUpgradingSecurityPackages = useIsUpgradingSecurityPackages();
|
||||
const [pagination, setPagination] = useState({
|
||||
page: 1,
|
||||
perPage: RULES_TABLE_INITIAL_PAGE_SIZE,
|
||||
|
@ -182,319 +158,34 @@ export const UpgradePrebuiltRulesTableContextProvider = ({
|
|||
);
|
||||
|
||||
const {
|
||||
data: upgradeReviewResponse,
|
||||
refetch,
|
||||
dataUpdatedAt,
|
||||
ruleUpgradeStates,
|
||||
upgradeReviewResponse,
|
||||
isFetched,
|
||||
isLoading,
|
||||
isFetching,
|
||||
isRefetching,
|
||||
} = usePrebuiltRulesUpgradeReview(
|
||||
{
|
||||
page: pagination.page,
|
||||
per_page: pagination.perPage,
|
||||
sort: {
|
||||
field: findRulesSortField,
|
||||
order: sortingOptions.order,
|
||||
},
|
||||
filter: filterOptions,
|
||||
},
|
||||
{
|
||||
refetchInterval: REVIEW_PREBUILT_RULES_UPGRADE_REFRESH_INTERVAL,
|
||||
keepPreviousData: true, // Use this option so that the state doesn't jump between "success" and "loading" on page change
|
||||
}
|
||||
);
|
||||
|
||||
const upgradeableRules = useMemo(
|
||||
() => upgradeReviewResponse?.rules ?? [],
|
||||
[upgradeReviewResponse]
|
||||
);
|
||||
|
||||
const { rulesUpgradeState, setRuleFieldResolvedValue } =
|
||||
usePrebuiltRulesUpgradeState(upgradeableRules);
|
||||
const ruleUpgradeStates = useMemo(() => Object.values(rulesUpgradeState), [rulesUpgradeState]);
|
||||
|
||||
const {
|
||||
modal: confirmLegacyMlJobsUpgradeModal,
|
||||
confirmLegacyMLJobs,
|
||||
isLoading: areMlJobsLoading,
|
||||
} = useOutdatedMlJobsUpgradeModal();
|
||||
const { modal: upgradeConflictsModal, confirmConflictsUpgrade } = useUpgradeWithConflictsModal();
|
||||
|
||||
const { mutateAsync: upgradeRulesRequest } = usePerformUpgradeRules();
|
||||
|
||||
const upgradeRulesToResolved = useCallback(
|
||||
async (ruleIds: RuleSignatureId[]) => {
|
||||
const conflictRuleIdsSet = new Set(
|
||||
ruleIds.filter(
|
||||
(ruleId) =>
|
||||
rulesUpgradeState[ruleId].diff.num_fields_with_conflicts > 0 &&
|
||||
rulesUpgradeState[ruleId].hasUnresolvedConflicts
|
||||
)
|
||||
);
|
||||
const upgradingRuleIds = ruleIds.filter((ruleId) => !conflictRuleIdsSet.has(ruleId));
|
||||
const ruleUpgradeSpecifiers: RuleUpgradeSpecifier[] = upgradingRuleIds.map((ruleId) => ({
|
||||
rule_id: ruleId,
|
||||
version: rulesUpgradeState[ruleId].target_rule.version,
|
||||
revision: rulesUpgradeState[ruleId].revision,
|
||||
fields: constructRuleFieldsToUpgrade(rulesUpgradeState[ruleId]),
|
||||
}));
|
||||
|
||||
setLoadingRules((prev) => [...prev, ...upgradingRuleIds]);
|
||||
|
||||
try {
|
||||
// Handle MLJobs modal
|
||||
if (!(await confirmLegacyMLJobs())) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (conflictRuleIdsSet.size > 0 && !(await confirmConflictsUpgrade())) {
|
||||
return;
|
||||
}
|
||||
|
||||
await upgradeRulesRequest({
|
||||
mode: 'SPECIFIC_RULES',
|
||||
pick_version: 'MERGED',
|
||||
rules: ruleUpgradeSpecifiers,
|
||||
});
|
||||
} catch {
|
||||
// Error is handled by the mutation's onError callback, so no need to do anything here
|
||||
} finally {
|
||||
const upgradedRuleIdsSet = new Set(upgradingRuleIds);
|
||||
|
||||
setLoadingRules((prev) => prev.filter((id) => !upgradedRuleIdsSet.has(id)));
|
||||
}
|
||||
},
|
||||
[confirmLegacyMLJobs, confirmConflictsUpgrade, rulesUpgradeState, upgradeRulesRequest]
|
||||
);
|
||||
|
||||
const upgradeRulesToTarget = useCallback(
|
||||
async (ruleIds: RuleSignatureId[]) => {
|
||||
const ruleUpgradeSpecifiers: RuleUpgradeSpecifier[] = ruleIds.map((ruleId) => ({
|
||||
rule_id: ruleId,
|
||||
version: rulesUpgradeState[ruleId].target_rule.version,
|
||||
revision: rulesUpgradeState[ruleId].revision,
|
||||
}));
|
||||
|
||||
setLoadingRules((prev) => [...prev, ...ruleIds]);
|
||||
|
||||
try {
|
||||
// Handle MLJobs modal
|
||||
if (!(await confirmLegacyMLJobs())) {
|
||||
return;
|
||||
}
|
||||
|
||||
await upgradeRulesRequest({
|
||||
mode: 'SPECIFIC_RULES',
|
||||
pick_version: 'TARGET',
|
||||
rules: ruleUpgradeSpecifiers,
|
||||
});
|
||||
} catch {
|
||||
// Error is handled by the mutation's onError callback, so no need to do anything here
|
||||
} finally {
|
||||
const upgradedRuleIdsSet = new Set(ruleIds);
|
||||
|
||||
setLoadingRules((prev) => prev.filter((id) => !upgradedRuleIdsSet.has(id)));
|
||||
}
|
||||
},
|
||||
[confirmLegacyMLJobs, rulesUpgradeState, upgradeRulesRequest]
|
||||
);
|
||||
|
||||
const upgradeRules = useCallback(
|
||||
async (ruleIds: RuleSignatureId[]) => {
|
||||
if (isRulesCustomizationEnabled) {
|
||||
await upgradeRulesToResolved(ruleIds);
|
||||
} else {
|
||||
await upgradeRulesToTarget(ruleIds);
|
||||
}
|
||||
},
|
||||
[isRulesCustomizationEnabled, upgradeRulesToResolved, upgradeRulesToTarget]
|
||||
);
|
||||
|
||||
const upgradeAllRules = useCallback(async () => {
|
||||
setLoadingRules((prev) => [...prev, ...upgradeableRules.map((rule) => rule.rule_id)]);
|
||||
|
||||
try {
|
||||
// Handle MLJobs modal
|
||||
if (!(await confirmLegacyMLJobs())) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dryRunResults = await upgradeRulesRequest({
|
||||
mode: 'ALL_RULES',
|
||||
pick_version: isRulesCustomizationEnabled ? 'MERGED' : 'TARGET',
|
||||
filter: filterOptions,
|
||||
dry_run: true,
|
||||
on_conflict: 'SKIP',
|
||||
});
|
||||
|
||||
const hasConflicts = dryRunResults.results.skipped.some(
|
||||
(skippedRule) => skippedRule.reason === 'CONFLICT'
|
||||
);
|
||||
|
||||
if (hasConflicts && !(await confirmConflictsUpgrade())) {
|
||||
return;
|
||||
}
|
||||
|
||||
await upgradeRulesRequest({
|
||||
mode: 'ALL_RULES',
|
||||
pick_version: isRulesCustomizationEnabled ? 'MERGED' : 'TARGET',
|
||||
filter: filterOptions,
|
||||
on_conflict: 'SKIP',
|
||||
});
|
||||
} catch {
|
||||
// Error is handled by the mutation's onError callback, so no need to do anything here
|
||||
} finally {
|
||||
setLoadingRules([]);
|
||||
}
|
||||
}, [
|
||||
upgradeableRules,
|
||||
confirmLegacyMLJobs,
|
||||
upgradeRulesRequest,
|
||||
isRulesCustomizationEnabled,
|
||||
filterOptions,
|
||||
confirmConflictsUpgrade,
|
||||
]);
|
||||
|
||||
const subHeaderFactory = useCallback(
|
||||
(rule: RuleResponse) =>
|
||||
rulesUpgradeState[rule.rule_id] ? (
|
||||
<UpgradeFlyoutSubHeader ruleUpgradeState={rulesUpgradeState[rule.rule_id]} />
|
||||
) : null,
|
||||
[rulesUpgradeState]
|
||||
);
|
||||
const ruleActionsFactory = useCallback(
|
||||
(rule: RuleResponse, closeRulePreview: () => void, isEditingRule: boolean) => {
|
||||
const ruleUpgradeState = rulesUpgradeState[rule.rule_id];
|
||||
if (!ruleUpgradeState) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hasRuleTypeChange = ruleUpgradeState.diff.fields.type?.has_update ?? false;
|
||||
return (
|
||||
<EuiButton
|
||||
disabled={
|
||||
loadingRules.includes(rule.rule_id) ||
|
||||
isRefetching ||
|
||||
isUpgradingSecurityPackages ||
|
||||
(ruleUpgradeState.hasUnresolvedConflicts && !hasRuleTypeChange) ||
|
||||
isEditingRule
|
||||
}
|
||||
onClick={() => {
|
||||
if (hasRuleTypeChange || isRulesCustomizationEnabled === false) {
|
||||
// If there is a rule type change, we can't resolve conflicts, only accept the target rule
|
||||
upgradeRulesToTarget([rule.rule_id]);
|
||||
} else {
|
||||
upgradeRulesToResolved([rule.rule_id]);
|
||||
}
|
||||
closeRulePreview();
|
||||
}}
|
||||
fill
|
||||
data-test-subj="updatePrebuiltRuleFromFlyoutButton"
|
||||
>
|
||||
{i18n.UPDATE_BUTTON_LABEL}
|
||||
</EuiButton>
|
||||
);
|
||||
},
|
||||
[
|
||||
rulesUpgradeState,
|
||||
loadingRules,
|
||||
isRefetching,
|
||||
isUpgradingSecurityPackages,
|
||||
isRulesCustomizationEnabled,
|
||||
upgradeRulesToTarget,
|
||||
upgradeRulesToResolved,
|
||||
]
|
||||
);
|
||||
const extraTabsFactory = useCallback(
|
||||
(rule: RuleResponse) => {
|
||||
const ruleUpgradeState = rulesUpgradeState[rule.rule_id];
|
||||
|
||||
if (!ruleUpgradeState) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const hasRuleTypeChange = ruleUpgradeState.diff.fields.type?.has_update ?? false;
|
||||
const hasCustomizations =
|
||||
ruleUpgradeState.current_rule.rule_source.type === 'external' &&
|
||||
ruleUpgradeState.current_rule.rule_source.is_customized;
|
||||
|
||||
let headerCallout = null;
|
||||
if (
|
||||
hasCustomizations &&
|
||||
customizationDisabledReason === PrebuiltRulesCustomizationDisabledReason.License
|
||||
) {
|
||||
headerCallout = <CustomizationDisabledCallout />;
|
||||
} else if (hasRuleTypeChange && isRulesCustomizationEnabled) {
|
||||
headerCallout = <RuleTypeChangeCallout hasCustomizations={hasCustomizations} />;
|
||||
}
|
||||
|
||||
let updateTabContent = (
|
||||
<PerFieldRuleDiffTab header={headerCallout} ruleDiff={ruleUpgradeState.diff} />
|
||||
);
|
||||
|
||||
// Show the resolver tab only if rule customization is enabled and there
|
||||
// is no rule type change. In case of rule type change users can't resolve
|
||||
// conflicts, only accept the target rule.
|
||||
if (isRulesCustomizationEnabled && !hasRuleTypeChange) {
|
||||
updateTabContent = (
|
||||
<RuleUpgradeTab
|
||||
ruleUpgradeState={ruleUpgradeState}
|
||||
setRuleFieldResolvedValue={setRuleFieldResolvedValue}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const updatesTab = {
|
||||
id: 'updates',
|
||||
name: (
|
||||
<EuiToolTip position="top" content={i18n.UPDATE_FLYOUT_PER_FIELD_TOOLTIP_DESCRIPTION}>
|
||||
<>{ruleDetailsI18n.UPDATES_TAB_LABEL}</>
|
||||
</EuiToolTip>
|
||||
),
|
||||
content: <TabContentPadding>{updateTabContent}</TabContentPadding>,
|
||||
};
|
||||
|
||||
const jsonViewTab = {
|
||||
id: 'jsonViewUpdates',
|
||||
name: (
|
||||
<EuiToolTip position="top" content={i18n.UPDATE_FLYOUT_JSON_VIEW_TOOLTIP_DESCRIPTION}>
|
||||
<>{ruleDetailsI18n.JSON_VIEW_UPDATES_TAB_LABEL}</>
|
||||
</EuiToolTip>
|
||||
),
|
||||
content: (
|
||||
<div>
|
||||
<RuleDiffTab
|
||||
oldRule={ruleUpgradeState.current_rule}
|
||||
newRule={ruleUpgradeState.target_rule}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
return [updatesTab, jsonViewTab];
|
||||
},
|
||||
[
|
||||
rulesUpgradeState,
|
||||
customizationDisabledReason,
|
||||
isRulesCustomizationEnabled,
|
||||
setRuleFieldResolvedValue,
|
||||
]
|
||||
);
|
||||
const { rulePreviewFlyout, openRulePreview } = useRulePreviewFlyout({
|
||||
rules: ruleUpgradeStates.map(({ target_rule: targetRule }) => targetRule),
|
||||
subHeaderFactory,
|
||||
ruleActionsFactory,
|
||||
extraTabsFactory,
|
||||
flyoutProps: {
|
||||
id: PREBUILT_RULE_UPDATE_FLYOUT_ANCHOR,
|
||||
dataTestSubj: PREBUILT_RULE_UPDATE_FLYOUT_ANCHOR,
|
||||
isUpgradingSecurityPackages,
|
||||
loadingRules,
|
||||
lastUpdated,
|
||||
rulePreviewFlyout,
|
||||
confirmLegacyMlJobsUpgradeModal,
|
||||
upgradeConflictsModal,
|
||||
openRulePreview,
|
||||
reFetchRules,
|
||||
upgradeRules,
|
||||
upgradeAllRules,
|
||||
} = usePrebuiltRulesUpgrade({
|
||||
pagination,
|
||||
sort: {
|
||||
field: findRulesSortField,
|
||||
order: sortingOptions.order,
|
||||
},
|
||||
filter: filterOptions,
|
||||
});
|
||||
|
||||
const actions = useMemo<UpgradePrebuiltRulesTableActions>(
|
||||
() => ({
|
||||
reFetchRules: refetch,
|
||||
reFetchRules,
|
||||
upgradeRules,
|
||||
upgradeAllRules,
|
||||
setFilterOptions,
|
||||
|
@ -502,7 +193,7 @@ export const UpgradePrebuiltRulesTableContextProvider = ({
|
|||
setPagination,
|
||||
setSortingOptions,
|
||||
}),
|
||||
[refetch, upgradeRules, upgradeAllRules, openRulePreview]
|
||||
[reFetchRules, upgradeRules, upgradeAllRules, openRulePreview]
|
||||
);
|
||||
|
||||
const providerValue = useMemo<UpgradePrebuiltRulesContextType>(
|
||||
|
@ -513,12 +204,12 @@ export const UpgradePrebuiltRulesTableContextProvider = ({
|
|||
filterOptions,
|
||||
tags: tags ?? [],
|
||||
isFetched,
|
||||
isLoading: isLoading || areMlJobsLoading,
|
||||
isLoading,
|
||||
isFetching,
|
||||
isRefetching,
|
||||
isUpgradingSecurityPackages,
|
||||
loadingRules,
|
||||
lastUpdated: dataUpdatedAt,
|
||||
lastUpdated,
|
||||
pagination: {
|
||||
...pagination,
|
||||
total: upgradeReviewResponse?.total ?? 0,
|
||||
|
@ -534,12 +225,11 @@ export const UpgradePrebuiltRulesTableContextProvider = ({
|
|||
tags,
|
||||
isFetched,
|
||||
isLoading,
|
||||
areMlJobsLoading,
|
||||
isFetching,
|
||||
isRefetching,
|
||||
isUpgradingSecurityPackages,
|
||||
loadingRules,
|
||||
dataUpdatedAt,
|
||||
lastUpdated,
|
||||
pagination,
|
||||
upgradeReviewResponse?.total,
|
||||
sortingOptions,
|
||||
|
@ -568,20 +258,3 @@ export const useUpgradePrebuiltRulesTableContext = (): UpgradePrebuiltRulesConte
|
|||
|
||||
return rulesTableContext;
|
||||
};
|
||||
|
||||
function constructRuleFieldsToUpgrade(ruleUpgradeState: RuleUpgradeState): RuleFieldsToUpgrade {
|
||||
const ruleFieldsToUpgrade: Record<string, unknown> = {};
|
||||
|
||||
for (const [fieldName, fieldUpgradeState] of Object.entries(
|
||||
ruleUpgradeState.fieldsUpgradeState
|
||||
)) {
|
||||
if (fieldUpgradeState.state === FieldUpgradeStateEnum.Accepted) {
|
||||
ruleFieldsToUpgrade[fieldName] = {
|
||||
pick_version: 'RESOLVED',
|
||||
resolved_value: fieldUpgradeState.resolvedValue,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return ruleFieldsToUpgrade;
|
||||
}
|
||||
|
|
|
@ -429,6 +429,7 @@ function createRuleUpgradeInfoMock(
|
|||
num_fields_with_non_solvable_conflicts: 0,
|
||||
fields: {},
|
||||
},
|
||||
has_base_version: true,
|
||||
version: 1,
|
||||
revision: 1,
|
||||
...rewrites,
|
||||
|
|
|
@ -134,6 +134,9 @@ export function usePrebuiltRulesUpgradeState(
|
|||
fieldState === FieldUpgradeStateEnum.SolvableConflict ||
|
||||
fieldState === FieldUpgradeStateEnum.NonSolvableConflict
|
||||
);
|
||||
const hasNonSolvableFieldConflicts = Object.values(fieldsUpgradeState).some(
|
||||
({ state: fieldState }) => fieldState === FieldUpgradeStateEnum.NonSolvableConflict
|
||||
);
|
||||
|
||||
state[ruleUpgradeInfo.rule_id] = {
|
||||
...ruleUpgradeInfo,
|
||||
|
@ -141,6 +144,9 @@ export function usePrebuiltRulesUpgradeState(
|
|||
hasUnresolvedConflicts: isRulesCustomizationEnabled
|
||||
? hasRuleTypeChange || hasFieldConflicts
|
||||
: false,
|
||||
hasNonSolvableUnresolvedConflicts: isRulesCustomizationEnabled
|
||||
? hasRuleTypeChange || hasNonSolvableFieldConflicts
|
||||
: false,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -144,6 +144,7 @@ const MODIFIED_COLUMN: TableColumn = {
|
|||
|
||||
const createUpgradeButtonColumn = (
|
||||
upgradeRules: UpgradePrebuiltRulesTableActions['upgradeRules'],
|
||||
openRulePreview: UpgradePrebuiltRulesTableActions['openRulePreview'],
|
||||
loadingRules: RuleSignatureId[],
|
||||
isDisabled: boolean,
|
||||
isPrebuiltRulesCustomizationEnabled: boolean
|
||||
|
@ -154,7 +155,7 @@ const createUpgradeButtonColumn = (
|
|||
const isRuleUpgrading = loadingRules.includes(ruleId);
|
||||
const isDisabledByConflicts =
|
||||
isPrebuiltRulesCustomizationEnabled && record.hasUnresolvedConflicts;
|
||||
const isUpgradeButtonDisabled = isRuleUpgrading || isDisabled || isDisabledByConflicts;
|
||||
const isUpgradeButtonDisabled = isRuleUpgrading || isDisabled;
|
||||
const spinner = (
|
||||
<EuiLoadingSpinner
|
||||
size="s"
|
||||
|
@ -162,21 +163,30 @@ const createUpgradeButtonColumn = (
|
|||
/>
|
||||
);
|
||||
|
||||
const tooltipContent = isDisabledByConflicts
|
||||
? i18n.UPDATE_RULE_BUTTON_TOOLTIP_CONFLICTS
|
||||
: undefined;
|
||||
if (isDisabledByConflicts) {
|
||||
return (
|
||||
<EuiToolTip content={i18n.UPDATE_RULE_BUTTON_TOOLTIP_CONFLICTS}>
|
||||
<EuiButtonEmpty
|
||||
size="s"
|
||||
disabled={isUpgradeButtonDisabled}
|
||||
onClick={() => openRulePreview(ruleId)}
|
||||
data-test-subj={`reviewSinglePrebuiltRuleButton-${ruleId}`}
|
||||
>
|
||||
{isRuleUpgrading ? spinner : i18n.REVIEW_RULE_BUTTON}
|
||||
</EuiButtonEmpty>
|
||||
</EuiToolTip>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiToolTip content={tooltipContent}>
|
||||
<EuiButtonEmpty
|
||||
size="s"
|
||||
disabled={isUpgradeButtonDisabled}
|
||||
onClick={() => upgradeRules([ruleId])}
|
||||
data-test-subj={`upgradeSinglePrebuiltRuleButton-${ruleId}`}
|
||||
>
|
||||
{isRuleUpgrading ? spinner : i18n.UPDATE_RULE_BUTTON}
|
||||
</EuiButtonEmpty>
|
||||
</EuiToolTip>
|
||||
<EuiButtonEmpty
|
||||
size="s"
|
||||
disabled={isUpgradeButtonDisabled}
|
||||
onClick={() => upgradeRules([ruleId])}
|
||||
data-test-subj={`upgradeSinglePrebuiltRuleButton-${ruleId}`}
|
||||
>
|
||||
{isRuleUpgrading ? spinner : i18n.UPDATE_RULE_BUTTON}
|
||||
</EuiButtonEmpty>
|
||||
);
|
||||
},
|
||||
width: '10%',
|
||||
|
@ -189,7 +199,7 @@ export const useUpgradePrebuiltRulesTableColumns = (): TableColumn[] => {
|
|||
const [showRelatedIntegrations] = useUiSetting$<boolean>(SHOW_RELATED_INTEGRATIONS_SETTING);
|
||||
const {
|
||||
state: { loadingRules, isRefetching, isUpgradingSecurityPackages },
|
||||
actions: { upgradeRules },
|
||||
actions: { upgradeRules, openRulePreview },
|
||||
} = useUpgradePrebuiltRulesTableContext();
|
||||
const isDisabled = isRefetching || isUpgradingSecurityPackages;
|
||||
|
||||
|
@ -234,6 +244,7 @@ export const useUpgradePrebuiltRulesTableColumns = (): TableColumn[] => {
|
|||
? [
|
||||
createUpgradeButtonColumn(
|
||||
upgradeRules,
|
||||
openRulePreview,
|
||||
loadingRules,
|
||||
isDisabled,
|
||||
isRulesCustomizationEnabled
|
||||
|
@ -246,6 +257,7 @@ export const useUpgradePrebuiltRulesTableColumns = (): TableColumn[] => {
|
|||
showRelatedIntegrations,
|
||||
hasCRUDPermissions,
|
||||
upgradeRules,
|
||||
openRulePreview,
|
||||
loadingRules,
|
||||
isDisabled,
|
||||
isRulesCustomizationEnabled,
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui';
|
||||
import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiToolTip, EuiSpacer } from '@elastic/eui';
|
||||
import { MaintenanceWindowCallout } from '@kbn/alerts-ui-shared';
|
||||
import React, { useCallback } from 'react';
|
||||
import { DEFAULT_APP_CATEGORIES } from '@kbn/core-application-common';
|
||||
|
@ -35,6 +35,7 @@ import { AllRules } from '../../components/rules_table';
|
|||
import { RulesTableContextProvider } from '../../components/rules_table/rules_table/rules_table_context';
|
||||
import { useInvalidateFetchCoverageOverviewQuery } from '../../../rule_management/api/hooks/use_fetch_coverage_overview_query';
|
||||
import { HeaderPage } from '../../../../common/components/header_page';
|
||||
import { RuleUpdateCallouts } from '../../components/rule_update_callouts/rule_update_callouts';
|
||||
|
||||
const RulesPageComponent: React.FC = () => {
|
||||
const [isImportModalVisible, showImportModal, hideImportModal] = useBoolState();
|
||||
|
@ -168,6 +169,8 @@ const RulesPageComponent: React.FC = () => {
|
|||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</HeaderPage>
|
||||
<RuleUpdateCallouts shouldShowUpdateRulesCallout={true} />
|
||||
<EuiSpacer size="s" />
|
||||
<MaintenanceWindowCallout
|
||||
kibanaServices={kibanaServices}
|
||||
categories={[DEFAULT_APP_CATEGORIES.security.id]}
|
||||
|
|
|
@ -1091,6 +1091,28 @@ export const CLEAR_RULES_TABLE_FILTERS = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const HAS_RULE_UPDATE_DETAILS_CALLOUT_MESSAGE = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.ruleDetailsUpgrade.calloutMessage',
|
||||
{
|
||||
defaultMessage: 'Review the update to see the latest improvements, then update your rule.',
|
||||
}
|
||||
);
|
||||
|
||||
export const HAS_RULE_UPDATE_EDITING_CALLOUT_MESSAGE = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.ruleDetailsUpgrade.calloutMessage',
|
||||
{
|
||||
defaultMessage:
|
||||
'Before editing this rule, we strongly recommend that you update it to ensure you get the latest improvements.',
|
||||
}
|
||||
);
|
||||
|
||||
export const HAS_RULE_UPDATE_EDITING_CALLOUT_BUTTON = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.ruleEditingUpdate.calloutButton',
|
||||
{
|
||||
defaultMessage: 'Return to details',
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Bulk Export
|
||||
*/
|
||||
|
@ -1433,6 +1455,13 @@ export const UPDATE_RULE_BUTTON = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const REVIEW_RULE_BUTTON = i18n.translate(
|
||||
'xpack.securitySolution.addRules.reviewRuleButton',
|
||||
{
|
||||
defaultMessage: 'Review rule',
|
||||
}
|
||||
);
|
||||
|
||||
export const UPDATE_RULE_BUTTON_TOOLTIP_CONFLICTS = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.rules.upgradeRules.button.conflicts',
|
||||
{
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
import { pickBy } from 'lodash';
|
||||
import { isRuleCustomized } from '../../../../../../common/detection_engine/rule_management/utils';
|
||||
import { withSecuritySpanSync } from '../../../../../utils/with_security_span';
|
||||
import type { PromisePoolError } from '../../../../../utils/promise_pool';
|
||||
import {
|
||||
|
@ -58,17 +59,22 @@ export const createModifiedPrebuiltRuleAssets = ({
|
|||
assertPickVersionIsTarget({ ruleId, requestBody });
|
||||
}
|
||||
|
||||
const calculatedRuleDiff = calculateRuleFieldsDiff({
|
||||
base_version: upgradeableRule.base
|
||||
? convertRuleToDiffable(
|
||||
convertPrebuiltRuleAssetToRuleResponse(upgradeableRule.base)
|
||||
)
|
||||
: MissingVersion,
|
||||
current_version: convertRuleToDiffable(upgradeableRule.current),
|
||||
target_version: convertRuleToDiffable(
|
||||
convertPrebuiltRuleAssetToRuleResponse(upgradeableRule.target)
|
||||
),
|
||||
}) as AllFieldsDiff;
|
||||
const isCustomized = isRuleCustomized(current);
|
||||
|
||||
const calculatedRuleDiff = calculateRuleFieldsDiff(
|
||||
{
|
||||
base_version: upgradeableRule.base
|
||||
? convertRuleToDiffable(
|
||||
convertPrebuiltRuleAssetToRuleResponse(upgradeableRule.base)
|
||||
)
|
||||
: MissingVersion,
|
||||
current_version: convertRuleToDiffable(upgradeableRule.current),
|
||||
target_version: convertRuleToDiffable(
|
||||
convertPrebuiltRuleAssetToRuleResponse(upgradeableRule.target)
|
||||
),
|
||||
},
|
||||
isCustomized
|
||||
) as AllFieldsDiff;
|
||||
|
||||
if (mode === 'ALL_RULES' && globalPickVersion === 'MERGED') {
|
||||
const fieldsWithConflicts = Object.keys(getFieldsDiffConflicts(calculatedRuleDiff));
|
||||
|
|
|
@ -23,6 +23,7 @@ export const calculateRuleUpgradeInfo = (
|
|||
const { ruleDiff, ruleVersions } = result;
|
||||
const installedCurrentVersion = ruleVersions.input.current;
|
||||
const targetVersion = ruleVersions.input.target;
|
||||
const baseVersion = ruleVersions.input.base;
|
||||
invariant(installedCurrentVersion != null, 'installedCurrentVersion not found');
|
||||
invariant(targetVersion != null, 'targetVersion not found');
|
||||
|
||||
|
@ -43,6 +44,7 @@ export const calculateRuleUpgradeInfo = (
|
|||
version: installedCurrentVersion.version,
|
||||
current_rule: installedCurrentVersion,
|
||||
target_rule: targetRule,
|
||||
has_base_version: baseVersion !== undefined,
|
||||
diff: {
|
||||
fields: pickBy<ThreeWayDiff<unknown>>(
|
||||
ruleDiff.fields,
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { isRuleCustomized } from '../../../../../../common/detection_engine/rule_management/utils';
|
||||
import type {
|
||||
DiffableRule,
|
||||
FullRuleDiff,
|
||||
|
@ -66,9 +67,8 @@ export const calculateRuleDiff = (args: RuleVersions): CalculateRuleDiffResult =
|
|||
const { base, current, target } = args;
|
||||
|
||||
invariant(current != null, 'current version is required');
|
||||
const diffableCurrentVersion = convertRuleToDiffable(
|
||||
convertPrebuiltRuleAssetToRuleResponse(current)
|
||||
);
|
||||
const diffableCurrentVersion = convertRuleToDiffable(current);
|
||||
const isCustomized = isRuleCustomized(current);
|
||||
|
||||
invariant(target != null, 'target version is required');
|
||||
const diffableTargetVersion = convertRuleToDiffable(
|
||||
|
@ -80,11 +80,14 @@ export const calculateRuleDiff = (args: RuleVersions): CalculateRuleDiffResult =
|
|||
? convertRuleToDiffable(convertPrebuiltRuleAssetToRuleResponse(base))
|
||||
: undefined;
|
||||
|
||||
const fieldsDiff = calculateRuleFieldsDiff({
|
||||
base_version: diffableBaseVersion || MissingVersion,
|
||||
current_version: diffableCurrentVersion,
|
||||
target_version: diffableTargetVersion,
|
||||
});
|
||||
const fieldsDiff = calculateRuleFieldsDiff(
|
||||
{
|
||||
base_version: diffableBaseVersion || MissingVersion,
|
||||
current_version: diffableCurrentVersion,
|
||||
target_version: diffableTargetVersion,
|
||||
},
|
||||
isCustomized
|
||||
);
|
||||
|
||||
const {
|
||||
numberFieldsWithUpdates,
|
||||
|
|
|
@ -36,7 +36,7 @@ describe('dataSourceDiffAlgorithm', () => {
|
|||
},
|
||||
};
|
||||
|
||||
const result = dataSourceDiffAlgorithm(mockVersions);
|
||||
const result = dataSourceDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -55,7 +55,7 @@ describe('dataSourceDiffAlgorithm', () => {
|
|||
target_version: { type: DataSourceType.data_view, data_view_id: '123' },
|
||||
};
|
||||
|
||||
const result = dataSourceDiffAlgorithm(mockVersions);
|
||||
const result = dataSourceDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -82,7 +82,7 @@ describe('dataSourceDiffAlgorithm', () => {
|
|||
},
|
||||
};
|
||||
|
||||
const result = dataSourceDiffAlgorithm(mockVersions);
|
||||
const result = dataSourceDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -110,7 +110,7 @@ describe('dataSourceDiffAlgorithm', () => {
|
|||
},
|
||||
};
|
||||
|
||||
const result = dataSourceDiffAlgorithm(mockVersions);
|
||||
const result = dataSourceDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -135,7 +135,7 @@ describe('dataSourceDiffAlgorithm', () => {
|
|||
},
|
||||
};
|
||||
|
||||
const result = dataSourceDiffAlgorithm(mockVersions);
|
||||
const result = dataSourceDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -159,7 +159,7 @@ describe('dataSourceDiffAlgorithm', () => {
|
|||
},
|
||||
};
|
||||
|
||||
const result = dataSourceDiffAlgorithm(mockVersions);
|
||||
const result = dataSourceDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -178,7 +178,7 @@ describe('dataSourceDiffAlgorithm', () => {
|
|||
target_version: { type: DataSourceType.data_view, data_view_id: '456' },
|
||||
};
|
||||
|
||||
const result = dataSourceDiffAlgorithm(mockVersions);
|
||||
const result = dataSourceDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -208,7 +208,7 @@ describe('dataSourceDiffAlgorithm', () => {
|
|||
},
|
||||
};
|
||||
|
||||
const result = dataSourceDiffAlgorithm(mockVersions);
|
||||
const result = dataSourceDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -227,7 +227,7 @@ describe('dataSourceDiffAlgorithm', () => {
|
|||
target_version: { type: DataSourceType.data_view, data_view_id: '456' },
|
||||
};
|
||||
|
||||
const result = dataSourceDiffAlgorithm(mockVersions);
|
||||
const result = dataSourceDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -262,7 +262,7 @@ describe('dataSourceDiffAlgorithm', () => {
|
|||
index_patterns: ['one', 'four', 'five'],
|
||||
};
|
||||
|
||||
const result = dataSourceDiffAlgorithm(mockVersions);
|
||||
const result = dataSourceDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -281,7 +281,7 @@ describe('dataSourceDiffAlgorithm', () => {
|
|||
target_version: { type: DataSourceType.data_view, data_view_id: '789' },
|
||||
};
|
||||
|
||||
const result = dataSourceDiffAlgorithm(mockVersions);
|
||||
const result = dataSourceDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -311,7 +311,7 @@ describe('dataSourceDiffAlgorithm', () => {
|
|||
index_patterns: ['one', 'three', 'four', 'two', 'five'],
|
||||
};
|
||||
|
||||
const result = dataSourceDiffAlgorithm(mockVersions);
|
||||
const result = dataSourceDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -338,7 +338,7 @@ describe('dataSourceDiffAlgorithm', () => {
|
|||
data_view_id: '123',
|
||||
};
|
||||
|
||||
const result = dataSourceDiffAlgorithm(mockVersions);
|
||||
const result = dataSourceDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -360,7 +360,7 @@ describe('dataSourceDiffAlgorithm', () => {
|
|||
target_version: { type: DataSourceType.data_view, data_view_id: '789' },
|
||||
};
|
||||
|
||||
const result = dataSourceDiffAlgorithm(mockVersions);
|
||||
const result = dataSourceDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -385,7 +385,7 @@ describe('dataSourceDiffAlgorithm', () => {
|
|||
target_version: { type: DataSourceType.data_view, data_view_id: '789' },
|
||||
};
|
||||
|
||||
const result = dataSourceDiffAlgorithm(mockVersions);
|
||||
const result = dataSourceDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -407,7 +407,7 @@ describe('dataSourceDiffAlgorithm', () => {
|
|||
},
|
||||
};
|
||||
|
||||
const result = dataSourceDiffAlgorithm(mockVersions);
|
||||
const result = dataSourceDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -421,81 +421,162 @@ describe('dataSourceDiffAlgorithm', () => {
|
|||
});
|
||||
|
||||
describe('if base_version is missing', () => {
|
||||
it('returns current_version as merged output if current_version and target_version are the same - scenario -AA', () => {
|
||||
const mockVersions: ThreeVersionsOf<RuleDataSource | undefined> = {
|
||||
base_version: MissingVersion,
|
||||
current_version: {
|
||||
type: DataSourceType.index_patterns,
|
||||
index_patterns: ['one', 'three', 'four'],
|
||||
},
|
||||
target_version: {
|
||||
type: DataSourceType.index_patterns,
|
||||
index_patterns: ['one', 'three', 'four'],
|
||||
},
|
||||
};
|
||||
|
||||
const result = dataSourceDiffAlgorithm(mockVersions);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
has_base_version: false,
|
||||
base_version: undefined,
|
||||
merged_version: mockVersions.current_version,
|
||||
diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate,
|
||||
merge_outcome: ThreeWayMergeOutcome.Current,
|
||||
conflict: ThreeWayDiffConflict.NONE,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
describe('returns target_version as merged output if current_version and target_version are different - scenario -AB', () => {
|
||||
it('if versions are different types', () => {
|
||||
describe('if current_version and target_version are the same - scenario -AA', () => {
|
||||
it('returns NONE conflict if rule is NOT customized', () => {
|
||||
const mockVersions: ThreeVersionsOf<RuleDataSource | undefined> = {
|
||||
base_version: MissingVersion,
|
||||
current_version: { type: DataSourceType.data_view, data_view_id: '456' },
|
||||
current_version: {
|
||||
type: DataSourceType.index_patterns,
|
||||
index_patterns: ['one', 'three', 'four'],
|
||||
},
|
||||
target_version: {
|
||||
type: DataSourceType.index_patterns,
|
||||
index_patterns: ['one', 'three', 'four'],
|
||||
},
|
||||
};
|
||||
|
||||
const result = dataSourceDiffAlgorithm(mockVersions);
|
||||
const result = dataSourceDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
has_base_version: false,
|
||||
base_version: undefined,
|
||||
merged_version: mockVersions.target_version,
|
||||
diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate,
|
||||
diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate,
|
||||
merge_outcome: ThreeWayMergeOutcome.Target,
|
||||
conflict: ThreeWayDiffConflict.SOLVABLE,
|
||||
conflict: ThreeWayDiffConflict.NONE,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('if current version is undefined', () => {
|
||||
it('returns NONE conflict if rule is customized', () => {
|
||||
const mockVersions: ThreeVersionsOf<RuleDataSource | undefined> = {
|
||||
base_version: MissingVersion,
|
||||
current_version: undefined,
|
||||
current_version: {
|
||||
type: DataSourceType.index_patterns,
|
||||
index_patterns: ['one', 'three', 'four'],
|
||||
},
|
||||
target_version: {
|
||||
type: DataSourceType.index_patterns,
|
||||
index_patterns: ['one', 'three', 'four'],
|
||||
},
|
||||
};
|
||||
|
||||
const result = dataSourceDiffAlgorithm(mockVersions);
|
||||
const result = dataSourceDiffAlgorithm(mockVersions, true);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
has_base_version: false,
|
||||
base_version: undefined,
|
||||
merged_version: mockVersions.target_version,
|
||||
diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate,
|
||||
diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate,
|
||||
merge_outcome: ThreeWayMergeOutcome.Target,
|
||||
conflict: ThreeWayDiffConflict.SOLVABLE,
|
||||
conflict: ThreeWayDiffConflict.NONE,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('if current_version and target_version are different - scenario -AB', () => {
|
||||
describe('returns NONE conflict if rule is NOT customized', () => {
|
||||
it('if versions are different types', () => {
|
||||
const mockVersions: ThreeVersionsOf<RuleDataSource | undefined> = {
|
||||
base_version: MissingVersion,
|
||||
current_version: { type: DataSourceType.data_view, data_view_id: '456' },
|
||||
target_version: {
|
||||
type: DataSourceType.index_patterns,
|
||||
index_patterns: ['one', 'three', 'four'],
|
||||
},
|
||||
};
|
||||
|
||||
const result = dataSourceDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
has_base_version: false,
|
||||
base_version: undefined,
|
||||
merged_version: mockVersions.target_version,
|
||||
diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate,
|
||||
merge_outcome: ThreeWayMergeOutcome.Target,
|
||||
conflict: ThreeWayDiffConflict.NONE,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('if current version is undefined', () => {
|
||||
const mockVersions: ThreeVersionsOf<RuleDataSource | undefined> = {
|
||||
base_version: MissingVersion,
|
||||
current_version: undefined,
|
||||
target_version: {
|
||||
type: DataSourceType.index_patterns,
|
||||
index_patterns: ['one', 'three', 'four'],
|
||||
},
|
||||
};
|
||||
|
||||
const result = dataSourceDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
has_base_version: false,
|
||||
base_version: undefined,
|
||||
merged_version: mockVersions.target_version,
|
||||
diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate,
|
||||
merge_outcome: ThreeWayMergeOutcome.Target,
|
||||
conflict: ThreeWayDiffConflict.NONE,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('returns SOLVABLE conflict if rule is customized', () => {
|
||||
it('if versions are different types', () => {
|
||||
const mockVersions: ThreeVersionsOf<RuleDataSource | undefined> = {
|
||||
base_version: MissingVersion,
|
||||
current_version: { type: DataSourceType.data_view, data_view_id: '456' },
|
||||
target_version: {
|
||||
type: DataSourceType.index_patterns,
|
||||
index_patterns: ['one', 'three', 'four'],
|
||||
},
|
||||
};
|
||||
|
||||
const result = dataSourceDiffAlgorithm(mockVersions, true);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
has_base_version: false,
|
||||
base_version: undefined,
|
||||
merged_version: mockVersions.target_version,
|
||||
diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate,
|
||||
merge_outcome: ThreeWayMergeOutcome.Target,
|
||||
conflict: ThreeWayDiffConflict.SOLVABLE,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('if current version is undefined', () => {
|
||||
const mockVersions: ThreeVersionsOf<RuleDataSource | undefined> = {
|
||||
base_version: MissingVersion,
|
||||
current_version: undefined,
|
||||
target_version: {
|
||||
type: DataSourceType.index_patterns,
|
||||
index_patterns: ['one', 'three', 'four'],
|
||||
},
|
||||
};
|
||||
|
||||
const result = dataSourceDiffAlgorithm(mockVersions, true);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
has_base_version: false,
|
||||
base_version: undefined,
|
||||
merged_version: mockVersions.target_version,
|
||||
diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate,
|
||||
merge_outcome: ThreeWayMergeOutcome.Target,
|
||||
conflict: ThreeWayDiffConflict.SOLVABLE,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -27,7 +27,8 @@ import { getDedupedDataSourceVersion, mergeDedupedArrays } from './helpers';
|
|||
* Takes a type of `RuleDataSource | undefined` because the data source can be index patterns, a data view id, or neither in some cases
|
||||
*/
|
||||
export const dataSourceDiffAlgorithm = (
|
||||
versions: ThreeVersionsOf<RuleDataSource | undefined>
|
||||
versions: ThreeVersionsOf<RuleDataSource | undefined>,
|
||||
isRuleCustomized: boolean
|
||||
): ThreeWayDiff<RuleDataSource | undefined> => {
|
||||
const {
|
||||
base_version: baseVersion,
|
||||
|
@ -46,6 +47,7 @@ export const dataSourceDiffAlgorithm = (
|
|||
currentVersion,
|
||||
targetVersion,
|
||||
diffOutcome,
|
||||
isRuleCustomized,
|
||||
});
|
||||
|
||||
return {
|
||||
|
@ -73,6 +75,7 @@ interface MergeArgs {
|
|||
currentVersion: RuleDataSource | undefined;
|
||||
targetVersion: RuleDataSource | undefined;
|
||||
diffOutcome: ThreeWayDiffOutcome;
|
||||
isRuleCustomized: boolean;
|
||||
}
|
||||
|
||||
const mergeVersions = ({
|
||||
|
@ -80,6 +83,7 @@ const mergeVersions = ({
|
|||
currentVersion,
|
||||
targetVersion,
|
||||
diffOutcome,
|
||||
isRuleCustomized,
|
||||
}: MergeArgs): MergeResult => {
|
||||
const dedupedBaseVersion = baseVersion ? getDedupedDataSourceVersion(baseVersion) : baseVersion;
|
||||
const dedupedCurrentVersion = currentVersion
|
||||
|
@ -90,9 +94,6 @@ const mergeVersions = ({
|
|||
: targetVersion;
|
||||
|
||||
switch (diffOutcome) {
|
||||
// Scenario -AA is treated as scenario AAA:
|
||||
// https://github.com/elastic/kibana/pull/184889#discussion_r1636421293
|
||||
case ThreeWayDiffOutcome.MissingBaseNoUpdate:
|
||||
case ThreeWayDiffOutcome.StockValueNoUpdate:
|
||||
case ThreeWayDiffOutcome.CustomizedValueNoUpdate:
|
||||
case ThreeWayDiffOutcome.CustomizedValueSameUpdate:
|
||||
|
@ -140,14 +141,26 @@ const mergeVersions = ({
|
|||
};
|
||||
}
|
||||
|
||||
// Scenario -AB is treated as scenario ABC, but marked as
|
||||
// SOLVABLE, and returns the target version as the merged version
|
||||
// https://github.com/elastic/kibana/pull/184889#discussion_r1636421293
|
||||
// Missing base versions always return target version
|
||||
// Scenario -AA is treated as AAA
|
||||
// https://github.com/elastic/kibana/issues/210358#issuecomment-2654492854
|
||||
case ThreeWayDiffOutcome.MissingBaseNoUpdate: {
|
||||
return {
|
||||
conflict: ThreeWayDiffConflict.NONE,
|
||||
mergedVersion: dedupedTargetVersion,
|
||||
mergeOutcome: ThreeWayMergeOutcome.Target,
|
||||
};
|
||||
}
|
||||
|
||||
// Missing base versions always return target version
|
||||
// If the rule is customized, we return a SOLVABLE conflict
|
||||
// Otherwise we treat scenario -AB as AAB
|
||||
// https://github.com/elastic/kibana/issues/210358#issuecomment-2654492854
|
||||
case ThreeWayDiffOutcome.MissingBaseCanUpdate: {
|
||||
return {
|
||||
mergedVersion: targetVersion,
|
||||
conflict: isRuleCustomized ? ThreeWayDiffConflict.SOLVABLE : ThreeWayDiffConflict.NONE,
|
||||
mergedVersion: dedupedTargetVersion,
|
||||
mergeOutcome: ThreeWayMergeOutcome.Target,
|
||||
conflict: ThreeWayDiffConflict.SOLVABLE,
|
||||
};
|
||||
}
|
||||
default:
|
||||
|
|
|
@ -37,7 +37,7 @@ describe('eqlQueryDiffAlgorithm', () => {
|
|||
},
|
||||
};
|
||||
|
||||
const result = eqlQueryDiffAlgorithm(mockVersions);
|
||||
const result = eqlQueryDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -68,7 +68,7 @@ describe('eqlQueryDiffAlgorithm', () => {
|
|||
},
|
||||
};
|
||||
|
||||
const result = eqlQueryDiffAlgorithm(mockVersions);
|
||||
const result = eqlQueryDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -99,7 +99,7 @@ describe('eqlQueryDiffAlgorithm', () => {
|
|||
},
|
||||
};
|
||||
|
||||
const result = eqlQueryDiffAlgorithm(mockVersions);
|
||||
const result = eqlQueryDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -130,7 +130,7 @@ describe('eqlQueryDiffAlgorithm', () => {
|
|||
},
|
||||
};
|
||||
|
||||
const result = eqlQueryDiffAlgorithm(mockVersions);
|
||||
const result = eqlQueryDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -162,7 +162,7 @@ describe('eqlQueryDiffAlgorithm', () => {
|
|||
},
|
||||
};
|
||||
|
||||
const result = eqlQueryDiffAlgorithm(mockVersions);
|
||||
const result = eqlQueryDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -193,7 +193,7 @@ describe('eqlQueryDiffAlgorithm', () => {
|
|||
},
|
||||
};
|
||||
|
||||
const result = eqlQueryDiffAlgorithm(mockVersions);
|
||||
const result = eqlQueryDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -207,62 +207,124 @@ describe('eqlQueryDiffAlgorithm', () => {
|
|||
});
|
||||
|
||||
describe('if base_version is missing', () => {
|
||||
it('returns current_version as merged output if current_version and target_version are the same - scenario -AA', () => {
|
||||
const mockVersions: ThreeVersionsOf<RuleEqlQuery> = {
|
||||
base_version: MissingVersion,
|
||||
current_version: {
|
||||
query: 'query where true',
|
||||
language: 'eql',
|
||||
filters: [],
|
||||
},
|
||||
target_version: {
|
||||
query: 'query where true',
|
||||
language: 'eql',
|
||||
filters: [],
|
||||
},
|
||||
};
|
||||
describe('if current_version and target_version are the same - scenario -AA', () => {
|
||||
it('returns NONE conflict if rule is NOT customized', () => {
|
||||
const mockVersions: ThreeVersionsOf<RuleEqlQuery> = {
|
||||
base_version: MissingVersion,
|
||||
current_version: {
|
||||
query: 'query where true',
|
||||
language: 'eql',
|
||||
filters: [],
|
||||
},
|
||||
target_version: {
|
||||
query: 'query where true',
|
||||
language: 'eql',
|
||||
filters: [],
|
||||
},
|
||||
};
|
||||
|
||||
const result = eqlQueryDiffAlgorithm(mockVersions);
|
||||
const result = eqlQueryDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
has_base_version: false,
|
||||
base_version: undefined,
|
||||
merged_version: mockVersions.current_version,
|
||||
diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate,
|
||||
merge_outcome: ThreeWayMergeOutcome.Current,
|
||||
conflict: ThreeWayDiffConflict.NONE,
|
||||
})
|
||||
);
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
has_base_version: false,
|
||||
base_version: undefined,
|
||||
merged_version: mockVersions.target_version,
|
||||
diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate,
|
||||
merge_outcome: ThreeWayMergeOutcome.Target,
|
||||
conflict: ThreeWayDiffConflict.NONE,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('returns NONE conflict if rule is customized', () => {
|
||||
const mockVersions: ThreeVersionsOf<RuleEqlQuery> = {
|
||||
base_version: MissingVersion,
|
||||
current_version: {
|
||||
query: 'query where true',
|
||||
language: 'eql',
|
||||
filters: [],
|
||||
},
|
||||
target_version: {
|
||||
query: 'query where true',
|
||||
language: 'eql',
|
||||
filters: [],
|
||||
},
|
||||
};
|
||||
|
||||
const result = eqlQueryDiffAlgorithm(mockVersions, true);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
has_base_version: false,
|
||||
base_version: undefined,
|
||||
merged_version: mockVersions.target_version,
|
||||
diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate,
|
||||
merge_outcome: ThreeWayMergeOutcome.Target,
|
||||
conflict: ThreeWayDiffConflict.NONE,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('returns target_version as merged output if current_version and target_version are different - scenario -AB', () => {
|
||||
const mockVersions: ThreeVersionsOf<RuleEqlQuery> = {
|
||||
base_version: MissingVersion,
|
||||
current_version: {
|
||||
query: 'query where true',
|
||||
language: 'eql',
|
||||
filters: [],
|
||||
},
|
||||
target_version: {
|
||||
query: 'query where false',
|
||||
language: 'eql',
|
||||
filters: [],
|
||||
},
|
||||
};
|
||||
describe('if current_version and target_version are different - scenario -AB', () => {
|
||||
it('returns NONE conflict if rule is NOT customized', () => {
|
||||
const mockVersions: ThreeVersionsOf<RuleEqlQuery> = {
|
||||
base_version: MissingVersion,
|
||||
current_version: {
|
||||
query: 'query where true',
|
||||
language: 'eql',
|
||||
filters: [],
|
||||
},
|
||||
target_version: {
|
||||
query: 'query where false',
|
||||
language: 'eql',
|
||||
filters: [],
|
||||
},
|
||||
};
|
||||
|
||||
const result = eqlQueryDiffAlgorithm(mockVersions);
|
||||
const result = eqlQueryDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
has_base_version: false,
|
||||
base_version: undefined,
|
||||
merged_version: mockVersions.target_version,
|
||||
diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate,
|
||||
merge_outcome: ThreeWayMergeOutcome.Target,
|
||||
conflict: ThreeWayDiffConflict.SOLVABLE,
|
||||
})
|
||||
);
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
has_base_version: false,
|
||||
base_version: undefined,
|
||||
merged_version: mockVersions.target_version,
|
||||
diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate,
|
||||
merge_outcome: ThreeWayMergeOutcome.Target,
|
||||
conflict: ThreeWayDiffConflict.NONE,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('returns SOLVABLE conflict if rule is customized', () => {
|
||||
const mockVersions: ThreeVersionsOf<RuleEqlQuery> = {
|
||||
base_version: MissingVersion,
|
||||
current_version: {
|
||||
query: 'query where true',
|
||||
language: 'eql',
|
||||
filters: [],
|
||||
},
|
||||
target_version: {
|
||||
query: 'query where false',
|
||||
language: 'eql',
|
||||
filters: [],
|
||||
},
|
||||
};
|
||||
|
||||
const result = eqlQueryDiffAlgorithm(mockVersions, true);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
has_base_version: false,
|
||||
base_version: undefined,
|
||||
merged_version: mockVersions.target_version,
|
||||
diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate,
|
||||
merge_outcome: ThreeWayMergeOutcome.Target,
|
||||
conflict: ThreeWayDiffConflict.SOLVABLE,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -14,5 +14,7 @@ import { simpleDiffAlgorithm } from './simple_diff_algorithm';
|
|||
/**
|
||||
* Diff algorithm for eql query types
|
||||
*/
|
||||
export const eqlQueryDiffAlgorithm = (versions: ThreeVersionsOf<RuleEqlQuery>) =>
|
||||
simpleDiffAlgorithm<RuleEqlQuery>(versions);
|
||||
export const eqlQueryDiffAlgorithm = (
|
||||
versions: ThreeVersionsOf<RuleEqlQuery>,
|
||||
isRuleCustomized: boolean
|
||||
) => simpleDiffAlgorithm<RuleEqlQuery>(versions, isRuleCustomized);
|
||||
|
|
|
@ -34,7 +34,7 @@ describe('esqlQueryDiffAlgorithm', () => {
|
|||
},
|
||||
};
|
||||
|
||||
const result = esqlQueryDiffAlgorithm(mockVersions);
|
||||
const result = esqlQueryDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -62,7 +62,7 @@ describe('esqlQueryDiffAlgorithm', () => {
|
|||
},
|
||||
};
|
||||
|
||||
const result = esqlQueryDiffAlgorithm(mockVersions);
|
||||
const result = esqlQueryDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -90,7 +90,7 @@ describe('esqlQueryDiffAlgorithm', () => {
|
|||
},
|
||||
};
|
||||
|
||||
const result = esqlQueryDiffAlgorithm(mockVersions);
|
||||
const result = esqlQueryDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -118,7 +118,7 @@ describe('esqlQueryDiffAlgorithm', () => {
|
|||
},
|
||||
};
|
||||
|
||||
const result = esqlQueryDiffAlgorithm(mockVersions);
|
||||
const result = esqlQueryDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -147,7 +147,7 @@ describe('esqlQueryDiffAlgorithm', () => {
|
|||
},
|
||||
};
|
||||
|
||||
const result = esqlQueryDiffAlgorithm(mockVersions);
|
||||
const result = esqlQueryDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -161,58 +161,116 @@ describe('esqlQueryDiffAlgorithm', () => {
|
|||
});
|
||||
|
||||
describe('if base_version is missing', () => {
|
||||
it('returns current_version as merged output if current_version and target_version are the same - scenario -AA', () => {
|
||||
const mockVersions: ThreeVersionsOf<RuleEsqlQuery> = {
|
||||
base_version: MissingVersion,
|
||||
current_version: {
|
||||
query: 'query where true',
|
||||
language: 'esql',
|
||||
},
|
||||
target_version: {
|
||||
query: 'query where true',
|
||||
language: 'esql',
|
||||
},
|
||||
};
|
||||
describe('if current_version and target_version are the same - scenario -AA', () => {
|
||||
it('returns NONE conflict if rule is NOT customized', () => {
|
||||
const mockVersions: ThreeVersionsOf<RuleEsqlQuery> = {
|
||||
base_version: MissingVersion,
|
||||
current_version: {
|
||||
query: 'query where true',
|
||||
language: 'esql',
|
||||
},
|
||||
target_version: {
|
||||
query: 'query where true',
|
||||
language: 'esql',
|
||||
},
|
||||
};
|
||||
|
||||
const result = esqlQueryDiffAlgorithm(mockVersions);
|
||||
const result = esqlQueryDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
has_base_version: false,
|
||||
base_version: undefined,
|
||||
merged_version: mockVersions.current_version,
|
||||
diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate,
|
||||
merge_outcome: ThreeWayMergeOutcome.Current,
|
||||
conflict: ThreeWayDiffConflict.NONE,
|
||||
})
|
||||
);
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
has_base_version: false,
|
||||
base_version: undefined,
|
||||
merged_version: mockVersions.target_version,
|
||||
diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate,
|
||||
merge_outcome: ThreeWayMergeOutcome.Target,
|
||||
conflict: ThreeWayDiffConflict.NONE,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('returns NONE conflict if rule is customized', () => {
|
||||
const mockVersions: ThreeVersionsOf<RuleEsqlQuery> = {
|
||||
base_version: MissingVersion,
|
||||
current_version: {
|
||||
query: 'query where true',
|
||||
language: 'esql',
|
||||
},
|
||||
target_version: {
|
||||
query: 'query where true',
|
||||
language: 'esql',
|
||||
},
|
||||
};
|
||||
|
||||
const result = esqlQueryDiffAlgorithm(mockVersions, true);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
has_base_version: false,
|
||||
base_version: undefined,
|
||||
merged_version: mockVersions.target_version,
|
||||
diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate,
|
||||
merge_outcome: ThreeWayMergeOutcome.Target,
|
||||
conflict: ThreeWayDiffConflict.NONE,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('returns target_version as merged output if current_version and target_version are different - scenario -AB', () => {
|
||||
const mockVersions: ThreeVersionsOf<RuleEsqlQuery> = {
|
||||
base_version: MissingVersion,
|
||||
current_version: {
|
||||
query: 'query where true',
|
||||
language: 'esql',
|
||||
},
|
||||
target_version: {
|
||||
query: 'query where false',
|
||||
language: 'esql',
|
||||
},
|
||||
};
|
||||
describe('if current_version and target_version are different - scenario -AB', () => {
|
||||
it('returns NONE conflict if rule is NOT customized', () => {
|
||||
const mockVersions: ThreeVersionsOf<RuleEsqlQuery> = {
|
||||
base_version: MissingVersion,
|
||||
current_version: {
|
||||
query: 'query where true',
|
||||
language: 'esql',
|
||||
},
|
||||
target_version: {
|
||||
query: 'query where false',
|
||||
language: 'esql',
|
||||
},
|
||||
};
|
||||
|
||||
const result = esqlQueryDiffAlgorithm(mockVersions);
|
||||
const result = esqlQueryDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
has_base_version: false,
|
||||
base_version: undefined,
|
||||
merged_version: mockVersions.target_version,
|
||||
diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate,
|
||||
merge_outcome: ThreeWayMergeOutcome.Target,
|
||||
conflict: ThreeWayDiffConflict.SOLVABLE,
|
||||
})
|
||||
);
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
has_base_version: false,
|
||||
base_version: undefined,
|
||||
merged_version: mockVersions.target_version,
|
||||
diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate,
|
||||
merge_outcome: ThreeWayMergeOutcome.Target,
|
||||
conflict: ThreeWayDiffConflict.NONE,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('returns SOLVABLE conflict if rule is customized', () => {
|
||||
const mockVersions: ThreeVersionsOf<RuleEsqlQuery> = {
|
||||
base_version: MissingVersion,
|
||||
current_version: {
|
||||
query: 'query where true',
|
||||
language: 'esql',
|
||||
},
|
||||
target_version: {
|
||||
query: 'query where false',
|
||||
language: 'esql',
|
||||
},
|
||||
};
|
||||
|
||||
const result = esqlQueryDiffAlgorithm(mockVersions, true);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
has_base_version: false,
|
||||
base_version: undefined,
|
||||
merged_version: mockVersions.target_version,
|
||||
diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate,
|
||||
merge_outcome: ThreeWayMergeOutcome.Target,
|
||||
conflict: ThreeWayDiffConflict.SOLVABLE,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -14,5 +14,7 @@ import { simpleDiffAlgorithm } from './simple_diff_algorithm';
|
|||
/**
|
||||
* Diff algorithm for esql query types
|
||||
*/
|
||||
export const esqlQueryDiffAlgorithm = (versions: ThreeVersionsOf<RuleEsqlQuery>) =>
|
||||
simpleDiffAlgorithm<RuleEsqlQuery>(versions);
|
||||
export const esqlQueryDiffAlgorithm = (
|
||||
versions: ThreeVersionsOf<RuleEsqlQuery>,
|
||||
isRuleCustomized: boolean
|
||||
) => simpleDiffAlgorithm<RuleEsqlQuery>(versions, isRuleCustomized);
|
||||
|
|
|
@ -43,7 +43,7 @@ describe('kqlQueryDiffAlgorithm', () => {
|
|||
},
|
||||
};
|
||||
|
||||
const result = kqlQueryDiffAlgorithm(mockVersions);
|
||||
const result = kqlQueryDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -71,7 +71,7 @@ describe('kqlQueryDiffAlgorithm', () => {
|
|||
},
|
||||
};
|
||||
|
||||
const result = kqlQueryDiffAlgorithm(mockVersions);
|
||||
const result = kqlQueryDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -103,7 +103,7 @@ describe('kqlQueryDiffAlgorithm', () => {
|
|||
},
|
||||
};
|
||||
|
||||
const result = kqlQueryDiffAlgorithm(mockVersions);
|
||||
const result = kqlQueryDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -137,7 +137,7 @@ describe('kqlQueryDiffAlgorithm', () => {
|
|||
},
|
||||
};
|
||||
|
||||
const result = kqlQueryDiffAlgorithm(mockVersions);
|
||||
const result = kqlQueryDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -171,7 +171,7 @@ describe('kqlQueryDiffAlgorithm', () => {
|
|||
},
|
||||
};
|
||||
|
||||
const result = kqlQueryDiffAlgorithm(mockVersions);
|
||||
const result = kqlQueryDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -205,7 +205,7 @@ describe('kqlQueryDiffAlgorithm', () => {
|
|||
},
|
||||
};
|
||||
|
||||
const result = kqlQueryDiffAlgorithm(mockVersions);
|
||||
const result = kqlQueryDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -237,7 +237,7 @@ describe('kqlQueryDiffAlgorithm', () => {
|
|||
},
|
||||
};
|
||||
|
||||
const result = kqlQueryDiffAlgorithm(mockVersions);
|
||||
const result = kqlQueryDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -271,7 +271,7 @@ describe('kqlQueryDiffAlgorithm', () => {
|
|||
},
|
||||
};
|
||||
|
||||
const result = kqlQueryDiffAlgorithm(mockVersions);
|
||||
const result = kqlQueryDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -302,7 +302,7 @@ describe('kqlQueryDiffAlgorithm', () => {
|
|||
},
|
||||
};
|
||||
|
||||
const result = kqlQueryDiffAlgorithm(mockVersions);
|
||||
const result = kqlQueryDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -338,7 +338,7 @@ describe('kqlQueryDiffAlgorithm', () => {
|
|||
},
|
||||
};
|
||||
|
||||
const result = kqlQueryDiffAlgorithm(mockVersions);
|
||||
const result = kqlQueryDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -372,7 +372,7 @@ describe('kqlQueryDiffAlgorithm', () => {
|
|||
},
|
||||
};
|
||||
|
||||
const result = kqlQueryDiffAlgorithm(mockVersions);
|
||||
const result = kqlQueryDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -406,7 +406,7 @@ describe('kqlQueryDiffAlgorithm', () => {
|
|||
},
|
||||
};
|
||||
|
||||
const result = kqlQueryDiffAlgorithm(mockVersions);
|
||||
const result = kqlQueryDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -422,124 +422,250 @@ describe('kqlQueryDiffAlgorithm', () => {
|
|||
|
||||
describe('if base_version is missing', () => {
|
||||
describe('if current_version and target_version are the same - scenario -AA', () => {
|
||||
it('returns current_version as merged output if all versions are inline query types', () => {
|
||||
const mockVersions: ThreeVersionsOf<RuleKqlQuery> = {
|
||||
base_version: MissingVersion,
|
||||
current_version: {
|
||||
type: KqlQueryType.inline_query,
|
||||
query: 'query string = true',
|
||||
language: KqlQueryLanguageEnum.kuery,
|
||||
filters: [],
|
||||
},
|
||||
target_version: {
|
||||
type: KqlQueryType.inline_query,
|
||||
query: 'query string = true',
|
||||
language: KqlQueryLanguageEnum.kuery,
|
||||
filters: [],
|
||||
},
|
||||
};
|
||||
describe('if rule is NOT customized', () => {
|
||||
it('returns current_version as merged output if all versions are inline query types', () => {
|
||||
const mockVersions: ThreeVersionsOf<RuleKqlQuery> = {
|
||||
base_version: MissingVersion,
|
||||
current_version: {
|
||||
type: KqlQueryType.inline_query,
|
||||
query: 'query string = true',
|
||||
language: KqlQueryLanguageEnum.kuery,
|
||||
filters: [],
|
||||
},
|
||||
target_version: {
|
||||
type: KqlQueryType.inline_query,
|
||||
query: 'query string = true',
|
||||
language: KqlQueryLanguageEnum.kuery,
|
||||
filters: [],
|
||||
},
|
||||
};
|
||||
|
||||
const result = kqlQueryDiffAlgorithm(mockVersions);
|
||||
const result = kqlQueryDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
has_base_version: false,
|
||||
base_version: undefined,
|
||||
merged_version: mockVersions.current_version,
|
||||
diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate,
|
||||
merge_outcome: ThreeWayMergeOutcome.Current,
|
||||
conflict: ThreeWayDiffConflict.NONE,
|
||||
})
|
||||
);
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
has_base_version: false,
|
||||
base_version: undefined,
|
||||
merged_version: mockVersions.target_version,
|
||||
diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate,
|
||||
merge_outcome: ThreeWayMergeOutcome.Target,
|
||||
conflict: ThreeWayDiffConflict.NONE,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('returns current_version as merged output if all versions are saved query types', () => {
|
||||
const mockVersions: ThreeVersionsOf<RuleKqlQuery> = {
|
||||
base_version: MissingVersion,
|
||||
current_version: {
|
||||
type: KqlQueryType.saved_query,
|
||||
saved_query_id: 'saved-query-id',
|
||||
},
|
||||
target_version: {
|
||||
type: KqlQueryType.saved_query,
|
||||
saved_query_id: 'saved-query-id',
|
||||
},
|
||||
};
|
||||
|
||||
const result = kqlQueryDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
has_base_version: false,
|
||||
base_version: undefined,
|
||||
merged_version: mockVersions.target_version,
|
||||
diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate,
|
||||
merge_outcome: ThreeWayMergeOutcome.Target,
|
||||
conflict: ThreeWayDiffConflict.NONE,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('returns current_version as merged output if all versions are saved query types', () => {
|
||||
const mockVersions: ThreeVersionsOf<RuleKqlQuery> = {
|
||||
base_version: MissingVersion,
|
||||
current_version: {
|
||||
type: KqlQueryType.saved_query,
|
||||
saved_query_id: 'saved-query-id',
|
||||
},
|
||||
target_version: {
|
||||
type: KqlQueryType.saved_query,
|
||||
saved_query_id: 'saved-query-id',
|
||||
},
|
||||
};
|
||||
describe('if rule is customized', () => {
|
||||
it('returns current_version as merged output if all versions are inline query types', () => {
|
||||
const mockVersions: ThreeVersionsOf<RuleKqlQuery> = {
|
||||
base_version: MissingVersion,
|
||||
current_version: {
|
||||
type: KqlQueryType.inline_query,
|
||||
query: 'query string = true',
|
||||
language: KqlQueryLanguageEnum.kuery,
|
||||
filters: [],
|
||||
},
|
||||
target_version: {
|
||||
type: KqlQueryType.inline_query,
|
||||
query: 'query string = true',
|
||||
language: KqlQueryLanguageEnum.kuery,
|
||||
filters: [],
|
||||
},
|
||||
};
|
||||
|
||||
const result = kqlQueryDiffAlgorithm(mockVersions);
|
||||
const result = kqlQueryDiffAlgorithm(mockVersions, true);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
has_base_version: false,
|
||||
base_version: undefined,
|
||||
merged_version: mockVersions.current_version,
|
||||
diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate,
|
||||
merge_outcome: ThreeWayMergeOutcome.Current,
|
||||
conflict: ThreeWayDiffConflict.NONE,
|
||||
})
|
||||
);
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
has_base_version: false,
|
||||
base_version: undefined,
|
||||
merged_version: mockVersions.target_version,
|
||||
diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate,
|
||||
merge_outcome: ThreeWayMergeOutcome.Target,
|
||||
conflict: ThreeWayDiffConflict.NONE,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('returns current_version as merged output if all versions are saved query types', () => {
|
||||
const mockVersions: ThreeVersionsOf<RuleKqlQuery> = {
|
||||
base_version: MissingVersion,
|
||||
current_version: {
|
||||
type: KqlQueryType.saved_query,
|
||||
saved_query_id: 'saved-query-id',
|
||||
},
|
||||
target_version: {
|
||||
type: KqlQueryType.saved_query,
|
||||
saved_query_id: 'saved-query-id',
|
||||
},
|
||||
};
|
||||
|
||||
const result = kqlQueryDiffAlgorithm(mockVersions, true);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
has_base_version: false,
|
||||
base_version: undefined,
|
||||
merged_version: mockVersions.target_version,
|
||||
diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate,
|
||||
merge_outcome: ThreeWayMergeOutcome.Target,
|
||||
conflict: ThreeWayDiffConflict.NONE,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('if current_version and target_version are different - scenario -AB', () => {
|
||||
it('returns target_version as merged output if current and target versions have the same types', () => {
|
||||
const mockVersions: ThreeVersionsOf<RuleKqlQuery> = {
|
||||
base_version: MissingVersion,
|
||||
current_version: {
|
||||
type: KqlQueryType.inline_query,
|
||||
query: 'query string = true',
|
||||
language: KqlQueryLanguageEnum.kuery,
|
||||
filters: [],
|
||||
},
|
||||
target_version: {
|
||||
type: KqlQueryType.inline_query,
|
||||
query: 'query string = false',
|
||||
language: KqlQueryLanguageEnum.kuery,
|
||||
filters: [],
|
||||
},
|
||||
};
|
||||
describe('if rule is NOT customized', () => {
|
||||
it('returns NONE conflict if current and target versions have the same types', () => {
|
||||
const mockVersions: ThreeVersionsOf<RuleKqlQuery> = {
|
||||
base_version: MissingVersion,
|
||||
current_version: {
|
||||
type: KqlQueryType.inline_query,
|
||||
query: 'query string = true',
|
||||
language: KqlQueryLanguageEnum.kuery,
|
||||
filters: [],
|
||||
},
|
||||
target_version: {
|
||||
type: KqlQueryType.inline_query,
|
||||
query: 'query string = false',
|
||||
language: KqlQueryLanguageEnum.kuery,
|
||||
filters: [],
|
||||
},
|
||||
};
|
||||
|
||||
const result = kqlQueryDiffAlgorithm(mockVersions);
|
||||
const result = kqlQueryDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
has_base_version: false,
|
||||
base_version: undefined,
|
||||
merged_version: mockVersions.target_version,
|
||||
diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate,
|
||||
merge_outcome: ThreeWayMergeOutcome.Target,
|
||||
conflict: ThreeWayDiffConflict.SOLVABLE,
|
||||
})
|
||||
);
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
has_base_version: false,
|
||||
base_version: undefined,
|
||||
merged_version: mockVersions.target_version,
|
||||
diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate,
|
||||
merge_outcome: ThreeWayMergeOutcome.Target,
|
||||
conflict: ThreeWayDiffConflict.NONE,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('returns NONE conflict if current and target versions have different types', () => {
|
||||
const mockVersions: ThreeVersionsOf<RuleKqlQuery> = {
|
||||
base_version: MissingVersion,
|
||||
current_version: {
|
||||
type: KqlQueryType.saved_query,
|
||||
saved_query_id: 'saved-query-id-2',
|
||||
},
|
||||
target_version: {
|
||||
type: KqlQueryType.inline_query,
|
||||
query: 'query string = false',
|
||||
language: KqlQueryLanguageEnum.kuery,
|
||||
filters: [],
|
||||
},
|
||||
};
|
||||
|
||||
const result = kqlQueryDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
has_base_version: false,
|
||||
base_version: undefined,
|
||||
merged_version: mockVersions.target_version,
|
||||
diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate,
|
||||
merge_outcome: ThreeWayMergeOutcome.Target,
|
||||
conflict: ThreeWayDiffConflict.NONE,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('returns target_version as merged output if current and target versions have different types', () => {
|
||||
const mockVersions: ThreeVersionsOf<RuleKqlQuery> = {
|
||||
base_version: MissingVersion,
|
||||
current_version: {
|
||||
type: KqlQueryType.saved_query,
|
||||
saved_query_id: 'saved-query-id-2',
|
||||
},
|
||||
target_version: {
|
||||
type: KqlQueryType.inline_query,
|
||||
query: 'query string = false',
|
||||
language: KqlQueryLanguageEnum.kuery,
|
||||
filters: [],
|
||||
},
|
||||
};
|
||||
describe('if rule is customized', () => {
|
||||
it('returns SOLVABLE conflict if current and target versions have the same types', () => {
|
||||
const mockVersions: ThreeVersionsOf<RuleKqlQuery> = {
|
||||
base_version: MissingVersion,
|
||||
current_version: {
|
||||
type: KqlQueryType.inline_query,
|
||||
query: 'query string = true',
|
||||
language: KqlQueryLanguageEnum.kuery,
|
||||
filters: [],
|
||||
},
|
||||
target_version: {
|
||||
type: KqlQueryType.inline_query,
|
||||
query: 'query string = false',
|
||||
language: KqlQueryLanguageEnum.kuery,
|
||||
filters: [],
|
||||
},
|
||||
};
|
||||
|
||||
const result = kqlQueryDiffAlgorithm(mockVersions);
|
||||
const result = kqlQueryDiffAlgorithm(mockVersions, true);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
has_base_version: false,
|
||||
base_version: undefined,
|
||||
merged_version: mockVersions.target_version,
|
||||
diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate,
|
||||
merge_outcome: ThreeWayMergeOutcome.Target,
|
||||
conflict: ThreeWayDiffConflict.SOLVABLE,
|
||||
})
|
||||
);
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
has_base_version: false,
|
||||
base_version: undefined,
|
||||
merged_version: mockVersions.target_version,
|
||||
diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate,
|
||||
merge_outcome: ThreeWayMergeOutcome.Target,
|
||||
conflict: ThreeWayDiffConflict.SOLVABLE,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('returns SOLVABLE conflict if current and target versions have different types', () => {
|
||||
const mockVersions: ThreeVersionsOf<RuleKqlQuery> = {
|
||||
base_version: MissingVersion,
|
||||
current_version: {
|
||||
type: KqlQueryType.saved_query,
|
||||
saved_query_id: 'saved-query-id-2',
|
||||
},
|
||||
target_version: {
|
||||
type: KqlQueryType.inline_query,
|
||||
query: 'query string = false',
|
||||
language: KqlQueryLanguageEnum.kuery,
|
||||
filters: [],
|
||||
},
|
||||
};
|
||||
|
||||
const result = kqlQueryDiffAlgorithm(mockVersions, true);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
has_base_version: false,
|
||||
base_version: undefined,
|
||||
merged_version: mockVersions.target_version,
|
||||
diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate,
|
||||
merge_outcome: ThreeWayMergeOutcome.Target,
|
||||
conflict: ThreeWayDiffConflict.SOLVABLE,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -15,5 +15,6 @@ import { simpleDiffAlgorithm } from './simple_diff_algorithm';
|
|||
* Diff algorithm for all kql query types (`inline_query` and `saved_query`)
|
||||
*/
|
||||
export const kqlQueryDiffAlgorithm = <TValue extends RuleKqlQuery>(
|
||||
versions: ThreeVersionsOf<TValue>
|
||||
) => simpleDiffAlgorithm<TValue>(versions);
|
||||
versions: ThreeVersionsOf<TValue>,
|
||||
isRuleCustomized: boolean
|
||||
) => simpleDiffAlgorithm<TValue>(versions, isRuleCustomized);
|
||||
|
|
|
@ -32,7 +32,7 @@ describe('multiLineStringDiffAlgorithm', () => {
|
|||
target_version: TEXT_M_A,
|
||||
};
|
||||
|
||||
const result = multiLineStringDiffAlgorithm(mockVersions);
|
||||
const result = multiLineStringDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -51,7 +51,7 @@ describe('multiLineStringDiffAlgorithm', () => {
|
|||
target_version: TEXT_M_A,
|
||||
};
|
||||
|
||||
const result = multiLineStringDiffAlgorithm(mockVersions);
|
||||
const result = multiLineStringDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -70,7 +70,7 @@ describe('multiLineStringDiffAlgorithm', () => {
|
|||
target_version: TEXT_M_B,
|
||||
};
|
||||
|
||||
const result = multiLineStringDiffAlgorithm(mockVersions);
|
||||
const result = multiLineStringDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -89,7 +89,7 @@ describe('multiLineStringDiffAlgorithm', () => {
|
|||
target_version: TEXT_M_B,
|
||||
};
|
||||
|
||||
const result = multiLineStringDiffAlgorithm(mockVersions);
|
||||
const result = multiLineStringDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -109,7 +109,7 @@ describe('multiLineStringDiffAlgorithm', () => {
|
|||
target_version: TEXT_M_C,
|
||||
};
|
||||
|
||||
const result = multiLineStringDiffAlgorithm(mockVersions);
|
||||
const result = multiLineStringDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -130,7 +130,7 @@ describe('multiLineStringDiffAlgorithm', () => {
|
|||
target_version: 'My description.\nThis is a MODIFIED second line.',
|
||||
};
|
||||
|
||||
const result = multiLineStringDiffAlgorithm(mockVersions);
|
||||
const result = multiLineStringDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -151,7 +151,7 @@ describe('multiLineStringDiffAlgorithm', () => {
|
|||
target_version: 'My EXCELLENT description.\nThis is a second line.',
|
||||
};
|
||||
|
||||
const result = multiLineStringDiffAlgorithm(mockVersions);
|
||||
const result = multiLineStringDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -171,7 +171,7 @@ describe('multiLineStringDiffAlgorithm', () => {
|
|||
};
|
||||
|
||||
const startTime = performance.now();
|
||||
const result = multiLineStringDiffAlgorithm(mockVersions);
|
||||
const result = multiLineStringDiffAlgorithm(mockVersions, false);
|
||||
const endTime = performance.now();
|
||||
|
||||
// If the regex merge in this function takes over 2 sec, this test fails
|
||||
|
@ -192,46 +192,92 @@ describe('multiLineStringDiffAlgorithm', () => {
|
|||
});
|
||||
|
||||
describe('if base_version is missing', () => {
|
||||
it('returns current_version as merged output if current_version and target_version are the same - scenario -AA', () => {
|
||||
const mockVersions: ThreeVersionsOf<string> = {
|
||||
base_version: MissingVersion,
|
||||
current_version: TEXT_M_A,
|
||||
target_version: TEXT_M_A,
|
||||
};
|
||||
describe('if target_version as merged output if current_version and target_version are the same - scenario -AA', () => {
|
||||
it('returns NONE conflict if rule is not customized', () => {
|
||||
const mockVersions: ThreeVersionsOf<string> = {
|
||||
base_version: MissingVersion,
|
||||
current_version: TEXT_M_A,
|
||||
target_version: TEXT_M_A,
|
||||
};
|
||||
|
||||
const result = multiLineStringDiffAlgorithm(mockVersions);
|
||||
const result = multiLineStringDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
has_base_version: false,
|
||||
base_version: undefined,
|
||||
merged_version: mockVersions.current_version,
|
||||
diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate,
|
||||
merge_outcome: ThreeWayMergeOutcome.Current,
|
||||
conflict: ThreeWayDiffConflict.NONE,
|
||||
})
|
||||
);
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
has_base_version: false,
|
||||
base_version: undefined,
|
||||
merged_version: mockVersions.target_version,
|
||||
diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate,
|
||||
merge_outcome: ThreeWayMergeOutcome.Target,
|
||||
conflict: ThreeWayDiffConflict.NONE,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('returns NONE conflict if rule is customized', () => {
|
||||
const mockVersions: ThreeVersionsOf<string> = {
|
||||
base_version: MissingVersion,
|
||||
current_version: TEXT_M_A,
|
||||
target_version: TEXT_M_A,
|
||||
};
|
||||
|
||||
const result = multiLineStringDiffAlgorithm(mockVersions, true);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
has_base_version: false,
|
||||
base_version: undefined,
|
||||
merged_version: mockVersions.target_version,
|
||||
diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate,
|
||||
merge_outcome: ThreeWayMergeOutcome.Target,
|
||||
conflict: ThreeWayDiffConflict.NONE,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('returns target_version as merged output if current_version and target_version are different - scenario -AB', () => {
|
||||
const mockVersions: ThreeVersionsOf<string> = {
|
||||
base_version: MissingVersion,
|
||||
current_version: TEXT_M_A,
|
||||
target_version: TEXT_M_B,
|
||||
};
|
||||
describe('returns NONE conflict if current_version and target_version are different and rule is not customized - scenario -AB', () => {
|
||||
it('returns NONE conflict if rule is not customized', () => {
|
||||
const mockVersions: ThreeVersionsOf<string> = {
|
||||
base_version: MissingVersion,
|
||||
current_version: TEXT_M_A,
|
||||
target_version: TEXT_M_B,
|
||||
};
|
||||
|
||||
const result = multiLineStringDiffAlgorithm(mockVersions);
|
||||
const result = multiLineStringDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
has_base_version: false,
|
||||
base_version: undefined,
|
||||
merged_version: mockVersions.target_version,
|
||||
diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate,
|
||||
merge_outcome: ThreeWayMergeOutcome.Target,
|
||||
conflict: ThreeWayDiffConflict.SOLVABLE,
|
||||
})
|
||||
);
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
has_base_version: false,
|
||||
base_version: undefined,
|
||||
merged_version: mockVersions.target_version,
|
||||
diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate,
|
||||
merge_outcome: ThreeWayMergeOutcome.Target,
|
||||
conflict: ThreeWayDiffConflict.NONE,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('returns SOLVABLE conflict if rule is customized', () => {
|
||||
const mockVersions: ThreeVersionsOf<string> = {
|
||||
base_version: MissingVersion,
|
||||
current_version: TEXT_M_A,
|
||||
target_version: TEXT_M_B,
|
||||
};
|
||||
|
||||
const result = multiLineStringDiffAlgorithm(mockVersions, true);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
has_base_version: false,
|
||||
base_version: undefined,
|
||||
merged_version: mockVersions.target_version,
|
||||
diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate,
|
||||
merge_outcome: ThreeWayMergeOutcome.Target,
|
||||
conflict: ThreeWayDiffConflict.SOLVABLE,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -24,7 +24,8 @@ import {
|
|||
* Diff algorithm used for string fields that contain multiple lines
|
||||
*/
|
||||
export const multiLineStringDiffAlgorithm = (
|
||||
versions: ThreeVersionsOf<string>
|
||||
versions: ThreeVersionsOf<string>,
|
||||
isRuleCustomized: boolean
|
||||
): ThreeWayDiff<string> => {
|
||||
const {
|
||||
base_version: baseVersion,
|
||||
|
@ -42,6 +43,7 @@ export const multiLineStringDiffAlgorithm = (
|
|||
currentVersion,
|
||||
targetVersion,
|
||||
diffOutcome,
|
||||
isRuleCustomized,
|
||||
});
|
||||
|
||||
return {
|
||||
|
@ -69,6 +71,7 @@ interface MergeArgs {
|
|||
currentVersion: string;
|
||||
targetVersion: string;
|
||||
diffOutcome: ThreeWayDiffOutcome;
|
||||
isRuleCustomized: boolean;
|
||||
}
|
||||
|
||||
const mergeVersions = ({
|
||||
|
@ -76,11 +79,9 @@ const mergeVersions = ({
|
|||
currentVersion,
|
||||
targetVersion,
|
||||
diffOutcome,
|
||||
isRuleCustomized,
|
||||
}: MergeArgs): MergeResult => {
|
||||
switch (diffOutcome) {
|
||||
// Scenario -AA is treated as scenario AAA:
|
||||
// https://github.com/elastic/kibana/pull/184889#discussion_r1636421293
|
||||
case ThreeWayDiffOutcome.MissingBaseNoUpdate:
|
||||
case ThreeWayDiffOutcome.StockValueNoUpdate:
|
||||
case ThreeWayDiffOutcome.CustomizedValueNoUpdate:
|
||||
case ThreeWayDiffOutcome.CustomizedValueSameUpdate:
|
||||
|
@ -118,14 +119,27 @@ const mergeVersions = ({
|
|||
};
|
||||
}
|
||||
|
||||
// Scenario -AB is treated as scenario ABC, but marked as
|
||||
// SOLVABLE, and returns the target version as the merged version
|
||||
// https://github.com/elastic/kibana/pull/184889#discussion_r1636421293
|
||||
case ThreeWayDiffOutcome.MissingBaseCanUpdate: {
|
||||
// Missing base versions always return target version
|
||||
// Scenario -AA is treated as AAA
|
||||
// https://github.com/elastic/kibana/issues/210358#issuecomment-2654492854
|
||||
case ThreeWayDiffOutcome.MissingBaseNoUpdate: {
|
||||
return {
|
||||
conflict: ThreeWayDiffConflict.NONE,
|
||||
mergedVersion: targetVersion,
|
||||
mergeOutcome: ThreeWayMergeOutcome.Target,
|
||||
};
|
||||
}
|
||||
|
||||
// Missing base versions always return target version
|
||||
// If the rule is customized, we return a SOLVABLE conflict
|
||||
// Since multi-line string fields are mergeable, we would typically return a merged value
|
||||
// as per https://github.com/elastic/kibana/pull/211862, but with no base version we cannot
|
||||
// complete a full diff merge and so just return the target version
|
||||
case ThreeWayDiffOutcome.MissingBaseCanUpdate: {
|
||||
return {
|
||||
conflict: isRuleCustomized ? ThreeWayDiffConflict.SOLVABLE : ThreeWayDiffConflict.NONE,
|
||||
mergedVersion: targetVersion,
|
||||
mergeOutcome: ThreeWayMergeOutcome.Target,
|
||||
conflict: ThreeWayDiffConflict.SOLVABLE,
|
||||
};
|
||||
}
|
||||
default:
|
||||
|
|
|
@ -22,7 +22,7 @@ describe('numberDiffAlgorithm', () => {
|
|||
target_version: 1,
|
||||
};
|
||||
|
||||
const result = numberDiffAlgorithm(mockVersions);
|
||||
const result = numberDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -41,7 +41,7 @@ describe('numberDiffAlgorithm', () => {
|
|||
target_version: 1,
|
||||
};
|
||||
|
||||
const result = numberDiffAlgorithm(mockVersions);
|
||||
const result = numberDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -60,7 +60,7 @@ describe('numberDiffAlgorithm', () => {
|
|||
target_version: 2,
|
||||
};
|
||||
|
||||
const result = numberDiffAlgorithm(mockVersions);
|
||||
const result = numberDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -79,7 +79,7 @@ describe('numberDiffAlgorithm', () => {
|
|||
target_version: 2,
|
||||
};
|
||||
|
||||
const result = numberDiffAlgorithm(mockVersions);
|
||||
const result = numberDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -98,7 +98,7 @@ describe('numberDiffAlgorithm', () => {
|
|||
target_version: 3,
|
||||
};
|
||||
|
||||
const result = numberDiffAlgorithm(mockVersions);
|
||||
const result = numberDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -111,46 +111,92 @@ describe('numberDiffAlgorithm', () => {
|
|||
});
|
||||
|
||||
describe('if base_version is missing', () => {
|
||||
it('returns current_version as merged output if current_version and target_version are the same - scenario -AA', () => {
|
||||
const mockVersions: ThreeVersionsOf<number> = {
|
||||
base_version: MissingVersion,
|
||||
current_version: 1,
|
||||
target_version: 1,
|
||||
};
|
||||
describe('if current_version and target_version are the same - scenario -AA', () => {
|
||||
it('returns NONE conflict if rule is NOT customized', () => {
|
||||
const mockVersions: ThreeVersionsOf<number> = {
|
||||
base_version: MissingVersion,
|
||||
current_version: 1,
|
||||
target_version: 1,
|
||||
};
|
||||
|
||||
const result = numberDiffAlgorithm(mockVersions);
|
||||
const result = numberDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
has_base_version: false,
|
||||
base_version: undefined,
|
||||
merged_version: mockVersions.current_version,
|
||||
diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate,
|
||||
merge_outcome: ThreeWayMergeOutcome.Current,
|
||||
conflict: ThreeWayDiffConflict.NONE,
|
||||
})
|
||||
);
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
has_base_version: false,
|
||||
base_version: undefined,
|
||||
merged_version: mockVersions.target_version,
|
||||
diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate,
|
||||
merge_outcome: ThreeWayMergeOutcome.Target,
|
||||
conflict: ThreeWayDiffConflict.NONE,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('returns NONE conflict if rule is customized', () => {
|
||||
const mockVersions: ThreeVersionsOf<number> = {
|
||||
base_version: MissingVersion,
|
||||
current_version: 1,
|
||||
target_version: 1,
|
||||
};
|
||||
|
||||
const result = numberDiffAlgorithm(mockVersions, true);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
has_base_version: false,
|
||||
base_version: undefined,
|
||||
merged_version: mockVersions.target_version,
|
||||
diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate,
|
||||
merge_outcome: ThreeWayMergeOutcome.Target,
|
||||
conflict: ThreeWayDiffConflict.NONE,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('returns target_version as merged output if current_version and target_version are different - scenario -AB', () => {
|
||||
const mockVersions: ThreeVersionsOf<number> = {
|
||||
base_version: MissingVersion,
|
||||
current_version: 1,
|
||||
target_version: 2,
|
||||
};
|
||||
describe('if current_version and target_version are different - scenario -AB', () => {
|
||||
it('returns NONE conflict if rule is NOT customized', () => {
|
||||
const mockVersions: ThreeVersionsOf<number> = {
|
||||
base_version: MissingVersion,
|
||||
current_version: 1,
|
||||
target_version: 2,
|
||||
};
|
||||
|
||||
const result = numberDiffAlgorithm(mockVersions);
|
||||
const result = numberDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
has_base_version: false,
|
||||
base_version: undefined,
|
||||
merged_version: mockVersions.target_version,
|
||||
diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate,
|
||||
merge_outcome: ThreeWayMergeOutcome.Target,
|
||||
conflict: ThreeWayDiffConflict.SOLVABLE,
|
||||
})
|
||||
);
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
has_base_version: false,
|
||||
base_version: undefined,
|
||||
merged_version: mockVersions.target_version,
|
||||
diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate,
|
||||
merge_outcome: ThreeWayMergeOutcome.Target,
|
||||
conflict: ThreeWayDiffConflict.NONE,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('returns SOLVABLE conflict if rule is customized', () => {
|
||||
const mockVersions: ThreeVersionsOf<number> = {
|
||||
base_version: MissingVersion,
|
||||
current_version: 1,
|
||||
target_version: 2,
|
||||
};
|
||||
|
||||
const result = numberDiffAlgorithm(mockVersions, true);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
has_base_version: false,
|
||||
base_version: undefined,
|
||||
merged_version: mockVersions.target_version,
|
||||
diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate,
|
||||
merge_outcome: ThreeWayMergeOutcome.Target,
|
||||
conflict: ThreeWayDiffConflict.SOLVABLE,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -9,5 +9,6 @@ import type { ThreeVersionsOf } from '../../../../../../../../common/api/detecti
|
|||
import { simpleDiffAlgorithm } from './simple_diff_algorithm';
|
||||
|
||||
export const numberDiffAlgorithm = <TValue extends number | undefined>(
|
||||
versions: ThreeVersionsOf<TValue>
|
||||
) => simpleDiffAlgorithm<TValue>(versions);
|
||||
versions: ThreeVersionsOf<TValue>,
|
||||
isRuleCustomized: boolean
|
||||
) => simpleDiffAlgorithm<TValue>(versions, isRuleCustomized);
|
||||
|
|
|
@ -119,47 +119,51 @@ describe('ruleTypeDiffAlgorithm', () => {
|
|||
});
|
||||
|
||||
describe('if base_version is missing', () => {
|
||||
it('returns current_version as merged output if current_version and target_version are the same - scenario -AA', () => {
|
||||
const mockVersions: ThreeVersionsOf<DiffableRuleTypes> = {
|
||||
base_version: MissingVersion,
|
||||
current_version: 'query',
|
||||
target_version: 'query',
|
||||
};
|
||||
describe('if current_version and target_version are the same - scenario -AA', () => {
|
||||
it('returns NONE conflict', () => {
|
||||
const mockVersions: ThreeVersionsOf<DiffableRuleTypes> = {
|
||||
base_version: MissingVersion,
|
||||
current_version: 'query',
|
||||
target_version: 'query',
|
||||
};
|
||||
|
||||
const result = ruleTypeDiffAlgorithm(mockVersions);
|
||||
const result = ruleTypeDiffAlgorithm(mockVersions);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
has_base_version: false,
|
||||
base_version: undefined,
|
||||
merged_version: mockVersions.target_version,
|
||||
diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate,
|
||||
merge_outcome: ThreeWayMergeOutcome.Target,
|
||||
conflict: ThreeWayDiffConflict.NONE,
|
||||
})
|
||||
);
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
has_base_version: false,
|
||||
base_version: undefined,
|
||||
merged_version: mockVersions.target_version,
|
||||
diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate,
|
||||
merge_outcome: ThreeWayMergeOutcome.Target,
|
||||
conflict: ThreeWayDiffConflict.NONE,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('returns target_version as merged output if current_version and target_version are different - scenario -AB', () => {
|
||||
// User can change rule type field between `query` and `saved_query` in the UI, no other rule types
|
||||
const mockVersions: ThreeVersionsOf<DiffableRuleTypes> = {
|
||||
base_version: MissingVersion,
|
||||
current_version: 'query',
|
||||
target_version: 'saved_query',
|
||||
};
|
||||
describe('returns target_version as merged output if current_version and target_version are different - scenario -AB', () => {
|
||||
it('returns NON_SOLVABLE conflict', () => {
|
||||
// User can change rule type field between `query` and `saved_query` in the UI, no other rule types
|
||||
const mockVersions: ThreeVersionsOf<DiffableRuleTypes> = {
|
||||
base_version: MissingVersion,
|
||||
current_version: 'query',
|
||||
target_version: 'saved_query',
|
||||
};
|
||||
|
||||
const result = ruleTypeDiffAlgorithm(mockVersions);
|
||||
const result = ruleTypeDiffAlgorithm(mockVersions);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
has_base_version: false,
|
||||
base_version: undefined,
|
||||
merged_version: mockVersions.target_version,
|
||||
diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate,
|
||||
merge_outcome: ThreeWayMergeOutcome.Target,
|
||||
conflict: ThreeWayDiffConflict.NON_SOLVABLE,
|
||||
})
|
||||
);
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
has_base_version: false,
|
||||
base_version: undefined,
|
||||
merged_version: mockVersions.target_version,
|
||||
diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate,
|
||||
merge_outcome: ThreeWayMergeOutcome.Target,
|
||||
conflict: ThreeWayDiffConflict.NON_SOLVABLE,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -69,8 +69,9 @@ const mergeVersions = <TValue>({
|
|||
diffOutcome,
|
||||
}: MergeArgs<TValue>): MergeResult<TValue> => {
|
||||
switch (diffOutcome) {
|
||||
// Scenario -AA is treated as scenario AAA:
|
||||
// https://github.com/elastic/kibana/pull/184889#discussion_r1636421293
|
||||
// Missing base versions always return target version
|
||||
// Scenario -AA is treated as AAA
|
||||
// https://github.com/elastic/kibana/issues/210358#issuecomment-2654492854
|
||||
case ThreeWayDiffOutcome.MissingBaseNoUpdate:
|
||||
case ThreeWayDiffOutcome.StockValueNoUpdate:
|
||||
return {
|
||||
|
@ -83,8 +84,9 @@ const mergeVersions = <TValue>({
|
|||
case ThreeWayDiffOutcome.StockValueCanUpdate:
|
||||
// NOTE: This scenario is currently inaccessible via normal UI or API workflows, but the logic is covered just in case
|
||||
case ThreeWayDiffOutcome.CustomizedValueCanUpdate:
|
||||
// Scenario -AB is treated as scenario ABC:
|
||||
// https://github.com/elastic/kibana/pull/184889#discussion_r1636421293
|
||||
// Missing base versions always return target version
|
||||
// We return all -AB rule type fields as NON_SOLVABLE, whether or not the rule is customized
|
||||
// https://github.com/elastic/kibana/issues/210358#issuecomment-2654492854
|
||||
case ThreeWayDiffOutcome.MissingBaseCanUpdate: {
|
||||
return {
|
||||
mergedVersion: targetVersion,
|
||||
|
|
|
@ -22,7 +22,7 @@ describe('scalarArrayDiffAlgorithm', () => {
|
|||
target_version: ['one', 'two', 'three'],
|
||||
};
|
||||
|
||||
const result = scalarArrayDiffAlgorithm(mockVersions);
|
||||
const result = scalarArrayDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -41,7 +41,7 @@ describe('scalarArrayDiffAlgorithm', () => {
|
|||
target_version: ['one', 'two', 'three'],
|
||||
};
|
||||
|
||||
const result = scalarArrayDiffAlgorithm(mockVersions);
|
||||
const result = scalarArrayDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -60,7 +60,7 @@ describe('scalarArrayDiffAlgorithm', () => {
|
|||
target_version: ['one', 'four', 'three'],
|
||||
};
|
||||
|
||||
const result = scalarArrayDiffAlgorithm(mockVersions);
|
||||
const result = scalarArrayDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -79,7 +79,7 @@ describe('scalarArrayDiffAlgorithm', () => {
|
|||
target_version: ['one', 'four', 'three'],
|
||||
};
|
||||
|
||||
const result = scalarArrayDiffAlgorithm(mockVersions);
|
||||
const result = scalarArrayDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -99,7 +99,7 @@ describe('scalarArrayDiffAlgorithm', () => {
|
|||
};
|
||||
const expectedMergedVersion = ['three', 'four', 'five', 'six'];
|
||||
|
||||
const result = scalarArrayDiffAlgorithm(mockVersions);
|
||||
const result = scalarArrayDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -112,46 +112,94 @@ describe('scalarArrayDiffAlgorithm', () => {
|
|||
});
|
||||
|
||||
describe('if base_version is missing', () => {
|
||||
it('returns current_version as merged output if current_version and target_version are the same - scenario -AA', () => {
|
||||
const mockVersions: ThreeVersionsOf<string[]> = {
|
||||
base_version: MissingVersion,
|
||||
current_version: ['one', 'two', 'three'],
|
||||
target_version: ['one', 'two', 'three'],
|
||||
};
|
||||
describe('returns target_version as merged output if current_version and target_version are the same - scenario -AA', () => {
|
||||
it('returns NONE conflict if rule is not customized', () => {
|
||||
const mockVersions: ThreeVersionsOf<string[]> = {
|
||||
base_version: MissingVersion,
|
||||
current_version: ['one', 'two', 'three'],
|
||||
target_version: ['one', 'two', 'three'],
|
||||
};
|
||||
|
||||
const result = scalarArrayDiffAlgorithm(mockVersions);
|
||||
const result = scalarArrayDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
has_base_version: false,
|
||||
base_version: undefined,
|
||||
merged_version: mockVersions.current_version,
|
||||
diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate,
|
||||
merge_outcome: ThreeWayMergeOutcome.Current,
|
||||
conflict: ThreeWayDiffConflict.NONE,
|
||||
})
|
||||
);
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
has_base_version: false,
|
||||
base_version: undefined,
|
||||
merged_version: mockVersions.target_version,
|
||||
diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate,
|
||||
merge_outcome: ThreeWayMergeOutcome.Target,
|
||||
conflict: ThreeWayDiffConflict.NONE,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('returns NONE conflict if rule is customized', () => {
|
||||
const mockVersions: ThreeVersionsOf<string[]> = {
|
||||
base_version: MissingVersion,
|
||||
current_version: ['one', 'two', 'three'],
|
||||
target_version: ['one', 'two', 'three'],
|
||||
};
|
||||
|
||||
const result = scalarArrayDiffAlgorithm(mockVersions, true);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
has_base_version: false,
|
||||
base_version: undefined,
|
||||
merged_version: mockVersions.target_version,
|
||||
diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate,
|
||||
merge_outcome: ThreeWayMergeOutcome.Target,
|
||||
conflict: ThreeWayDiffConflict.NONE,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('returns target_version as merged output if current_version and target_version are different - scenario -AB', () => {
|
||||
const mockVersions: ThreeVersionsOf<string[]> = {
|
||||
base_version: MissingVersion,
|
||||
current_version: ['one', 'two', 'three'],
|
||||
target_version: ['one', 'four', 'three'],
|
||||
};
|
||||
describe('if current_version and target_version are different - scenario -AB', () => {
|
||||
it('returns target_version as merged output and NONE conflict if rule is not customized', () => {
|
||||
const mockVersions: ThreeVersionsOf<string[]> = {
|
||||
base_version: MissingVersion,
|
||||
current_version: ['one', 'two', 'three'],
|
||||
target_version: ['one', 'four', 'three'],
|
||||
};
|
||||
|
||||
const result = scalarArrayDiffAlgorithm(mockVersions);
|
||||
const result = scalarArrayDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
has_base_version: false,
|
||||
base_version: undefined,
|
||||
merged_version: mockVersions.target_version,
|
||||
diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate,
|
||||
merge_outcome: ThreeWayMergeOutcome.Target,
|
||||
conflict: ThreeWayDiffConflict.SOLVABLE,
|
||||
})
|
||||
);
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
has_base_version: false,
|
||||
base_version: undefined,
|
||||
merged_version: mockVersions.target_version,
|
||||
diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate,
|
||||
merge_outcome: ThreeWayMergeOutcome.Target,
|
||||
conflict: ThreeWayDiffConflict.NONE,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('returns merged version of current and target as merged output if rule is customized', () => {
|
||||
const mockVersions: ThreeVersionsOf<string[]> = {
|
||||
base_version: MissingVersion,
|
||||
current_version: ['one', 'two', 'three'],
|
||||
target_version: ['one', 'four', 'three'],
|
||||
};
|
||||
|
||||
const expectedMergedVersion = ['one', 'two', 'three', 'four'];
|
||||
|
||||
const result = scalarArrayDiffAlgorithm(mockVersions, true);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
has_base_version: false,
|
||||
base_version: undefined,
|
||||
merged_version: expectedMergedVersion,
|
||||
diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate,
|
||||
merge_outcome: ThreeWayMergeOutcome.Merged,
|
||||
conflict: ThreeWayDiffConflict.SOLVABLE,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -163,7 +211,7 @@ describe('scalarArrayDiffAlgorithm', () => {
|
|||
target_version: ['three', 'one', 'two'],
|
||||
};
|
||||
|
||||
const result = scalarArrayDiffAlgorithm(mockVersions);
|
||||
const result = scalarArrayDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -184,7 +232,7 @@ describe('scalarArrayDiffAlgorithm', () => {
|
|||
};
|
||||
const expectedMergedVersion = ['one', 'two'];
|
||||
|
||||
const result = scalarArrayDiffAlgorithm(mockVersions);
|
||||
const result = scalarArrayDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -204,7 +252,7 @@ describe('scalarArrayDiffAlgorithm', () => {
|
|||
};
|
||||
const expectedMergedVersion = ['one', 'two'];
|
||||
|
||||
const result = scalarArrayDiffAlgorithm(mockVersions);
|
||||
const result = scalarArrayDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -224,7 +272,7 @@ describe('scalarArrayDiffAlgorithm', () => {
|
|||
};
|
||||
const expectedMergedVersion = ['one', 'two'];
|
||||
|
||||
const result = scalarArrayDiffAlgorithm(mockVersions);
|
||||
const result = scalarArrayDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -244,7 +292,7 @@ describe('scalarArrayDiffAlgorithm', () => {
|
|||
};
|
||||
const expectedMergedVersion = ['three'];
|
||||
|
||||
const result = scalarArrayDiffAlgorithm(mockVersions);
|
||||
const result = scalarArrayDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -265,7 +313,7 @@ describe('scalarArrayDiffAlgorithm', () => {
|
|||
target_version: ['one', 'two'],
|
||||
};
|
||||
|
||||
const result = scalarArrayDiffAlgorithm(mockVersions);
|
||||
const result = scalarArrayDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -284,7 +332,7 @@ describe('scalarArrayDiffAlgorithm', () => {
|
|||
target_version: ['one', 'two'],
|
||||
};
|
||||
|
||||
const result = scalarArrayDiffAlgorithm(mockVersions);
|
||||
const result = scalarArrayDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -303,7 +351,7 @@ describe('scalarArrayDiffAlgorithm', () => {
|
|||
target_version: [],
|
||||
};
|
||||
|
||||
const result = scalarArrayDiffAlgorithm(mockVersions);
|
||||
const result = scalarArrayDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -322,7 +370,7 @@ describe('scalarArrayDiffAlgorithm', () => {
|
|||
target_version: [],
|
||||
};
|
||||
|
||||
const result = scalarArrayDiffAlgorithm(mockVersions);
|
||||
const result = scalarArrayDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { uniq } from 'lodash';
|
||||
import { union, uniq } from 'lodash';
|
||||
import { assertUnreachable } from '../../../../../../../../common/utility_types';
|
||||
import type {
|
||||
ThreeVersionsOf,
|
||||
|
@ -27,7 +27,8 @@ import { mergeDedupedArrays } from './helpers';
|
|||
* NOTE: Diffing logic will be agnostic to array order
|
||||
*/
|
||||
export const scalarArrayDiffAlgorithm = <TValue>(
|
||||
versions: ThreeVersionsOf<TValue[]>
|
||||
versions: ThreeVersionsOf<TValue[]>,
|
||||
isRuleCustomized: boolean
|
||||
): ThreeWayDiff<TValue[]> => {
|
||||
const {
|
||||
base_version: baseVersion,
|
||||
|
@ -45,6 +46,7 @@ export const scalarArrayDiffAlgorithm = <TValue>(
|
|||
currentVersion,
|
||||
targetVersion,
|
||||
diffOutcome,
|
||||
isRuleCustomized,
|
||||
});
|
||||
|
||||
return {
|
||||
|
@ -72,6 +74,7 @@ interface MergeArgs<TValue> {
|
|||
currentVersion: TValue[];
|
||||
targetVersion: TValue[];
|
||||
diffOutcome: ThreeWayDiffOutcome;
|
||||
isRuleCustomized: boolean;
|
||||
}
|
||||
|
||||
const mergeVersions = <TValue>({
|
||||
|
@ -79,15 +82,13 @@ const mergeVersions = <TValue>({
|
|||
currentVersion,
|
||||
targetVersion,
|
||||
diffOutcome,
|
||||
isRuleCustomized,
|
||||
}: MergeArgs<TValue>): MergeResult<TValue> => {
|
||||
const dedupedBaseVersion = uniq(baseVersion);
|
||||
const dedupedCurrentVersion = uniq(currentVersion);
|
||||
const dedupedTargetVersion = uniq(targetVersion);
|
||||
|
||||
switch (diffOutcome) {
|
||||
// Scenario -AA is treated as scenario AAA:
|
||||
// https://github.com/elastic/kibana/pull/184889#discussion_r1636421293
|
||||
case ThreeWayDiffOutcome.MissingBaseNoUpdate:
|
||||
case ThreeWayDiffOutcome.StockValueNoUpdate:
|
||||
case ThreeWayDiffOutcome.CustomizedValueNoUpdate:
|
||||
case ThreeWayDiffOutcome.CustomizedValueSameUpdate:
|
||||
|
@ -118,16 +119,34 @@ const mergeVersions = <TValue>({
|
|||
mergeOutcome: ThreeWayMergeOutcome.Merged,
|
||||
};
|
||||
}
|
||||
// Scenario -AB is treated as scenario ABC, but marked as
|
||||
// SOLVABLE, and returns the target version as the merged version
|
||||
// https://github.com/elastic/kibana/pull/184889#discussion_r1636421293
|
||||
case ThreeWayDiffOutcome.MissingBaseCanUpdate: {
|
||||
|
||||
// Missing base versions always return target version
|
||||
// Scenario -AA is treated as AAA
|
||||
// https://github.com/elastic/kibana/issues/210358#issuecomment-2654492854
|
||||
case ThreeWayDiffOutcome.MissingBaseNoUpdate: {
|
||||
return {
|
||||
conflict: ThreeWayDiffConflict.NONE,
|
||||
mergedVersion: targetVersion,
|
||||
mergeOutcome: ThreeWayMergeOutcome.Target,
|
||||
conflict: ThreeWayDiffConflict.SOLVABLE,
|
||||
};
|
||||
}
|
||||
|
||||
// If the rule is customized, we return a SOLVABLE conflict with a merged outcome
|
||||
// Otherwise we treat scenario -AB as AAB
|
||||
// https://github.com/elastic/kibana/issues/210358#issuecomment-2654492854
|
||||
case ThreeWayDiffOutcome.MissingBaseCanUpdate: {
|
||||
return isRuleCustomized
|
||||
? {
|
||||
mergedVersion: union(dedupedCurrentVersion, dedupedTargetVersion),
|
||||
mergeOutcome: ThreeWayMergeOutcome.Merged,
|
||||
conflict: ThreeWayDiffConflict.SOLVABLE,
|
||||
}
|
||||
: {
|
||||
mergedVersion: targetVersion,
|
||||
mergeOutcome: ThreeWayMergeOutcome.Target,
|
||||
conflict: ThreeWayDiffConflict.NONE,
|
||||
};
|
||||
}
|
||||
default:
|
||||
return assertUnreachable(diffOutcome);
|
||||
}
|
||||
|
|
|
@ -25,7 +25,8 @@ import {
|
|||
* Meant to be used with primitive types (strings, numbers, booleans), NOT Arrays or Objects
|
||||
*/
|
||||
export const simpleDiffAlgorithm = <TValue>(
|
||||
versions: ThreeVersionsOf<TValue>
|
||||
versions: ThreeVersionsOf<TValue>,
|
||||
isRuleCustomized: boolean
|
||||
): ThreeWayDiff<TValue> => {
|
||||
const {
|
||||
base_version: baseVersion,
|
||||
|
@ -39,10 +40,10 @@ export const simpleDiffAlgorithm = <TValue>(
|
|||
const hasBaseVersion = baseVersion !== MissingVersion;
|
||||
|
||||
const { mergeOutcome, conflict, mergedVersion } = mergeVersions({
|
||||
hasBaseVersion,
|
||||
currentVersion,
|
||||
targetVersion,
|
||||
diffOutcome,
|
||||
isRuleCustomized,
|
||||
});
|
||||
|
||||
return {
|
||||
|
@ -66,22 +67,19 @@ interface MergeResult<TValue> {
|
|||
}
|
||||
|
||||
interface MergeArgs<TValue> {
|
||||
hasBaseVersion: boolean;
|
||||
currentVersion: TValue;
|
||||
targetVersion: TValue;
|
||||
diffOutcome: ThreeWayDiffOutcome;
|
||||
isRuleCustomized: boolean;
|
||||
}
|
||||
|
||||
const mergeVersions = <TValue>({
|
||||
hasBaseVersion,
|
||||
currentVersion,
|
||||
targetVersion,
|
||||
diffOutcome,
|
||||
isRuleCustomized,
|
||||
}: MergeArgs<TValue>): MergeResult<TValue> => {
|
||||
switch (diffOutcome) {
|
||||
// Scenario -AA is treated as scenario AAA:
|
||||
// https://github.com/elastic/kibana/pull/184889#discussion_r1636421293
|
||||
case ThreeWayDiffOutcome.MissingBaseNoUpdate:
|
||||
case ThreeWayDiffOutcome.StockValueNoUpdate:
|
||||
case ThreeWayDiffOutcome.CustomizedValueNoUpdate:
|
||||
case ThreeWayDiffOutcome.CustomizedValueSameUpdate:
|
||||
|
@ -106,14 +104,26 @@ const mergeVersions = <TValue>({
|
|||
};
|
||||
}
|
||||
|
||||
// Scenario -AB is treated as scenario ABC, but marked as
|
||||
// SOLVABLE, and returns the target version as the merged version
|
||||
// https://github.com/elastic/kibana/pull/184889#discussion_r1636421293
|
||||
case ThreeWayDiffOutcome.MissingBaseCanUpdate: {
|
||||
// Missing base versions always return target version
|
||||
// Scenario -AA is treated as AAA
|
||||
// https://github.com/elastic/kibana/issues/210358#issuecomment-2654492854
|
||||
case ThreeWayDiffOutcome.MissingBaseNoUpdate: {
|
||||
return {
|
||||
conflict: ThreeWayDiffConflict.NONE,
|
||||
mergedVersion: targetVersion,
|
||||
mergeOutcome: ThreeWayMergeOutcome.Target,
|
||||
};
|
||||
}
|
||||
|
||||
// Missing base versions always return target version
|
||||
// If the rule is customized, we return a SOLVABLE conflict
|
||||
// Otherwise we treat scenario -AB as AAB
|
||||
// https://github.com/elastic/kibana/issues/210358#issuecomment-2654492854
|
||||
case ThreeWayDiffOutcome.MissingBaseCanUpdate: {
|
||||
return {
|
||||
conflict: isRuleCustomized ? ThreeWayDiffConflict.SOLVABLE : ThreeWayDiffConflict.NONE,
|
||||
mergedVersion: targetVersion,
|
||||
mergeOutcome: ThreeWayMergeOutcome.Target,
|
||||
conflict: ThreeWayDiffConflict.SOLVABLE,
|
||||
};
|
||||
}
|
||||
default:
|
||||
|
|
|
@ -22,7 +22,7 @@ describe('singleLineStringDiffAlgorithm', () => {
|
|||
target_version: 'A',
|
||||
};
|
||||
|
||||
const result = singleLineStringDiffAlgorithm(mockVersions);
|
||||
const result = singleLineStringDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -41,7 +41,7 @@ describe('singleLineStringDiffAlgorithm', () => {
|
|||
target_version: 'A',
|
||||
};
|
||||
|
||||
const result = singleLineStringDiffAlgorithm(mockVersions);
|
||||
const result = singleLineStringDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -60,7 +60,7 @@ describe('singleLineStringDiffAlgorithm', () => {
|
|||
target_version: 'B',
|
||||
};
|
||||
|
||||
const result = singleLineStringDiffAlgorithm(mockVersions);
|
||||
const result = singleLineStringDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -79,7 +79,7 @@ describe('singleLineStringDiffAlgorithm', () => {
|
|||
target_version: 'B',
|
||||
};
|
||||
|
||||
const result = singleLineStringDiffAlgorithm(mockVersions);
|
||||
const result = singleLineStringDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -98,7 +98,7 @@ describe('singleLineStringDiffAlgorithm', () => {
|
|||
target_version: 'C',
|
||||
};
|
||||
|
||||
const result = singleLineStringDiffAlgorithm(mockVersions);
|
||||
const result = singleLineStringDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -111,46 +111,92 @@ describe('singleLineStringDiffAlgorithm', () => {
|
|||
});
|
||||
|
||||
describe('if base_version is missing', () => {
|
||||
it('returns current_version as merged output if current_version and target_version are the same - scenario -AA', () => {
|
||||
const mockVersions: ThreeVersionsOf<string> = {
|
||||
base_version: MissingVersion,
|
||||
current_version: 'A',
|
||||
target_version: 'A',
|
||||
};
|
||||
describe('returns current_version as merged output if current_version and target_version are the same - scenario -AA', () => {
|
||||
it('returns NONE conflict if rule is NOT customized', () => {
|
||||
const mockVersions: ThreeVersionsOf<string> = {
|
||||
base_version: MissingVersion,
|
||||
current_version: 'A',
|
||||
target_version: 'A',
|
||||
};
|
||||
|
||||
const result = singleLineStringDiffAlgorithm(mockVersions);
|
||||
const result = singleLineStringDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
has_base_version: false,
|
||||
base_version: undefined,
|
||||
merged_version: mockVersions.current_version,
|
||||
diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate,
|
||||
merge_outcome: ThreeWayMergeOutcome.Current,
|
||||
conflict: ThreeWayDiffConflict.NONE,
|
||||
})
|
||||
);
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
has_base_version: false,
|
||||
base_version: undefined,
|
||||
merged_version: mockVersions.target_version,
|
||||
diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate,
|
||||
merge_outcome: ThreeWayMergeOutcome.Target,
|
||||
conflict: ThreeWayDiffConflict.NONE,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('returns NONE conflict if rule is customized', () => {
|
||||
const mockVersions: ThreeVersionsOf<string> = {
|
||||
base_version: MissingVersion,
|
||||
current_version: 'A',
|
||||
target_version: 'A',
|
||||
};
|
||||
|
||||
const result = singleLineStringDiffAlgorithm(mockVersions, true);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
has_base_version: false,
|
||||
base_version: undefined,
|
||||
merged_version: mockVersions.target_version,
|
||||
diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate,
|
||||
merge_outcome: ThreeWayMergeOutcome.Target,
|
||||
conflict: ThreeWayDiffConflict.NONE,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('returns target_version as merged output if current_version and target_version are different - scenario -AB', () => {
|
||||
const mockVersions: ThreeVersionsOf<string> = {
|
||||
base_version: MissingVersion,
|
||||
current_version: 'A',
|
||||
target_version: 'B',
|
||||
};
|
||||
describe('returns target_version as merged output if current_version and target_version are different - scenario -AB', () => {
|
||||
it('returns NONE conflict if rule is NOT customized', () => {
|
||||
const mockVersions: ThreeVersionsOf<string> = {
|
||||
base_version: MissingVersion,
|
||||
current_version: 'A',
|
||||
target_version: 'B',
|
||||
};
|
||||
|
||||
const result = singleLineStringDiffAlgorithm(mockVersions);
|
||||
const result = singleLineStringDiffAlgorithm(mockVersions, false);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
has_base_version: false,
|
||||
base_version: undefined,
|
||||
merged_version: mockVersions.target_version,
|
||||
diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate,
|
||||
merge_outcome: ThreeWayMergeOutcome.Target,
|
||||
conflict: ThreeWayDiffConflict.SOLVABLE,
|
||||
})
|
||||
);
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
has_base_version: false,
|
||||
base_version: undefined,
|
||||
merged_version: mockVersions.target_version,
|
||||
diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate,
|
||||
merge_outcome: ThreeWayMergeOutcome.Target,
|
||||
conflict: ThreeWayDiffConflict.NONE,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('returns SOLVABLE conflict if rule is customized', () => {
|
||||
const mockVersions: ThreeVersionsOf<string> = {
|
||||
base_version: MissingVersion,
|
||||
current_version: 'A',
|
||||
target_version: 'B',
|
||||
};
|
||||
|
||||
const result = singleLineStringDiffAlgorithm(mockVersions, true);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
has_base_version: false,
|
||||
base_version: undefined,
|
||||
merged_version: mockVersions.target_version,
|
||||
diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate,
|
||||
merge_outcome: ThreeWayMergeOutcome.Target,
|
||||
conflict: ThreeWayDiffConflict.SOLVABLE,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -9,5 +9,6 @@ import type { ThreeVersionsOf } from '../../../../../../../../common/api/detecti
|
|||
import { simpleDiffAlgorithm } from './simple_diff_algorithm';
|
||||
|
||||
export const singleLineStringDiffAlgorithm = <TValue extends string | undefined>(
|
||||
versions: ThreeVersionsOf<TValue>
|
||||
) => simpleDiffAlgorithm<TValue>(versions);
|
||||
versions: ThreeVersionsOf<TValue>,
|
||||
isRuleCustomized: boolean
|
||||
) => simpleDiffAlgorithm<TValue>(versions, isRuleCustomized);
|
||||
|
|
|
@ -60,9 +60,10 @@ const TARGET_TYPE_ERROR = `Target version can't be of different rule type`;
|
|||
* three-way diffs calculated for those fields.
|
||||
*/
|
||||
export const calculateRuleFieldsDiff = (
|
||||
ruleVersions: ThreeVersionsOf<DiffableRule>
|
||||
ruleVersions: ThreeVersionsOf<DiffableRule>,
|
||||
isRuleCustomized: boolean = false
|
||||
): RuleFieldsDiff => {
|
||||
const commonFieldsDiff = calculateCommonFieldsDiff(ruleVersions);
|
||||
const commonFieldsDiff = calculateCommonFieldsDiff(ruleVersions, isRuleCustomized);
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const { base_version, current_version, target_version } = ruleVersions;
|
||||
const hasBaseVersion = base_version !== MissingVersion;
|
||||
|
@ -78,11 +79,14 @@ export const calculateRuleFieldsDiff = (
|
|||
// only for fields of a single rule type, and need to calculate it for all fields
|
||||
// of all the rule types we have.
|
||||
// TODO: Try to get rid of "as" casting
|
||||
return calculateAllFieldsDiff({
|
||||
base_version: base_version as DiffableAllFields | MissingVersion,
|
||||
current_version: current_version as DiffableAllFields,
|
||||
target_version: target_version as DiffableAllFields,
|
||||
}) as RuleFieldsDiff;
|
||||
return calculateAllFieldsDiff(
|
||||
{
|
||||
base_version: base_version as DiffableAllFields | MissingVersion,
|
||||
current_version: current_version as DiffableAllFields,
|
||||
target_version: target_version as DiffableAllFields,
|
||||
},
|
||||
isRuleCustomized
|
||||
) as RuleFieldsDiff;
|
||||
}
|
||||
|
||||
switch (current_version.type) {
|
||||
|
@ -93,7 +97,10 @@ export const calculateRuleFieldsDiff = (
|
|||
invariant(target_version.type === 'query', TARGET_TYPE_ERROR);
|
||||
return {
|
||||
...commonFieldsDiff,
|
||||
...calculateCustomQueryFieldsDiff({ base_version, current_version, target_version }),
|
||||
...calculateCustomQueryFieldsDiff(
|
||||
{ base_version, current_version, target_version },
|
||||
isRuleCustomized
|
||||
),
|
||||
};
|
||||
}
|
||||
case 'saved_query': {
|
||||
|
@ -103,7 +110,10 @@ export const calculateRuleFieldsDiff = (
|
|||
invariant(target_version.type === 'saved_query', TARGET_TYPE_ERROR);
|
||||
return {
|
||||
...commonFieldsDiff,
|
||||
...calculateSavedQueryFieldsDiff({ base_version, current_version, target_version }),
|
||||
...calculateSavedQueryFieldsDiff(
|
||||
{ base_version, current_version, target_version },
|
||||
isRuleCustomized
|
||||
),
|
||||
};
|
||||
}
|
||||
case 'eql': {
|
||||
|
@ -113,7 +123,10 @@ export const calculateRuleFieldsDiff = (
|
|||
invariant(target_version.type === 'eql', TARGET_TYPE_ERROR);
|
||||
return {
|
||||
...commonFieldsDiff,
|
||||
...calculateEqlFieldsDiff({ base_version, current_version, target_version }),
|
||||
...calculateEqlFieldsDiff(
|
||||
{ base_version, current_version, target_version },
|
||||
isRuleCustomized
|
||||
),
|
||||
};
|
||||
}
|
||||
case 'threat_match': {
|
||||
|
@ -123,7 +136,10 @@ export const calculateRuleFieldsDiff = (
|
|||
invariant(target_version.type === 'threat_match', TARGET_TYPE_ERROR);
|
||||
return {
|
||||
...commonFieldsDiff,
|
||||
...calculateThreatMatchFieldsDiff({ base_version, current_version, target_version }),
|
||||
...calculateThreatMatchFieldsDiff(
|
||||
{ base_version, current_version, target_version },
|
||||
isRuleCustomized
|
||||
),
|
||||
};
|
||||
}
|
||||
case 'threshold': {
|
||||
|
@ -133,7 +149,10 @@ export const calculateRuleFieldsDiff = (
|
|||
invariant(target_version.type === 'threshold', TARGET_TYPE_ERROR);
|
||||
return {
|
||||
...commonFieldsDiff,
|
||||
...calculateThresholdFieldsDiff({ base_version, current_version, target_version }),
|
||||
...calculateThresholdFieldsDiff(
|
||||
{ base_version, current_version, target_version },
|
||||
isRuleCustomized
|
||||
),
|
||||
};
|
||||
}
|
||||
case 'machine_learning': {
|
||||
|
@ -143,7 +162,10 @@ export const calculateRuleFieldsDiff = (
|
|||
invariant(target_version.type === 'machine_learning', TARGET_TYPE_ERROR);
|
||||
return {
|
||||
...commonFieldsDiff,
|
||||
...calculateMachineLearningFieldsDiff({ base_version, current_version, target_version }),
|
||||
...calculateMachineLearningFieldsDiff(
|
||||
{ base_version, current_version, target_version },
|
||||
isRuleCustomized
|
||||
),
|
||||
};
|
||||
}
|
||||
case 'new_terms': {
|
||||
|
@ -153,7 +175,10 @@ export const calculateRuleFieldsDiff = (
|
|||
invariant(target_version.type === 'new_terms', TARGET_TYPE_ERROR);
|
||||
return {
|
||||
...commonFieldsDiff,
|
||||
...calculateNewTermsFieldsDiff({ base_version, current_version, target_version }),
|
||||
...calculateNewTermsFieldsDiff(
|
||||
{ base_version, current_version, target_version },
|
||||
isRuleCustomized
|
||||
),
|
||||
};
|
||||
}
|
||||
case 'esql': {
|
||||
|
@ -163,7 +188,10 @@ export const calculateRuleFieldsDiff = (
|
|||
invariant(target_version.type === 'esql', TARGET_TYPE_ERROR);
|
||||
return {
|
||||
...commonFieldsDiff,
|
||||
...calculateEsqlFieldsDiff({ base_version, current_version, target_version }),
|
||||
...calculateEsqlFieldsDiff(
|
||||
{ base_version, current_version, target_version },
|
||||
isRuleCustomized
|
||||
),
|
||||
};
|
||||
}
|
||||
default: {
|
||||
|
@ -173,9 +201,10 @@ export const calculateRuleFieldsDiff = (
|
|||
};
|
||||
|
||||
const calculateCommonFieldsDiff = (
|
||||
ruleVersions: ThreeVersionsOf<DiffableCommonFields>
|
||||
ruleVersions: ThreeVersionsOf<DiffableCommonFields>,
|
||||
isRuleCustomized: boolean
|
||||
): CommonFieldsDiff => {
|
||||
return calculateFieldsDiffFor(ruleVersions, commonFieldsDiffAlgorithms);
|
||||
return calculateFieldsDiffFor(ruleVersions, commonFieldsDiffAlgorithms, isRuleCustomized);
|
||||
};
|
||||
|
||||
const commonFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor<DiffableCommonFields> = {
|
||||
|
@ -209,9 +238,10 @@ const commonFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor<DiffableCommonFields>
|
|||
};
|
||||
|
||||
const calculateCustomQueryFieldsDiff = (
|
||||
ruleVersions: ThreeVersionsOf<DiffableCustomQueryFields>
|
||||
ruleVersions: ThreeVersionsOf<DiffableCustomQueryFields>,
|
||||
isRuleCustomized: boolean
|
||||
): CustomQueryFieldsDiff => {
|
||||
return calculateFieldsDiffFor(ruleVersions, customQueryFieldsDiffAlgorithms);
|
||||
return calculateFieldsDiffFor(ruleVersions, customQueryFieldsDiffAlgorithms, isRuleCustomized);
|
||||
};
|
||||
|
||||
const customQueryFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor<DiffableCustomQueryFields> = {
|
||||
|
@ -222,9 +252,10 @@ const customQueryFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor<DiffableCustomQue
|
|||
};
|
||||
|
||||
const calculateSavedQueryFieldsDiff = (
|
||||
ruleVersions: ThreeVersionsOf<DiffableSavedQueryFields>
|
||||
ruleVersions: ThreeVersionsOf<DiffableSavedQueryFields>,
|
||||
isRuleCustomized: boolean
|
||||
): SavedQueryFieldsDiff => {
|
||||
return calculateFieldsDiffFor(ruleVersions, savedQueryFieldsDiffAlgorithms);
|
||||
return calculateFieldsDiffFor(ruleVersions, savedQueryFieldsDiffAlgorithms, isRuleCustomized);
|
||||
};
|
||||
|
||||
const savedQueryFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor<DiffableSavedQueryFields> = {
|
||||
|
@ -235,9 +266,10 @@ const savedQueryFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor<DiffableSavedQuery
|
|||
};
|
||||
|
||||
const calculateEqlFieldsDiff = (
|
||||
ruleVersions: ThreeVersionsOf<DiffableEqlFields>
|
||||
ruleVersions: ThreeVersionsOf<DiffableEqlFields>,
|
||||
isRuleCustomized: boolean
|
||||
): EqlFieldsDiff => {
|
||||
return calculateFieldsDiffFor(ruleVersions, eqlFieldsDiffAlgorithms);
|
||||
return calculateFieldsDiffFor(ruleVersions, eqlFieldsDiffAlgorithms, isRuleCustomized);
|
||||
};
|
||||
|
||||
const eqlFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor<DiffableEqlFields> = {
|
||||
|
@ -248,9 +280,10 @@ const eqlFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor<DiffableEqlFields> = {
|
|||
};
|
||||
|
||||
const calculateEsqlFieldsDiff = (
|
||||
ruleVersions: ThreeVersionsOf<DiffableEsqlFields>
|
||||
ruleVersions: ThreeVersionsOf<DiffableEsqlFields>,
|
||||
isRuleCustomized: boolean
|
||||
): EsqlFieldsDiff => {
|
||||
return calculateFieldsDiffFor(ruleVersions, esqlFieldsDiffAlgorithms);
|
||||
return calculateFieldsDiffFor(ruleVersions, esqlFieldsDiffAlgorithms, isRuleCustomized);
|
||||
};
|
||||
|
||||
const esqlFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor<DiffableEsqlFields> = {
|
||||
|
@ -260,9 +293,10 @@ const esqlFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor<DiffableEsqlFields> = {
|
|||
};
|
||||
|
||||
const calculateThreatMatchFieldsDiff = (
|
||||
ruleVersions: ThreeVersionsOf<DiffableThreatMatchFields>
|
||||
ruleVersions: ThreeVersionsOf<DiffableThreatMatchFields>,
|
||||
isRuleCustomized: boolean
|
||||
): ThreatMatchFieldsDiff => {
|
||||
return calculateFieldsDiffFor(ruleVersions, threatMatchFieldsDiffAlgorithms);
|
||||
return calculateFieldsDiffFor(ruleVersions, threatMatchFieldsDiffAlgorithms, isRuleCustomized);
|
||||
};
|
||||
|
||||
const threatMatchFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor<DiffableThreatMatchFields> = {
|
||||
|
@ -277,9 +311,10 @@ const threatMatchFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor<DiffableThreatMat
|
|||
};
|
||||
|
||||
const calculateThresholdFieldsDiff = (
|
||||
ruleVersions: ThreeVersionsOf<DiffableThresholdFields>
|
||||
ruleVersions: ThreeVersionsOf<DiffableThresholdFields>,
|
||||
isRuleCustomized: boolean
|
||||
): ThresholdFieldsDiff => {
|
||||
return calculateFieldsDiffFor(ruleVersions, thresholdFieldsDiffAlgorithms);
|
||||
return calculateFieldsDiffFor(ruleVersions, thresholdFieldsDiffAlgorithms, isRuleCustomized);
|
||||
};
|
||||
|
||||
const thresholdFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor<DiffableThresholdFields> = {
|
||||
|
@ -291,9 +326,14 @@ const thresholdFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor<DiffableThresholdFi
|
|||
};
|
||||
|
||||
const calculateMachineLearningFieldsDiff = (
|
||||
ruleVersions: ThreeVersionsOf<DiffableMachineLearningFields>
|
||||
ruleVersions: ThreeVersionsOf<DiffableMachineLearningFields>,
|
||||
isRuleCustomized: boolean
|
||||
): MachineLearningFieldsDiff => {
|
||||
return calculateFieldsDiffFor(ruleVersions, machineLearningFieldsDiffAlgorithms);
|
||||
return calculateFieldsDiffFor(
|
||||
ruleVersions,
|
||||
machineLearningFieldsDiffAlgorithms,
|
||||
isRuleCustomized
|
||||
);
|
||||
};
|
||||
|
||||
const machineLearningFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor<DiffableMachineLearningFields> =
|
||||
|
@ -305,9 +345,10 @@ const machineLearningFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor<DiffableMachi
|
|||
};
|
||||
|
||||
const calculateNewTermsFieldsDiff = (
|
||||
ruleVersions: ThreeVersionsOf<DiffableNewTermsFields>
|
||||
ruleVersions: ThreeVersionsOf<DiffableNewTermsFields>,
|
||||
isRuleCustomized: boolean
|
||||
): NewTermsFieldsDiff => {
|
||||
return calculateFieldsDiffFor(ruleVersions, newTermsFieldsDiffAlgorithms);
|
||||
return calculateFieldsDiffFor(ruleVersions, newTermsFieldsDiffAlgorithms, isRuleCustomized);
|
||||
};
|
||||
|
||||
const newTermsFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor<DiffableNewTermsFields> = {
|
||||
|
@ -320,9 +361,10 @@ const newTermsFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor<DiffableNewTermsFiel
|
|||
};
|
||||
|
||||
const calculateAllFieldsDiff = (
|
||||
ruleVersions: ThreeVersionsOf<DiffableAllFields>
|
||||
ruleVersions: ThreeVersionsOf<DiffableAllFields>,
|
||||
isRuleCustomized: boolean
|
||||
): AllFieldsDiff => {
|
||||
return calculateFieldsDiffFor(ruleVersions, allFieldsDiffAlgorithms);
|
||||
return calculateFieldsDiffFor(ruleVersions, allFieldsDiffAlgorithms, isRuleCustomized);
|
||||
};
|
||||
|
||||
const allFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor<DiffableAllFields> = {
|
||||
|
|
|
@ -15,11 +15,12 @@ import { MissingVersion } from '../../../../../../../common/api/detection_engine
|
|||
|
||||
export const calculateFieldsDiffFor = <TObject extends object>(
|
||||
ruleVersions: ThreeVersionsOf<TObject>,
|
||||
fieldsDiffAlgorithms: FieldsDiffAlgorithmsFor<TObject>
|
||||
fieldsDiffAlgorithms: FieldsDiffAlgorithmsFor<TObject>,
|
||||
isRuleCustomized: boolean
|
||||
): FieldsDiff<TObject> => {
|
||||
const result = mapValues(fieldsDiffAlgorithms, (calculateFieldDiff, fieldName) => {
|
||||
const fieldVersions = pickField(fieldName as keyof TObject, ruleVersions);
|
||||
const fieldDiff = calculateFieldDiff(fieldVersions);
|
||||
const fieldDiff = calculateFieldDiff(fieldVersions, isRuleCustomized);
|
||||
return fieldDiff;
|
||||
});
|
||||
|
||||
|
|
|
@ -297,10 +297,11 @@ export function referencesField({ getService }: FtrProviderContext): void {
|
|||
ruleUpgradeAssets,
|
||||
diffableRuleFieldName: 'references',
|
||||
expectedDiffOutcome: ThreeWayDiffOutcome.MissingBaseCanUpdate,
|
||||
isMergableField: true,
|
||||
expectedFieldDiffValues: {
|
||||
current: ['http://url-3'],
|
||||
target: ['http://url-1', 'http://url-2'],
|
||||
merged: ['http://url-1', 'http://url-2'],
|
||||
merged: ['http://url-3', 'http://url-1', 'http://url-2'],
|
||||
},
|
||||
},
|
||||
getService
|
||||
|
|
|
@ -297,10 +297,11 @@ export function tagsField({ getService }: FtrProviderContext): void {
|
|||
ruleUpgradeAssets,
|
||||
diffableRuleFieldName: 'tags',
|
||||
expectedDiffOutcome: ThreeWayDiffOutcome.MissingBaseCanUpdate,
|
||||
isMergableField: true,
|
||||
expectedFieldDiffValues: {
|
||||
current: ['tagB'],
|
||||
target: ['tagC'],
|
||||
merged: ['tagC'],
|
||||
merged: ['tagB', 'tagC'],
|
||||
},
|
||||
},
|
||||
getService
|
||||
|
|
|
@ -78,6 +78,7 @@ type ExpectedDiffOutcome =
|
|||
| {
|
||||
expectedDiffOutcome: ThreeWayDiffOutcome.MissingBaseCanUpdate;
|
||||
expectedFieldDiffValues: MissingHistoricalRuleVersionsFieldDiffValueVersions;
|
||||
isMergableField?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -160,6 +161,7 @@ export function testFieldUpgradeReview(
|
|||
expectMissingBaseABFieldDiff(diff, {
|
||||
diffableRuleFieldName: params.diffableRuleFieldName,
|
||||
valueVersions: params.expectedFieldDiffValues,
|
||||
isMergableField: params.isMergableField,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
@ -495,6 +497,7 @@ function expectMissingBaseAAFieldDiff(
|
|||
interface MissingBaseFieldAssertParams {
|
||||
diffableRuleFieldName: string;
|
||||
valueVersions: MissingHistoricalRuleVersionsFieldDiffValueVersions;
|
||||
isMergableField?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -518,7 +521,9 @@ function expectMissingBaseABFieldDiff(
|
|||
target_version: fieldAssertParams.valueVersions.target,
|
||||
merged_version: fieldAssertParams.valueVersions.merged,
|
||||
diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate,
|
||||
merge_outcome: ThreeWayMergeOutcome.Target,
|
||||
merge_outcome: fieldAssertParams.isMergableField
|
||||
? ThreeWayMergeOutcome.Merged
|
||||
: ThreeWayMergeOutcome.Target,
|
||||
conflict: ThreeWayDiffConflict.SOLVABLE,
|
||||
},
|
||||
isUndefined
|
||||
|
|
|
@ -302,10 +302,11 @@ export function newTermsFieldsField({ getService }: FtrProviderContext): void {
|
|||
ruleUpgradeAssets,
|
||||
diffableRuleFieldName: 'new_terms_fields',
|
||||
expectedDiffOutcome: ThreeWayDiffOutcome.MissingBaseCanUpdate,
|
||||
isMergableField: true,
|
||||
expectedFieldDiffValues: {
|
||||
current: ['fieldB'],
|
||||
target: ['fieldA', 'fieldC'],
|
||||
merged: ['fieldA', 'fieldC'],
|
||||
merged: ['fieldB', 'fieldA', 'fieldC'],
|
||||
},
|
||||
},
|
||||
getService
|
||||
|
|
|
@ -302,10 +302,11 @@ export function threatIndexField({ getService }: FtrProviderContext): void {
|
|||
ruleUpgradeAssets,
|
||||
diffableRuleFieldName: 'threat_index',
|
||||
expectedDiffOutcome: ThreeWayDiffOutcome.MissingBaseCanUpdate,
|
||||
isMergableField: true,
|
||||
expectedFieldDiffValues: {
|
||||
current: ['indexD'],
|
||||
target: ['indexB', 'indexC'],
|
||||
merged: ['indexB', 'indexC'],
|
||||
merged: ['indexD', 'indexB', 'indexC'],
|
||||
},
|
||||
},
|
||||
getService
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
RULES_UPDATES_TABLE,
|
||||
UPGRADE_ALL_RULES_BUTTON,
|
||||
UPGRADE_SELECTED_RULES_BUTTON,
|
||||
getReviewSingleRuleButtonByRuleId,
|
||||
getUpgradeSingleRuleButtonByRuleId,
|
||||
} from '../../../../screens/alerts_detection_rules';
|
||||
import { selectRulesByName } from '../../../../tasks/alerts_detection_rules';
|
||||
|
@ -102,11 +103,11 @@ describe(
|
|||
clickRuleUpdatesTab();
|
||||
});
|
||||
|
||||
it('should disable individual upgrade buttons for all prebuilt rules with conflicts', () => {
|
||||
// All buttons should be disabled because of conflicts
|
||||
it('should display individual review buttons for all prebuilt rules with conflicts', () => {
|
||||
// All buttons should be review buttons because of conflicts
|
||||
for (const rule of [OUTDATED_RULE_1, OUTDATED_RULE_2]) {
|
||||
const { rule_id: ruleId } = rule['security-rule'];
|
||||
expect(cy.get(getUpgradeSingleRuleButtonByRuleId(ruleId)).should('be.disabled'));
|
||||
expect(cy.get(getReviewSingleRuleButtonByRuleId(ruleId)).should('exist'));
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -203,24 +204,24 @@ describe(
|
|||
assertRulesPresentInRuleUpdatesTable([OUTDATED_RULE_3]);
|
||||
});
|
||||
|
||||
it('should disable the upgrade button for conflicting rules while allowing upgrades of no-conflict rules', () => {
|
||||
// Verify the conflicting rule's upgrade button is disabled
|
||||
it('should switch to a review button for conflicting rules while allowing upgrades of no-conflict rules', () => {
|
||||
// Verify the conflicting rule's upgrade button has the review label
|
||||
expect(
|
||||
cy
|
||||
.get(getUpgradeSingleRuleButtonByRuleId(OUTDATED_RULE_1['security-rule'].rule_id))
|
||||
.should('be.disabled')
|
||||
.get(getReviewSingleRuleButtonByRuleId(OUTDATED_RULE_1['security-rule'].rule_id))
|
||||
.should('exist')
|
||||
);
|
||||
|
||||
// Verify non-conflicting rules' upgrade buttons are enabled
|
||||
// Verify non-conflicting rules' upgrade buttons do not have the review label
|
||||
expect(
|
||||
cy
|
||||
.get(getUpgradeSingleRuleButtonByRuleId(OUTDATED_RULE_2['security-rule'].rule_id))
|
||||
.should('not.be.disabled')
|
||||
.should('exist')
|
||||
);
|
||||
expect(
|
||||
cy
|
||||
.get(getUpgradeSingleRuleButtonByRuleId(OUTDATED_RULE_3['security-rule'].rule_id))
|
||||
.should('not.be.disabled')
|
||||
.should('exist')
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -305,10 +306,10 @@ describe(
|
|||
});
|
||||
|
||||
it('should disable individual upgrade button for all rules', () => {
|
||||
// All buttons should be disabled because rule type changes are considered conflicts
|
||||
// All buttons should be displayed as review buttons because rule type changes are considered conflicts
|
||||
for (const rule of [OUTDATED_QUERY_RULE_1, OUTDATED_QUERY_RULE_2]) {
|
||||
const { rule_id: ruleId } = rule['security-rule'];
|
||||
expect(cy.get(getUpgradeSingleRuleButtonByRuleId(ruleId)).should('be.disabled'));
|
||||
expect(cy.get(getReviewSingleRuleButtonByRuleId(ruleId)).should('exist'));
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -196,6 +196,10 @@ export const getUpgradeSingleRuleButtonByRuleId = (ruleId: string) => {
|
|||
return `[data-test-subj="upgradeSinglePrebuiltRuleButton-${ruleId}"]`;
|
||||
};
|
||||
|
||||
export const getReviewSingleRuleButtonByRuleId = (ruleId: string) => {
|
||||
return `[data-test-subj="reviewSinglePrebuiltRuleButton-${ruleId}"]`;
|
||||
};
|
||||
|
||||
export const NO_RULES_AVAILABLE_FOR_INSTALL_MESSAGE =
|
||||
'[data-test-subj="noPrebuiltRulesAvailableForInstall"]';
|
||||
export const NO_RULES_AVAILABLE_FOR_UPGRADE_MESSAGE =
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue