mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[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:
parent
9b0302c22b
commit
8822613dc6
4 changed files with 553 additions and 6 deletions
|
@ -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 },
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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 } =
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue