[8.x] [Security Solution] Implement concurrency control for Prebuilt Upgrade workflow (#203604) (#205877)

# Backport

This will backport the following commits from `main` to `8.x`:
- [[Security Solution] Implement concurrency control for Prebuilt
Upgrade workflow
(#203604)](https://github.com/elastic/kibana/pull/203604)

<!--- Backport version: 9.4.3 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Maxim
Palenov","email":"maxim.palenov@elastic.co"},"sourceCommit":{"committedDate":"2025-01-08T12:10:20Z","message":"[Security
Solution] Implement concurrency control for Prebuilt Upgrade workflow
(#203604)\n\n**Resolves:**
https://github.com/elastic/kibana/issues/200134\r\n\r\n##
Summary\r\n\r\nThis PR implements concurrency control to make sure user
has the recent rule updates data in Rule Upgrade flyout. Any
modifications saved in Rule Upgrade flyout are reset upon new `revision`
or `version` detected.\r\n\r\n## Details\r\n\r\nConcurrency control is
important to provide better UX. Multiple users work in Kibana in
parallel and new prebuilt rules package version can be released in any
time. Attempts to upgrade a rule with outdated `revision` and/or
`version` results in failed request. Users may experience multiple rule
upgrade failure in that case causing a lot of confusion. More
experienced users may guess to reload the page to
continue.\r\n\r\nTypical reasons leading to `revision` and/or `version`
change are the following\r\n\r\n- Current rule has been edited will bump
rule's `revision`. For example the rule currently shown in Rule Upgrade
flyout has been edited by someone else.\r\n- Prebuilt rules package got
released will give provide rule assets with higher `version`. Rules
having upgrades in the currently installed package and in a new one are
affected.\r\n\r\nThis PR mitigates the described issues by implementing
concurrency control. It sets up `_review` API endpoint refetch interval
to 5 minutes to fetch fresh data. In case a higher `revision` or
`version` is detected for some rule this rule's resolved conflicts and
customizations performed in Rule Upgrade flyout get cleared.\r\n\r\n##
Screenshots\r\n\r\n- `revision` change (refresh interval was reduced to
30 seconds to make the video
shorter)\r\n\r\nhttps://github.com/user-attachments/assets/98d2a22f-9338-482a-a7b2-1e170b9642ce\r\n\r\n-
`version` change (refresh interval was reduced to 1 minute to make the
video
shorter)\r\n\r\nhttps://github.com/user-attachments/assets/2b7c23f0-5a50-471e-aa7f-8d9b2aecc957\r\n\r\n##
How to test locally\r\n\r\nThere are two cases for testing\r\n\r\n-
`revision` change\r\n- `version` change\r\n\r\n### Test `revision`
change\r\n\r\nRevision change means the rule has been edited. Use the
following steps to test it \r\n\r\n- Ensure the
`prebuiltRulesCustomizationEnabled` feature flag is enabled\r\n- Allow
internal APIs via adding `server.restrictInternalApis: false` to
`kibana.dev.yaml`\r\n- Clear Elasticsearch data\r\n- Run Elasticsearch
and Kibana locally (do not open Kibana in a web browser)\r\n- Install an
outdated version of the `security_detection_engine` Fleet
package\r\n```bash\r\ncurl -X POST --user elastic:changeme -H
'Content-Type: application/json' -H 'kbn-xsrf: 123' -H
\"elastic-api-version: 2023-10-31\" -d '{\"force\":true}'
http://localhost:5601/kbn/api/fleet/epm/packages/security_detection_engine/8.14.1\r\n```\r\n\r\n-
Install prebuilt rules\r\n```bash\r\ncurl -X POST --user
elastic:changeme -H 'Content-Type: application/json' -H 'kbn-xsrf: 123'
-H \"elastic-api-version: 1\" -d '{\"mode\":\"ALL_RULES\"}'
http://localhost:5601/kbn/internal/detection_engine/prebuilt_rules/installation/_perform\r\n```\r\n-
Open `Detection Rules (SIEM)` Page -> `Rule Updates`\r\n- Open Rule
upgrade flyout for some rule\r\n- Make changes to rule field(s) and save
them (do not upgrade the rule)\r\n- Open the other web browser tab with
Kibana\r\n- Navigate to the same rule's editing page\r\n- Change any
field and save the changes\r\n- Return back to the first tab and wait
for data to be refetched (data refresh interval is 5 minutes, wait for
`_review` request in the Dev Tool's Network tab)\r\n- Make sure the
changes you made for field(s) got reverted\r\n\r\n### Test `version`
change\r\n\r\nVersion change means a new package version was released.
Do the following to test it\r\n\r\n- Ensure the
`prebuiltRulesCustomizationEnabled` feature flag is enabled\r\n- Allow
internal APIs via adding `server.restrictInternalApis: false` to
`kibana.dev.yaml`\r\n- Clear Elasticsearch data\r\n- Run Elasticsearch
and Kibana locally (do not open Kibana in a web browser)\r\n- Set
`xpack.securitySolution.prebuiltRulesPackageVersion: 8.15.2` in
`kibana.dev.yaml`\r\n- Install an outdated version of the
`security_detection_engine` Fleet package\r\n```bash\r\ncurl -X POST
--user elastic:changeme -H 'Content-Type: application/json' -H
'kbn-xsrf: 123' -H \"elastic-api-version: 2023-10-31\" -d
'{\"force\":true}'
http://localhost:5601/kbn/api/fleet/epm/packages/security_detection_engine/8.14.1\r\n```\r\n\r\n-
Install prebuilt rules\r\n```bash\r\ncurl -X POST --user
elastic:changeme -H 'Content-Type: application/json' -H 'kbn-xsrf: 123'
-H \"elastic-api-version: 1\" -d '{\"mode\":\"ALL_RULES\"}'
http://localhost:5601/kbn/internal/detection_engine/prebuilt_rules/installation/_perform\r\n```\r\n-
Open `Detection Rules (SIEM)` Page -> `Rule Updates`\r\n- Open Rule
upgrade flyout for a rule having updates in packages `v8.15.2` and
`.8.17.1-beta.1` for example `Suspicious Web Browser Sensitive File
Access`\r\n- Make changes to rule field(s) and save them (do not upgrade
the rule)\r\n- Set `xpack.securitySolution.prebuiltRulesPackageVersion:
8.17.1-beta.1` in `kibana.dev.yaml`\r\n- Open the other web browser tab
with Kibana\r\n- Navigate to Security Solution plugin to install the\r\n
OR\r\n install the package `8.17.1-beta.1` via API
request\r\n```bash\r\ncurl -X POST --user elastic:changeme -H
'Content-Type: application/json' -H 'kbn-xsrf: 123' -H
\"elastic-api-version: 2023-10-31\" -d '{\"force\":true}'
http://localhost:5601/kbn/api/fleet/epm/packages/security_detection_engine/8.17.1-beta.1\r\n```\r\n-
Return back to the first tab and wait for data to be refetched (data
refresh interval is 5 minutes, wait for `_review` request in the Dev
Tool's Network tab)\r\n- Make sure the changes you made for field(s) got
the recent target rule values\r\n\r\nAlternatively you can spin up EPR
locally and publish package updates with rule's version
bumped.","sha":"19292792aa20d8bae51261974fb8cf56264f6967","branchLabelMapping":{"^v9.0.0$":"main","^v8.18.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"],"title":"[Security Solution]
Implement concurrency control for Prebuilt Upgrade
workflow","number":203604,"url":"https://github.com/elastic/kibana/pull/203604","mergeCommit":{"message":"[Security
Solution] Implement concurrency control for Prebuilt Upgrade workflow
(#203604)\n\n**Resolves:**
https://github.com/elastic/kibana/issues/200134\r\n\r\n##
Summary\r\n\r\nThis PR implements concurrency control to make sure user
has the recent rule updates data in Rule Upgrade flyout. Any
modifications saved in Rule Upgrade flyout are reset upon new `revision`
or `version` detected.\r\n\r\n## Details\r\n\r\nConcurrency control is
important to provide better UX. Multiple users work in Kibana in
parallel and new prebuilt rules package version can be released in any
time. Attempts to upgrade a rule with outdated `revision` and/or
`version` results in failed request. Users may experience multiple rule
upgrade failure in that case causing a lot of confusion. More
experienced users may guess to reload the page to
continue.\r\n\r\nTypical reasons leading to `revision` and/or `version`
change are the following\r\n\r\n- Current rule has been edited will bump
rule's `revision`. For example the rule currently shown in Rule Upgrade
flyout has been edited by someone else.\r\n- Prebuilt rules package got
released will give provide rule assets with higher `version`. Rules
having upgrades in the currently installed package and in a new one are
affected.\r\n\r\nThis PR mitigates the described issues by implementing
concurrency control. It sets up `_review` API endpoint refetch interval
to 5 minutes to fetch fresh data. In case a higher `revision` or
`version` is detected for some rule this rule's resolved conflicts and
customizations performed in Rule Upgrade flyout get cleared.\r\n\r\n##
Screenshots\r\n\r\n- `revision` change (refresh interval was reduced to
30 seconds to make the video
shorter)\r\n\r\nhttps://github.com/user-attachments/assets/98d2a22f-9338-482a-a7b2-1e170b9642ce\r\n\r\n-
`version` change (refresh interval was reduced to 1 minute to make the
video
shorter)\r\n\r\nhttps://github.com/user-attachments/assets/2b7c23f0-5a50-471e-aa7f-8d9b2aecc957\r\n\r\n##
How to test locally\r\n\r\nThere are two cases for testing\r\n\r\n-
`revision` change\r\n- `version` change\r\n\r\n### Test `revision`
change\r\n\r\nRevision change means the rule has been edited. Use the
following steps to test it \r\n\r\n- Ensure the
`prebuiltRulesCustomizationEnabled` feature flag is enabled\r\n- Allow
internal APIs via adding `server.restrictInternalApis: false` to
`kibana.dev.yaml`\r\n- Clear Elasticsearch data\r\n- Run Elasticsearch
and Kibana locally (do not open Kibana in a web browser)\r\n- Install an
outdated version of the `security_detection_engine` Fleet
package\r\n```bash\r\ncurl -X POST --user elastic:changeme -H
'Content-Type: application/json' -H 'kbn-xsrf: 123' -H
\"elastic-api-version: 2023-10-31\" -d '{\"force\":true}'
http://localhost:5601/kbn/api/fleet/epm/packages/security_detection_engine/8.14.1\r\n```\r\n\r\n-
Install prebuilt rules\r\n```bash\r\ncurl -X POST --user
elastic:changeme -H 'Content-Type: application/json' -H 'kbn-xsrf: 123'
-H \"elastic-api-version: 1\" -d '{\"mode\":\"ALL_RULES\"}'
http://localhost:5601/kbn/internal/detection_engine/prebuilt_rules/installation/_perform\r\n```\r\n-
Open `Detection Rules (SIEM)` Page -> `Rule Updates`\r\n- Open Rule
upgrade flyout for some rule\r\n- Make changes to rule field(s) and save
them (do not upgrade the rule)\r\n- Open the other web browser tab with
Kibana\r\n- Navigate to the same rule's editing page\r\n- Change any
field and save the changes\r\n- Return back to the first tab and wait
for data to be refetched (data refresh interval is 5 minutes, wait for
`_review` request in the Dev Tool's Network tab)\r\n- Make sure the
changes you made for field(s) got reverted\r\n\r\n### Test `version`
change\r\n\r\nVersion change means a new package version was released.
Do the following to test it\r\n\r\n- Ensure the
`prebuiltRulesCustomizationEnabled` feature flag is enabled\r\n- Allow
internal APIs via adding `server.restrictInternalApis: false` to
`kibana.dev.yaml`\r\n- Clear Elasticsearch data\r\n- Run Elasticsearch
and Kibana locally (do not open Kibana in a web browser)\r\n- Set
`xpack.securitySolution.prebuiltRulesPackageVersion: 8.15.2` in
`kibana.dev.yaml`\r\n- Install an outdated version of the
`security_detection_engine` Fleet package\r\n```bash\r\ncurl -X POST
--user elastic:changeme -H 'Content-Type: application/json' -H
'kbn-xsrf: 123' -H \"elastic-api-version: 2023-10-31\" -d
'{\"force\":true}'
http://localhost:5601/kbn/api/fleet/epm/packages/security_detection_engine/8.14.1\r\n```\r\n\r\n-
Install prebuilt rules\r\n```bash\r\ncurl -X POST --user
elastic:changeme -H 'Content-Type: application/json' -H 'kbn-xsrf: 123'
-H \"elastic-api-version: 1\" -d '{\"mode\":\"ALL_RULES\"}'
http://localhost:5601/kbn/internal/detection_engine/prebuilt_rules/installation/_perform\r\n```\r\n-
Open `Detection Rules (SIEM)` Page -> `Rule Updates`\r\n- Open Rule
upgrade flyout for a rule having updates in packages `v8.15.2` and
`.8.17.1-beta.1` for example `Suspicious Web Browser Sensitive File
Access`\r\n- Make changes to rule field(s) and save them (do not upgrade
the rule)\r\n- Set `xpack.securitySolution.prebuiltRulesPackageVersion:
8.17.1-beta.1` in `kibana.dev.yaml`\r\n- Open the other web browser tab
with Kibana\r\n- Navigate to Security Solution plugin to install the\r\n
OR\r\n install the package `8.17.1-beta.1` via API
request\r\n```bash\r\ncurl -X POST --user elastic:changeme -H
'Content-Type: application/json' -H 'kbn-xsrf: 123' -H
\"elastic-api-version: 2023-10-31\" -d '{\"force\":true}'
http://localhost:5601/kbn/api/fleet/epm/packages/security_detection_engine/8.17.1-beta.1\r\n```\r\n-
Return back to the first tab and wait for data to be refetched (data
refresh interval is 5 minutes, wait for `_review` request in the Dev
Tool's Network tab)\r\n- Make sure the changes you made for field(s) got
the recent target rule values\r\n\r\nAlternatively you can spin up EPR
locally and publish package updates with rule's version
bumped.","sha":"19292792aa20d8bae51261974fb8cf56264f6967"}},"sourceBranch":"main","suggestedTargetBranches":["8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/203604","number":203604,"mergeCommit":{"message":"[Security
Solution] Implement concurrency control for Prebuilt Upgrade workflow
(#203604)\n\n**Resolves:**
https://github.com/elastic/kibana/issues/200134\r\n\r\n##
Summary\r\n\r\nThis PR implements concurrency control to make sure user
has the recent rule updates data in Rule Upgrade flyout. Any
modifications saved in Rule Upgrade flyout are reset upon new `revision`
or `version` detected.\r\n\r\n## Details\r\n\r\nConcurrency control is
important to provide better UX. Multiple users work in Kibana in
parallel and new prebuilt rules package version can be released in any
time. Attempts to upgrade a rule with outdated `revision` and/or
`version` results in failed request. Users may experience multiple rule
upgrade failure in that case causing a lot of confusion. More
experienced users may guess to reload the page to
continue.\r\n\r\nTypical reasons leading to `revision` and/or `version`
change are the following\r\n\r\n- Current rule has been edited will bump
rule's `revision`. For example the rule currently shown in Rule Upgrade
flyout has been edited by someone else.\r\n- Prebuilt rules package got
released will give provide rule assets with higher `version`. Rules
having upgrades in the currently installed package and in a new one are
affected.\r\n\r\nThis PR mitigates the described issues by implementing
concurrency control. It sets up `_review` API endpoint refetch interval
to 5 minutes to fetch fresh data. In case a higher `revision` or
`version` is detected for some rule this rule's resolved conflicts and
customizations performed in Rule Upgrade flyout get cleared.\r\n\r\n##
Screenshots\r\n\r\n- `revision` change (refresh interval was reduced to
30 seconds to make the video
shorter)\r\n\r\nhttps://github.com/user-attachments/assets/98d2a22f-9338-482a-a7b2-1e170b9642ce\r\n\r\n-
`version` change (refresh interval was reduced to 1 minute to make the
video
shorter)\r\n\r\nhttps://github.com/user-attachments/assets/2b7c23f0-5a50-471e-aa7f-8d9b2aecc957\r\n\r\n##
How to test locally\r\n\r\nThere are two cases for testing\r\n\r\n-
`revision` change\r\n- `version` change\r\n\r\n### Test `revision`
change\r\n\r\nRevision change means the rule has been edited. Use the
following steps to test it \r\n\r\n- Ensure the
`prebuiltRulesCustomizationEnabled` feature flag is enabled\r\n- Allow
internal APIs via adding `server.restrictInternalApis: false` to
`kibana.dev.yaml`\r\n- Clear Elasticsearch data\r\n- Run Elasticsearch
and Kibana locally (do not open Kibana in a web browser)\r\n- Install an
outdated version of the `security_detection_engine` Fleet
package\r\n```bash\r\ncurl -X POST --user elastic:changeme -H
'Content-Type: application/json' -H 'kbn-xsrf: 123' -H
\"elastic-api-version: 2023-10-31\" -d '{\"force\":true}'
http://localhost:5601/kbn/api/fleet/epm/packages/security_detection_engine/8.14.1\r\n```\r\n\r\n-
Install prebuilt rules\r\n```bash\r\ncurl -X POST --user
elastic:changeme -H 'Content-Type: application/json' -H 'kbn-xsrf: 123'
-H \"elastic-api-version: 1\" -d '{\"mode\":\"ALL_RULES\"}'
http://localhost:5601/kbn/internal/detection_engine/prebuilt_rules/installation/_perform\r\n```\r\n-
Open `Detection Rules (SIEM)` Page -> `Rule Updates`\r\n- Open Rule
upgrade flyout for some rule\r\n- Make changes to rule field(s) and save
them (do not upgrade the rule)\r\n- Open the other web browser tab with
Kibana\r\n- Navigate to the same rule's editing page\r\n- Change any
field and save the changes\r\n- Return back to the first tab and wait
for data to be refetched (data refresh interval is 5 minutes, wait for
`_review` request in the Dev Tool's Network tab)\r\n- Make sure the
changes you made for field(s) got reverted\r\n\r\n### Test `version`
change\r\n\r\nVersion change means a new package version was released.
Do the following to test it\r\n\r\n- Ensure the
`prebuiltRulesCustomizationEnabled` feature flag is enabled\r\n- Allow
internal APIs via adding `server.restrictInternalApis: false` to
`kibana.dev.yaml`\r\n- Clear Elasticsearch data\r\n- Run Elasticsearch
and Kibana locally (do not open Kibana in a web browser)\r\n- Set
`xpack.securitySolution.prebuiltRulesPackageVersion: 8.15.2` in
`kibana.dev.yaml`\r\n- Install an outdated version of the
`security_detection_engine` Fleet package\r\n```bash\r\ncurl -X POST
--user elastic:changeme -H 'Content-Type: application/json' -H
'kbn-xsrf: 123' -H \"elastic-api-version: 2023-10-31\" -d
'{\"force\":true}'
http://localhost:5601/kbn/api/fleet/epm/packages/security_detection_engine/8.14.1\r\n```\r\n\r\n-
Install prebuilt rules\r\n```bash\r\ncurl -X POST --user
elastic:changeme -H 'Content-Type: application/json' -H 'kbn-xsrf: 123'
-H \"elastic-api-version: 1\" -d '{\"mode\":\"ALL_RULES\"}'
http://localhost:5601/kbn/internal/detection_engine/prebuilt_rules/installation/_perform\r\n```\r\n-
Open `Detection Rules (SIEM)` Page -> `Rule Updates`\r\n- Open Rule
upgrade flyout for a rule having updates in packages `v8.15.2` and
`.8.17.1-beta.1` for example `Suspicious Web Browser Sensitive File
Access`\r\n- Make changes to rule field(s) and save them (do not upgrade
the rule)\r\n- Set `xpack.securitySolution.prebuiltRulesPackageVersion:
8.17.1-beta.1` in `kibana.dev.yaml`\r\n- Open the other web browser tab
with Kibana\r\n- Navigate to Security Solution plugin to install the\r\n
OR\r\n install the package `8.17.1-beta.1` via API
request\r\n```bash\r\ncurl -X POST --user elastic:changeme -H
'Content-Type: application/json' -H 'kbn-xsrf: 123' -H
\"elastic-api-version: 2023-10-31\" -d '{\"force\":true}'
http://localhost:5601/kbn/api/fleet/epm/packages/security_detection_engine/8.17.1-beta.1\r\n```\r\n-
Return back to the first tab and wait for data to be refetched (data
refresh interval is 5 minutes, wait for `_review` request in the Dev
Tool's Network tab)\r\n- Make sure the changes you made for field(s) got
the recent target rule values\r\n\r\nAlternatively you can spin up EPR
locally and publish package updates with rule's version
bumped.","sha":"19292792aa20d8bae51261974fb8cf56264f6967"}},{"branch":"8.x","label":"v8.18.0","branchLabelMappingKey":"^v8.18.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->

Co-authored-by: Maxim Palenov <maxim.palenov@elastic.co>
This commit is contained in:
Kibana Machine 2025-01-09 01:17:48 +11:00 committed by GitHub
parent 9b0302c22b
commit 8822613dc6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 553 additions and 6 deletions

View file

@ -140,3 +140,37 @@ export const RULE_MODIFIED_BADGE_DESCRIPTION = i18n.translate(
'The rule was edited after installation and field values differs from the values upon installation',
}
);
export const RULE_NEW_REVISION_DETECTED_WARNING = i18n.translate(
'xpack.securitySolution.detectionEngine.upgradeFlyout.ruleNewRevisionDetectedWarning',
{
defaultMessage: 'Installed rule changed',
}
);
export const RULE_NEW_REVISION_DETECTED_WARNING_DESCRIPTION = (ruleName: string) =>
i18n.translate(
'xpack.securitySolution.detectionEngine.upgradeFlyout.ruleNewVersionDetectedWarningDescription',
{
defaultMessage:
'Someone edited the installed rule "{ruleName}". Upgrade resolved conflicts were reset.',
values: { ruleName },
}
);
export const RULE_NEW_VERSION_DETECTED_WARNING = i18n.translate(
'xpack.securitySolution.detectionEngine.upgradeFlyout.ruleNewRevisionDetectedWarning',
{
defaultMessage: 'New prebuilt rules package was installed',
}
);
export const RULE_NEW_VERSION_DETECTED_WARNING_DESCRIPTION = (ruleName: string) =>
i18n.translate(
'xpack.securitySolution.detectionEngine.upgradeFlyout.ruleNewRevisionDetectedWarningDescription',
{
defaultMessage:
'Newer prebuilt rules package were installed in background. It contains a newer rule version for "{ruleName}". Upgrade resolved conflicts were reset.',
values: { ruleName },
}
);

View file

@ -39,6 +39,8 @@ import { UpgradeFlyoutSubHeader } from './upgrade_flyout_subheader';
import * as ruleDetailsI18n from '../../../../rule_management/components/rule_details/translations';
import * as i18n from './translations';
const REVIEW_PREBUILT_RULES_UPGRADE_REFRESH_INTERVAL = 5 * 60 * 1000;
export interface UpgradePrebuiltRulesTableState {
/**
* Rule upgrade state after applying `filterOptions`
@ -110,6 +112,13 @@ interface UpgradePrebuiltRulesTableContextProviderProps {
children: React.ReactNode;
}
/**
* Provides necessary data and actions for Rules Upgrade table.
*
* It periodically re-fetches prebuilt rules upgrade review data to detect possible cases of:
* - editing prebuilt rules (revision change)
* - releasing a new prebuilt rules package (version change)
*/
export const UpgradePrebuiltRulesTableContextProvider = ({
children,
}: UpgradePrebuiltRulesTableContextProviderProps) => {
@ -135,7 +144,7 @@ export const UpgradePrebuiltRulesTableContextProvider = ({
isLoading,
isRefetching,
} = usePrebuiltRulesUpgradeReview({
refetchInterval: false, // Disable automatic refetching since request is expensive
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 { rulesUpgradeState, setRuleFieldResolvedValue } =

View file

@ -0,0 +1,436 @@
/*
* 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 { FieldUpgradeStateEnum } from '../../../../rule_management/model/prebuilt_rule_upgrade';
import type { RuleResponse } from '../../../../../../common/api/detection_engine';
import { useAppToasts } from '../../../../../common/hooks/use_app_toasts';
import {
type RuleUpgradeInfoForReview,
ThreeWayDiffConflict,
ThreeWayDiffOutcome,
ThreeWayMergeOutcome,
} from '../../../../../../common/api/detection_engine';
import { act, renderHook } from '@testing-library/react';
import { usePrebuiltRulesUpgradeState } from './use_prebuilt_rules_upgrade_state';
jest.mock('../../../../rule_management/hooks/use_is_prebuilt_rules_customization_enabled', () => ({
useIsPrebuiltRulesCustomizationEnabled: jest.fn(() => true),
}));
jest.mock('../../../../../common/hooks/use_app_toasts', () => ({
useAppToasts: jest.fn().mockReturnValue({
addWarning: jest.fn(),
}),
}));
describe('usePrebuiltRulesUpgradeState', () => {
it('returns rule upgrade state', () => {
const ruleUpgradeInfosMock: RuleUpgradeInfoForReview[] = [createRuleUpgradeInfoMock()];
const {
result: {
current: { rulesUpgradeState },
},
} = renderHook(usePrebuiltRulesUpgradeState, {
initialProps: ruleUpgradeInfosMock,
});
expect(rulesUpgradeState).toEqual({
'test-rule-id-1': expect.any(Object),
});
});
describe('fields upgrade state', () => {
it('returns empty state when there are no fields to upgrade', () => {
const ruleUpgradeInfosMock: RuleUpgradeInfoForReview[] = [createRuleUpgradeInfoMock()];
const { result } = renderHook(usePrebuiltRulesUpgradeState, {
initialProps: ruleUpgradeInfosMock,
});
expect(result.current.rulesUpgradeState).toEqual({
'test-rule-id-1': expect.objectContaining({
fieldsUpgradeState: {},
}),
});
});
it('returns NO CONFLICT fields', () => {
const ruleUpgradeInfosMock: RuleUpgradeInfoForReview[] = [
createRuleUpgradeInfoMock({
diff: {
num_fields_with_updates: 1,
num_fields_with_conflicts: 0,
num_fields_with_non_solvable_conflicts: 0,
fields: {
name: {
base_version: 'base',
current_version: 'base',
target_version: 'target',
merged_version: 'target',
diff_outcome: ThreeWayDiffOutcome.StockValueCanUpdate,
merge_outcome: ThreeWayMergeOutcome.Target,
has_base_version: true,
has_update: true,
conflict: ThreeWayDiffConflict.NONE,
},
},
},
}),
];
const { result } = renderHook(usePrebuiltRulesUpgradeState, {
initialProps: ruleUpgradeInfosMock,
});
expect(result.current.rulesUpgradeState).toEqual({
'test-rule-id-1': expect.objectContaining({
fieldsUpgradeState: {
name: { state: FieldUpgradeStateEnum.NoConflict },
},
}),
});
});
it('returns SOLVABLE CONFLICT fields', () => {
const ruleUpgradeInfosMock: RuleUpgradeInfoForReview[] = [
createRuleUpgradeInfoMock({
diff: {
num_fields_with_updates: 1,
num_fields_with_conflicts: 1,
num_fields_with_non_solvable_conflicts: 0,
fields: {
name: {
base_version: 'base',
current_version: 'current',
target_version: 'target',
merged_version: 'target',
diff_outcome: ThreeWayDiffOutcome.StockValueCanUpdate,
merge_outcome: ThreeWayMergeOutcome.Merged,
has_base_version: true,
has_update: true,
conflict: ThreeWayDiffConflict.SOLVABLE,
},
},
},
}),
];
const { result } = renderHook(usePrebuiltRulesUpgradeState, {
initialProps: ruleUpgradeInfosMock,
});
expect(result.current.rulesUpgradeState).toEqual({
'test-rule-id-1': expect.objectContaining({
fieldsUpgradeState: {
name: { state: FieldUpgradeStateEnum.SolvableConflict },
},
}),
});
});
it('returns NON SOLVABLE CONFLICT fields', () => {
const ruleUpgradeInfosMock: RuleUpgradeInfoForReview[] = [
createRuleUpgradeInfoMock({
diff: {
num_fields_with_updates: 1,
num_fields_with_conflicts: 1,
num_fields_with_non_solvable_conflicts: 1,
fields: {
name: {
base_version: 'base',
current_version: 'current',
target_version: 'target',
merged_version: 'target',
diff_outcome: ThreeWayDiffOutcome.StockValueCanUpdate,
merge_outcome: ThreeWayMergeOutcome.Merged,
has_base_version: true,
has_update: true,
conflict: ThreeWayDiffConflict.NON_SOLVABLE,
},
},
},
}),
];
const { result } = renderHook(usePrebuiltRulesUpgradeState, {
initialProps: ruleUpgradeInfosMock,
});
expect(result.current.rulesUpgradeState).toEqual({
'test-rule-id-1': expect.objectContaining({
fieldsUpgradeState: {
name: { state: FieldUpgradeStateEnum.NonSolvableConflict },
},
}),
});
});
it('returns ACCEPTED fields after resolving a conflict', () => {
const ruleUpgradeInfosMock: RuleUpgradeInfoForReview[] = [
createRuleUpgradeInfoMock({
rule_id: 'test-rule-id-1',
diff: {
num_fields_with_updates: 1,
num_fields_with_conflicts: 1,
num_fields_with_non_solvable_conflicts: 1,
fields: {
name: {
base_version: 'base',
current_version: 'current',
target_version: 'target',
merged_version: 'target',
diff_outcome: ThreeWayDiffOutcome.StockValueCanUpdate,
merge_outcome: ThreeWayMergeOutcome.Merged,
has_base_version: true,
has_update: true,
conflict: ThreeWayDiffConflict.NON_SOLVABLE,
},
},
},
}),
];
const { result } = renderHook(usePrebuiltRulesUpgradeState, {
initialProps: ruleUpgradeInfosMock,
});
act(() => {
result.current.setRuleFieldResolvedValue({
ruleId: 'test-rule-id-1',
fieldName: 'name',
resolvedValue: 'resolved',
});
});
expect(result.current.rulesUpgradeState).toEqual({
'test-rule-id-1': expect.objectContaining({
fieldsUpgradeState: {
name: { state: FieldUpgradeStateEnum.Accepted, resolvedValue: 'resolved' },
},
}),
});
});
});
// Test handling revision and version changes
// - user edited a rule (revision change)
// - a new prebuilt rules package got released (version change)
describe('concurrency control', () => {
describe('revision change', () => {
const createMock = ({ revision }: { revision: number }) => [
createRuleUpgradeInfoMock({
rule_id: 'test-rule-id-1',
revision,
current_rule: createRuleResponseMock({
name: 'current',
revision,
}),
diff: {
num_fields_with_updates: 1,
num_fields_with_conflicts: 1,
num_fields_with_non_solvable_conflicts: 1,
fields: {
name: {
base_version: 'base',
current_version: 'current',
target_version: 'target',
merged_version: 'target',
diff_outcome: ThreeWayDiffOutcome.StockValueCanUpdate,
merge_outcome: ThreeWayMergeOutcome.Merged,
has_base_version: true,
has_update: true,
conflict: ThreeWayDiffConflict.NON_SOLVABLE,
},
},
},
}),
];
it('invalidates resolved conflicts', () => {
const { result, rerender } = renderHook(usePrebuiltRulesUpgradeState, {
initialProps: createMock({ revision: 1 }),
});
act(() => {
result.current.setRuleFieldResolvedValue({
ruleId: 'test-rule-id-1',
fieldName: 'name',
resolvedValue: 'resolved',
});
});
expect(result.current.rulesUpgradeState).toEqual({
'test-rule-id-1': expect.objectContaining({
fieldsUpgradeState: {
name: { state: FieldUpgradeStateEnum.Accepted, resolvedValue: 'resolved' },
},
}),
});
rerender(createMock({ revision: 2 }));
expect(result.current.rulesUpgradeState).toEqual({
'test-rule-id-1': expect.objectContaining({
fieldsUpgradeState: {
name: { state: FieldUpgradeStateEnum.NonSolvableConflict },
},
}),
});
});
it('shows a notification', () => {
const addWarningMock = jest.fn();
(useAppToasts as jest.Mock).mockImplementation(() => ({
addWarning: addWarningMock,
}));
const { result, rerender } = renderHook(usePrebuiltRulesUpgradeState, {
initialProps: createMock({ revision: 1 }),
});
act(() => {
result.current.setRuleFieldResolvedValue({
ruleId: 'test-rule-id-1',
fieldName: 'name',
resolvedValue: 'resolved',
});
});
rerender(createMock({ revision: 2 }));
expect(addWarningMock).toHaveBeenCalled();
});
});
describe('version change', () => {
const createMock = ({ version }: { version: number }) => [
createRuleUpgradeInfoMock({
rule_id: 'test-rule-id-1',
revision: 1,
target_rule: createRuleResponseMock({
name: 'target',
version,
}),
diff: {
num_fields_with_updates: 1,
num_fields_with_conflicts: 1,
num_fields_with_non_solvable_conflicts: 1,
fields: {
name: {
base_version: 'base',
current_version: 'current',
target_version: 'target',
merged_version: 'target',
diff_outcome: ThreeWayDiffOutcome.StockValueCanUpdate,
merge_outcome: ThreeWayMergeOutcome.Merged,
has_base_version: true,
has_update: true,
conflict: ThreeWayDiffConflict.NON_SOLVABLE,
},
},
},
}),
];
it('invalidates resolved conflicts upon version change', () => {
const addWarningMock = jest.fn();
(useAppToasts as jest.Mock).mockImplementation(() => ({
addWarning: addWarningMock,
}));
const { result, rerender } = renderHook(usePrebuiltRulesUpgradeState, {
initialProps: createMock({ version: 1 }),
});
act(() => {
result.current.setRuleFieldResolvedValue({
ruleId: 'test-rule-id-1',
fieldName: 'name',
resolvedValue: 'resolved',
});
});
expect(result.current.rulesUpgradeState).toEqual({
'test-rule-id-1': expect.objectContaining({
fieldsUpgradeState: {
name: { state: FieldUpgradeStateEnum.Accepted, resolvedValue: 'resolved' },
},
}),
});
rerender(createMock({ version: 2 }));
expect(result.current.rulesUpgradeState).toEqual({
'test-rule-id-1': expect.objectContaining({
fieldsUpgradeState: {
name: { state: FieldUpgradeStateEnum.NonSolvableConflict },
},
}),
});
});
it('shows a notification', () => {
const addWarningMock = jest.fn();
(useAppToasts as jest.Mock).mockImplementation(() => ({
addWarning: addWarningMock,
}));
const { result, rerender } = renderHook(usePrebuiltRulesUpgradeState, {
initialProps: createMock({ version: 1 }),
});
act(() => {
result.current.setRuleFieldResolvedValue({
ruleId: 'test-rule-id-1',
fieldName: 'name',
resolvedValue: 'resolved',
});
});
expect(result.current.rulesUpgradeState).toEqual({
'test-rule-id-1': expect.objectContaining({
fieldsUpgradeState: {
name: { state: FieldUpgradeStateEnum.Accepted, resolvedValue: 'resolved' },
},
}),
});
rerender(createMock({ version: 2 }));
expect(addWarningMock).toHaveBeenCalled();
});
});
});
});
function createRuleUpgradeInfoMock(
rewrites?: Partial<RuleUpgradeInfoForReview>
): RuleUpgradeInfoForReview {
return {
id: 'test-id-1',
rule_id: 'test-rule-id-1',
current_rule: createRuleResponseMock(),
target_rule: createRuleResponseMock(),
diff: {
num_fields_with_updates: 0,
num_fields_with_conflicts: 0,
num_fields_with_non_solvable_conflicts: 0,
fields: {},
},
revision: 1,
...rewrites,
};
}
function createRuleResponseMock(rewrites?: Partial<RuleResponse>): RuleResponse {
return {
version: 1,
revision: 1,
...rewrites,
} as RuleResponse;
}

View file

@ -5,7 +5,8 @@
* 2.0.
*/
import { useCallback, useMemo, useState } from 'react';
import { useCallback, useMemo, useState, useRef, useEffect } from 'react';
import { useAppToasts } from '../../../../../common/hooks/use_app_toasts';
import { useIsPrebuiltRulesCustomizationEnabled } from '../../../../rule_management/hooks/use_is_prebuilt_rules_customization_enabled';
import type {
RulesUpgradeState,
@ -23,10 +24,18 @@ import {
ThreeWayDiffOutcome,
} from '../../../../../../common/api/detection_engine';
import { assertUnreachable } from '../../../../../../common/utility_types';
import * as i18n from './translations';
type RuleResolvedConflicts = Partial<DiffableAllFields>;
type RulesResolvedConflicts = Record<RuleSignatureId, RuleResolvedConflicts>;
interface RuleConcurrencyControl {
version: number;
revision: number;
}
type RulesConcurrencyControl = Record<RuleSignatureId, RuleConcurrencyControl>;
interface UseRulesUpgradeStateResult {
rulesUpgradeState: RulesUpgradeState;
setRuleFieldResolvedValue: SetRuleFieldResolvedValueFn;
@ -36,11 +45,22 @@ export function usePrebuiltRulesUpgradeState(
ruleUpgradeInfos: RuleUpgradeInfoForReview[]
): UseRulesUpgradeStateResult {
const isPrebuiltRulesCustomizationEnabled = useIsPrebuiltRulesCustomizationEnabled();
const [rulesResolvedConflicts, setRulesResolvedConflicts] = useState<RulesResolvedConflicts>({});
const [rulesResolvedValues, setRulesResolvedValues] = useState<RulesResolvedConflicts>({});
const resetRuleResolvedValues = useCallback(
(ruleId: RuleSignatureId) => {
setRulesResolvedValues((prevRulesResolvedConflicts) => ({
...prevRulesResolvedConflicts,
[ruleId]: {},
}));
},
[setRulesResolvedValues]
);
const concurrencyControl = useRef<RulesConcurrencyControl>({});
const { addWarning } = useAppToasts();
const setRuleFieldResolvedValue = useCallback(
(...[params]: Parameters<SetRuleFieldResolvedValueFn>) => {
setRulesResolvedConflicts((prevRulesResolvedConflicts) => ({
setRulesResolvedValues((prevRulesResolvedConflicts) => ({
...prevRulesResolvedConflicts,
[params.ruleId]: {
...(prevRulesResolvedConflicts[params.ruleId] ?? {}),
@ -51,13 +71,61 @@ export function usePrebuiltRulesUpgradeState(
[]
);
// Implements concurrency control.
// Rule may be edited or a new prebuilt rules package version gets released.
// In any case current rule's `revision` or target rule's version
// will have higher values.
// Reset resolved conflicts in case of revision`s or version`s mismatch.
useEffect(() => {
for (const {
rule_id: ruleId,
current_rule: { revision: nextRevision, name },
target_rule: { version: nextVersion },
} of ruleUpgradeInfos) {
const cc = concurrencyControl.current[ruleId];
const hasNewerRevision = cc ? nextRevision > cc.revision : false;
const hasNewerVersion = cc ? nextVersion > cc.version : false;
const hasResolvedValues = Object.keys(rulesResolvedValues[ruleId] ?? {}).length > 0;
if (hasNewerRevision && hasResolvedValues) {
addWarning({
title: i18n.RULE_NEW_REVISION_DETECTED_WARNING,
text: i18n.RULE_NEW_REVISION_DETECTED_WARNING_DESCRIPTION(name),
});
}
if (hasNewerVersion && hasResolvedValues) {
addWarning({
title: i18n.RULE_NEW_VERSION_DETECTED_WARNING,
text: i18n.RULE_NEW_VERSION_DETECTED_WARNING_DESCRIPTION(name),
});
}
if ((hasNewerRevision || hasNewerVersion) && hasResolvedValues) {
resetRuleResolvedValues(ruleId);
}
concurrencyControl.current[ruleId] = {
version: nextVersion,
revision: nextRevision,
};
}
}, [
ruleUpgradeInfos,
concurrencyControl,
rulesResolvedValues,
setRulesResolvedValues,
resetRuleResolvedValues,
addWarning,
]);
const rulesUpgradeState = useMemo(() => {
const state: RulesUpgradeState = {};
for (const ruleUpgradeInfo of ruleUpgradeInfos) {
const fieldsUpgradeState = calcFieldsState(
ruleUpgradeInfo.diff.fields,
rulesResolvedConflicts[ruleUpgradeInfo.rule_id] ?? {}
rulesResolvedValues[ruleUpgradeInfo.rule_id] ?? {}
);
const hasRuleTypeChange = Boolean(ruleUpgradeInfo.diff.fields.type);
@ -77,7 +145,7 @@ export function usePrebuiltRulesUpgradeState(
}
return state;
}, [ruleUpgradeInfos, rulesResolvedConflicts, isPrebuiltRulesCustomizationEnabled]);
}, [ruleUpgradeInfos, rulesResolvedValues, isPrebuiltRulesCustomizationEnabled]);
return {
rulesUpgradeState,