mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[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.  - 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:  - On the rule update flyout:  - On the rule details page:  - 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:
parent
5b78d3295a
commit
199378c60c
101 changed files with 1133 additions and 673 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -44990,6 +44990,7 @@ components:
|
|||
Security_Detections_API_BulkActionsDryRunErrCode:
|
||||
enum:
|
||||
- IMMUTABLE
|
||||
- PREBUILT_CUSTOMIZATION_LICENSE
|
||||
- MACHINE_LEARNING_AUTH
|
||||
- MACHINE_LEARNING_INDEX_PATTERN
|
||||
- ESQL_INDEX_PATTERN
|
||||
|
|
|
@ -51546,6 +51546,7 @@ components:
|
|||
Security_Detections_API_BulkActionsDryRunErrCode:
|
||||
enum:
|
||||
- IMMUTABLE
|
||||
- PREBUILT_CUSTOMIZATION_LICENSE
|
||||
- MACHINE_LEARNING_AUTH
|
||||
- MACHINE_LEARNING_INDEX_PATTERN
|
||||
- ESQL_INDEX_PATTERN
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -135,4 +135,5 @@ export const securityDefaultProductFeaturesConfig: DefaultSecurityProductFeature
|
|||
|
||||
// Security PLIs
|
||||
[ProductFeatureSecurityKey.automaticImport]: {},
|
||||
[ProductFeatureSecurityKey.prebuiltRuleCustomization]: {},
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -75,6 +75,7 @@ components:
|
|||
type: string
|
||||
enum:
|
||||
- IMMUTABLE
|
||||
- PREBUILT_CUSTOMIZATION_LICENSE
|
||||
- MACHINE_LEARNING_AUTH
|
||||
- MACHINE_LEARNING_INDEX_PATTERN
|
||||
- ESQL_INDEX_PATTERN
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -1761,6 +1761,7 @@ components:
|
|||
BulkActionsDryRunErrCode:
|
||||
enum:
|
||||
- IMMUTABLE
|
||||
- PREBUILT_CUSTOMIZATION_LICENSE
|
||||
- MACHINE_LEARNING_AUTH
|
||||
- MACHINE_LEARNING_INDEX_PATTERN
|
||||
- ESQL_INDEX_PATTERN
|
||||
|
|
|
@ -1037,6 +1037,7 @@ components:
|
|||
BulkActionsDryRunErrCode:
|
||||
enum:
|
||||
- IMMUTABLE
|
||||
- PREBUILT_CUSTOMIZATION_LICENSE
|
||||
- MACHINE_LEARNING_AUTH
|
||||
- MACHINE_LEARNING_INDEX_PATTERN
|
||||
- ESQL_INDEX_PATTERN
|
||||
|
|
|
@ -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>
|
||||
));
|
||||
};
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
: [];
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
};
|
|
@ -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;
|
||||
};
|
|
@ -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,
|
||||
};
|
||||
};
|
|
@ -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,
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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),
|
||||
},
|
||||
|
|
|
@ -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: [],
|
||||
|
|
|
@ -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'],
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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.',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
]
|
||||
);
|
||||
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
]
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
]
|
||||
);
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 } =
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -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(', ')}`
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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: [
|
||||
{
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
);
|
||||
};
|
||||
|
|
|
@ -19,6 +19,7 @@ const createDetectionRulesClientMock = () => {
|
|||
upgradePrebuiltRule: jest.fn(),
|
||||
importRule: jest.fn(),
|
||||
importRules: jest.fn(),
|
||||
getRuleCustomizationStatus: jest.fn(),
|
||||
};
|
||||
return mocked;
|
||||
};
|
||||
|
|
|
@ -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(),
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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(),
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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(),
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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(),
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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([[], []]);
|
||||
|
|
|
@ -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(),
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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(),
|
||||
});
|
||||
});
|
||||
},
|
||||
|
|
|
@ -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(),
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}),
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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'),
|
||||
},
|
||||
];
|
||||
|
|
|
@ -29,6 +29,7 @@ export const PLI_PRODUCT_FEATURES: PliProductFeatures = {
|
|||
ProductFeatureKey.casesConnectors,
|
||||
ProductFeatureKey.externalRuleActions,
|
||||
ProductFeatureKey.automaticImport,
|
||||
ProductFeatureKey.prebuiltRuleCustomization,
|
||||
],
|
||||
},
|
||||
endpoint: {
|
||||
|
|
|
@ -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) ?? ''
|
||||
),
|
||||
},
|
||||
];
|
||||
|
|
|
@ -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'));
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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',
|
||||
},
|
||||
};
|
||||
|
|
@ -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: [],
|
||||
});
|
|
@ -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',
|
||||
])}`,
|
||||
],
|
||||
});
|
|
@ -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'));
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
};
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue