[Security Solution] Implement rule customization license checks (#206079)

**Resolves:** https://github.com/elastic/security-team/issues/10410

## Summary

We want to make Rule Customization available at higher license tiers.  

### **Intended Workflows/UX**  

#### **Basic/Platinum/Security Essentials License Tiers**  
- **Editing Prebuilt Rules:**  
- Allow the 8.16 behavior: only actions, exceptions, snoozing, and
enable/disable options can be modified.
- On the rule editing page, all tabs except *Actions* are disabled.
Disabled tabs will display a hover explanation:
- "Upgrade to Enterprise to enable prebuilt rule customization" for ECH.
- "Upgrade to Security Complete to enable prebuilt rule customization"
for Serverless.
<img width="356" alt="image"
src="https://github.com/user-attachments/assets/72e60933-aaaf-45a0-9660-4cd066d3afec"
/>

- Rule editing via API is not restricted (tracked separately:
https://github.com/elastic/security-team/issues/11504.

- **Bulk Actions:**  
- Modifications to rule content via bulk actions are not allowed.
Prebuilt rules are excluded from bulk actions if the license level is
insufficient. Users will see an explanation for the exclusion.
    - Serverless
<img width="737" alt="image"
src="https://github.com/user-attachments/assets/99fef72f-dd38-4c73-a9e3-7b4c8018b4ed"
/>
    - ECH

- On the API level (`_bulk_action`), an error is returned if a user
tries to modify a prebuilt rule without the required license. Response
in this case looks like this:
    ```json
    {
      "statusCode": 500,
      "error": "Internal Server Error",
      "message": "Bulk edit failed",
      "attributes": {
         "errors": [
           {
             "message": "Elastic rule can't be edited",
             "status_code": 500,
             "rules": []
           }
         ]
      }
    }
    ```

- **Rule Updates:**  
  - Updates are restricted to Elastic’s incoming updates only.  
  - The rule upgrade flyout is in read-only mode.
<img width="949" alt="image"
src="https://github.com/user-attachments/assets/16a56430-63e6-4096-8ffd-b97f828abdd4"
/>
- For previously customized rules where customization is now disabled
due to insufficient licensing, a notification will appear on the upgrade
flyout, clarifying that only an upgrade to Elastic's version is
available.

![image](https://github.com/user-attachments/assets/34ef5168-4fe3-42d0-9444-14180ed86500)
- On the API level (`_perform`), only requests with `pick_version =
target` are permitted. Requests with `rule.fields` values are not
allowed.
    API response when `pick_version` is not `target`:
    ```json
    {
"message": "Only the 'TARGET' version can be selected for a rule update;
received: 'CURRENT'",
      "status_code": 400
    }
    ```
    API response when the `fields` value is provided:
    ```json
    {
"message": "Rule field customization is not allowed. Received fields:
name, description",
      "status_code": 400
    }
    ```

- **Customized Rules:**  
- Existing customizations remain intact, and the “Modified” badge is
retained
    - On the rule management, monitoring, and update tables:

![image](https://github.com/user-attachments/assets/c7990c8f-5ed3-40ab-b0c6-ddc329e69b09)
    - On the rule update flyout:

![image](https://github.com/user-attachments/assets/f74cb0bc-e7e5-49d5-8fec-b447517b5b52)
    - On the rule details page:

![image](https://github.com/user-attachments/assets/b28990f1-9e84-481e-b966-0232495f4882)

- When we edit a rule with customizations (e.g., change rule's actions),
the rule should stay marked as customized


- **Import/Export Scenarios:**  
- These are handled separately
(https://github.com/elastic/security-team/issues/11502)

#### **Enterprise/Security Complete License Tiers**  
- All rules can be fully edited
- Upgraded prebuilt or customized rules will have an editable view,
enabling full customization
This commit is contained in:
Dmitrii Shevchenko 2025-01-29 18:20:47 +01:00 committed by GitHub
parent 5b78d3295a
commit 199378c60c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
101 changed files with 1133 additions and 673 deletions

View file

@ -78,7 +78,8 @@ enabled:
- x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/configs/serverless.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/update_prebuilt_rules_package/trial_license_complete_tier/configs/serverless.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/customization_enabled/configs/serverless.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/customization_disabled/configs/serverless.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/customization_disabled/configs/serverless_complete_tier.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/customization_disabled/configs/serverless_essentials_tier.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_bulk_actions/trial_license_complete_tier/configs/serverless.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_delete/trial_license_complete_tier/configs/serverless.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_delete/basic_license_essentials_tier/configs/serverless.config.ts

View file

@ -59,7 +59,8 @@ enabled:
- x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/configs/ess.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/update_prebuilt_rules_package/trial_license_complete_tier/configs/ess.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/customization_enabled/configs/ess.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/customization_disabled/configs/ess.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/customization_disabled/configs/ess_basic_license.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/customization_disabled/configs/ess_trial_license.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_bulk_actions/trial_license_complete_tier/configs/ess.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_delete/trial_license_complete_tier/configs/ess.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_delete/basic_license_essentials_tier/configs/ess.config.ts

View file

@ -44990,6 +44990,7 @@ components:
Security_Detections_API_BulkActionsDryRunErrCode:
enum:
- IMMUTABLE
- PREBUILT_CUSTOMIZATION_LICENSE
- MACHINE_LEARNING_AUTH
- MACHINE_LEARNING_INDEX_PATTERN
- ESQL_INDEX_PATTERN

View file

@ -51546,6 +51546,7 @@ components:
Security_Detections_API_BulkActionsDryRunErrCode:
enum:
- IMMUTABLE
- PREBUILT_CUSTOMIZATION_LICENSE
- MACHINE_LEARNING_AUTH
- MACHINE_LEARNING_INDEX_PATTERN
- ESQL_INDEX_PATTERN

View file

@ -91,6 +91,11 @@ export enum ProductFeatureSecurityKey {
/** Enables Endpoint Workflow Insights */
securityWorkflowInsights = 'security_workflow_insights',
/**
* Enables customization of prebuilt Elastic rules
*/
prebuiltRuleCustomization = 'prebuilt_rule_customization',
}
export enum ProductFeatureCasesKey {

View file

@ -135,4 +135,5 @@ export const securityDefaultProductFeaturesConfig: DefaultSecurityProductFeature
// Security PLIs
[ProductFeatureSecurityKey.automaticImport]: {},
[ProductFeatureSecurityKey.prebuiltRuleCustomization]: {},
};

View file

@ -54,3 +54,11 @@ export const UPGRADE_NOTES_MANAGEMENT_USER_FILTER = (requiredLicense: string) =>
requiredLicense,
},
});
export const UPGRADE_PREBUILT_RULE_CUSTOMIZATION = (requiredLicense: string) =>
i18n.translate('securitySolutionPackages.ruleManagement.prebuiltRuleCustomization.upsell', {
defaultMessage: 'Upgrade to {requiredLicense} to enable prebuilt rule customization',
values: {
requiredLicense,
},
});

View file

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

View file

@ -50,6 +50,7 @@ export const RuleDetailsInError = z.object({
export type BulkActionsDryRunErrCode = z.infer<typeof BulkActionsDryRunErrCode>;
export const BulkActionsDryRunErrCode = z.enum([
'IMMUTABLE',
'PREBUILT_CUSTOMIZATION_LICENSE',
'MACHINE_LEARNING_AUTH',
'MACHINE_LEARNING_INDEX_PATTERN',
'ESQL_INDEX_PATTERN',

View file

@ -75,6 +75,7 @@ components:
type: string
enum:
- IMMUTABLE
- PREBUILT_CUSTOMIZATION_LICENSE
- MACHINE_LEARNING_AUTH
- MACHINE_LEARNING_INDEX_PATTERN
- ESQL_INDEX_PATTERN

View file

@ -329,6 +329,7 @@ export const UNAUTHENTICATED_USER = 'Unauthenticated' as const;
Licensing requirements
*/
export const MINIMUM_ML_LICENSE = 'platinum' as const;
export const MINIMUM_RULE_CUSTOMIZATION_LICENSE = 'enterprise' as const;
/**
Machine Learning constants
@ -438,19 +439,6 @@ export const RULE_DETAILS_EXECUTION_LOG_TABLE_SHOW_METRIC_COLUMNS_STORAGE_KEY =
export const RULE_DETAILS_EXECUTION_LOG_TABLE_SHOW_SOURCE_EVENT_TIME_RANGE_STORAGE_KEY =
'securitySolution.ruleDetails.ruleExecutionLog.showSourceEventTimeRange.v8.15';
// TODO: https://github.com/elastic/kibana/pull/142950
/**
* Error codes that can be thrown during _bulk_action API dry_run call and be processed and displayed to end user
*/
export enum BulkActionsDryRunErrCode {
IMMUTABLE = 'IMMUTABLE',
MACHINE_LEARNING_AUTH = 'MACHINE_LEARNING_AUTH',
MACHINE_LEARNING_INDEX_PATTERN = 'MACHINE_LEARNING_INDEX_PATTERN',
ESQL_INDEX_PATTERN = 'ESQL_INDEX_PATTERN',
MANUAL_RULE_RUN_FEATURE = 'MANUAL_RULE_RUN_FEATURE',
MANUAL_RULE_RUN_DISABLED_RULE = 'MANUAL_RULE_RUN_DISABLED_RULE',
}
export const MAX_NUMBER_OF_NEW_TERMS_FIELDS = 3;
export const BULK_ADD_TO_TIMELINE_LIMIT = 2000;

View file

@ -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.
*/
export enum PrebuiltRulesCustomizationDisabledReason {
License = 'License',
FeatureFlag = 'FeatureFlag',
}
export interface PrebuiltRulesCustomizationStatus {
isRulesCustomizationEnabled: boolean;
customizationDisabledReason?: PrebuiltRulesCustomizationDisabledReason;
}

View file

@ -1761,6 +1761,7 @@ components:
BulkActionsDryRunErrCode:
enum:
- IMMUTABLE
- PREBUILT_CUSTOMIZATION_LICENSE
- MACHINE_LEARNING_AUTH
- MACHINE_LEARNING_INDEX_PATTERN
- ESQL_INDEX_PATTERN

View file

@ -1037,6 +1037,7 @@ components:
BulkActionsDryRunErrCode:
enum:
- IMMUTABLE
- PREBUILT_CUSTOMIZATION_LICENSE
- MACHINE_LEARNING_AUTH
- MACHINE_LEARNING_INDEX_PATTERN
- ESQL_INDEX_PATTERN

View file

@ -15,6 +15,7 @@ import {
EuiSpacer,
EuiTab,
EuiTabs,
EuiToolTip,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import type { FC } from 'react';
@ -69,8 +70,10 @@ import { VALIDATION_WARNING_CODE_FIELD_NAME_MAP } from '../../../rule_creation/c
import { useRuleForms, useRuleIndexPattern } from '../form';
import { useEsqlIndex, useEsqlQueryForAboutStep } from '../../hooks';
import { CustomHeaderPageMemo } from '..';
import { useIsPrebuiltRulesCustomizationEnabled } from '../../../rule_management/hooks/use_is_prebuilt_rules_customization_enabled';
import { usePrebuiltRulesCustomizationStatus } from '../../../rule_management/logic/prebuilt_rules/use_prebuilt_rules_customization_status';
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';
const EditRulePageComponent: FC<{ rule: RuleResponse }> = ({ rule }) => {
const { addSuccess } = useAppToasts();
@ -87,14 +90,17 @@ const EditRulePageComponent: FC<{ rule: RuleResponse }> = ({ rule }) => {
useListsConfig();
const { application, triggersActionsUi } = useKibana().services;
const { navigateToApp } = application;
const isPrebuiltRulesCustomizationEnabled = useIsPrebuiltRulesCustomizationEnabled();
const { isRulesCustomizationEnabled, customizationDisabledReason } =
usePrebuiltRulesCustomizationStatus();
const canEditRule = isRulesCustomizationEnabled || !rule.immutable;
const prebuiltCustomizationUpsellingMessage = usePrebuiltRuleCustomizationUpsellingMessage();
const { detailName: ruleId } = useParams<{ detailName: string }>();
const [activeStep, setActiveStep] = useState<RuleStep>(
!isPrebuiltRulesCustomizationEnabled && rule.immutable
? RuleStep.ruleActions
: RuleStep.defineRule
canEditRule ? RuleStep.defineRule : RuleStep.ruleActions
);
const { mutateAsync: updateRule, isLoading } = useUpdateRule();
const [isRulePreviewVisible, setIsRulePreviewVisible] = useState(true);
@ -207,13 +213,19 @@ const EditRulePageComponent: FC<{ rule: RuleResponse }> = ({ rule }) => {
dataViewId: defineStepData.dataViewId,
});
const customizationDisabledTooltip =
!canEditRule && customizationDisabledReason === PrebuiltRulesCustomizationDisabledReason.License
? prebuiltCustomizationUpsellingMessage
: undefined;
const tabs = useMemo(
() => [
{
'data-test-subj': 'edit-rule-define-tab',
id: RuleStep.defineRule,
name: ruleI18n.DEFINITION,
disabled: !isPrebuiltRulesCustomizationEnabled && rule?.immutable,
disabled: !canEditRule,
tooltip: customizationDisabledTooltip,
content: (
<div
style={{
@ -252,7 +264,8 @@ const EditRulePageComponent: FC<{ rule: RuleResponse }> = ({ rule }) => {
'data-test-subj': 'edit-rule-about-tab',
id: RuleStep.aboutRule,
name: ruleI18n.ABOUT,
disabled: !isPrebuiltRulesCustomizationEnabled && rule?.immutable,
disabled: !canEditRule,
tooltip: customizationDisabledTooltip,
content: (
<div
style={{
@ -285,7 +298,8 @@ const EditRulePageComponent: FC<{ rule: RuleResponse }> = ({ rule }) => {
'data-test-subj': 'edit-rule-schedule-tab',
id: RuleStep.scheduleRule,
name: ruleI18n.SCHEDULE,
disabled: !isPrebuiltRulesCustomizationEnabled && rule?.immutable,
disabled: !canEditRule,
tooltip: customizationDisabledTooltip,
content: (
<div
style={{
@ -337,10 +351,8 @@ const EditRulePageComponent: FC<{ rule: RuleResponse }> = ({ rule }) => {
},
],
[
isPrebuiltRulesCustomizationEnabled,
rule?.immutable,
rule.rule_source,
rule?.id,
canEditRule,
customizationDisabledTooltip,
activeStep,
loading,
isSavedQueryLoading,
@ -351,11 +363,13 @@ const EditRulePageComponent: FC<{ rule: RuleResponse }> = ({ rule }) => {
indexPattern,
isIndexPatternLoading,
isQueryBarValid,
defineStepData,
memoizedIndex,
defineStepData,
aboutStepData,
aboutStepForm,
esqlQueryForAboutStep,
rule.rule_source,
rule?.id,
scheduleStepData,
scheduleStepForm,
actionsStepData,
@ -407,10 +421,9 @@ const EditRulePageComponent: FC<{ rule: RuleResponse }> = ({ rule }) => {
const onSubmit = useCallback(async () => {
const actionsStepFormValid = await actionsStepForm.validate();
if (!isPrebuiltRulesCustomizationEnabled && rule.immutable) {
if (!canEditRule) {
// Since users cannot edit Define, About and Schedule tabs of the rule, we skip validation of those to avoid
// user confusion of seeing that those tabs have error and not being able to see or do anything about that.
// We will need to remove this condition once rule customization work is done.
if (actionsStepFormValid) {
await saveChanges();
}
@ -452,8 +465,7 @@ const EditRulePageComponent: FC<{ rule: RuleResponse }> = ({ rule }) => {
await saveChanges();
}, [
actionsStepForm,
isPrebuiltRulesCustomizationEnabled,
rule.immutable,
canEditRule,
defineStepForm,
aboutStepForm,
scheduleStepForm,
@ -468,15 +480,16 @@ const EditRulePageComponent: FC<{ rule: RuleResponse }> = ({ rule }) => {
const renderTabs = () => {
return tabs.map((tab, index) => (
<EuiTab
key={index}
onClick={() => onTabClick(tab)}
isSelected={tab.id === activeStep}
disabled={tab.disabled}
data-test-subj={tab['data-test-subj']}
>
{tab.name}
</EuiTab>
<EuiToolTip key={index} position="top" content={tab.tooltip}>
<EuiTab
onClick={() => onTabClick(tab)}
isSelected={tab.id === activeStep}
disabled={tab.disabled}
data-test-subj={tab['data-test-subj']}
>
{tab.name}
</EuiTab>
</EuiToolTip>
));
};

View file

@ -31,13 +31,13 @@ import type {
BulkManualRuleRun,
CoverageOverviewResponse,
GetRuleManagementFiltersResponse,
BulkActionsDryRunErrCode,
} from '../../../../common/api/detection_engine/rule_management';
import {
RULE_MANAGEMENT_FILTERS_URL,
RULE_MANAGEMENT_COVERAGE_OVERVIEW_URL,
BulkActionTypeEnum,
} from '../../../../common/api/detection_engine/rule_management';
import type { BulkActionsDryRunErrCode } from '../../../../common/constants';
import {
DETECTION_ENGINE_RULES_BULK_ACTION,
DETECTION_ENGINE_RULES_IMPORT_URL,

View file

@ -5,12 +5,13 @@
* 2.0.
*/
import React from 'react';
import { EuiBadge } from '@elastic/eui';
import * as i18n from './translations';
import { isCustomizedPrebuiltRule } from '../../../../../common/api/detection_engine';
import React from 'react';
import type { RuleResponse } from '../../../../../common/api/detection_engine';
import { useIsPrebuiltRulesCustomizationEnabled } from '../../hooks/use_is_prebuilt_rules_customization_enabled';
import { isCustomizedPrebuiltRule } from '../../../../../common/api/detection_engine';
import { PrebuiltRulesCustomizationDisabledReason } from '../../../../../common/detection_engine/prebuilt_rules/prebuilt_rule_customization_status';
import { usePrebuiltRulesCustomizationStatus } from '../../logic/prebuilt_rules/use_prebuilt_rules_customization_status';
import * as i18n from './translations';
interface CustomizedPrebuiltRuleBadgeProps {
rule: RuleResponse | null;
@ -19,9 +20,13 @@ interface CustomizedPrebuiltRuleBadgeProps {
export const CustomizedPrebuiltRuleBadge: React.FC<CustomizedPrebuiltRuleBadgeProps> = ({
rule,
}) => {
const isPrebuiltRulesCustomizationEnabled = useIsPrebuiltRulesCustomizationEnabled();
const { isRulesCustomizationEnabled, customizationDisabledReason } =
usePrebuiltRulesCustomizationStatus();
if (!isPrebuiltRulesCustomizationEnabled) {
if (
!isRulesCustomizationEnabled &&
customizationDisabledReason === PrebuiltRulesCustomizationDisabledReason.FeatureFlag
) {
return null;
}

View file

@ -7,7 +7,7 @@
import { useKibana } from '../../../common/lib/kibana/kibana_react';
import { DEFAULT_INDEX_KEY, DEFAULT_INDEX_PATTERN } from '../../../../common/constants';
import { useIsPrebuiltRulesCustomizationEnabled } from './use_is_prebuilt_rules_customization_enabled';
import { usePrebuiltRulesCustomizationStatus } from '../logic/prebuilt_rules/use_prebuilt_rules_customization_status';
/**
* Gets the default index pattern for cases when rule has neither index patterns or data view.
@ -15,9 +15,9 @@ import { useIsPrebuiltRulesCustomizationEnabled } from './use_is_prebuilt_rules_
*/
export function useDefaultIndexPattern(): string[] {
const { services } = useKibana();
const isPrebuiltRulesCustomizationEnabled = useIsPrebuiltRulesCustomizationEnabled();
const { isRulesCustomizationEnabled } = usePrebuiltRulesCustomizationStatus();
return isPrebuiltRulesCustomizationEnabled
return isRulesCustomizationEnabled
? services.settings.client.get(DEFAULT_INDEX_KEY, DEFAULT_INDEX_PATTERN)
: [];
}

View file

@ -1,12 +0,0 @@
/*
* 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 { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features';
export const useIsPrebuiltRulesCustomizationEnabled = () => {
return useIsExperimentalFeatureEnabled('prebuiltRulesCustomizationEnabled');
};

View file

@ -0,0 +1,24 @@
/*
* 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 { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
import { useUpsellingMessage } from '../../../../common/hooks/use_upselling';
/**
* This hook returns an upselling message when the license level is insufficient
* for prebuilt rule customization. If the license level is sufficient, it
* returns `undefined`.
*/
export const usePrebuiltRuleCustomizationUpsellingMessage = () => {
// Upselling message is returned when the license level is insufficient,
// otherwise it's undefined
const upsellingMessage = useUpsellingMessage('prebuilt_rule_customization');
// We show the upselling message only if the feature flag is enabled
const isFeatureFlagEnabled = useIsExperimentalFeatureEnabled('prebuiltRulesCustomizationEnabled');
return isFeatureFlagEnabled ? upsellingMessage : undefined;
};

View file

@ -0,0 +1,37 @@
/*
* 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 type { PrebuiltRulesCustomizationStatus } from '../../../../../common/detection_engine/prebuilt_rules/prebuilt_rule_customization_status';
import { PrebuiltRulesCustomizationDisabledReason } from '../../../../../common/detection_engine/prebuilt_rules/prebuilt_rule_customization_status';
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
import { usePrebuiltRuleCustomizationUpsellingMessage } from './use_prebuilt_rule_customization_upselling_message';
/**
* Custom hook to determine prebuilt rules customization status.
*
* This hook checks if the feature flag for prebuilt rules customization is
* enabled and returns the reason why it's disabled if it's the case.
*/
export const usePrebuiltRulesCustomizationStatus = (): PrebuiltRulesCustomizationStatus => {
const isFeatureFlagEnabled = useIsExperimentalFeatureEnabled('prebuiltRulesCustomizationEnabled');
// Upselling message is returned when the license level is insufficient,
// otherwise it's undefined
const upsellingMessage = usePrebuiltRuleCustomizationUpsellingMessage();
const isRulesCustomizationEnabled = isFeatureFlagEnabled && !upsellingMessage;
let customizationDisabledReason;
if (!isRulesCustomizationEnabled) {
customizationDisabledReason = !isFeatureFlagEnabled
? PrebuiltRulesCustomizationDisabledReason.FeatureFlag
: PrebuiltRulesCustomizationDisabledReason.License;
}
return {
isRulesCustomizationEnabled,
customizationDisabledReason,
};
};

View file

@ -5,30 +5,22 @@
* 2.0.
*/
import type { FC, PropsWithChildren } from 'react';
import React from 'react';
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
import { render, screen } from '@testing-library/react';
import React from 'react';
import {
BulkActionTypeEnum,
BulkActionsDryRunErrCodeEnum,
} from '../../../../../../common/api/detection_engine/rule_management';
import { TestProviders } from '../../../../../common/mock';
import { BulkActionRuleErrorsList } from './bulk_action_rule_errors_list';
import { BulkActionsDryRunErrCode } from '../../../../../../common/constants';
import type { DryRunResult } from './types';
import { BulkActionTypeEnum } from '../../../../../../common/api/detection_engine/rule_management';
const Wrapper: FC<PropsWithChildren<unknown>> = ({ children }) => {
return (
<IntlProvider locale="en">
<>{children}</>
</IntlProvider>
);
};
describe('Component BulkEditRuleErrorsList', () => {
test('should not render component if no errors present', () => {
const { container } = render(
<BulkActionRuleErrorsList bulkAction={BulkActionTypeEnum.edit} ruleErrors={[]} />,
{
wrapper: Wrapper,
wrapper: TestProviders,
}
);
@ -49,7 +41,7 @@ describe('Component BulkEditRuleErrorsList', () => {
render(
<BulkActionRuleErrorsList bulkAction={BulkActionTypeEnum.edit} ruleErrors={ruleErrors} />,
{
wrapper: Wrapper,
wrapper: TestProviders,
}
);
@ -59,19 +51,19 @@ describe('Component BulkEditRuleErrorsList', () => {
test.each([
[
BulkActionsDryRunErrCode.IMMUTABLE,
BulkActionsDryRunErrCodeEnum.IMMUTABLE,
'2 prebuilt Elastic rules (editing prebuilt rules is not supported)',
],
[
BulkActionsDryRunErrCode.MACHINE_LEARNING_INDEX_PATTERN,
BulkActionsDryRunErrCodeEnum.MACHINE_LEARNING_INDEX_PATTERN,
"2 machine learning rules (these rules don't have index patterns)",
],
[
BulkActionsDryRunErrCode.ESQL_INDEX_PATTERN,
BulkActionsDryRunErrCodeEnum.ESQL_INDEX_PATTERN,
"2 ES|QL rules (these rules don't have index patterns)",
],
[
BulkActionsDryRunErrCode.MACHINE_LEARNING_AUTH,
BulkActionsDryRunErrCodeEnum.MACHINE_LEARNING_AUTH,
"2 machine learning rules can't be edited (test failure)",
],
[undefined, "2 rules can't be edited (test failure)"],
@ -86,7 +78,7 @@ describe('Component BulkEditRuleErrorsList', () => {
render(
<BulkActionRuleErrorsList bulkAction={BulkActionTypeEnum.edit} ruleErrors={ruleErrors} />,
{
wrapper: Wrapper,
wrapper: TestProviders,
}
);
@ -95,11 +87,11 @@ describe('Component BulkEditRuleErrorsList', () => {
test.each([
[
BulkActionsDryRunErrCode.MANUAL_RULE_RUN_FEATURE,
BulkActionsDryRunErrCodeEnum.MANUAL_RULE_RUN_FEATURE,
'2 rules (Manual rule run feature is disabled)',
],
[
BulkActionsDryRunErrCode.MANUAL_RULE_RUN_DISABLED_RULE,
BulkActionsDryRunErrCodeEnum.MANUAL_RULE_RUN_DISABLED_RULE,
'2 rules (Cannot schedule manual rule run for disabled rules)',
],
])('should render correct message for "%s" errorCode', (errorCode, value) => {
@ -113,7 +105,7 @@ describe('Component BulkEditRuleErrorsList', () => {
render(
<BulkActionRuleErrorsList bulkAction={BulkActionTypeEnum.run} ruleErrors={ruleErrors} />,
{
wrapper: Wrapper,
wrapper: TestProviders,
}
);

View file

@ -9,10 +9,14 @@ import React from 'react';
import { EuiSpacer } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { BulkActionsDryRunErrCode } from '../../../../../../common/constants';
import { BulkActionTypeEnum } from '../../../../../../common/api/detection_engine/rule_management';
import type { BulkActionsDryRunErrCode } from '../../../../../../common/api/detection_engine/rule_management';
import {
BulkActionTypeEnum,
BulkActionsDryRunErrCodeEnum,
} from '../../../../../../common/api/detection_engine/rule_management';
import type { DryRunResult, BulkActionForConfirmation } from './types';
import { usePrebuiltRuleCustomizationUpsellingMessage } from '../../../../rule_management/logic/prebuilt_rules/use_prebuilt_rule_customization_upselling_message';
interface BulkActionRuleErrorItemProps {
errorCode: BulkActionsDryRunErrCode | undefined;
@ -25,8 +29,10 @@ const BulkEditRuleErrorItem = ({
message,
rulesCount,
}: BulkActionRuleErrorItemProps) => {
const upsellingMessage = usePrebuiltRuleCustomizationUpsellingMessage();
switch (errorCode) {
case BulkActionsDryRunErrCode.IMMUTABLE:
case BulkActionsDryRunErrCodeEnum.IMMUTABLE:
return (
<li key={message}>
<FormattedMessage
@ -36,7 +42,17 @@ const BulkEditRuleErrorItem = ({
/>
</li>
);
case BulkActionsDryRunErrCode.MACHINE_LEARNING_INDEX_PATTERN:
case BulkActionsDryRunErrCodeEnum.PREBUILT_CUSTOMIZATION_LICENSE:
return (
<li key={message}>
<FormattedMessage
id="xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.prebuiltRulesLicenseDescription"
defaultMessage="{rulesCount, plural, =1 {# prebuilt rule} other {# prebuilt rules}} ({upsellingMessage})"
values={{ rulesCount, upsellingMessage }}
/>
</li>
);
case BulkActionsDryRunErrCodeEnum.MACHINE_LEARNING_INDEX_PATTERN:
return (
<li key={message}>
<FormattedMessage
@ -46,7 +62,7 @@ const BulkEditRuleErrorItem = ({
/>
</li>
);
case BulkActionsDryRunErrCode.MACHINE_LEARNING_AUTH:
case BulkActionsDryRunErrCodeEnum.MACHINE_LEARNING_AUTH:
return (
<li key={message}>
<FormattedMessage
@ -56,7 +72,7 @@ const BulkEditRuleErrorItem = ({
/>
</li>
);
case BulkActionsDryRunErrCode.ESQL_INDEX_PATTERN:
case BulkActionsDryRunErrCodeEnum.ESQL_INDEX_PATTERN:
return (
<li key={message}>
<FormattedMessage
@ -85,7 +101,7 @@ const BulkExportRuleErrorItem = ({
rulesCount,
}: BulkActionRuleErrorItemProps) => {
switch (errorCode) {
case BulkActionsDryRunErrCode.IMMUTABLE:
case BulkActionsDryRunErrCodeEnum.IMMUTABLE:
return (
<li key={message}>
<FormattedMessage
@ -114,7 +130,7 @@ const BulkManualRuleRunErrorItem = ({
rulesCount,
}: BulkActionRuleErrorItemProps) => {
switch (errorCode) {
case BulkActionsDryRunErrCode.MANUAL_RULE_RUN_FEATURE:
case BulkActionsDryRunErrCodeEnum.MANUAL_RULE_RUN_FEATURE:
return (
<li key={message}>
<FormattedMessage
@ -124,7 +140,7 @@ const BulkManualRuleRunErrorItem = ({
/>
</li>
);
case BulkActionsDryRunErrCode.MANUAL_RULE_RUN_DISABLED_RULE:
case BulkActionsDryRunErrCodeEnum.MANUAL_RULE_RUN_DISABLED_RULE:
return (
<li key={message}>
<FormattedMessage

View file

@ -5,8 +5,10 @@
* 2.0.
*/
import type { BulkActionsDryRunErrCode } from '../../../../../../common/constants';
import type { BulkActionTypeEnum } from '../../../../../../common/api/detection_engine/rule_management';
import type {
BulkActionTypeEnum,
BulkActionsDryRunErrCode,
} from '../../../../../../common/api/detection_engine/rule_management';
/**
* Only 3 bulk actions are supported for for confirmation dry run modal:

View file

@ -5,8 +5,10 @@
* 2.0.
*/
import { BulkActionsDryRunErrCode } from '../../../../../../../common/constants';
import type { ExportRulesDetails } from '../../../../../../../common/api/detection_engine/rule_management';
import {
BulkActionsDryRunErrCodeEnum,
type ExportRulesDetails,
} from '../../../../../../../common/api/detection_engine/rule_management';
import type { BulkActionResponse } from '../../../../../rule_management/logic';
import type { DryRunResult } from '../types';
@ -44,7 +46,7 @@ export const transformExportDetailsToDryRunResult = (details: ExportRulesDetails
ruleErrors: details.missing_rules.length
? [
{
errorCode: BulkActionsDryRunErrCode.IMMUTABLE,
errorCode: BulkActionsDryRunErrCodeEnum.IMMUTABLE,
message: "Prebuilt rules can't be exported.",
ruleIds: details.missing_rules.map(({ rule_id: ruleId }) => ruleId),
},

View file

@ -9,9 +9,9 @@ import type { DryRunResult } from '../types';
import type { FilterOptions } from '../../../../../rule_management/logic/types';
import { convertRulesFilterToKQL } from '../../../../../../../common/detection_engine/rule_management/rule_filtering';
import { BulkActionsDryRunErrCode } from '../../../../../../../common/constants';
import { prepareSearchParams } from './prepare_search_params';
import { BulkActionsDryRunErrCodeEnum } from '../../../../../../../common/api/detection_engine';
jest.mock('../../../../../../../common/detection_engine/rule_management/rule_filtering', () => ({
convertRulesFilterToKQL: jest.fn().mockReturnValue('str'),
@ -44,7 +44,7 @@ describe('prepareSearchParams', () => {
test.each([
[
BulkActionsDryRunErrCode.MACHINE_LEARNING_INDEX_PATTERN,
BulkActionsDryRunErrCodeEnum.MACHINE_LEARNING_INDEX_PATTERN,
{
filter: '',
tags: [],
@ -54,7 +54,7 @@ describe('prepareSearchParams', () => {
},
],
[
BulkActionsDryRunErrCode.MACHINE_LEARNING_AUTH,
BulkActionsDryRunErrCodeEnum.MACHINE_LEARNING_AUTH,
{
filter: '',
tags: [],
@ -64,7 +64,7 @@ describe('prepareSearchParams', () => {
},
],
[
BulkActionsDryRunErrCode.ESQL_INDEX_PATTERN,
BulkActionsDryRunErrCodeEnum.ESQL_INDEX_PATTERN,
{
filter: '',
tags: [],
@ -74,7 +74,7 @@ describe('prepareSearchParams', () => {
},
],
[
BulkActionsDryRunErrCode.IMMUTABLE,
BulkActionsDryRunErrCodeEnum.IMMUTABLE,
{
filter: '',
tags: [],

View file

@ -10,7 +10,7 @@ import type { QueryOrIds } from '../../../../../rule_management/logic';
import type { DryRunResult } from '../types';
import type { FilterOptions } from '../../../../../rule_management/logic/types';
import { BulkActionsDryRunErrCode } from '../../../../../../../common/constants';
import { BulkActionsDryRunErrCodeEnum } from '../../../../../../../common/api/detection_engine';
type PrepareSearchFilterProps =
| { selectedRuleIds: string[]; dryRunResult?: DryRunResult }
@ -39,17 +39,17 @@ export const prepareSearchParams = ({
let modifiedFilterOptions = { ...props.filterOptions };
dryRunResult?.ruleErrors.forEach(({ errorCode }) => {
switch (errorCode) {
case BulkActionsDryRunErrCode.IMMUTABLE:
case BulkActionsDryRunErrCodeEnum.IMMUTABLE:
modifiedFilterOptions = { ...modifiedFilterOptions, showCustomRules: true };
break;
case BulkActionsDryRunErrCode.MACHINE_LEARNING_INDEX_PATTERN:
case BulkActionsDryRunErrCode.MACHINE_LEARNING_AUTH:
case BulkActionsDryRunErrCodeEnum.MACHINE_LEARNING_INDEX_PATTERN:
case BulkActionsDryRunErrCodeEnum.MACHINE_LEARNING_AUTH:
modifiedFilterOptions = {
...modifiedFilterOptions,
excludeRuleTypes: [...(modifiedFilterOptions.excludeRuleTypes ?? []), 'machine_learning'],
};
break;
case BulkActionsDryRunErrCode.ESQL_INDEX_PATTERN:
case BulkActionsDryRunErrCodeEnum.ESQL_INDEX_PATTERN:
modifiedFilterOptions = {
...modifiedFilterOptions,
excludeRuleTypes: [...(modifiedFilterOptions.excludeRuleTypes ?? []), 'esql'],

View file

@ -0,0 +1,26 @@
/*
* 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 { CUSTOMIZATION_DISABLED_CALLOUT_DESCRIPTION } from './translations';
import { usePrebuiltRuleCustomizationUpsellingMessage } from '../../../../rule_management/logic/prebuilt_rules/use_prebuilt_rule_customization_upselling_message';
export function CustomizationDisabledCallout() {
const upsellingMessage = usePrebuiltRuleCustomizationUpsellingMessage();
// Upselling message is returned only when the license level is insufficient
if (!upsellingMessage) {
return null;
}
return (
<EuiCallOut title={upsellingMessage} size="s" color="primary">
<p>{CUSTOMIZATION_DISABLED_CALLOUT_DESCRIPTION}</p>
</EuiCallOut>
);
}

View file

@ -174,3 +174,11 @@ export const RULE_NEW_VERSION_DETECTED_WARNING_DESCRIPTION = (ruleName: string)
values: { ruleName },
}
);
export const CUSTOMIZATION_DISABLED_CALLOUT_DESCRIPTION = i18n.translate(
'xpack.securitySolution.detectionEngine.upgradeRules.customizationDisabledCalloutDescription',
{
defaultMessage:
'Prebuilt rule customization is disabled. Only updates to Elastic version are available.',
}
);

View file

@ -11,6 +11,7 @@ import type { RuleUpgradeState } from '../../../../rule_management/model/prebuil
import { useUserData } from '../../../../../detections/components/user_info';
import * as i18n from './translations';
import { useUpgradePrebuiltRulesTableContext } from './upgrade_prebuilt_rules_table_context';
import { usePrebuiltRulesCustomizationStatus } from '../../../../rule_management/logic/prebuilt_rules/use_prebuilt_rules_customization_status';
interface UpgradePrebuiltRulesTableButtonsProps {
selectedRules: RuleUpgradeState[];
@ -26,10 +27,10 @@ export const UpgradePrebuiltRulesTableButtons = ({
loadingRules,
isRefetching,
isUpgradingSecurityPackages,
isPrebuiltRulesCustomizationEnabled,
},
actions: { upgradeRules, upgradeAllRules },
} = useUpgradePrebuiltRulesTableContext();
const { isRulesCustomizationEnabled } = usePrebuiltRulesCustomizationStatus();
const [{ loading: isUserDataLoading, canUserCRUD }] = useUserData();
const canUserEditRules = canUserCRUD && !isUserDataLoading;
@ -40,17 +41,17 @@ export const UpgradePrebuiltRulesTableButtons = ({
const isRequestInProgress = isRuleUpgrading || isRefetching || isUpgradingSecurityPackages;
const doAllSelectedRulesHaveConflicts =
isPrebuiltRulesCustomizationEnabled &&
isRulesCustomizationEnabled &&
selectedRules.every(({ hasUnresolvedConflicts }) => hasUnresolvedConflicts);
const doAllRulesHaveConflicts =
isPrebuiltRulesCustomizationEnabled &&
isRulesCustomizationEnabled &&
ruleUpgradeStates.every(({ hasUnresolvedConflicts }) => hasUnresolvedConflicts);
const { selectedRulesButtonTooltip, allRulesButtonTooltip } = useBulkUpdateButtonsTooltipContent({
canUserEditRules,
doAllSelectedRulesHaveConflicts,
doAllRulesHaveConflicts,
isPrebuiltRulesCustomizationEnabled,
isPrebuiltRulesCustomizationEnabled: isRulesCustomizationEnabled,
});
const upgradeSelectedRules = useCallback(

View file

@ -8,11 +8,12 @@
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 type {
RuleFieldsToUpgrade,
RuleUpgradeSpecifier,
} from '../../../../../../common/api/detection_engine';
import { useIsPrebuiltRulesCustomizationEnabled } from '../../../../rule_management/hooks/use_is_prebuilt_rules_customization_enabled';
import { usePrebuiltRulesCustomizationStatus } from '../../../../rule_management/logic/prebuilt_rules/use_prebuilt_rules_customization_status';
import { useAppToasts } from '../../../../../common/hooks/use_app_toasts';
import type { RuleUpgradeState } from '../../../../rule_management/model/prebuilt_rule_upgrade';
import { RuleUpgradeTab } from '../../../../rule_management/components/rule_details/three_way_diff';
@ -38,6 +39,7 @@ 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';
const REVIEW_PREBUILT_RULES_UPGRADE_REFRESH_INTERVAL = 5 * 60 * 1000;
@ -83,10 +85,6 @@ export interface UpgradePrebuiltRulesTableState {
* The timestamp for when the rules were successfully fetched
*/
lastUpdated: number;
/**
* Feature Flag to enable prebuilt rules customization
*/
isPrebuiltRulesCustomizationEnabled: boolean;
}
export const PREBUILT_RULE_UPDATE_FLYOUT_ANCHOR = 'updatePrebuiltRulePreview';
@ -122,7 +120,8 @@ interface UpgradePrebuiltRulesTableContextProviderProps {
export const UpgradePrebuiltRulesTableContextProvider = ({
children,
}: UpgradePrebuiltRulesTableContextProviderProps) => {
const isPrebuiltRulesCustomizationEnabled = useIsPrebuiltRulesCustomizationEnabled();
const { isRulesCustomizationEnabled, customizationDisabledReason } =
usePrebuiltRulesCustomizationStatus();
const [loadingRules, setLoadingRules] = useState<RuleSignatureId[]>([]);
const [filterOptions, setFilterOptions] = useState<UpgradePrebuiltRulesTableFilterOptions>({
filter: '',
@ -247,13 +246,13 @@ export const UpgradePrebuiltRulesTableContextProvider = ({
const upgradeRules = useCallback(
async (ruleIds: RuleSignatureId[]) => {
if (isPrebuiltRulesCustomizationEnabled) {
if (isRulesCustomizationEnabled) {
await upgradeRulesToResolved(ruleIds);
} else {
await upgradeRulesToTarget(ruleIds);
}
},
[isPrebuiltRulesCustomizationEnabled, upgradeRulesToResolved, upgradeRulesToTarget]
[isRulesCustomizationEnabled, upgradeRulesToResolved, upgradeRulesToTarget]
);
const upgradeAllRules = useCallback(
@ -286,7 +285,7 @@ export const UpgradePrebuiltRulesTableContextProvider = ({
(ruleUpgradeState.hasUnresolvedConflicts && !hasRuleTypeChange)
}
onClick={() => {
if (hasRuleTypeChange) {
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 {
@ -306,6 +305,7 @@ export const UpgradePrebuiltRulesTableContextProvider = ({
loadingRules,
isRefetching,
isUpgradingSecurityPackages,
isRulesCustomizationEnabled,
upgradeRulesToTarget,
upgradeRulesToResolved,
]
@ -322,24 +322,25 @@ export const UpgradePrebuiltRulesTableContextProvider = ({
const hasCustomizations =
ruleUpgradeState.current_rule.rule_source.type === 'external' &&
ruleUpgradeState.current_rule.rule_source.is_customized;
const shouldShowRuleTypeChangeCallout =
hasRuleTypeChange && isPrebuiltRulesCustomizationEnabled;
let headerCallout = null;
if (
hasCustomizations &&
customizationDisabledReason === PrebuiltRulesCustomizationDisabledReason.License
) {
headerCallout = <CustomizationDisabledCallout />;
} else if (hasRuleTypeChange && isRulesCustomizationEnabled) {
headerCallout = <RuleTypeChangeCallout hasCustomizations={hasCustomizations} />;
}
let updateTabContent = (
<PerFieldRuleDiffTab
header={
shouldShowRuleTypeChangeCallout && (
<RuleTypeChangeCallout hasCustomizations={hasCustomizations} />
)
}
ruleDiff={ruleUpgradeState.diff}
/>
<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 (isPrebuiltRulesCustomizationEnabled && !hasRuleTypeChange) {
if (isRulesCustomizationEnabled && !hasRuleTypeChange) {
updateTabContent = (
<RuleUpgradeTab
ruleUpgradeState={ruleUpgradeState}
@ -377,7 +378,12 @@ export const UpgradePrebuiltRulesTableContextProvider = ({
return [updatesTab, jsonViewTab];
},
[rulesUpgradeState, setRuleFieldResolvedValue, isPrebuiltRulesCustomizationEnabled]
[
rulesUpgradeState,
customizationDisabledReason,
isRulesCustomizationEnabled,
setRuleFieldResolvedValue,
]
);
const filteredRules = useMemo(
() => filteredRuleUpgradeStates.map(({ target_rule: targetRule }) => targetRule),
@ -418,7 +424,6 @@ export const UpgradePrebuiltRulesTableContextProvider = ({
isUpgradingSecurityPackages,
loadingRules,
lastUpdated: dataUpdatedAt,
isPrebuiltRulesCustomizationEnabled,
},
actions,
}),
@ -435,7 +440,6 @@ export const UpgradePrebuiltRulesTableContextProvider = ({
loadingRules,
dataUpdatedAt,
actions,
isPrebuiltRulesCustomizationEnabled,
]
);

View file

@ -9,7 +9,7 @@ import { EuiFilterGroup, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { isEqual } from 'lodash/fp';
import React, { useCallback } from 'react';
import styled from 'styled-components';
import { useIsPrebuiltRulesCustomizationEnabled } from '../../../../rule_management/hooks/use_is_prebuilt_rules_customization_enabled';
import { usePrebuiltRulesCustomizationStatus } from '../../../../rule_management/logic/prebuilt_rules/use_prebuilt_rules_customization_status';
import type { RuleCustomizationEnum } from '../../../../rule_management/logic';
import * as i18n from './translations';
import { TagsFilterPopover } from '../rules_table_filters/tags_filter_popover';
@ -31,7 +31,7 @@ const UpgradePrebuiltRulesTableFiltersComponent = () => {
actions: { setFilterOptions },
} = useUpgradePrebuiltRulesTableContext();
const isPrebuiltRulesCustomizationEnabled = useIsPrebuiltRulesCustomizationEnabled();
const { isRulesCustomizationEnabled } = usePrebuiltRulesCustomizationStatus();
const { tags: selectedTags, ruleSource: selectedRuleSource = [] } = filterOptions;
@ -78,7 +78,7 @@ const UpgradePrebuiltRulesTableFiltersComponent = () => {
/>
<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize="s">
{isPrebuiltRulesCustomizationEnabled && (
{isRulesCustomizationEnabled && (
<EuiFilterGroup>
<RuleCustomizationFilterPopover
onSelectedRuleSourceChanged={handleSelectedRuleSource}

View file

@ -16,10 +16,7 @@ import {
} 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),
}));
import { TestProviders } from '../../../../../common/mock';
jest.mock('../../../../../common/hooks/use_app_toasts', () => ({
useAppToasts: jest.fn().mockReturnValue({
@ -37,6 +34,7 @@ describe('usePrebuiltRulesUpgradeState', () => {
},
} = renderHook(usePrebuiltRulesUpgradeState, {
initialProps: ruleUpgradeInfosMock,
wrapper: TestProviders,
});
expect(rulesUpgradeState).toEqual({
@ -50,6 +48,7 @@ describe('usePrebuiltRulesUpgradeState', () => {
const { result } = renderHook(usePrebuiltRulesUpgradeState, {
initialProps: ruleUpgradeInfosMock,
wrapper: TestProviders,
});
expect(result.current.rulesUpgradeState).toEqual({
@ -85,6 +84,7 @@ describe('usePrebuiltRulesUpgradeState', () => {
const { result } = renderHook(usePrebuiltRulesUpgradeState, {
initialProps: ruleUpgradeInfosMock,
wrapper: TestProviders,
});
expect(result.current.rulesUpgradeState).toEqual({
@ -122,6 +122,7 @@ describe('usePrebuiltRulesUpgradeState', () => {
const { result } = renderHook(usePrebuiltRulesUpgradeState, {
initialProps: ruleUpgradeInfosMock,
wrapper: TestProviders,
});
expect(result.current.rulesUpgradeState).toEqual({
@ -159,6 +160,7 @@ describe('usePrebuiltRulesUpgradeState', () => {
const { result } = renderHook(usePrebuiltRulesUpgradeState, {
initialProps: ruleUpgradeInfosMock,
wrapper: TestProviders,
});
expect(result.current.rulesUpgradeState).toEqual({
@ -197,6 +199,7 @@ describe('usePrebuiltRulesUpgradeState', () => {
const { result } = renderHook(usePrebuiltRulesUpgradeState, {
initialProps: ruleUpgradeInfosMock,
wrapper: TestProviders,
});
act(() => {
@ -254,6 +257,7 @@ describe('usePrebuiltRulesUpgradeState', () => {
it('invalidates resolved conflicts', () => {
const { result, rerender } = renderHook(usePrebuiltRulesUpgradeState, {
initialProps: createMock({ revision: 1 }),
wrapper: TestProviders,
});
act(() => {
@ -291,6 +295,7 @@ describe('usePrebuiltRulesUpgradeState', () => {
const { result, rerender } = renderHook(usePrebuiltRulesUpgradeState, {
initialProps: createMock({ revision: 1 }),
wrapper: TestProviders,
});
act(() => {
@ -345,6 +350,7 @@ describe('usePrebuiltRulesUpgradeState', () => {
const { result, rerender } = renderHook(usePrebuiltRulesUpgradeState, {
initialProps: createMock({ version: 1 }),
wrapper: TestProviders,
});
act(() => {
@ -382,6 +388,7 @@ describe('usePrebuiltRulesUpgradeState', () => {
const { result, rerender } = renderHook(usePrebuiltRulesUpgradeState, {
initialProps: createMock({ version: 1 }),
wrapper: TestProviders,
});
act(() => {

View file

@ -7,7 +7,7 @@
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 { usePrebuiltRulesCustomizationStatus } from '../../../../rule_management/logic/prebuilt_rules/use_prebuilt_rules_customization_status';
import type {
RulesUpgradeState,
FieldsUpgradeState,
@ -44,7 +44,7 @@ interface UseRulesUpgradeStateResult {
export function usePrebuiltRulesUpgradeState(
ruleUpgradeInfos: RuleUpgradeInfoForReview[]
): UseRulesUpgradeStateResult {
const isPrebuiltRulesCustomizationEnabled = useIsPrebuiltRulesCustomizationEnabled();
const { isRulesCustomizationEnabled } = usePrebuiltRulesCustomizationStatus();
const [rulesResolvedValues, setRulesResolvedValues] = useState<RulesResolvedConflicts>({});
const resetRuleResolvedValues = useCallback(
(ruleId: RuleSignatureId) => {
@ -138,14 +138,14 @@ export function usePrebuiltRulesUpgradeState(
state[ruleUpgradeInfo.rule_id] = {
...ruleUpgradeInfo,
fieldsUpgradeState,
hasUnresolvedConflicts: isPrebuiltRulesCustomizationEnabled
hasUnresolvedConflicts: isRulesCustomizationEnabled
? hasRuleTypeChange || hasFieldConflicts
: false,
};
}
return state;
}, [ruleUpgradeInfos, rulesResolvedValues, isPrebuiltRulesCustomizationEnabled]);
}, [ruleUpgradeInfos, rulesResolvedValues, isRulesCustomizationEnabled]);
return {
rulesUpgradeState,

View file

@ -30,6 +30,8 @@ import type { Rule } from '../../../../rule_management/logic';
import { getNormalizedSeverity } from '../helpers';
import type { UpgradePrebuiltRulesTableActions } from './upgrade_prebuilt_rules_table_context';
import { useUpgradePrebuiltRulesTableContext } from './upgrade_prebuilt_rules_table_context';
import { usePrebuiltRulesCustomizationStatus } from '../../../../rule_management/logic/prebuilt_rules/use_prebuilt_rules_customization_status';
import { PrebuiltRulesCustomizationDisabledReason } from '../../../../../../common/detection_engine/prebuilt_rules/prebuilt_rule_customization_status';
export type TableColumn = EuiBasicTableColumn<RuleUpgradeState>;
@ -186,25 +188,25 @@ export const useUpgradePrebuiltRulesTableColumns = (): TableColumn[] => {
const hasCRUDPermissions = hasUserCRUDPermission(canUserCRUD);
const [showRelatedIntegrations] = useUiSetting$<boolean>(SHOW_RELATED_INTEGRATIONS_SETTING);
const {
state: {
loadingRules,
isRefetching,
isUpgradingSecurityPackages,
isPrebuiltRulesCustomizationEnabled,
},
state: { loadingRules, isRefetching, isUpgradingSecurityPackages },
actions: { upgradeRules },
} = useUpgradePrebuiltRulesTableContext();
const isDisabled = isRefetching || isUpgradingSecurityPackages;
// TODO: move this change to the `INTEGRATIONS_COLUMN` when `prebuiltRulesCustomizationEnabled` feature flag is removed
if (isPrebuiltRulesCustomizationEnabled) {
const { isRulesCustomizationEnabled, customizationDisabledReason } =
usePrebuiltRulesCustomizationStatus();
const shouldShowModifiedColumn =
isRulesCustomizationEnabled ||
customizationDisabledReason === PrebuiltRulesCustomizationDisabledReason.License;
if (shouldShowModifiedColumn) {
INTEGRATIONS_COLUMN.width = '70px';
}
return useMemo(
() => [
RULE_NAME_COLUMN,
...(isPrebuiltRulesCustomizationEnabled ? [MODIFIED_COLUMN] : []),
...(shouldShowModifiedColumn ? [MODIFIED_COLUMN] : []),
...(showRelatedIntegrations ? [INTEGRATIONS_COLUMN] : []),
TAGS_COLUMN,
{
@ -234,18 +236,19 @@ export const useUpgradePrebuiltRulesTableColumns = (): TableColumn[] => {
upgradeRules,
loadingRules,
isDisabled,
isPrebuiltRulesCustomizationEnabled
isRulesCustomizationEnabled
),
]
: []),
],
[
shouldShowModifiedColumn,
showRelatedIntegrations,
hasCRUDPermissions,
upgradeRules,
loadingRules,
isDisabled,
showRelatedIntegrations,
upgradeRules,
isPrebuiltRulesCustomizationEnabled,
isRulesCustomizationEnabled,
]
);
};

View file

@ -46,7 +46,8 @@ import { useRulesTableActions } from './use_rules_table_actions';
import { MlRuleWarningPopover } from '../ml_rule_warning_popover/ml_rule_warning_popover';
import { getMachineLearningJobId } from '../../../../detections/pages/detection_engine/rules/helpers';
import type { TimeRange } from '../../../rule_gaps/types';
import { useIsPrebuiltRulesCustomizationEnabled } from '../../../rule_management/hooks/use_is_prebuilt_rules_customization_enabled';
import { usePrebuiltRulesCustomizationStatus } from '../../../rule_management/logic/prebuilt_rules/use_prebuilt_rules_customization_status';
import { PrebuiltRulesCustomizationDisabledReason } from '../../../../../common/detection_engine/prebuilt_rules/prebuilt_rule_customization_status';
export type TableColumn = EuiBasicTableColumn<Rule> | EuiTableActionsColumnType<Rule>;
@ -295,7 +296,12 @@ export const useRulesColumns = ({
});
const ruleNameColumn = useRuleNameColumn();
const [showRelatedIntegrations] = useUiSetting$<boolean>(SHOW_RELATED_INTEGRATIONS_SETTING);
const isPrebuiltRulesCustomizationEnabled = useIsPrebuiltRulesCustomizationEnabled();
const { isRulesCustomizationEnabled, customizationDisabledReason } =
usePrebuiltRulesCustomizationStatus();
const shouldShowModifiedColumn =
isRulesCustomizationEnabled ||
customizationDisabledReason === PrebuiltRulesCustomizationDisabledReason.License;
const enabledColumn = useEnabledColumn({
hasCRUDPermissions,
isLoadingJobs,
@ -310,15 +316,14 @@ export const useRulesColumns = ({
});
const snoozeColumn = useRuleSnoozeColumn();
// TODO: move this change to the `INTEGRATIONS_COLUMN` when `prebuiltRulesCustomizationEnabled` feature flag is removed
if (isPrebuiltRulesCustomizationEnabled) {
if (shouldShowModifiedColumn) {
INTEGRATIONS_COLUMN.width = '70px';
}
return useMemo(
() => [
ruleNameColumn,
...(isPrebuiltRulesCustomizationEnabled ? [MODIFIED_COLUMN] : []),
...(shouldShowModifiedColumn ? [MODIFIED_COLUMN] : []),
...(showRelatedIntegrations ? [INTEGRATIONS_COLUMN] : []),
TAGS_COLUMN,
{
@ -390,7 +395,7 @@ export const useRulesColumns = ({
],
[
ruleNameColumn,
isPrebuiltRulesCustomizationEnabled,
shouldShowModifiedColumn,
showRelatedIntegrations,
executionStatusColumn,
snoozeColumn,
@ -418,7 +423,12 @@ export const useMonitoringColumns = ({
});
const ruleNameColumn = useRuleNameColumn();
const [showRelatedIntegrations] = useUiSetting$<boolean>(SHOW_RELATED_INTEGRATIONS_SETTING);
const isPrebuiltRulesCustomizationEnabled = useIsPrebuiltRulesCustomizationEnabled();
const { isRulesCustomizationEnabled, customizationDisabledReason } =
usePrebuiltRulesCustomizationStatus();
const shouldShowModifiedColumn =
isRulesCustomizationEnabled ||
customizationDisabledReason === PrebuiltRulesCustomizationDisabledReason.License;
const enabledColumn = useEnabledColumn({
hasCRUDPermissions,
isLoadingJobs,
@ -432,8 +442,7 @@ export const useMonitoringColumns = ({
mlJobs,
});
// TODO: move this change to the `INTEGRATIONS_COLUMN` when `prebuiltRulesCustomizationEnabled` feature flag is removed
if (isPrebuiltRulesCustomizationEnabled) {
if (shouldShowModifiedColumn) {
INTEGRATIONS_COLUMN.width = '70px';
}
@ -443,7 +452,7 @@ export const useMonitoringColumns = ({
...ruleNameColumn,
width: '28%',
},
...(isPrebuiltRulesCustomizationEnabled ? [MODIFIED_COLUMN] : []),
...(shouldShowModifiedColumn ? [MODIFIED_COLUMN] : []),
...(showRelatedIntegrations ? [INTEGRATIONS_COLUMN] : []),
TAGS_COLUMN,
{
@ -562,8 +571,8 @@ export const useMonitoringColumns = ({
enabledColumn,
executionStatusColumn,
hasCRUDPermissions,
isPrebuiltRulesCustomizationEnabled,
ruleNameColumn,
shouldShowModifiedColumn,
showRelatedIntegrations,
]
);

View file

@ -25,7 +25,7 @@ import { useDownloadExportedRules } from '../../../rule_management/logic/bulk_ac
import { useHasActionsPrivileges } from './use_has_actions_privileges';
import type { TimeRange } from '../../../rule_gaps/types';
import { useScheduleRuleRun } from '../../../rule_gaps/logic/use_schedule_rule_run';
import { useIsPrebuiltRulesCustomizationEnabled } from '../../../rule_management/hooks/use_is_prebuilt_rules_customization_enabled';
import { usePrebuiltRulesCustomizationStatus } from '../../../rule_management/logic/prebuilt_rules/use_prebuilt_rules_customization_status';
import { ManualRuleRunEventTypes } from '../../../../common/lib/telemetry';
export const useRulesTableActions = ({
@ -47,7 +47,7 @@ export const useRulesTableActions = ({
const { bulkExport } = useBulkExport();
const downloadExportedRules = useDownloadExportedRules();
const { scheduleRuleRun } = useScheduleRuleRun();
const isPrebuiltRulesCustomizationEnabled = useIsPrebuiltRulesCustomizationEnabled();
const { isRulesCustomizationEnabled } = usePrebuiltRulesCustomizationStatus();
return [
{
@ -118,7 +118,7 @@ export const useRulesTableActions = ({
await downloadExportedRules(response);
}
},
enabled: (rule: Rule) => isPrebuiltRulesCustomizationEnabled || !rule.immutable,
enabled: (rule: Rule) => isRulesCustomizationEnabled || !rule.immutable,
},
{
type: 'icon',

View file

@ -16,7 +16,7 @@ import {
EuiSpacer,
} from '@elastic/eui';
import { useIsPrebuiltRulesCustomizationEnabled } from '../../../../../detection_engine/rule_management/hooks/use_is_prebuilt_rules_customization_enabled';
import { usePrebuiltRulesCustomizationStatus } from '../../../../../detection_engine/rule_management/logic/prebuilt_rules/use_prebuilt_rules_customization_status';
import type { RelatedIntegrationArray } from '../../../../../../common/api/detection_engine/model/rule_schema';
import { IntegrationDescription } from '../integrations_description';
import { useRelatedIntegrations } from '../use_related_integrations';
@ -55,7 +55,7 @@ const IntegrationListItem = styled('li')`
const IntegrationsPopoverComponent = ({ relatedIntegrations }: IntegrationsPopoverProps) => {
const [isPopoverOpen, setPopoverOpen] = useState(false);
const { integrations, isLoaded } = useRelatedIntegrations(relatedIntegrations);
const isPrebuiltRulesCustomizationEnabled = useIsPrebuiltRulesCustomizationEnabled();
const { isRulesCustomizationEnabled } = usePrebuiltRulesCustomizationStatus();
const enabledIntegrations = useMemo(() => {
return integrations.filter(
@ -67,13 +67,13 @@ const IntegrationsPopoverComponent = ({ relatedIntegrations }: IntegrationsPopov
const numIntegrationsEnabled = enabledIntegrations.length;
const badgeTitle = useMemo(() => {
if (isPrebuiltRulesCustomizationEnabled) {
if (isRulesCustomizationEnabled) {
return isLoaded ? `${numIntegrationsEnabled}/${numIntegrations}` : `${numIntegrations}`;
}
return isLoaded
? `${numIntegrationsEnabled}/${numIntegrations} ${i18n.INTEGRATIONS_BADGE}`
: `${numIntegrations} ${i18n.INTEGRATIONS_BADGE}`;
}, [isLoaded, isPrebuiltRulesCustomizationEnabled, numIntegrations, numIntegrationsEnabled]);
}, [isLoaded, isRulesCustomizationEnabled, numIntegrations, numIntegrationsEnabled]);
return (
<IntegrationsPopoverWrapper

View file

@ -5,18 +5,14 @@
* 2.0.
*/
import { render, fireEvent, waitFor } from '@testing-library/react';
import { fireEvent, render, waitFor } from '@testing-library/react';
import React from 'react';
import { RuleActionsOverflow } from '.';
import { ManualRuleRunEventTypes } from '../../../../common/lib/telemetry';
import { TestProviders } from '../../../../common/mock';
import { useBulkExport } from '../../../../detection_engine/rule_management/logic/bulk_actions/use_bulk_export';
import { useExecuteBulkAction } from '../../../../detection_engine/rule_management/logic/bulk_actions/use_execute_bulk_action';
import { useScheduleRuleRun } from '../../../../detection_engine/rule_gaps/logic/use_schedule_rule_run';
import { RuleActionsOverflow } from '.';
import { mockRule } from '../../../../detection_engine/rule_management_ui/components/rules_table/__mocks__/mock';
import { TestProviders } from '../../../../common/mock';
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
import { ManualRuleRunEventTypes } from '../../../../common/lib/telemetry';
const showBulkDuplicateExceptionsConfirmation = () => Promise.resolve(null);
const showManualRuleRunConfirmation = () => Promise.resolve(null);
@ -26,48 +22,34 @@ jest.mock(
'../../../../detection_engine/rule_management/logic/bulk_actions/use_execute_bulk_action'
);
jest.mock('../../../../detection_engine/rule_management/logic/bulk_actions/use_bulk_export');
jest.mock('../../../../detection_engine/rule_gaps/logic/use_schedule_rule_run');
jest.mock('../../../../common/lib/apm/use_start_transaction');
jest.mock('../../../../common/hooks/use_app_toasts');
const mockReportEvent = jest.fn();
jest.mock('../../../../common/lib/kibana', () => {
const actual = jest.requireActual('../../../../common/lib/kibana');
return {
...actual,
useKibana: jest.fn().mockReturnValue({
services: {
telemetry: {
reportEvent: (eventType: ManualRuleRunEventTypes, params: { type: 'single' | 'bulk' }) =>
mockReportEvent(eventType, params),
useKibana: jest.fn().mockImplementation(() => {
const useKibana = actual.useKibana();
return {
...useKibana,
services: {
...useKibana.services,
telemetry: {
reportEvent: (
eventType: ManualRuleRunEventTypes,
params: { type: 'single' | 'bulk' }
) => mockReportEvent(eventType, params),
},
},
application: {
navigateToApp: jest.fn(),
},
},
};
}),
};
});
const useExecuteBulkActionMock = useExecuteBulkAction as jest.Mock;
const useBulkExportMock = useBulkExport as jest.Mock;
const useScheduleRuleRunMock = useScheduleRuleRun as jest.Mock;
const useIsExperimentalFeatureEnabledMock = useIsExperimentalFeatureEnabled as jest.Mock;
describe('RuleActionsOverflow', () => {
const scheduleRuleRun = jest.fn();
afterEach(() => {
jest.clearAllMocks();
});
afterAll(() => {
jest.clearAllMocks();
});
beforeEach(() => {
useIsExperimentalFeatureEnabledMock.mockReturnValue(true);
useScheduleRuleRunMock.mockReturnValue({ scheduleRuleRun });
});
describe('rules details menu panel', () => {
test('menu items rendered when a rule is passed to the component', () => {
const { getByTestId } = render(

View file

@ -14,7 +14,7 @@ import {
} from '@elastic/eui';
import React, { useCallback, useMemo } from 'react';
import styled from 'styled-components';
import { useIsPrebuiltRulesCustomizationEnabled } from '../../../../detection_engine/rule_management/hooks/use_is_prebuilt_rules_customization_enabled';
import { usePrebuiltRulesCustomizationStatus } from '../../../../detection_engine/rule_management/logic/prebuilt_rules/use_prebuilt_rules_customization_status';
import { useScheduleRuleRun } from '../../../../detection_engine/rule_gaps/logic/use_schedule_rule_run';
import type { TimeRange } from '../../../../detection_engine/rule_gaps/types';
import { APP_UI_ID, SecurityPageName } from '../../../../../common/constants';
@ -73,7 +73,7 @@ const RuleActionsOverflowComponent = ({
application: { navigateToApp },
telemetry,
} = useKibana().services;
const isPrebuiltRulesCustomizationEnabled = useIsPrebuiltRulesCustomizationEnabled();
const { isRulesCustomizationEnabled } = usePrebuiltRulesCustomizationStatus();
const { startTransaction } = useStartTransaction();
const { executeBulkAction } = useExecuteBulkAction({ suppressSuccessToast: true });
const { bulkExport } = useBulkExport();
@ -140,8 +140,7 @@ const RuleActionsOverflowComponent = ({
key={i18nActions.EXPORT_RULE}
icon="exportAction"
disabled={
!userHasPermissions ||
(isPrebuiltRulesCustomizationEnabled === false && rule.immutable)
!userHasPermissions || (isRulesCustomizationEnabled === false && rule.immutable)
}
data-test-subj="rules-details-export-rule"
onClick={async () => {
@ -211,7 +210,7 @@ const RuleActionsOverflowComponent = ({
rule,
canDuplicateRuleWithActions,
userHasPermissions,
isPrebuiltRulesCustomizationEnabled,
isRulesCustomizationEnabled,
startTransaction,
closePopover,
showBulkDuplicateExceptionsConfirmation,

View file

@ -8,7 +8,6 @@ import { pickBy } from 'lodash';
import { withSecuritySpanSync } from '../../../../../utils/with_security_span';
import type { PromisePoolError } from '../../../../../utils/promise_pool';
import {
PickVersionValuesEnum,
type PerformRuleUpgradeRequestBody,
type PickVersionValues,
type AllFieldsDiff,
@ -26,7 +25,7 @@ import { getValueForField } from './get_value_for_field';
interface CreateModifiedPrebuiltRuleAssetsProps {
upgradeableRules: RuleTriad[];
requestBody: PerformRuleUpgradeRequestBody;
prebuiltRulesCustomizationEnabled: boolean;
defaultPickVersion: PickVersionValues;
}
interface ProcessedRules {
@ -37,12 +36,9 @@ interface ProcessedRules {
export const createModifiedPrebuiltRuleAssets = ({
upgradeableRules,
requestBody,
prebuiltRulesCustomizationEnabled,
defaultPickVersion,
}: CreateModifiedPrebuiltRuleAssetsProps) => {
return withSecuritySpanSync(createModifiedPrebuiltRuleAssets.name, () => {
const defaultPickVersion = prebuiltRulesCustomizationEnabled
? PickVersionValuesEnum.MERGED
: PickVersionValuesEnum.TARGET;
const { pick_version: globalPickVersion = defaultPickVersion, mode } = requestBody;
const { modifiedPrebuiltRuleAssets, processingErrors } =

View file

@ -11,6 +11,7 @@ import {
PERFORM_RULE_UPGRADE_URL,
PerformRuleUpgradeRequestBody,
ModeEnum,
PickVersionValuesEnum,
} from '../../../../../../common/api/detection_engine/prebuilt_rules';
import type { PerformRuleUpgradeResponseBody } from '../../../../../../common/api/detection_engine/prebuilt_rules';
import type { SecuritySolutionPluginRouter } from '../../../../../types';
@ -25,12 +26,9 @@ import { PREBUILT_RULES_OPERATION_SOCKET_TIMEOUT_MS } from '../../constants';
import { getUpgradeableRules } from './get_upgradeable_rules';
import { createModifiedPrebuiltRuleAssets } from './create_upgradeable_rules_payload';
import { getRuleGroups } from '../../model/rule_groups/get_rule_groups';
import type { ConfigType } from '../../../../../config';
import { validatePerformRuleUpgradeRequest } from './validate_perform_rule_upgrade_request';
export const performRuleUpgradeRoute = (
router: SecuritySolutionPluginRouter,
config: ConfigType
) => {
export const performRuleUpgradeRoute = (router: SecuritySolutionPluginRouter) => {
router.versioned
.post({
access: 'internal',
@ -59,13 +57,24 @@ export const performRuleUpgradeRoute = (
const siemResponse = buildSiemResponse(response);
try {
const ctx = await context.resolve(['core', 'alerting', 'securitySolution']);
const ctx = await context.resolve(['core', 'alerting', 'securitySolution', 'licensing']);
const soClient = ctx.core.savedObjects.client;
const rulesClient = await ctx.alerting.getRulesClient();
const detectionRulesClient = ctx.securitySolution.getDetectionRulesClient();
const ruleAssetsClient = createPrebuiltRuleAssetsClient(soClient);
const ruleObjectsClient = createPrebuiltRuleObjectsClient(rulesClient);
const { isRulesCustomizationEnabled } = detectionRulesClient.getRuleCustomizationStatus();
const defaultPickVersion = isRulesCustomizationEnabled
? PickVersionValuesEnum.MERGED
: PickVersionValuesEnum.TARGET;
validatePerformRuleUpgradeRequest({
isRulesCustomizationEnabled,
payload: request.body,
defaultPickVersion,
});
const { mode } = request.body;
const versionSpecifiers = mode === ModeEnum.ALL_RULES ? undefined : request.body.rules;
@ -83,12 +92,11 @@ export const performRuleUpgradeRoute = (
mode,
});
const { prebuiltRulesCustomizationEnabled } = config.experimentalFeatures;
const { modifiedPrebuiltRuleAssets, processingErrors } = createModifiedPrebuiltRuleAssets(
{
upgradeableRules,
requestBody: request.body,
prebuiltRulesCustomizationEnabled,
defaultPickVersion,
}
);

View file

@ -0,0 +1,51 @@
/*
* 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 { BadRequestError } from '@kbn/securitysolution-es-utils';
import type { PerformRuleUpgradeRequestBody } from '../../../../../../common/api/detection_engine/prebuilt_rules';
import { ModeEnum } from '../../../../../../common/api/detection_engine/prebuilt_rules';
export function validatePerformRuleUpgradeRequest({
isRulesCustomizationEnabled,
payload,
defaultPickVersion,
}: {
isRulesCustomizationEnabled: boolean;
payload: PerformRuleUpgradeRequestBody;
defaultPickVersion: string;
}) {
if (isRulesCustomizationEnabled) {
// Rule customization is enabled; no additional validation is needed
return;
}
// Rule can be upgraded to the default (TARGET) version only
if (payload.pick_version && payload.pick_version !== defaultPickVersion) {
throw new BadRequestError(
`Only the '${defaultPickVersion}' version can be selected for a rule update; received: '${payload.pick_version}'`
);
}
// If specific rules are provided, ensure that there are no customizations and
// that the default pick version is selected
if (payload.mode === ModeEnum.SPECIFIC_RULES) {
payload.rules.forEach((rule) => {
if (rule.pick_version && rule.pick_version !== defaultPickVersion) {
throw new BadRequestError(
`Only the '${defaultPickVersion}' version can be selected for a rule update; received: '${rule.pick_version}'`
);
}
if (rule.fields && Object.keys(rule.fields).length > 0) {
throw new BadRequestError(
`Rule field customization is not allowed. Received fields: ${Object.keys(
rule.fields
).join(', ')}`
);
}
});
}
}

View file

@ -6,7 +6,6 @@
*/
import type { SecuritySolutionPluginRouter } from '../../../../types';
import type { ConfigType } from '../../../../config';
import { getPrebuiltRulesAndTimelinesStatusRoute } from './get_prebuilt_rules_and_timelines_status/get_prebuilt_rules_and_timelines_status_route';
import { getPrebuiltRulesStatusRoute } from './get_prebuilt_rules_status/get_prebuilt_rules_status_route';
import { installPrebuiltRulesAndTimelinesRoute } from './install_prebuilt_rules_and_timelines/install_prebuilt_rules_and_timelines_route';
@ -16,10 +15,7 @@ import { performRuleInstallationRoute } from './perform_rule_installation/perfor
import { performRuleUpgradeRoute } from './perform_rule_upgrade/perform_rule_upgrade_route';
import { bootstrapPrebuiltRulesRoute } from './bootstrap_prebuilt_rules/bootstrap_prebuilt_rules';
export const registerPrebuiltRulesRoutes = (
router: SecuritySolutionPluginRouter,
config: ConfigType
) => {
export const registerPrebuiltRulesRoutes = (router: SecuritySolutionPluginRouter) => {
// Legacy endpoints that we're going to deprecate
getPrebuiltRulesAndTimelinesStatusRoute(router);
installPrebuiltRulesAndTimelinesRoute(router);
@ -27,7 +23,7 @@ export const registerPrebuiltRulesRoutes = (
// New endpoints for the rule upgrade and installation workflows
getPrebuiltRulesStatusRoute(router);
performRuleInstallationRoute(router);
performRuleUpgradeRoute(router, config);
performRuleUpgradeRoute(router);
reviewRuleInstallationRoute(router);
reviewRuleUpgradeRoute(router);
bootstrapPrebuiltRulesRoute(router);

View file

@ -37,7 +37,7 @@ export const registerRuleManagementRoutes = (
deleteRuleRoute(router);
// Rules bulk actions
performBulkActionRoute(router, config, ml, logger);
performBulkActionRoute(router, ml);
// Rules export/import
exportRulesRoute(router, config, logger);

View file

@ -19,9 +19,9 @@ import type {
import type {
BulkActionType,
BulkEditActionResponse,
BulkActionsDryRunErrCode,
} from '../../../../../../../common/api/detection_engine/rule_management';
import { BulkActionTypeEnum } from '../../../../../../../common/api/detection_engine/rule_management';
import type { BulkActionsDryRunErrCode } from '../../../../../../../common/constants';
import type { PromisePoolError } from '../../../../../../utils/promise_pool';
import type { RuleAlertType } from '../../../../rule_schema';
import type { DryRunError } from '../../../logic/bulk_actions/dry_run';

View file

@ -8,7 +8,6 @@
import type { BulkOperationError, RulesClient } from '@kbn/alerting-plugin/server';
import type { ScheduleBackfillParams } from '@kbn/alerting-plugin/server/application/backfill/methods/schedule/types';
import type { BulkManualRuleRun } from '../../../../../../../common/api/detection_engine';
import type { ExperimentalFeatures } from '../../../../../../../common';
import type { PromisePoolError } from '../../../../../../utils/promise_pool';
import type { MlAuthz } from '../../../../../machine_learning/authz';
import type { RuleAlertType } from '../../../../rule_schema';
@ -20,7 +19,6 @@ interface BulkScheduleBackfillArgs {
rulesClient: RulesClient;
mlAuthz: MlAuthz;
runPayload: BulkManualRuleRun['run'];
experimentalFeatures: ExperimentalFeatures;
}
interface BulkScheduleBackfillOutcome {
@ -34,7 +32,6 @@ export const bulkScheduleBackfill = async ({
rulesClient,
mlAuthz,
runPayload,
experimentalFeatures,
}: BulkScheduleBackfillArgs): Promise<BulkScheduleBackfillOutcome> => {
const errors: Array<PromisePoolError<RuleAlertType, Error> | BulkOperationError> = [];
@ -46,7 +43,6 @@ export const bulkScheduleBackfill = async ({
await validateBulkScheduleBackfill({
mlAuthz,
rule,
experimentalFeatures,
});
validatedRules.push(rule);
} catch (error) {

View file

@ -5,10 +5,7 @@
* 2.0.
*/
import {
DETECTION_ENGINE_RULES_BULK_ACTION,
BulkActionsDryRunErrCode,
} from '../../../../../../../common/constants';
import { DETECTION_ENGINE_RULES_BULK_ACTION } from '../../../../../../../common/constants';
import { mlServicesMock } from '../../../../../machine_learning/mocks';
import { buildMlAuthz } from '../../../../../machine_learning/authz';
import {
@ -18,37 +15,28 @@ import {
getFindResultWithSingleHit,
getFindResultWithMultiHits,
} from '../../../../routes/__mocks__/request_responses';
import {
createMockConfig,
requestContextMock,
serverMock,
requestMock,
} from '../../../../routes/__mocks__';
import { requestContextMock, serverMock, requestMock } from '../../../../routes/__mocks__';
import { performBulkActionRoute } from './route';
import {
getPerformBulkActionEditSchemaMock,
getBulkDisableRuleActionSchemaMock,
} from '../../../../../../../common/api/detection_engine/rule_management/mocks';
import { loggingSystemMock } from '@kbn/core/server/mocks';
import { readRules } from '../../../logic/detection_rules_client/read_rules';
import { BulkActionsDryRunErrCodeEnum } from '../../../../../../../common/api/detection_engine';
jest.mock('../../../../../machine_learning/authz');
jest.mock('../../../logic/detection_rules_client/read_rules', () => ({ readRules: jest.fn() }));
describe('Perform bulk action route', () => {
const readRulesMock = readRules as jest.Mock;
let config: ReturnType<typeof createMockConfig>;
let server: ReturnType<typeof serverMock.create>;
let { clients, context } = requestContextMock.createTools();
let ml: ReturnType<typeof mlServicesMock.createSetupContract>;
let logger: ReturnType<typeof loggingSystemMock.createLogger>;
const mockRule = getFindResultWithSingleHit().data[0];
beforeEach(() => {
server = serverMock.create();
logger = loggingSystemMock.createLogger();
({ clients, context } = requestContextMock.createTools());
config = createMockConfig();
ml = mlServicesMock.createSetupContract();
clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit());
@ -57,7 +45,7 @@ describe('Perform bulk action route', () => {
errors: [],
total: 1,
});
performBulkActionRoute(server.router, config, ml, logger);
performBulkActionRoute(server.router, ml);
});
describe('status codes', () => {
@ -184,7 +172,7 @@ describe('Perform bulk action route', () => {
errors: [
{
message: 'mocked validation message',
err_code: BulkActionsDryRunErrCode.MACHINE_LEARNING_AUTH,
err_code: BulkActionsDryRunErrCodeEnum.MACHINE_LEARNING_AUTH,
status_code: 403,
rules: [
{
@ -224,7 +212,7 @@ describe('Perform bulk action route', () => {
errors: [
{
message: 'mocked validation message',
err_code: BulkActionsDryRunErrCode.MACHINE_LEARNING_AUTH,
err_code: BulkActionsDryRunErrCodeEnum.MACHINE_LEARNING_AUTH,
status_code: 403,
rules: [
{

View file

@ -5,12 +5,11 @@
* 2.0.
*/
import type { IKibanaResponse, Logger } from '@kbn/core/server';
import type { IKibanaResponse } from '@kbn/core/server';
import { AbortError } from '@kbn/kibana-utils-plugin/common';
import { transformError } from '@kbn/securitysolution-es-utils';
import { buildRouteValidationWithZod } from '@kbn/zod-helpers';
import type { BulkActionSkipResult } from '@kbn/alerting-plugin/common';
import type { ConfigType } from '../../../../../../config';
import type { PerformRulesBulkActionResponse } from '../../../../../../../common/api/detection_engine/rule_management';
import {
BulkActionTypeEnum,
@ -53,9 +52,7 @@ const MAX_ROUTE_CONCURRENCY = 5;
export const performBulkActionRoute = (
router: SecuritySolutionPluginRouter,
config: ConfigType,
ml: SetupPlugins['ml'],
logger: Logger
ml: SetupPlugins['ml']
) => {
router.versioned
.post({
@ -287,7 +284,7 @@ export const performBulkActionRoute = (
exporter,
request,
actionsClient,
config.experimentalFeatures.prebuiltRulesCustomizationEnabled
detectionRulesClient.getRuleCustomizationStatus().isRulesCustomizationEnabled
);
const responseBody = `${exported.rulesNdjson}${exported.exceptionLists}${exported.actionConnectors}${exported.exportDetails}`;
@ -301,10 +298,9 @@ export const performBulkActionRoute = (
});
}
// will be processed only when isDryRun === true
// during dry run only validation is getting performed and rule is not saved in ES
case BulkActionTypeEnum.edit: {
if (isDryRun) {
// during dry run only validation is getting performed and rule is not saved in ES
const bulkActionOutcome = await initPromisePool({
concurrency: MAX_RULES_TO_UPDATE_IN_PARALLEL,
items: rules,
@ -313,7 +309,7 @@ export const performBulkActionRoute = (
mlAuthz,
rule,
edit: body.edit,
experimentalFeatures: config.experimentalFeatures,
ruleCustomizationStatus: detectionRulesClient.getRuleCustomizationStatus(),
});
return rule;
@ -332,7 +328,7 @@ export const performBulkActionRoute = (
rules,
actions: body.edit,
mlAuthz,
experimentalFeatures: config.experimentalFeatures,
ruleCustomizationStatus: detectionRulesClient.getRuleCustomizationStatus(),
});
updated = bulkEditResult.rules;
skipped = bulkEditResult.skipped;
@ -348,7 +344,6 @@ export const performBulkActionRoute = (
rulesClient,
mlAuthz,
runPayload: body.run,
experimentalFeatures: config.experimentalFeatures,
});
errors.push(...bulkActionErrors);
updated = backfilled.filter((rule): rule is RuleAlertType => rule !== null);

View file

@ -56,15 +56,24 @@ export const exportRulesRoute = (
},
async (context, request, response) => {
const siemResponse = buildSiemResponse(response);
const rulesClient = await (await context.alerting).getRulesClient();
const exceptionsClient = (await context.lists)?.getExceptionListClient();
const actionsClient = (await context.actions)?.getActionsClient();
const ctx = await context.resolve([
'core',
'securitySolution',
'alerting',
'actions',
'lists',
]);
const { getExporter, getClient } = (await context.core).savedObjects;
const rulesClient = await ctx.alerting.getRulesClient();
const exceptionsClient = ctx.lists?.getExceptionListClient();
const actionsClient = ctx.actions.getActionsClient();
const detectionRulesClient = ctx.securitySolution.getDetectionRulesClient();
const { getExporter, getClient } = ctx.core.savedObjects;
const client = getClient({ includedHiddenTypes: ['action'] });
const actionsExporter = getExporter(client);
const { prebuiltRulesCustomizationEnabled } = config.experimentalFeatures;
const { isRulesCustomizationEnabled } = detectionRulesClient.getRuleCustomizationStatus();
try {
const exportSizeLimit = config.maxRuleImportExportSize;
@ -76,7 +85,7 @@ export const exportRulesRoute = (
} else {
let rulesCount = 0;
if (prebuiltRulesCustomizationEnabled) {
if (isRulesCustomizationEnabled) {
rulesCount = await getRulesCount({
rulesClient,
filter: '',
@ -103,7 +112,7 @@ export const exportRulesRoute = (
actionsExporter,
request,
actionsClient,
prebuiltRulesCustomizationEnabled
isRulesCustomizationEnabled
)
: await getExportAll(
rulesClient,
@ -111,7 +120,7 @@ export const exportRulesRoute = (
actionsExporter,
request,
actionsClient,
prebuiltRulesCustomizationEnabled
isRulesCustomizationEnabled
);
const responseBody = request.query.exclude_export_details

View file

@ -57,6 +57,9 @@ describe('Import rules route', () => {
clients.rulesClient.update.mockResolvedValue(getRuleMock(getQueryRuleParams()));
clients.detectionRulesClient.createCustomRule.mockResolvedValue(getRulesSchemaMock());
clients.detectionRulesClient.importRule.mockResolvedValue(getRulesSchemaMock());
clients.detectionRulesClient.getRuleCustomizationStatus.mockReturnValue({
isRulesCustomizationEnabled: false,
});
clients.actionsClient.getAll.mockResolvedValue([]);
context.core.elasticsearch.client.asCurrentUser.search.mockResolvedValue(
elasticsearchClientMock.createSuccessTransportRequestPromise(getBasicEmptySearchResponse())
@ -145,10 +148,9 @@ describe('Import rules route', () => {
describe('with prebuilt rules customization enabled', () => {
beforeEach(() => {
clients.detectionRulesClient.importRules.mockResolvedValueOnce([]);
server = serverMock.create(); // old server already registered this route
config = configMock.withExperimentalFeature(config, 'prebuiltRulesCustomizationEnabled');
importRulesRoute(server.router, config);
clients.detectionRulesClient.getRuleCustomizationStatus.mockReturnValue({
isRulesCustomizationEnabled: true,
});
});
test('returns 500 if importing fails', async () => {

View file

@ -85,8 +85,8 @@ export const importRulesRoute = (router: SecuritySolutionPluginRouter, config: C
'licensing',
]);
const { prebuiltRulesCustomizationEnabled } = config.experimentalFeatures;
const detectionRulesClient = ctx.securitySolution.getDetectionRulesClient();
const { isRulesCustomizationEnabled } = detectionRulesClient.getRuleCustomizationStatus();
const actionsClient = ctx.actions.getActionsClient();
const actionSOClient = ctx.core.savedObjects.getClient({
includedHiddenTypes: ['action'],
@ -158,6 +158,7 @@ export const importRulesRoute = (router: SecuritySolutionPluginRouter, config: C
config,
context: ctx.securitySolution,
prebuiltRuleAssetsClient: createPrebuiltRuleAssetsClient(savedObjectsClient),
ruleCustomizationStatus: detectionRulesClient.getRuleCustomizationStatus(),
});
const [parsedRules, parsedRuleErrors] = partition(isRuleToImport, parsedRuleStream);
@ -165,7 +166,7 @@ export const importRulesRoute = (router: SecuritySolutionPluginRouter, config: C
let importRuleResponse: ImportRuleResponse[] = [];
if (prebuiltRulesCustomizationEnabled) {
if (isRulesCustomizationEnabled) {
importRuleResponse = await importRules({
ruleChunks,
overwriteRules: request.query.overwrite,

View file

@ -8,7 +8,6 @@
import type { ActionsClient } from '@kbn/actions-plugin/server';
import type { RulesClient } from '@kbn/alerting-plugin/server';
import type { ExperimentalFeatures } from '../../../../../../common';
import type { BulkActionEditPayload } from '../../../../../../common/api/detection_engine/rule_management';
import type { MlAuthz } from '../../../../machine_learning/authz';
@ -22,6 +21,7 @@ import { bulkEditActionToRulesClientOperation } from './action_to_rules_client_o
import { ruleParamsModifier } from './rule_params_modifier';
import { splitBulkEditActions } from './split_bulk_edit_actions';
import { validateBulkEditRule } from './validations';
import type { PrebuiltRulesCustomizationStatus } from '../../../../../../common/detection_engine/prebuilt_rules/prebuilt_rule_customization_status';
export interface BulkEditRulesArguments {
actionsClient: ActionsClient;
@ -30,7 +30,7 @@ export interface BulkEditRulesArguments {
actions: BulkActionEditPayload[];
rules: RuleAlertType[];
mlAuthz: MlAuthz;
experimentalFeatures: ExperimentalFeatures;
ruleCustomizationStatus: PrebuiltRulesCustomizationStatus;
}
/**
@ -47,7 +47,7 @@ export const bulkEditRules = async ({
rules,
actions,
mlAuthz,
experimentalFeatures,
ruleCustomizationStatus,
}: BulkEditRulesArguments) => {
// Split operations
const { attributesActions, paramsActions } = splitBulkEditActions(actions);
@ -78,12 +78,11 @@ export const bulkEditRules = async ({
ruleType: ruleParams.type,
edit: actions,
immutable: ruleParams.immutable,
experimentalFeatures,
ruleCustomizationStatus,
});
const { modifiedParams, isParamsUpdateSkipped } = ruleParamsModifier(
ruleParams,
paramsActions,
experimentalFeatures
paramsActions
);
// Update rule source
@ -97,7 +96,7 @@ export const bulkEditRules = async ({
isCustomized = calculateIsCustomized({
baseRule: baseVersionsMap.get(ruleResponse.rule_id),
nextRule: ruleResponse,
isRuleCustomizationEnabled: experimentalFeatures.prebuiltRulesCustomizationEnabled,
ruleCustomizationStatus,
});
}

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import type { BulkActionsDryRunErrCode } from '../../../../../../common/constants';
import type { BulkActionsDryRunErrCode } from '../../../../../../common/api/detection_engine';
/**
* Error instance that has properties: errorCode & statusCode to use within run_dry

View file

@ -8,9 +8,6 @@
import { addItemsToArray, deleteItemsFromArray, ruleParamsModifier } from './rule_params_modifier';
import { BulkActionEditTypeEnum } from '../../../../../../common/api/detection_engine/rule_management';
import type { RuleAlertType } from '../../../rule_schema';
import type { ExperimentalFeatures } from '../../../../../../common';
const mockExperimentalFeatures = {} as ExperimentalFeatures;
describe('addItemsToArray', () => {
test('should add single item to array', () => {
@ -48,30 +45,22 @@ describe('ruleParamsModifier', () => {
} as RuleAlertType['params'];
test('should increment version if rule is custom (immutable === false)', () => {
const { modifiedParams } = ruleParamsModifier(
ruleParamsMock,
[
{
type: BulkActionEditTypeEnum.add_index_patterns,
value: ['my-index-*'],
},
],
mockExperimentalFeatures
);
const { modifiedParams } = ruleParamsModifier(ruleParamsMock, [
{
type: BulkActionEditTypeEnum.add_index_patterns,
value: ['my-index-*'],
},
]);
expect(modifiedParams).toHaveProperty('version', ruleParamsMock.version + 1);
});
test('should not increment version if rule is prebuilt (immutable === true)', () => {
const { modifiedParams } = ruleParamsModifier(
{ ...ruleParamsMock, immutable: true },
[
{
type: BulkActionEditTypeEnum.add_index_patterns,
value: ['my-index-*'],
},
],
mockExperimentalFeatures
);
const { modifiedParams } = ruleParamsModifier({ ...ruleParamsMock, immutable: true }, [
{
type: BulkActionEditTypeEnum.add_index_patterns,
value: ['my-index-*'],
},
]);
expect(modifiedParams).toHaveProperty('version', ruleParamsMock.version);
});
@ -144,8 +133,7 @@ describe('ruleParamsModifier', () => {
type: BulkActionEditTypeEnum.add_index_patterns,
value: indexPatternsToAdd,
},
],
mockExperimentalFeatures
]
);
expect(modifiedParams).toHaveProperty('index', resultingIndexPatterns);
expect(isParamsUpdateSkipped).toBe(isUpdateSkipped);
@ -209,8 +197,7 @@ describe('ruleParamsModifier', () => {
type: BulkActionEditTypeEnum.delete_index_patterns,
value: indexPatternsToDelete,
},
],
mockExperimentalFeatures
]
);
expect(modifiedParams).toHaveProperty('index', resultingIndexPatterns);
expect(isParamsUpdateSkipped).toBe(isUpdateSkipped);
@ -265,8 +252,7 @@ describe('ruleParamsModifier', () => {
type: BulkActionEditTypeEnum.set_index_patterns,
value: indexPatternsToOverwrite,
},
],
mockExperimentalFeatures
]
);
expect(modifiedParams).toHaveProperty('index', resultingIndexPatterns);
expect(isParamsUpdateSkipped).toBe(isUpdateSkipped);
@ -284,8 +270,7 @@ describe('ruleParamsModifier', () => {
type: BulkActionEditTypeEnum.delete_index_patterns,
value: ['index-2-*'],
},
],
mockExperimentalFeatures
]
);
expect(modifiedParams).not.toHaveProperty('index');
expect(isParamsUpdateSkipped).toBe(true);
@ -300,8 +285,7 @@ describe('ruleParamsModifier', () => {
value: ['index'],
overwrite_data_views: true,
},
],
mockExperimentalFeatures
]
);
expect(modifiedParams).toHaveProperty('dataViewId', undefined);
expect(isParamsUpdateSkipped).toBe(false);
@ -316,8 +300,7 @@ describe('ruleParamsModifier', () => {
value: ['index'],
overwrite_data_views: true,
},
],
mockExperimentalFeatures
]
);
expect(modifiedParams).toHaveProperty('dataViewId', undefined);
expect(isParamsUpdateSkipped).toBe(false);
@ -332,8 +315,7 @@ describe('ruleParamsModifier', () => {
value: ['index'],
overwrite_data_views: true,
},
],
mockExperimentalFeatures
]
);
expect(modifiedParams).toHaveProperty('dataViewId', undefined);
expect(modifiedParams).toHaveProperty('index', ['test-*']);
@ -349,8 +331,7 @@ describe('ruleParamsModifier', () => {
value: ['index'],
overwrite_data_views: true,
},
],
mockExperimentalFeatures
]
);
expect(modifiedParams).toHaveProperty('dataViewId', undefined);
expect(modifiedParams).toHaveProperty('index', undefined);
@ -359,16 +340,12 @@ describe('ruleParamsModifier', () => {
test('should throw error on adding index pattern if rule is of machine learning type', () => {
expect(() =>
ruleParamsModifier(
{ type: 'machine_learning' } as RuleAlertType['params'],
[
{
type: BulkActionEditTypeEnum.add_index_patterns,
value: ['my-index-*'],
},
],
mockExperimentalFeatures
)
ruleParamsModifier({ type: 'machine_learning' } as RuleAlertType['params'], [
{
type: BulkActionEditTypeEnum.add_index_patterns,
value: ['my-index-*'],
},
])
).toThrow(
"Index patterns can't be added. Machine learning rule doesn't have index patterns property"
);
@ -376,16 +353,12 @@ describe('ruleParamsModifier', () => {
test('should throw error on deleting index pattern if rule is of machine learning type', () => {
expect(() =>
ruleParamsModifier(
{ type: 'machine_learning' } as RuleAlertType['params'],
[
{
type: BulkActionEditTypeEnum.delete_index_patterns,
value: ['my-index-*'],
},
],
mockExperimentalFeatures
)
ruleParamsModifier({ type: 'machine_learning' } as RuleAlertType['params'], [
{
type: BulkActionEditTypeEnum.delete_index_patterns,
value: ['my-index-*'],
},
])
).toThrow(
"Index patterns can't be deleted. Machine learning rule doesn't have index patterns property"
);
@ -393,16 +366,12 @@ describe('ruleParamsModifier', () => {
test('should throw error on overwriting index pattern if rule is of machine learning type', () => {
expect(() =>
ruleParamsModifier(
{ type: 'machine_learning' } as RuleAlertType['params'],
[
{
type: BulkActionEditTypeEnum.set_index_patterns,
value: ['my-index-*'],
},
],
mockExperimentalFeatures
)
ruleParamsModifier({ type: 'machine_learning' } as RuleAlertType['params'], [
{
type: BulkActionEditTypeEnum.set_index_patterns,
value: ['my-index-*'],
},
])
).toThrow(
"Index patterns can't be overwritten. Machine learning rule doesn't have index patterns property"
);
@ -410,46 +379,34 @@ describe('ruleParamsModifier', () => {
test('should throw error on adding index pattern if rule is of ES|QL type', () => {
expect(() =>
ruleParamsModifier(
{ type: 'esql' } as RuleAlertType['params'],
[
{
type: BulkActionEditTypeEnum.add_index_patterns,
value: ['my-index-*'],
},
],
mockExperimentalFeatures
)
ruleParamsModifier({ type: 'esql' } as RuleAlertType['params'], [
{
type: BulkActionEditTypeEnum.add_index_patterns,
value: ['my-index-*'],
},
])
).toThrow("Index patterns can't be added. ES|QL rule doesn't have index patterns property");
});
test('should throw error on deleting index pattern if rule is of ES|QL type', () => {
expect(() =>
ruleParamsModifier(
{ type: 'esql' } as RuleAlertType['params'],
[
{
type: BulkActionEditTypeEnum.delete_index_patterns,
value: ['my-index-*'],
},
],
mockExperimentalFeatures
)
ruleParamsModifier({ type: 'esql' } as RuleAlertType['params'], [
{
type: BulkActionEditTypeEnum.delete_index_patterns,
value: ['my-index-*'],
},
])
).toThrow("Index patterns can't be deleted. ES|QL rule doesn't have index patterns property");
});
test('should throw error on overwriting index pattern if rule is of ES|QL type', () => {
expect(() =>
ruleParamsModifier(
{ type: 'esql' } as RuleAlertType['params'],
[
{
type: BulkActionEditTypeEnum.set_index_patterns,
value: ['my-index-*'],
},
],
mockExperimentalFeatures
)
ruleParamsModifier({ type: 'esql' } as RuleAlertType['params'], [
{
type: BulkActionEditTypeEnum.set_index_patterns,
value: ['my-index-*'],
},
])
).toThrow(
"Index patterns can't be overwritten. ES|QL rule doesn't have index patterns property"
);
@ -549,8 +506,7 @@ describe('ruleParamsModifier', () => {
type: BulkActionEditTypeEnum.add_investigation_fields,
value: investigationFieldsToAdd,
},
],
mockExperimentalFeatures
]
);
expect(modifiedParams).toHaveProperty(
'investigationFields',
@ -638,8 +594,7 @@ describe('ruleParamsModifier', () => {
type: BulkActionEditTypeEnum.delete_investigation_fields,
value: investigationFieldsToDelete,
},
],
mockExperimentalFeatures
]
);
expect(modifiedParams).toHaveProperty(
'investigationFields',
@ -718,8 +673,7 @@ describe('ruleParamsModifier', () => {
type: BulkActionEditTypeEnum.set_investigation_fields,
value: investigationFieldsToOverwrite,
},
],
mockExperimentalFeatures
]
);
expect(modifiedParams).toHaveProperty(
'investigationFields',
@ -733,19 +687,15 @@ describe('ruleParamsModifier', () => {
describe('timeline', () => {
test('should set timeline', () => {
const { modifiedParams, isParamsUpdateSkipped } = ruleParamsModifier(
ruleParamsMock,
[
{
type: BulkActionEditTypeEnum.set_timeline,
value: {
timeline_id: '91832785-286d-4ebe-b884-1a208d111a70',
timeline_title: 'Test timeline',
},
const { modifiedParams, isParamsUpdateSkipped } = ruleParamsModifier(ruleParamsMock, [
{
type: BulkActionEditTypeEnum.set_timeline,
value: {
timeline_id: '91832785-286d-4ebe-b884-1a208d111a70',
timeline_title: 'Test timeline',
},
],
mockExperimentalFeatures
);
},
]);
expect(modifiedParams.timelineId).toBe('91832785-286d-4ebe-b884-1a208d111a70');
expect(modifiedParams.timelineTitle).toBe('Test timeline');
@ -758,19 +708,15 @@ describe('ruleParamsModifier', () => {
const INTERVAL_IN_MINUTES = 5;
const LOOKBACK_IN_MINUTES = 1;
const FROM_IN_SECONDS = (INTERVAL_IN_MINUTES + LOOKBACK_IN_MINUTES) * 60;
const { modifiedParams, isParamsUpdateSkipped } = ruleParamsModifier(
ruleParamsMock,
[
{
type: BulkActionEditTypeEnum.set_schedule,
value: {
interval: `${INTERVAL_IN_MINUTES}m`,
lookback: `${LOOKBACK_IN_MINUTES}m`,
},
const { modifiedParams, isParamsUpdateSkipped } = ruleParamsModifier(ruleParamsMock, [
{
type: BulkActionEditTypeEnum.set_schedule,
value: {
interval: `${INTERVAL_IN_MINUTES}m`,
lookback: `${LOOKBACK_IN_MINUTES}m`,
},
],
mockExperimentalFeatures
);
},
]);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
expect((modifiedParams as any).interval).toBeUndefined();

View file

@ -6,7 +6,6 @@
*/
import type { RuleParamsModifierResult } from '@kbn/alerting-plugin/server/rules_client/methods/bulk_edit';
import type { ExperimentalFeatures } from '../../../../../../common';
import type { InvestigationFieldsCombined, RuleAlertType } from '../../../rule_schema';
import type {
BulkActionEditForRuleParams,
@ -108,8 +107,7 @@ const shouldSkipInvestigationFieldsBulkAction = (
// eslint-disable-next-line complexity
const applyBulkActionEditToRuleParams = (
existingRuleParams: RuleAlertType['params'],
action: BulkActionEditForRuleParams,
experimentalFeatures: ExperimentalFeatures
action: BulkActionEditForRuleParams
): {
ruleParams: RuleAlertType['params'];
isActionSkipped: boolean;
@ -281,17 +279,12 @@ const applyBulkActionEditToRuleParams = (
*/
export const ruleParamsModifier = (
existingRuleParams: RuleAlertType['params'],
actions: BulkActionEditForRuleParams[],
experimentalFeatures: ExperimentalFeatures
actions: BulkActionEditForRuleParams[]
): RuleParamsModifierResult<RuleAlertType['params']> => {
let isParamsUpdateSkipped = true;
const modifiedParams = actions.reduce((acc, action) => {
const { ruleParams, isActionSkipped } = applyBulkActionEditToRuleParams(
acc,
action,
experimentalFeatures
);
const { ruleParams, isActionSkipped } = applyBulkActionEditToRuleParams(acc, action);
// The rule was updated with at least one action, so mark our rule as updated
if (!isActionSkipped) {

View file

@ -6,46 +6,30 @@
*/
import type { Type as RuleType } from '@kbn/securitysolution-io-ts-alerting-types';
import type { ExperimentalFeatures } from '../../../../../../common';
import { invariant } from '../../../../../../common/utils/invariant';
import { isMlRule } from '../../../../../../common/machine_learning/helpers';
import { isEsqlRule } from '../../../../../../common/detection_engine/utils';
import { BulkActionsDryRunErrCode } from '../../../../../../common/constants';
import type {
BulkActionEditPayload,
BulkActionEditType,
} from '../../../../../../common/api/detection_engine/rule_management';
import { BulkActionEditTypeEnum } from '../../../../../../common/api/detection_engine/rule_management';
import type { RuleAlertType } from '../../../rule_schema';
import { isIndexPatternsBulkEditAction } from './utils';
import { throwDryRunError } from './dry_run';
import {
BulkActionEditTypeEnum,
BulkActionsDryRunErrCodeEnum,
} from '../../../../../../common/api/detection_engine/rule_management';
import type { PrebuiltRulesCustomizationStatus } from '../../../../../../common/detection_engine/prebuilt_rules/prebuilt_rule_customization_status';
import { PrebuiltRulesCustomizationDisabledReason } from '../../../../../../common/detection_engine/prebuilt_rules/prebuilt_rule_customization_status';
import { isEsqlRule } from '../../../../../../common/detection_engine/utils';
import { isMlRule } from '../../../../../../common/machine_learning/helpers';
import { invariant } from '../../../../../../common/utils/invariant';
import type { MlAuthz } from '../../../../machine_learning/authz';
import { throwAuthzError } from '../../../../machine_learning/validation';
import type { RuleAlertType } from '../../../rule_schema';
import { throwDryRunError } from './dry_run';
import { isIndexPatternsBulkEditAction } from './utils';
interface BulkActionsValidationArgs {
rule: RuleAlertType;
mlAuthz: MlAuthz;
}
interface BulkEditBulkActionsValidationArgs {
ruleType: RuleType;
mlAuthz: MlAuthz;
edit: BulkActionEditPayload[];
immutable: boolean;
experimentalFeatures: ExperimentalFeatures;
}
interface DryRunBulkEditBulkActionsValidationArgs {
rule: RuleAlertType;
mlAuthz: MlAuthz;
edit: BulkActionEditPayload[];
experimentalFeatures: ExperimentalFeatures;
}
interface DryRunManualRuleRunBulkActionsValidationArgs extends BulkActionsValidationArgs {
experimentalFeatures: ExperimentalFeatures;
}
/**
* throws ML authorization error wrapped with MACHINE_LEARNING_AUTH error code
* @param mlAuthz - {@link MlAuthz}
@ -54,7 +38,7 @@ interface DryRunManualRuleRunBulkActionsValidationArgs extends BulkActionsValida
const throwMlAuthError = (mlAuthz: MlAuthz, ruleType: RuleType) =>
throwDryRunError(
async () => throwAuthzError(await mlAuthz.validateRuleType(ruleType)),
BulkActionsDryRunErrCode.MACHINE_LEARNING_AUTH
BulkActionsDryRunErrCodeEnum.MACHINE_LEARNING_AUTH
);
/**
@ -85,38 +69,51 @@ export const validateBulkDuplicateRule = async ({ rule, mlAuthz }: BulkActionsVa
* runs validation for bulk schedule backfill for a single rule
* @param params - {@link DryRunManualRuleRunBulkActionsValidationArgs}
*/
export const validateBulkScheduleBackfill = async ({
rule,
experimentalFeatures,
}: DryRunManualRuleRunBulkActionsValidationArgs) => {
// check whether "manual rule run" feature is enabled
export const validateBulkScheduleBackfill = async ({ rule }: BulkActionsValidationArgs) => {
await throwDryRunError(
() => invariant(rule.enabled, 'Cannot schedule manual rule run for a disabled rule'),
BulkActionsDryRunErrCode.MANUAL_RULE_RUN_DISABLED_RULE
BulkActionsDryRunErrCodeEnum.MANUAL_RULE_RUN_DISABLED_RULE
);
};
interface BulkEditBulkActionsValidationArgs {
ruleType: RuleType;
mlAuthz: MlAuthz;
edit: BulkActionEditPayload[];
immutable: boolean;
ruleCustomizationStatus: PrebuiltRulesCustomizationStatus;
}
/**
* runs validation for bulk edit for a single rule
* @param params - {@link BulkActionsValidationArgs}
*/
export const validateBulkEditRule = async ({
ruleType,
mlAuthz,
edit,
immutable,
experimentalFeatures,
ruleCustomizationStatus,
}: BulkEditBulkActionsValidationArgs) => {
await throwMlAuthError(mlAuthz, ruleType);
if (!experimentalFeatures.prebuiltRulesCustomizationEnabled) {
// if rule can't be edited error will be thrown
const canRuleBeEdited = !immutable || istEditApplicableToImmutableRule(edit);
await throwDryRunError(
() => invariant(canRuleBeEdited, "Elastic rule can't be edited"),
BulkActionsDryRunErrCode.IMMUTABLE
);
// Prebuilt rule customization checks
if (immutable) {
if (ruleCustomizationStatus.isRulesCustomizationEnabled) {
// Rule customization is enabled; prebuilt rules can be edited
return undefined;
}
// Rule customization is disabled; only certain actions can be applied to immutable rules
const canRuleBeEdited = istEditApplicableToImmutableRule(edit);
if (!canRuleBeEdited) {
await throwDryRunError(
() => invariant(canRuleBeEdited, "Elastic rule can't be edited"),
ruleCustomizationStatus.customizationDisabledReason ===
PrebuiltRulesCustomizationDisabledReason.FeatureFlag
? BulkActionsDryRunErrCodeEnum.IMMUTABLE
: BulkActionsDryRunErrCodeEnum.PREBUILT_CUSTOMIZATION_LICENSE
);
}
}
};
@ -131,22 +128,28 @@ const istEditApplicableToImmutableRule = (edit: BulkActionEditPayload[]): boolea
return edit.every(({ type }) => applicableActions.includes(type));
};
interface DryRunBulkEditBulkActionsValidationArgs {
rule: RuleAlertType;
mlAuthz: MlAuthz;
edit: BulkActionEditPayload[];
ruleCustomizationStatus: PrebuiltRulesCustomizationStatus;
}
/**
* executes dry run validations for bulk edit of a single rule
* @param params - {@link DryRunBulkEditBulkActionsValidationArgs}
*/
export const dryRunValidateBulkEditRule = async ({
rule,
edit,
mlAuthz,
experimentalFeatures,
ruleCustomizationStatus,
}: DryRunBulkEditBulkActionsValidationArgs) => {
await validateBulkEditRule({
ruleType: rule.params.type,
mlAuthz,
edit,
immutable: rule.params.immutable,
experimentalFeatures,
ruleCustomizationStatus,
});
// if rule is machine_learning, index pattern action can't be applied to it
@ -157,7 +160,7 @@ export const dryRunValidateBulkEditRule = async ({
!edit.some((action) => isIndexPatternsBulkEditAction(action.type)),
"Machine learning rule doesn't have index patterns"
),
BulkActionsDryRunErrCode.MACHINE_LEARNING_INDEX_PATTERN
BulkActionsDryRunErrCodeEnum.MACHINE_LEARNING_INDEX_PATTERN
);
// if rule is es|ql, index pattern action can't be applied to it
@ -168,6 +171,6 @@ export const dryRunValidateBulkEditRule = async ({
!edit.some((action) => isIndexPatternsBulkEditAction(action.type)),
"ES|QL rule doesn't have index patterns"
),
BulkActionsDryRunErrCode.ESQL_INDEX_PATTERN
BulkActionsDryRunErrCodeEnum.ESQL_INDEX_PATTERN
);
};

View file

@ -19,6 +19,7 @@ const createDetectionRulesClientMock = () => {
upgradePrebuiltRule: jest.fn(),
importRule: jest.fn(),
importRules: jest.fn(),
getRuleCustomizationStatus: jest.fn(),
};
return mocked;
};

View file

@ -21,6 +21,9 @@ import { buildMlAuthz } from '../../../../machine_learning/authz';
import { throwAuthzError } from '../../../../machine_learning/validation';
import { createDetectionRulesClient } from './detection_rules_client';
import type { IDetectionRulesClient } from './detection_rules_client_interface';
import { licenseMock } from '@kbn/licensing-plugin/common/licensing.mock';
import type { ExperimentalFeatures } from '../../../../../../common';
import { createProductFeaturesServiceMock } from '../../../../product_features_service/mocks';
jest.mock('../../../../machine_learning/authz');
jest.mock('../../../../machine_learning/validation');
@ -35,8 +38,6 @@ describe('DetectionRulesClient.createCustomRule', () => {
} as unknown as jest.Mocked<ActionsClient>;
beforeEach(() => {
jest.resetAllMocks();
actionsClient = {
isSystemAction: jest.fn((id: string) => id === 'system-connector-.cases'),
} as unknown as jest.Mocked<ActionsClient>;
@ -51,7 +52,9 @@ describe('DetectionRulesClient.createCustomRule', () => {
rulesClient,
mlAuthz,
savedObjectsClient,
isRuleCustomizationEnabled: true,
license: licenseMock.createLicenseMock(),
experimentalFeatures: { prebuiltRulesCustomizationEnabled: true } as ExperimentalFeatures,
productFeaturesService: createProductFeaturesServiceMock(),
});
});

View file

@ -21,6 +21,9 @@ import { buildMlAuthz } from '../../../../machine_learning/authz';
import { throwAuthzError } from '../../../../machine_learning/validation';
import { createDetectionRulesClient } from './detection_rules_client';
import type { IDetectionRulesClient } from './detection_rules_client_interface';
import { licenseMock } from '@kbn/licensing-plugin/common/licensing.mock';
import type { ExperimentalFeatures } from '../../../../../../common';
import { createProductFeaturesServiceMock } from '../../../../product_features_service/mocks';
jest.mock('../../../../machine_learning/authz');
jest.mock('../../../../machine_learning/validation');
@ -33,8 +36,6 @@ describe('DetectionRulesClient.createPrebuiltRule', () => {
let actionsClient: jest.Mocked<ActionsClient>;
beforeEach(() => {
jest.resetAllMocks();
rulesClient = rulesClientMock.create();
rulesClient.create.mockResolvedValue(getRuleMock(getQueryRuleParams()));
@ -44,7 +45,9 @@ describe('DetectionRulesClient.createPrebuiltRule', () => {
rulesClient,
mlAuthz,
savedObjectsClient,
isRuleCustomizationEnabled: true,
license: licenseMock.createLicenseMock(),
experimentalFeatures: { prebuiltRulesCustomizationEnabled: true } as ExperimentalFeatures,
productFeaturesService: createProductFeaturesServiceMock(),
});
});

View file

@ -12,6 +12,9 @@ import { savedObjectsClientMock } from '@kbn/core/server/mocks';
import { buildMlAuthz } from '../../../../machine_learning/authz';
import { createDetectionRulesClient } from './detection_rules_client';
import type { IDetectionRulesClient } from './detection_rules_client_interface';
import { licenseMock } from '@kbn/licensing-plugin/common/licensing.mock';
import { createProductFeaturesServiceMock } from '../../../../product_features_service/mocks';
import type { ExperimentalFeatures } from '../../../../../../common';
jest.mock('../../../../machine_learning/authz');
@ -30,7 +33,9 @@ describe('DetectionRulesClient.deleteRule', () => {
rulesClient,
mlAuthz,
savedObjectsClient,
isRuleCustomizationEnabled: true,
license: licenseMock.createLicenseMock(),
experimentalFeatures: { prebuiltRulesCustomizationEnabled: true } as ExperimentalFeatures,
productFeaturesService: createProductFeaturesServiceMock(),
});
});

View file

@ -18,6 +18,9 @@ import { createDetectionRulesClient } from './detection_rules_client';
import type { IDetectionRulesClient } from './detection_rules_client_interface';
import { getRuleByRuleId } from './methods/get_rule_by_rule_id';
import { getValidatedRuleToImportMock } from '../../../../../../common/api/detection_engine/rule_management/mocks';
import { licenseMock } from '@kbn/licensing-plugin/common/licensing.mock';
import type { ExperimentalFeatures } from '../../../../../../common';
import { createProductFeaturesServiceMock } from '../../../../product_features_service/mocks';
jest.mock('../../../../machine_learning/authz');
jest.mock('../../../../machine_learning/validation');
@ -51,7 +54,9 @@ describe('DetectionRulesClient.importRule', () => {
rulesClient,
mlAuthz,
savedObjectsClient,
isRuleCustomizationEnabled: true,
license: licenseMock.createLicenseMock(),
experimentalFeatures: { prebuiltRulesCustomizationEnabled: true } as ExperimentalFeatures,
productFeaturesService: createProductFeaturesServiceMock(),
});
});

View file

@ -17,6 +17,9 @@ import { createDetectionRulesClient } from './detection_rules_client';
import { importRule } from './methods/import_rule';
import { createRuleImportErrorObject } from '../import/errors';
import { checkRuleExceptionReferences } from '../import/check_rule_exception_references';
import { licenseMock } from '@kbn/licensing-plugin/common/licensing.mock';
import type { ExperimentalFeatures } from '../../../../../../common';
import { createProductFeaturesServiceMock } from '../../../../product_features_service/mocks';
jest.mock('./methods/import_rule');
jest.mock('../import/check_rule_exception_references');
@ -32,7 +35,9 @@ describe('detectionRulesClient.importRules', () => {
rulesClient: rulesClientMock.create(),
mlAuthz: buildMlAuthz(),
savedObjectsClient: savedObjectsClientMock.create(),
isRuleCustomizationEnabled: true,
license: licenseMock.createLicenseMock(),
experimentalFeatures: { prebuiltRulesCustomizationEnabled: true } as ExperimentalFeatures,
productFeaturesService: createProductFeaturesServiceMock(),
});
(checkRuleExceptionReferences as jest.Mock).mockReturnValue([[], []]);

View file

@ -22,6 +22,9 @@ import { throwAuthzError } from '../../../../machine_learning/validation';
import { createDetectionRulesClient } from './detection_rules_client';
import type { IDetectionRulesClient } from './detection_rules_client_interface';
import { savedObjectsClientMock } from '@kbn/core/server/mocks';
import { licenseMock } from '@kbn/licensing-plugin/common/licensing.mock';
import type { ExperimentalFeatures } from '../../../../../../common';
import { createProductFeaturesServiceMock } from '../../../../product_features_service/mocks';
jest.mock('../../../../machine_learning/authz');
jest.mock('../../../../machine_learning/validation');
@ -50,7 +53,9 @@ describe('DetectionRulesClient.patchRule', () => {
rulesClient,
mlAuthz,
savedObjectsClient,
isRuleCustomizationEnabled: true,
license: licenseMock.createLicenseMock(),
experimentalFeatures: { prebuiltRulesCustomizationEnabled: true } as ExperimentalFeatures,
productFeaturesService: createProductFeaturesServiceMock(),
});
});

View file

@ -5,13 +5,17 @@
* 2.0.
*/
import type { RulesClient } from '@kbn/alerting-plugin/server';
import type { ActionsClient } from '@kbn/actions-plugin/server';
import type { RulesClient } from '@kbn/alerting-plugin/server';
import type { SavedObjectsClientContract } from '@kbn/core/server';
import { ProductFeatureKey } from '@kbn/security-solution-features/keys';
import type { ILicense } from '@kbn/licensing-plugin/server';
import type { RuleResponse } from '../../../../../../common/api/detection_engine/model/rule_schema';
import { PrebuiltRulesCustomizationDisabledReason } from '../../../../../../common/detection_engine/prebuilt_rules/prebuilt_rule_customization_status';
import { withSecuritySpan } from '../../../../../utils/with_security_span';
import type { MlAuthz } from '../../../../machine_learning/authz';
import type { ProductFeaturesService } from '../../../../product_features_service';
import { createPrebuiltRuleAssetsClient } from '../../../prebuilt_rules/logic/rule_assets/prebuilt_rule_assets_client';
import type { RuleImportErrorObject } from '../import/errors';
import type {
@ -28,17 +32,21 @@ import type {
import { createRule } from './methods/create_rule';
import { deleteRule } from './methods/delete_rule';
import { importRule } from './methods/import_rule';
import { importRules } from './methods/import_rules';
import { patchRule } from './methods/patch_rule';
import { updateRule } from './methods/update_rule';
import { upgradePrebuiltRule } from './methods/upgrade_prebuilt_rule';
import { importRules } from './methods/import_rules';
import { MINIMUM_RULE_CUSTOMIZATION_LICENSE } from '../../../../../../common/constants';
import type { ExperimentalFeatures } from '../../../../../../common';
interface DetectionRulesClientParams {
actionsClient: ActionsClient;
rulesClient: RulesClient;
savedObjectsClient: SavedObjectsClientContract;
mlAuthz: MlAuthz;
isRuleCustomizationEnabled: boolean;
experimentalFeatures: ExperimentalFeatures;
productFeaturesService: ProductFeaturesService;
license: ILicense;
}
export const createDetectionRulesClient = ({
@ -46,11 +54,42 @@ export const createDetectionRulesClient = ({
rulesClient,
mlAuthz,
savedObjectsClient,
isRuleCustomizationEnabled,
experimentalFeatures,
productFeaturesService,
license,
}: DetectionRulesClientParams): IDetectionRulesClient => {
const prebuiltRuleAssetClient = createPrebuiltRuleAssetsClient(savedObjectsClient);
return {
getRuleCustomizationStatus() {
/**
* The prebuilt rules customization feature is gated by two things:
* 1. The feature flag `prebuiltRulesCustomizationEnabled` in the config.
* 2. The license level.
*
* The license level is verified against the minimum required level for
* the feature (Enterprise). However, since Serverless always operates at
* the Enterprise license level, we must also check if the feature is
* enabled in the product features. In Serverless, for different tiers,
* unavailable features are disabled.
*/
const isRulesCustomizationEnabled =
experimentalFeatures.prebuiltRulesCustomizationEnabled &&
license.hasAtLeast(MINIMUM_RULE_CUSTOMIZATION_LICENSE) &&
productFeaturesService.isEnabled(ProductFeatureKey.prebuiltRuleCustomization);
let customizationDisabledReason;
if (!isRulesCustomizationEnabled) {
customizationDisabledReason = !experimentalFeatures.prebuiltRulesCustomizationEnabled
? PrebuiltRulesCustomizationDisabledReason.FeatureFlag
: PrebuiltRulesCustomizationDisabledReason.License;
}
return {
isRulesCustomizationEnabled,
customizationDisabledReason,
};
},
async createCustomRule(args: CreateCustomRuleArgs): Promise<RuleResponse> {
return withSecuritySpan('DetectionRulesClient.createCustomRule', async () => {
return createRule({
@ -91,7 +130,7 @@ export const createDetectionRulesClient = ({
prebuiltRuleAssetClient,
mlAuthz,
ruleUpdate,
isRuleCustomizationEnabled,
ruleCustomizationStatus: this.getRuleCustomizationStatus(),
});
});
},
@ -104,7 +143,7 @@ export const createDetectionRulesClient = ({
prebuiltRuleAssetClient,
mlAuthz,
rulePatch,
isRuleCustomizationEnabled,
ruleCustomizationStatus: this.getRuleCustomizationStatus(),
});
});
},
@ -123,7 +162,7 @@ export const createDetectionRulesClient = ({
ruleAsset,
mlAuthz,
prebuiltRuleAssetClient,
isRuleCustomizationEnabled,
ruleCustomizationStatus: this.getRuleCustomizationStatus(),
});
});
},
@ -136,7 +175,7 @@ export const createDetectionRulesClient = ({
importRulePayload: args,
mlAuthz,
prebuiltRuleAssetClient,
isRuleCustomizationEnabled,
ruleCustomizationStatus: this.getRuleCustomizationStatus(),
});
});
},

View file

@ -22,6 +22,9 @@ import { throwAuthzError } from '../../../../machine_learning/validation';
import { createDetectionRulesClient } from './detection_rules_client';
import type { IDetectionRulesClient } from './detection_rules_client_interface';
import { savedObjectsClientMock } from '@kbn/core/server/mocks';
import { licenseMock } from '@kbn/licensing-plugin/common/licensing.mock';
import type { ExperimentalFeatures } from '../../../../../../common';
import { createProductFeaturesServiceMock } from '../../../../product_features_service/mocks';
jest.mock('../../../../machine_learning/authz');
jest.mock('../../../../machine_learning/validation');
@ -50,7 +53,9 @@ describe('DetectionRulesClient.updateRule', () => {
rulesClient,
mlAuthz,
savedObjectsClient,
isRuleCustomizationEnabled: true,
license: licenseMock.createLicenseMock(),
experimentalFeatures: { prebuiltRulesCustomizationEnabled: true } as ExperimentalFeatures,
productFeaturesService: createProductFeaturesServiceMock(),
});
});

View file

@ -23,6 +23,9 @@ import { throwAuthzError } from '../../../../machine_learning/validation';
import { createDetectionRulesClient } from './detection_rules_client';
import type { IDetectionRulesClient } from './detection_rules_client_interface';
import { savedObjectsClientMock } from '@kbn/core/server/mocks';
import { licenseMock } from '@kbn/licensing-plugin/common/licensing.mock';
import type { ExperimentalFeatures } from '../../../../../../common';
import { createProductFeaturesServiceMock } from '../../../../product_features_service/mocks';
jest.mock('../../../../machine_learning/authz');
jest.mock('../../../../machine_learning/validation');
@ -48,7 +51,9 @@ describe('DetectionRulesClient.upgradePrebuiltRule', () => {
rulesClient,
mlAuthz,
savedObjectsClient,
isRuleCustomizationEnabled: true,
license: licenseMock.createLicenseMock(),
experimentalFeatures: { prebuiltRulesCustomizationEnabled: true } as ExperimentalFeatures,
productFeaturesService: createProductFeaturesServiceMock(),
});
});
@ -110,7 +115,6 @@ describe('DetectionRulesClient.upgradePrebuiltRule', () => {
installedRule.rule_id = 'rule-id';
beforeEach(() => {
jest.resetAllMocks();
rulesClient.create.mockResolvedValue(getRuleMock(getQueryRuleParams()));
(getRuleByRuleId as jest.Mock).mockResolvedValue(installedRule);
});

View file

@ -17,8 +17,10 @@ import type {
import type { IRuleSourceImporter } from '../import/rule_source_importer';
import type { RuleImportErrorObject } from '../import/errors';
import type { PrebuiltRuleAsset } from '../../../prebuilt_rules';
import type { PrebuiltRulesCustomizationStatus } from '../../../../../../common/detection_engine/prebuilt_rules/prebuilt_rule_customization_status';
export interface IDetectionRulesClient {
getRuleCustomizationStatus: () => PrebuiltRulesCustomizationStatus;
createCustomRule: (args: CreateCustomRuleArgs) => Promise<RuleResponse>;
createPrebuiltRule: (args: CreatePrebuiltRuleArgs) => Promise<RuleResponse>;
updateRule: (args: UpdateRuleArgs) => Promise<RuleResponse>;

View file

@ -19,11 +19,16 @@ import {
getSavedQuerySchemaMock,
getThreatMatchingSchemaMock,
} from '../../../../../../../common/api/detection_engine/model/rule_schema/mocks';
import type { PrebuiltRulesCustomizationStatus } from '../../../../../../../common/detection_engine/prebuilt_rules/prebuilt_rule_customization_status';
import { createPrebuiltRuleAssetsClient } from '../../../../prebuilt_rules/logic/rule_assets/__mocks__/prebuilt_rule_assets_client';
import { applyRulePatch } from './apply_rule_patch';
const prebuiltRuleAssetClient = createPrebuiltRuleAssetsClient();
const ruleCustomizationStatus: PrebuiltRulesCustomizationStatus = {
isRulesCustomizationEnabled: true,
};
describe('applyRulePatch', () => {
describe('EQL', () => {
test('should accept EQL params when existing rule type is EQL', async () => {
@ -37,7 +42,7 @@ describe('applyRulePatch', () => {
rulePatch,
existingRule,
prebuiltRuleAssetClient,
isRuleCustomizationEnabled: true,
ruleCustomizationStatus,
});
expect(patchedRule).toEqual(
expect.objectContaining({
@ -66,7 +71,7 @@ describe('applyRulePatch', () => {
rulePatch,
existingRule,
prebuiltRuleAssetClient,
isRuleCustomizationEnabled: true,
ruleCustomizationStatus,
});
expect(patchedRule).toEqual(
expect.objectContaining({
@ -96,7 +101,7 @@ describe('applyRulePatch', () => {
rulePatch,
existingRule,
prebuiltRuleAssetClient,
isRuleCustomizationEnabled: true,
ruleCustomizationStatus,
})
).rejects.toThrowError(
'event_category_override: Expected string, received number, tiebreaker_field: Expected string, received number, timestamp_field: Expected string, received number'
@ -122,7 +127,7 @@ describe('applyRulePatch', () => {
rulePatch,
existingRule,
prebuiltRuleAssetClient,
isRuleCustomizationEnabled: true,
ruleCustomizationStatus,
})
).rejects.toThrowError('alert_suppression.group_by: Expected array, received string');
});
@ -138,7 +143,7 @@ describe('applyRulePatch', () => {
rulePatch,
existingRule,
prebuiltRuleAssetClient,
isRuleCustomizationEnabled: true,
ruleCustomizationStatus,
});
expect(patchedRule).toEqual(
expect.objectContaining({
@ -159,7 +164,7 @@ describe('applyRulePatch', () => {
rulePatch,
existingRule,
prebuiltRuleAssetClient,
isRuleCustomizationEnabled: true,
ruleCustomizationStatus,
})
).rejects.toThrowError(
'threat_query: Expected string, received number, threat_indicator_path: Expected string, received number'
@ -176,7 +181,7 @@ describe('applyRulePatch', () => {
rulePatch,
existingRule,
prebuiltRuleAssetClient,
isRuleCustomizationEnabled: true,
ruleCustomizationStatus,
});
expect(patchedRule).toEqual(
expect.objectContaining({
@ -197,7 +202,7 @@ describe('applyRulePatch', () => {
rulePatch,
existingRule,
prebuiltRuleAssetClient,
isRuleCustomizationEnabled: true,
ruleCustomizationStatus,
})
).rejects.toThrowError(
"index.0: Expected string, received number, language: Invalid enum value. Expected 'kuery' | 'lucene', received 'non-language'"
@ -214,7 +219,7 @@ describe('applyRulePatch', () => {
rulePatch,
existingRule,
prebuiltRuleAssetClient,
isRuleCustomizationEnabled: true,
ruleCustomizationStatus,
});
expect(patchedRule).toEqual(
expect.objectContaining({
@ -235,7 +240,7 @@ describe('applyRulePatch', () => {
rulePatch,
existingRule,
prebuiltRuleAssetClient,
isRuleCustomizationEnabled: true,
ruleCustomizationStatus,
})
).rejects.toThrowError(
"index.0: Expected string, received number, language: Invalid enum value. Expected 'kuery' | 'lucene', received 'non-language'"
@ -254,7 +259,7 @@ describe('applyRulePatch', () => {
rulePatch,
existingRule,
prebuiltRuleAssetClient,
isRuleCustomizationEnabled: true,
ruleCustomizationStatus,
});
expect(patchedRule).toEqual(
expect.objectContaining({
@ -279,7 +284,7 @@ describe('applyRulePatch', () => {
rulePatch,
existingRule,
prebuiltRuleAssetClient,
isRuleCustomizationEnabled: true,
ruleCustomizationStatus,
})
).rejects.toThrowError('threshold.value: Expected number, received string');
});
@ -297,7 +302,7 @@ describe('applyRulePatch', () => {
rulePatch,
existingRule,
prebuiltRuleAssetClient,
isRuleCustomizationEnabled: true,
ruleCustomizationStatus,
});
expect(patchedRule).toEqual(
expect.objectContaining({
@ -321,7 +326,7 @@ describe('applyRulePatch', () => {
rulePatch,
existingRule,
prebuiltRuleAssetClient,
isRuleCustomizationEnabled: true,
ruleCustomizationStatus,
});
expect(patchedRule).toEqual(
expect.objectContaining({
@ -344,7 +349,7 @@ describe('applyRulePatch', () => {
rulePatch,
existingRule,
prebuiltRuleAssetClient,
isRuleCustomizationEnabled: true,
ruleCustomizationStatus,
});
expect(patchedRule).toEqual(
expect.objectContaining({
@ -369,7 +374,7 @@ describe('applyRulePatch', () => {
rulePatch,
existingRule,
prebuiltRuleAssetClient,
isRuleCustomizationEnabled: true,
ruleCustomizationStatus,
});
expect(patchedRule).toEqual(
expect.objectContaining({
@ -392,7 +397,7 @@ describe('applyRulePatch', () => {
rulePatch,
existingRule,
prebuiltRuleAssetClient,
isRuleCustomizationEnabled: true,
ruleCustomizationStatus,
});
expect(patchedRule).toEqual(
expect.objectContaining({
@ -411,7 +416,7 @@ describe('applyRulePatch', () => {
rulePatch,
existingRule,
prebuiltRuleAssetClient,
isRuleCustomizationEnabled: true,
ruleCustomizationStatus,
})
).rejects.toThrowError('anomaly_threshold: Expected number, received string');
});
@ -428,7 +433,7 @@ describe('applyRulePatch', () => {
rulePatch,
existingRule,
prebuiltRuleAssetClient,
isRuleCustomizationEnabled: true,
ruleCustomizationStatus,
});
expect(patchedRule).toEqual(
@ -451,7 +456,7 @@ describe('applyRulePatch', () => {
rulePatch,
existingRule,
prebuiltRuleAssetClient,
isRuleCustomizationEnabled: true,
ruleCustomizationStatus,
});
expect(patchedRule).toEqual(
expect.objectContaining({
@ -470,7 +475,7 @@ describe('applyRulePatch', () => {
rulePatch,
existingRule,
prebuiltRuleAssetClient,
isRuleCustomizationEnabled: true,
ruleCustomizationStatus,
})
).rejects.toThrowError('new_terms_fields: Expected array, received string');
});
@ -493,7 +498,7 @@ describe('applyRulePatch', () => {
rulePatch,
existingRule,
prebuiltRuleAssetClient,
isRuleCustomizationEnabled: true,
ruleCustomizationStatus,
});
expect(patchedRule).toEqual(
expect.objectContaining({

View file

@ -46,12 +46,13 @@ import {
import { assertUnreachable } from '../../../../../../../common/utility_types';
import type { IPrebuiltRuleAssetsClient } from '../../../../prebuilt_rules/logic/rule_assets/prebuilt_rule_assets_client';
import { calculateRuleSource } from './rule_source/calculate_rule_source';
import type { PrebuiltRulesCustomizationStatus } from '../../../../../../../common/detection_engine/prebuilt_rules/prebuilt_rule_customization_status';
interface ApplyRulePatchProps {
prebuiltRuleAssetClient: IPrebuiltRuleAssetsClient;
existingRule: RuleResponse;
rulePatch: PatchRuleRequestBody;
isRuleCustomizationEnabled: boolean;
ruleCustomizationStatus: PrebuiltRulesCustomizationStatus;
}
// eslint-disable-next-line complexity
@ -59,7 +60,7 @@ export const applyRulePatch = async ({
rulePatch,
existingRule,
prebuiltRuleAssetClient,
isRuleCustomizationEnabled,
ruleCustomizationStatus,
}: ApplyRulePatchProps): Promise<RuleResponse> => {
const typeSpecificParams = patchTypeSpecificParams(rulePatch, existingRule);
@ -124,7 +125,7 @@ export const applyRulePatch = async ({
nextRule.rule_source = await calculateRuleSource({
rule: nextRule,
prebuiltRuleAssetClient,
isRuleCustomizationEnabled,
ruleCustomizationStatus,
});
return nextRule;

View file

@ -9,6 +9,7 @@ import type {
RuleResponse,
RuleUpdateProps,
} from '../../../../../../../common/api/detection_engine/model/rule_schema';
import type { PrebuiltRulesCustomizationStatus } from '../../../../../../../common/detection_engine/prebuilt_rules/prebuilt_rule_customization_status';
import type { IPrebuiltRuleAssetsClient } from '../../../../prebuilt_rules/logic/rule_assets/prebuilt_rule_assets_client';
import { applyRuleDefaults } from './apply_rule_defaults';
import { calculateRuleSource } from './rule_source/calculate_rule_source';
@ -17,14 +18,14 @@ interface ApplyRuleUpdateProps {
prebuiltRuleAssetClient: IPrebuiltRuleAssetsClient;
existingRule: RuleResponse;
ruleUpdate: RuleUpdateProps;
isRuleCustomizationEnabled: boolean;
ruleCustomizationStatus: PrebuiltRulesCustomizationStatus;
}
export const applyRuleUpdate = async ({
prebuiltRuleAssetClient,
existingRule,
ruleUpdate,
isRuleCustomizationEnabled,
ruleCustomizationStatus,
}: ApplyRuleUpdateProps): Promise<RuleResponse> => {
const nextRule: RuleResponse = {
...applyRuleDefaults(ruleUpdate),
@ -48,7 +49,7 @@ export const applyRuleUpdate = async ({
nextRule.rule_source = await calculateRuleSource({
rule: nextRule,
prebuiltRuleAssetClient,
isRuleCustomizationEnabled,
ruleCustomizationStatus,
});
return nextRule;

View file

@ -11,19 +11,26 @@ import type { PrebuiltRuleAsset } from '../../../../../prebuilt_rules';
import { calculateRuleFieldsDiff } from '../../../../../prebuilt_rules/logic/diff/calculation/calculate_rule_fields_diff';
import { convertRuleToDiffable } from '../../../../../../../../common/detection_engine/prebuilt_rules/diff/convert_rule_to_diffable';
import { convertPrebuiltRuleAssetToRuleResponse } from '../../converters/convert_prebuilt_rule_asset_to_rule_response';
import {
PrebuiltRulesCustomizationDisabledReason,
type PrebuiltRulesCustomizationStatus,
} from '../../../../../../../../common/detection_engine/prebuilt_rules/prebuilt_rule_customization_status';
interface CalculateIsCustomizedArgs {
baseRule: PrebuiltRuleAsset | undefined;
nextRule: RuleResponse;
isRuleCustomizationEnabled: boolean;
ruleCustomizationStatus: PrebuiltRulesCustomizationStatus;
}
export function calculateIsCustomized({
baseRule,
nextRule,
isRuleCustomizationEnabled,
ruleCustomizationStatus,
}: CalculateIsCustomizedArgs) {
if (!isRuleCustomizationEnabled) {
if (
ruleCustomizationStatus.customizationDisabledReason ===
PrebuiltRulesCustomizationDisabledReason.FeatureFlag
) {
// We don't want to accidentally mark rules as customized when customization is disabled.
return false;
}

View file

@ -5,6 +5,8 @@
* 2.0.
*/
import type { PrebuiltRulesCustomizationStatus } from '../../../../../../../../common/detection_engine/prebuilt_rules/prebuilt_rule_customization_status';
import { PrebuiltRulesCustomizationDisabledReason } from '../../../../../../../../common/detection_engine/prebuilt_rules/prebuilt_rule_customization_status';
import { createPrebuiltRuleAssetsClient } from '../../../../../prebuilt_rules/logic/rule_assets/__mocks__/prebuilt_rule_assets_client';
import { applyRuleDefaults } from '../apply_rule_defaults';
import { calculateRuleSource } from './calculate_rule_source';
@ -35,6 +37,10 @@ const getSampleRule = () => {
};
};
const ruleCustomizationStatus: PrebuiltRulesCustomizationStatus = {
isRulesCustomizationEnabled: true,
};
describe('calculateRuleSource', () => {
it('returns an internal rule source when the rule is not prebuilt', async () => {
const rule = getSampleRule();
@ -43,7 +49,7 @@ describe('calculateRuleSource', () => {
const result = await calculateRuleSource({
prebuiltRuleAssetClient,
rule,
isRuleCustomizationEnabled: true,
ruleCustomizationStatus,
});
expect(result).toEqual({
type: 'internal',
@ -60,7 +66,7 @@ describe('calculateRuleSource', () => {
const result = await calculateRuleSource({
prebuiltRuleAssetClient,
rule,
isRuleCustomizationEnabled: true,
ruleCustomizationStatus,
});
expect(result).toEqual(
expect.objectContaining({
@ -81,7 +87,7 @@ describe('calculateRuleSource', () => {
const result = await calculateRuleSource({
prebuiltRuleAssetClient,
rule,
isRuleCustomizationEnabled: true,
ruleCustomizationStatus,
});
expect(result).toEqual(
expect.objectContaining({
@ -104,7 +110,7 @@ describe('calculateRuleSource', () => {
const result = await calculateRuleSource({
prebuiltRuleAssetClient,
rule,
isRuleCustomizationEnabled: true,
ruleCustomizationStatus,
});
expect(result).toEqual(
expect.objectContaining({
@ -114,7 +120,7 @@ describe('calculateRuleSource', () => {
);
});
it('returns is_customized false when the rule is customized but customization is disabled', async () => {
it('returns is_customized false when the rule is customized but customization feature flag is disabled', async () => {
const rule = getSampleRule();
rule.immutable = true;
rule.name = 'Updated name';
@ -125,7 +131,10 @@ describe('calculateRuleSource', () => {
const result = await calculateRuleSource({
prebuiltRuleAssetClient,
rule,
isRuleCustomizationEnabled: false,
ruleCustomizationStatus: {
isRulesCustomizationEnabled: false,
customizationDisabledReason: PrebuiltRulesCustomizationDisabledReason.FeatureFlag,
},
});
expect(result).toEqual(
expect.objectContaining({
@ -134,4 +143,28 @@ describe('calculateRuleSource', () => {
})
);
});
it('returns is_customized true when the rule is customized and customization is disabled because of license', async () => {
const rule = getSampleRule();
rule.immutable = true;
rule.name = 'Updated name';
const baseRule = getSampleRuleAsset();
prebuiltRuleAssetClient.fetchAssetsByVersion.mockResolvedValueOnce([baseRule]);
const result = await calculateRuleSource({
prebuiltRuleAssetClient,
rule,
ruleCustomizationStatus: {
isRulesCustomizationEnabled: false,
customizationDisabledReason: PrebuiltRulesCustomizationDisabledReason.License,
},
});
expect(result).toEqual(
expect.objectContaining({
type: 'external',
is_customized: true,
})
);
});
});

View file

@ -9,6 +9,7 @@ import type {
RuleResponse,
RuleSource,
} from '../../../../../../../../common/api/detection_engine/model/rule_schema';
import type { PrebuiltRulesCustomizationStatus } from '../../../../../../../../common/detection_engine/prebuilt_rules/prebuilt_rule_customization_status';
import type { PrebuiltRuleAsset } from '../../../../../prebuilt_rules';
import type { IPrebuiltRuleAssetsClient } from '../../../../../prebuilt_rules/logic/rule_assets/prebuilt_rule_assets_client';
import { calculateIsCustomized } from './calculate_is_customized';
@ -16,13 +17,13 @@ import { calculateIsCustomized } from './calculate_is_customized';
interface CalculateRuleSourceProps {
prebuiltRuleAssetClient: IPrebuiltRuleAssetsClient;
rule: RuleResponse;
isRuleCustomizationEnabled: boolean;
ruleCustomizationStatus: PrebuiltRulesCustomizationStatus;
}
export async function calculateRuleSource({
prebuiltRuleAssetClient,
rule,
isRuleCustomizationEnabled,
ruleCustomizationStatus,
}: CalculateRuleSourceProps): Promise<RuleSource> {
if (rule.immutable) {
// This is a prebuilt rule and, despite the name, they are not immutable. So
@ -38,7 +39,7 @@ export async function calculateRuleSource({
const isCustomized = calculateIsCustomized({
baseRule,
nextRule: rule,
isRuleCustomizationEnabled,
ruleCustomizationStatus,
});
return {

View file

@ -19,6 +19,7 @@ import { validateMlAuth, toggleRuleEnabledOnUpdate } from '../utils';
import { createRule } from './create_rule';
import { getRuleByRuleId } from './get_rule_by_rule_id';
import { createRuleImportErrorObject } from '../../import/errors';
import type { PrebuiltRulesCustomizationStatus } from '../../../../../../../common/detection_engine/prebuilt_rules/prebuilt_rule_customization_status';
interface ImportRuleOptions {
actionsClient: ActionsClient;
@ -26,7 +27,7 @@ interface ImportRuleOptions {
prebuiltRuleAssetClient: IPrebuiltRuleAssetsClient;
importRulePayload: ImportRuleArgs;
mlAuthz: MlAuthz;
isRuleCustomizationEnabled: boolean;
ruleCustomizationStatus: PrebuiltRulesCustomizationStatus;
}
export const importRule = async ({
@ -35,7 +36,7 @@ export const importRule = async ({
importRulePayload,
prebuiltRuleAssetClient,
mlAuthz,
isRuleCustomizationEnabled,
ruleCustomizationStatus,
}: ImportRuleOptions): Promise<RuleResponse> => {
const { ruleToImport, overwriteRules, overrideFields, allowMissingConnectorSecrets } =
importRulePayload;
@ -62,7 +63,7 @@ export const importRule = async ({
prebuiltRuleAssetClient,
existingRule,
ruleUpdate: rule,
isRuleCustomizationEnabled,
ruleCustomizationStatus,
});
// applyRuleUpdate prefers the existing rule's values for `rule_source` and `immutable`, but we want to use the importing rule's calculated values
ruleWithUpdates = { ...ruleWithUpdates, ...overrideFields };

View file

@ -21,6 +21,7 @@ import { convertAlertingRuleToRuleResponse } from '../converters/convert_alertin
import { convertRuleResponseToAlertingRule } from '../converters/convert_rule_response_to_alerting_rule';
import { ClientError, toggleRuleEnabledOnUpdate, validateMlAuth } from '../utils';
import { getRuleByIdOrRuleId } from './get_rule_by_id_or_rule_id';
import type { PrebuiltRulesCustomizationStatus } from '../../../../../../../common/detection_engine/prebuilt_rules/prebuilt_rule_customization_status';
interface PatchRuleOptions {
actionsClient: ActionsClient;
@ -28,7 +29,7 @@ interface PatchRuleOptions {
prebuiltRuleAssetClient: IPrebuiltRuleAssetsClient;
rulePatch: RulePatchProps;
mlAuthz: MlAuthz;
isRuleCustomizationEnabled: boolean;
ruleCustomizationStatus: PrebuiltRulesCustomizationStatus;
}
export const patchRule = async ({
@ -37,7 +38,7 @@ export const patchRule = async ({
prebuiltRuleAssetClient,
rulePatch,
mlAuthz,
isRuleCustomizationEnabled,
ruleCustomizationStatus,
}: PatchRuleOptions): Promise<RuleResponse> => {
const { rule_id: ruleId, id } = rulePatch;
@ -60,7 +61,7 @@ export const patchRule = async ({
prebuiltRuleAssetClient,
existingRule,
rulePatch,
isRuleCustomizationEnabled,
ruleCustomizationStatus,
});
const patchedInternalRule = await rulesClient.update({

View file

@ -20,6 +20,7 @@ import type { RuleUpdateProps } from '../../../../../../../common/api/detection_
import type { IPrebuiltRuleAssetsClient } from '../../../../prebuilt_rules/logic/rule_assets/prebuilt_rule_assets_client';
import { getRuleByIdOrRuleId } from './get_rule_by_id_or_rule_id';
import { convertAlertingRuleToRuleResponse } from '../converters/convert_alerting_rule_to_rule_response';
import type { PrebuiltRulesCustomizationStatus } from '../../../../../../../common/detection_engine/prebuilt_rules/prebuilt_rule_customization_status';
interface UpdateRuleArguments {
actionsClient: ActionsClient;
@ -27,7 +28,7 @@ interface UpdateRuleArguments {
prebuiltRuleAssetClient: IPrebuiltRuleAssetsClient;
ruleUpdate: RuleUpdateProps;
mlAuthz: MlAuthz;
isRuleCustomizationEnabled: boolean;
ruleCustomizationStatus: PrebuiltRulesCustomizationStatus;
}
export const updateRule = async ({
@ -36,7 +37,7 @@ export const updateRule = async ({
prebuiltRuleAssetClient,
ruleUpdate,
mlAuthz,
isRuleCustomizationEnabled,
ruleCustomizationStatus,
}: UpdateRuleArguments): Promise<RuleResponse> => {
const { rule_id: ruleId, id } = ruleUpdate;
@ -59,7 +60,7 @@ export const updateRule = async ({
prebuiltRuleAssetClient,
existingRule,
ruleUpdate,
isRuleCustomizationEnabled,
ruleCustomizationStatus,
});
const updatedRule = await rulesClient.update({

View file

@ -18,6 +18,7 @@ import { applyRuleUpdate } from '../mergers/apply_rule_update';
import { ClientError, validateMlAuth } from '../utils';
import { createRule } from './create_rule';
import { getRuleByRuleId } from './get_rule_by_rule_id';
import type { PrebuiltRulesCustomizationStatus } from '../../../../../../../common/detection_engine/prebuilt_rules/prebuilt_rule_customization_status';
export const upgradePrebuiltRule = async ({
actionsClient,
@ -25,14 +26,14 @@ export const upgradePrebuiltRule = async ({
ruleAsset,
mlAuthz,
prebuiltRuleAssetClient,
isRuleCustomizationEnabled,
ruleCustomizationStatus,
}: {
actionsClient: ActionsClient;
rulesClient: RulesClient;
ruleAsset: PrebuiltRuleAsset;
mlAuthz: MlAuthz;
prebuiltRuleAssetClient: IPrebuiltRuleAssetsClient;
isRuleCustomizationEnabled: boolean;
ruleCustomizationStatus: PrebuiltRulesCustomizationStatus;
}): Promise<RuleResponse> => {
await validateMlAuth(mlAuthz, ruleAsset.type);
@ -75,7 +76,7 @@ export const upgradePrebuiltRule = async ({
prebuiltRuleAssetClient,
existingRule,
ruleUpdate: ruleAsset,
isRuleCustomizationEnabled,
ruleCustomizationStatus,
});
const updatedInternalRule = await rulesClient.update({

View file

@ -6,16 +6,21 @@
*/
import { getRulesSchemaMock } from '../../../../../../common/api/detection_engine/model/rule_schema/rule_response_schema.mock';
import type { PrebuiltRulesCustomizationStatus } from '../../../../../../common/detection_engine/prebuilt_rules/prebuilt_rule_customization_status';
import { getPrebuiltRuleMock } from '../../../prebuilt_rules/mocks';
import { calculateRuleSourceForImport } from './calculate_rule_source_for_import';
const ruleCustomizationStatus: PrebuiltRulesCustomizationStatus = {
isRulesCustomizationEnabled: true,
};
describe('calculateRuleSourceForImport', () => {
it('calculates as internal if no asset is found', () => {
const result = calculateRuleSourceForImport({
rule: getRulesSchemaMock(),
prebuiltRuleAssetsByRuleId: {},
isKnownPrebuiltRule: false,
isRuleCustomizationEnabled: true,
ruleCustomizationStatus,
});
expect(result).toEqual({
@ -34,7 +39,7 @@ describe('calculateRuleSourceForImport', () => {
rule,
prebuiltRuleAssetsByRuleId: {},
isKnownPrebuiltRule: true,
isRuleCustomizationEnabled: true,
ruleCustomizationStatus,
});
expect(result).toEqual({
@ -55,7 +60,7 @@ describe('calculateRuleSourceForImport', () => {
rule,
prebuiltRuleAssetsByRuleId,
isKnownPrebuiltRule: true,
isRuleCustomizationEnabled: true,
ruleCustomizationStatus,
});
expect(result).toEqual({
@ -76,7 +81,7 @@ describe('calculateRuleSourceForImport', () => {
rule,
prebuiltRuleAssetsByRuleId,
isKnownPrebuiltRule: true,
isRuleCustomizationEnabled: true,
ruleCustomizationStatus,
});
expect(result).toEqual({

View file

@ -9,6 +9,7 @@ import type {
RuleSource,
ValidatedRuleToImport,
} from '../../../../../../common/api/detection_engine';
import type { PrebuiltRulesCustomizationStatus } from '../../../../../../common/detection_engine/prebuilt_rules/prebuilt_rule_customization_status';
import type { PrebuiltRuleAsset } from '../../../prebuilt_rules';
import { calculateRuleSourceFromAsset } from './calculate_rule_source_from_asset';
import { convertRuleToImportToRuleResponse } from './converters/convert_rule_to_import_to_rule_response';
@ -28,12 +29,12 @@ export const calculateRuleSourceForImport = ({
rule,
prebuiltRuleAssetsByRuleId,
isKnownPrebuiltRule,
isRuleCustomizationEnabled,
ruleCustomizationStatus,
}: {
rule: ValidatedRuleToImport;
prebuiltRuleAssetsByRuleId: Record<string, PrebuiltRuleAsset>;
isKnownPrebuiltRule: boolean;
isRuleCustomizationEnabled: boolean;
ruleCustomizationStatus: PrebuiltRulesCustomizationStatus;
}): { ruleSource: RuleSource; immutable: boolean } => {
const assetWithMatchingVersion = prebuiltRuleAssetsByRuleId[rule.rule_id];
// We convert here so that RuleSource calculation can
@ -45,7 +46,7 @@ export const calculateRuleSourceForImport = ({
rule: ruleResponseForImport,
assetWithMatchingVersion,
isKnownPrebuiltRule,
isRuleCustomizationEnabled,
ruleCustomizationStatus,
});
return {

View file

@ -8,6 +8,11 @@
import { calculateRuleSourceFromAsset } from './calculate_rule_source_from_asset';
import { getRulesSchemaMock } from '../../../../../../common/api/detection_engine/model/rule_schema/mocks';
import { getPrebuiltRuleMock } from '../../../prebuilt_rules/mocks';
import type { PrebuiltRulesCustomizationStatus } from '../../../../../../common/detection_engine/prebuilt_rules/prebuilt_rule_customization_status';
const ruleCustomizationStatus: PrebuiltRulesCustomizationStatus = {
isRulesCustomizationEnabled: true,
};
describe('calculateRuleSourceFromAsset', () => {
it('calculates as internal if no asset is found', () => {
@ -15,7 +20,7 @@ describe('calculateRuleSourceFromAsset', () => {
rule: getRulesSchemaMock(),
assetWithMatchingVersion: undefined,
isKnownPrebuiltRule: false,
isRuleCustomizationEnabled: true,
ruleCustomizationStatus,
});
expect(result).toEqual({
@ -29,7 +34,7 @@ describe('calculateRuleSourceFromAsset', () => {
rule: ruleToImport,
assetWithMatchingVersion: undefined,
isKnownPrebuiltRule: true,
isRuleCustomizationEnabled: true,
ruleCustomizationStatus,
});
expect(result).toEqual({
@ -49,7 +54,7 @@ describe('calculateRuleSourceFromAsset', () => {
// no other overwrites -> no differences
}),
isKnownPrebuiltRule: true,
isRuleCustomizationEnabled: true,
ruleCustomizationStatus,
});
expect(result).toEqual({
@ -68,7 +73,7 @@ describe('calculateRuleSourceFromAsset', () => {
name: 'Customized name', // mock a customization
}),
isKnownPrebuiltRule: true,
isRuleCustomizationEnabled: true,
ruleCustomizationStatus,
});
expect(result).toEqual({

View file

@ -6,6 +6,7 @@
*/
import type { RuleResponse, RuleSource } from '../../../../../../common/api/detection_engine';
import type { PrebuiltRulesCustomizationStatus } from '../../../../../../common/detection_engine/prebuilt_rules/prebuilt_rule_customization_status';
import type { PrebuiltRuleAsset } from '../../../prebuilt_rules';
import { calculateIsCustomized } from '../detection_rules_client/mergers/rule_source/calculate_is_customized';
@ -24,12 +25,12 @@ export const calculateRuleSourceFromAsset = ({
rule,
assetWithMatchingVersion,
isKnownPrebuiltRule,
isRuleCustomizationEnabled,
ruleCustomizationStatus,
}: {
rule: RuleResponse;
assetWithMatchingVersion: PrebuiltRuleAsset | undefined;
isKnownPrebuiltRule: boolean;
isRuleCustomizationEnabled: boolean;
ruleCustomizationStatus: PrebuiltRulesCustomizationStatus;
}): RuleSource => {
if (!isKnownPrebuiltRule) {
return {
@ -40,14 +41,14 @@ export const calculateRuleSourceFromAsset = ({
if (assetWithMatchingVersion == null) {
return {
type: 'external',
is_customized: isRuleCustomizationEnabled ? true : false,
is_customized: ruleCustomizationStatus.isRulesCustomizationEnabled ? true : false,
};
}
const isCustomized = calculateIsCustomized({
baseRule: assetWithMatchingVersion,
nextRule: rule,
isRuleCustomizationEnabled,
ruleCustomizationStatus,
});
return {

View file

@ -10,7 +10,7 @@ import type {
ValidatedRuleToImport,
} from '../../../../../../../common/api/detection_engine';
import { createPrebuiltRuleAssetsClient as createPrebuiltRuleAssetsClientMock } from '../../../../prebuilt_rules/logic/rule_assets/__mocks__/prebuilt_rule_assets_client';
import { configMock, createMockConfig, requestContextMock } from '../../../../routes/__mocks__';
import { createMockConfig, requestContextMock } from '../../../../routes/__mocks__';
import { getPrebuiltRuleMock } from '../../../../prebuilt_rules/mocks';
import { createRuleSourceImporter } from './rule_source_importer';
import * as calculateRuleSourceModule from '../calculate_rule_source_for_import';
@ -25,7 +25,6 @@ describe('ruleSourceImporter', () => {
beforeEach(() => {
jest.clearAllMocks();
config = createMockConfig();
config = configMock.withExperimentalFeature(config, 'prebuiltRulesCustomizationEnabled');
context = requestContextMock.create().securitySolution;
ruleAssetsClientMock = createPrebuiltRuleAssetsClientMock();
ruleAssetsClientMock.fetchLatestAssets.mockResolvedValue([{}]);
@ -37,6 +36,7 @@ describe('ruleSourceImporter', () => {
context,
config,
prebuiltRuleAssetsClient: ruleAssetsClientMock,
ruleCustomizationStatus: { isRulesCustomizationEnabled: true },
});
});
@ -134,12 +134,13 @@ describe('ruleSourceImporter', () => {
await subject.calculateRuleSource(rule);
expect(calculatorSpy).toHaveBeenCalledTimes(1);
expect(calculatorSpy).toHaveBeenCalledWith({
rule,
prebuiltRuleAssetsByRuleId: { 'rule-1': expect.objectContaining({ rule_id: 'rule-1' }) },
isKnownPrebuiltRule: true,
isRuleCustomizationEnabled: true,
});
expect(calculatorSpy).toHaveBeenCalledWith(
expect.objectContaining({
rule,
prebuiltRuleAssetsByRuleId: { 'rule-1': expect.objectContaining({ rule_id: 'rule-1' }) },
isKnownPrebuiltRule: true,
})
);
});
it('throws an error if the rule is not known to the calculator', async () => {
@ -163,12 +164,15 @@ describe('ruleSourceImporter', () => {
await subject.calculateRuleSource(rule);
expect(calculatorSpy).toHaveBeenCalledTimes(1);
expect(calculatorSpy).toHaveBeenCalledWith({
rule,
prebuiltRuleAssetsByRuleId: { 'rule-1': expect.objectContaining({ rule_id: 'rule-1' }) },
isKnownPrebuiltRule: true,
isRuleCustomizationEnabled: true,
});
expect(calculatorSpy).toHaveBeenCalledWith(
expect.objectContaining({
rule,
prebuiltRuleAssetsByRuleId: {
'rule-1': expect.objectContaining({ rule_id: 'rule-1' }),
},
isKnownPrebuiltRule: true,
})
);
});
});
});

View file

@ -23,6 +23,7 @@ import type { IPrebuiltRuleAssetsClient } from '../../../../prebuilt_rules/logic
import { ensureLatestRulesPackageInstalled } from '../../../../prebuilt_rules/logic/ensure_latest_rules_package_installed';
import { calculateRuleSourceForImport } from '../calculate_rule_source_for_import';
import type { CalculatedRuleSource, IRuleSourceImporter } from './rule_source_importer_interface';
import type { PrebuiltRulesCustomizationStatus } from '../../../../../../../common/detection_engine/prebuilt_rules/prebuilt_rule_customization_status';
interface RuleSpecifier {
rule_id: string;
@ -95,6 +96,7 @@ export class RuleSourceImporter implements IRuleSourceImporter {
private context: SecuritySolutionApiRequestHandlerContext;
private config: ConfigType;
private ruleAssetsClient: IPrebuiltRuleAssetsClient;
private ruleCustomizationStatus: PrebuiltRulesCustomizationStatus;
private latestPackagesInstalled: boolean = false;
private matchingAssetsByRuleId: Record<string, PrebuiltRuleAsset> = {};
private knownRules: RuleSpecifier[] = [];
@ -104,14 +106,17 @@ export class RuleSourceImporter implements IRuleSourceImporter {
config,
context,
prebuiltRuleAssetsClient,
ruleCustomizationStatus,
}: {
config: ConfigType;
context: SecuritySolutionApiRequestHandlerContext;
prebuiltRuleAssetsClient: IPrebuiltRuleAssetsClient;
ruleCustomizationStatus: PrebuiltRulesCustomizationStatus;
}) {
this.config = config;
this.ruleAssetsClient = prebuiltRuleAssetsClient;
this.context = context;
this.config = config;
this.ruleCustomizationStatus = ruleCustomizationStatus;
}
/**
@ -143,8 +148,7 @@ export class RuleSourceImporter implements IRuleSourceImporter {
rule,
prebuiltRuleAssetsByRuleId: this.matchingAssetsByRuleId,
isKnownPrebuiltRule: this.availableRuleAssetIds.has(rule.rule_id),
isRuleCustomizationEnabled:
this.config.experimentalFeatures.prebuiltRulesCustomizationEnabled,
ruleCustomizationStatus: this.ruleCustomizationStatus,
});
}
@ -196,10 +200,17 @@ export const createRuleSourceImporter = ({
config,
context,
prebuiltRuleAssetsClient,
ruleCustomizationStatus,
}: {
config: ConfigType;
context: SecuritySolutionApiRequestHandlerContext;
prebuiltRuleAssetsClient: IPrebuiltRuleAssetsClient;
ruleCustomizationStatus: PrebuiltRulesCustomizationStatus;
}): RuleSourceImporter => {
return new RuleSourceImporter({ config, context, prebuiltRuleAssetsClient });
return new RuleSourceImporter({
config,
context,
prebuiltRuleAssetsClient,
ruleCustomizationStatus,
});
};

View file

@ -279,6 +279,7 @@ export class Plugin implements ISecuritySolutionPlugin {
kibanaVersion: pluginContext.env.packageInfo.version,
kibanaBranch: pluginContext.env.packageInfo.branch,
buildFlavor: pluginContext.env.packageInfo.buildFlavor,
productFeaturesService,
});
productFeaturesService.registerApiAccessControl(core.http);

View file

@ -36,6 +36,7 @@ import { buildMlAuthz } from './lib/machine_learning/authz';
import { EntityStoreDataClient } from './lib/entity_analytics/entity_store/entity_store_data_client';
import type { SiemMigrationsService } from './lib/siem_migrations/siem_migrations_service';
import { AssetInventoryDataClient } from './lib/asset_inventory/asset_inventory_data_client';
import type { ProductFeaturesService } from './lib/product_features_service';
export interface IRequestContextFactory {
create(
@ -55,6 +56,7 @@ interface ConstructorOptions {
kibanaVersion: string;
kibanaBranch: string;
buildFlavor: BuildFlavor;
productFeaturesService: ProductFeaturesService;
}
export class RequestContextFactory implements IRequestContextFactory {
@ -76,6 +78,7 @@ export class RequestContextFactory implements IRequestContextFactory {
endpointAppContextService,
ruleMonitoringService,
siemMigrationsService,
productFeaturesService,
} = options;
const { lists, ruleRegistry, security } = plugins;
@ -155,7 +158,9 @@ export class RequestContextFactory implements IRequestContextFactory {
actionsClient,
savedObjectsClient: coreContext.savedObjects.client,
mlAuthz,
isRuleCustomizationEnabled: config.experimentalFeatures.prebuiltRulesCustomizationEnabled,
experimentalFeatures: config.experimentalFeatures,
productFeaturesService,
license: licensing.license,
});
}),

View file

@ -76,7 +76,7 @@ export const initRoutes = (
) => {
registerFleetIntegrationsRoutes(router);
registerLegacyRuleActionsRoutes(router, logger);
registerPrebuiltRulesRoutes(router, config);
registerPrebuiltRulesRoutes(router);
registerRuleExceptionsRoutes(router);
registerManageExceptionsRoutes(router);
registerRuleManagementRoutes(router, config, ml, logger);

View file

@ -13,6 +13,7 @@ import {
UPGRADE_ALERT_ASSIGNMENTS,
UPGRADE_INVESTIGATION_GUIDE,
UPGRADE_NOTES_MANAGEMENT_USER_FILTER,
UPGRADE_PREBUILT_RULE_CUSTOMIZATION,
} from '@kbn/security-solution-upselling/messages';
import type {
MessageUpsellings,
@ -138,4 +139,9 @@ export const upsellingMessages: UpsellingMessages = [
minimumLicenseRequired: 'platinum',
message: UPGRADE_NOTES_MANAGEMENT_USER_FILTER('Platinum'),
},
{
id: 'prebuilt_rule_customization',
minimumLicenseRequired: 'enterprise',
message: UPGRADE_PREBUILT_RULE_CUSTOMIZATION('Enterprise'),
},
];

View file

@ -29,6 +29,7 @@ export const PLI_PRODUCT_FEATURES: PliProductFeatures = {
ProductFeatureKey.casesConnectors,
ProductFeatureKey.externalRuleActions,
ProductFeatureKey.automaticImport,
ProductFeatureKey.prebuiltRuleCustomization,
],
},
endpoint: {

View file

@ -10,6 +10,7 @@ import { SecurityPageName } from '@kbn/security-solution-plugin/common';
import {
UPGRADE_INVESTIGATION_GUIDE,
UPGRADE_INVESTIGATION_GUIDE_INTERACTIONS,
UPGRADE_PREBUILT_RULE_CUSTOMIZATION,
} from '@kbn/security-solution-upselling/messages';
import type {
UpsellingMessageId,
@ -162,4 +163,11 @@ export const upsellingMessages: UpsellingMessages = [
getProductTypeByPLI(ProductFeatureKey.investigationGuideInteractions) ?? ''
),
},
{
id: 'prebuilt_rule_customization',
pli: ProductFeatureKey.prebuiltRuleCustomization,
message: UPGRADE_PREBUILT_RULE_CUSTOMIZATION(
getProductTypeByPLI(ProductFeatureKey.prebuiltRuleCustomization) ?? ''
),
},
];

View file

@ -17,8 +17,6 @@ export default ({ loadTestFile }: FtrProviderContext): void => {
loadTestFile(require.resolve('./upgrade_prebuilt_rules'));
loadTestFile(require.resolve('./upgrade_prebuilt_rules_with_historical_versions'));
loadTestFile(require.resolve('./fleet_integration'));
loadTestFile(require.resolve('./upgrade_perform_prebuilt_rules.all_rules_mode'));
loadTestFile(require.resolve('./upgrade_perform_prebuilt_rules.specific_rules_mode'));
loadTestFile(require.resolve('./upgrade_review_prebuilt_rules.rule_type_fields'));
loadTestFile(require.resolve('./upgrade_review_prebuilt_rules.number_fields'));
loadTestFile(require.resolve('./upgrade_review_prebuilt_rules.single_line_string_fields'));

View file

@ -0,0 +1,34 @@
/*
* 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 { FtrConfigProviderContext } from '@kbn/test';
export default async function ({ readConfigFile }: FtrConfigProviderContext) {
const functionalConfig = await readConfigFile(
require.resolve('../../../../../../../config/ess/config.base.basic')
);
const testConfig = {
...functionalConfig.getAll(),
testFiles: [require.resolve('..')],
junit: {
reportName:
'Rules Management - Prebuilt Rule Customization Disabled Integration Tests - ESS Env Basic License',
},
};
testConfig.kbnTestServer.serverArgs = testConfig.kbnTestServer.serverArgs.map((arg: string) => {
// Override the default value of `--xpack.securitySolution.enableExperimental` to enable the prebuilt rules customization feature
if (arg.includes('--xpack.securitySolution.enableExperimental')) {
return `--xpack.securitySolution.enableExperimental=${JSON.stringify([
'prebuiltRulesCustomizationEnabled',
])}`;
}
return arg;
});
return testConfig;
}

View file

@ -17,7 +17,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
testFiles: [require.resolve('..')],
junit: {
reportName:
'Rules Management - Prebuilt Rule Customization Disabled Integration Tests - ESS Env',
'Rules Management - Prebuilt Rule Customization Disabled Integration Tests - ESS Env Trial License',
},
};

View file

@ -11,7 +11,7 @@ export default createTestConfig({
testFiles: [require.resolve('..')],
junit: {
reportName:
'Rules Management - Prebuilt Rule Customization Disabled Integration Tests - Serverless Env',
'Rules Management - Prebuilt Rule Customization Disabled Integration Tests - Serverless Env Complete Tier',
},
kbnTestServerArgs: [],
});

View file

@ -0,0 +1,21 @@
/*
* 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 { createTestConfig } from '../../../../../../../config/serverless/config.base.essentials';
export default createTestConfig({
testFiles: [require.resolve('..')],
junit: {
reportName:
'Rules Management - Prebuilt Rule Customization Disabled Integration Tests - Serverless Env Essentials Tier',
},
kbnTestServerArgs: [
`--xpack.securitySolution.enableExperimental=${JSON.stringify([
'prebuiltRulesCustomizationEnabled',
])}`,
],
});

View file

@ -10,5 +10,6 @@ import { FtrProviderContext } from '../../../../../../ftr_provider_context';
export default ({ loadTestFile }: FtrProviderContext): void => {
describe('Rules Management - Prebuilt Rules - Prebuilt Rule Customization Disabled', function () {
loadTestFile(require.resolve('./is_customized_calculation'));
loadTestFile(require.resolve('./upgrade_perform_prebuilt_rules'));
});
};

View file

@ -35,79 +35,6 @@ export default ({ getService }: FtrProviderContext) => {
await deleteAllPrebuiltRuleAssets(es, log);
});
it('should set is_customized to "false" on prebuilt rule PATCH', async () => {
await createPrebuiltRuleAssetSavedObjects(es, [ruleAsset]);
await installPrebuiltRules(es, supertest);
const { body: findResult } = await securitySolutionApi
.findRules({
query: {
per_page: 1,
filter: `alert.attributes.params.immutable: true`,
},
})
.expect(200);
const prebuiltRule = findResult.data[0];
// Check that the rule has been created and is not customized
expect(prebuiltRule).not.toBeNull();
expect(prebuiltRule.rule_source.is_customized).toEqual(false);
const { body } = await securitySolutionApi
.patchRule({
body: {
rule_id: 'test-rule-id',
name: 'some other rule name',
},
})
.expect(200);
// Check that the rule name has been updated and the rule is still not customized
expect(body).toEqual(
expect.objectContaining({
name: 'some other rule name',
})
);
expect(body.rule_source.is_customized).toEqual(false);
});
it('should set is_customized to "false" on prebuilt rule UPDATE', async () => {
await createPrebuiltRuleAssetSavedObjects(es, [ruleAsset]);
await installPrebuiltRules(es, supertest);
const { body: findResult } = await securitySolutionApi
.findRules({
query: {
per_page: 1,
filter: `alert.attributes.params.immutable: true`,
},
})
.expect(200);
const prebuiltRule = findResult.data[0];
// Check that the rule has been created and is not customized
expect(prebuiltRule).not.toBeNull();
expect(prebuiltRule.rule_source.is_customized).toEqual(false);
const { body } = await securitySolutionApi
.updateRule({
body: {
...prebuiltRule,
id: undefined, // id together with rule_id is not allowed
name: 'some other rule name',
},
})
.expect(200);
// Check that the rule name has been updated and the rule is still not customized
expect(body).toEqual(
expect.objectContaining({
name: 'some other rule name',
})
);
expect(body.rule_source.is_customized).toEqual(false);
});
it('should not allow prebuilt rule customization on import', async () => {
await createPrebuiltRuleAssetSavedObjects(es, [ruleAsset]);
await installPrebuiltRules(es, supertest);

View file

@ -0,0 +1,172 @@
/*
* 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 { Client } from '@elastic/elasticsearch';
import {
ModeEnum,
PERFORM_RULE_UPGRADE_URL,
PickVersionValuesEnum,
QueryRuleCreateFields,
} from '@kbn/security-solution-plugin/common/api/detection_engine';
import expect from 'expect';
import { deleteAllRules } from '../../../../../../../common/utils/security_solution';
import { FtrProviderContext } from '../../../../../../ftr_provider_context';
import {
createHistoricalPrebuiltRuleAssetSavedObjects,
createRuleAssetSavedObjectOfType,
deleteAllPrebuiltRuleAssets,
installPrebuiltRules,
} from '../../../../utils';
const createBaseRuleVersion = async (es: Client) => {
const ruleAsset = createRuleAssetSavedObjectOfType<QueryRuleCreateFields>('query');
await createHistoricalPrebuiltRuleAssetSavedObjects(es, [ruleAsset]);
return ruleAsset;
};
const createTargetRuleVersion = async (es: Client) => {
const ruleAsset = createRuleAssetSavedObjectOfType<QueryRuleCreateFields>('query');
ruleAsset['security-rule'].version += 1;
ruleAsset['security-rule'].name = 'New rule name';
await createHistoricalPrebuiltRuleAssetSavedObjects(es, [ruleAsset]);
return ruleAsset;
};
export default ({ getService }: FtrProviderContext): void => {
const es = getService('es');
const supertest = getService('supertest');
const log = getService('log');
describe('@ess @serverless @skipInServerlessMKI Perform Prebuilt Rule Upgrades - Customization Disabled', () => {
beforeEach(async () => {
await deleteAllRules(supertest, log);
await deleteAllPrebuiltRuleAssets(es, log);
});
// All pick version values but 'TARGET' should be blocked
[
PickVersionValuesEnum.BASE,
PickVersionValuesEnum.CURRENT,
PickVersionValuesEnum.MERGED,
].forEach((pickVersion) => {
it(`blocks upgrade to the ${pickVersion} version`, async () => {
// Install base prebuilt detection rule
await createBaseRuleVersion(es);
await installPrebuiltRules(es, supertest);
// Create a new target version of the rule
await createTargetRuleVersion(es);
const { body } = await supertest
.post(PERFORM_RULE_UPGRADE_URL)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '1')
.set('x-elastic-internal-origin', 'foo')
.send({
mode: ModeEnum.ALL_RULES,
pick_version: pickVersion,
})
.expect(400);
expect(body.message).toEqual(
`Only the 'TARGET' version can be selected for a rule update; received: '${pickVersion}'`
);
});
});
it('blocks upgrading rule fields to resolved values', async () => {
// Install base prebuilt detection rule
const baseVersion = await createBaseRuleVersion(es);
await installPrebuiltRules(es, supertest);
// Create a new target version of the rule
const targetVersion = await createTargetRuleVersion(es);
const { body } = await supertest
.post(PERFORM_RULE_UPGRADE_URL)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '1')
.set('x-elastic-internal-origin', 'foo')
.send({
mode: ModeEnum.SPECIFIC_RULES,
rules: [
{
rule_id: baseVersion['security-rule'].rule_id,
version: targetVersion['security-rule'].version,
revision: 0,
fields: {
name: {
pick_version: 'RESOLVED',
resolved_value: 'foo',
},
},
},
],
})
.expect(400);
expect(body.message).toEqual(
'Rule field customization is not allowed. Received fields: name'
);
});
it('allows upgrading all rules to the TARGET version', async () => {
// Install base prebuilt detection rule
await createBaseRuleVersion(es);
await installPrebuiltRules(es, supertest);
// Create a new target version of the rule
await createTargetRuleVersion(es);
const { body } = await supertest
.post(PERFORM_RULE_UPGRADE_URL)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '1')
.set('x-elastic-internal-origin', 'foo')
.send({
mode: ModeEnum.ALL_RULES,
pick_version: PickVersionValuesEnum.TARGET,
})
.expect(200);
expect(body.errors.length).toEqual(0);
expect(body.summary.succeeded).toEqual(1);
});
it('allows upgrading specific rules to the TARGET version', async () => {
// Install base prebuilt detection rule
const baseVersion = await createBaseRuleVersion(es);
await installPrebuiltRules(es, supertest);
// Create a new target version of the rule
const targetVersion = await createTargetRuleVersion(es);
const { body } = await supertest
.post(PERFORM_RULE_UPGRADE_URL)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '1')
.set('x-elastic-internal-origin', 'foo')
.send({
mode: ModeEnum.SPECIFIC_RULES,
rules: [
{
rule_id: baseVersion['security-rule'].rule_id,
version: targetVersion['security-rule'].version,
pick_version: PickVersionValuesEnum.TARGET,
revision: 0,
},
],
})
.expect(200);
expect(body.errors.length).toEqual(0);
expect(body.summary.succeeded).toEqual(1);
});
});
};

View file

@ -12,5 +12,7 @@ export default ({ loadTestFile }: FtrProviderContext): void => {
loadTestFile(require.resolve('./is_customized_calculation'));
loadTestFile(require.resolve('./import_rules'));
loadTestFile(require.resolve('./rules_export'));
loadTestFile(require.resolve('./upgrade_perform_prebuilt_rules.all_rules_mode'));
loadTestFile(require.resolve('./upgrade_perform_prebuilt_rules.specific_rules_mode'));
});
};

Some files were not shown because too many files have changed in this diff Show more