Repository: elastic/kibana
Pull request #213250: [Security Solution] Account for missing base rule versions in is_customized calculation
Author: login: xcrzx
Labels: name: release_note:skip
description: Skip the PR/issue when compiling release notes
name: v9.0.0
name: Team:Detections and Resp
description: Security Detection Response Team
name: Team: SecuritySolution
description: Security Solutions Team working on SIEM, Endpoint, Timeline, Resolver, etc.
name: Team:Detection Rule Management
description: Security Detection Rule Management Team
name: Feature:Prebuilt Detection Rules
description: Security Solution Prebuilt Detection Rules area
name: backport:version
description: Backport to applied version labels
name: v8.18.0
name: v9.1.0
name: v8.19.0
Created: 2025-03-05T14:40:45Z | Merged: 2025-03-06T18:22:17Z

**Partially addresses: https://github.com/elastic/kibana/issues/210358**

## Summary

### Editing of prebuilt rules with missing base versions

**When the base version** of a currently installed prebuilt rule **is missing** among the `security-rule` asset saved objects, and the user edits this rule:

- We should mark the rule as customized, only if the new rule settings are different from the current rule settings.
  - For example, adding a new tag should mark the rule as customized. Then, if the user removes this tag, the rule should remain to be marked as customized. This matches the current behavior.
  - However, if the user saves the rule without making any changes to it, it should keep its `is_customized` field as is. This is different from the current behavior.

### Importing of prebuilt rules with missing base versions

**When the base version** of a prebuilt rule that is being imported **is missing** among the `security-rule` asset saved objects, and the user imports this rule:

- If this rule is not installed, it should be created with `is_customized` field set to `false`.
- If this rule is already installed, it should be updated.
  - Its `is_customized` field should be set to `true` if the rule from the import payload is not equal to the installed rule.
  - Its `is_customized` field should be be kept unchanged (`false` or `true`) if the rule from the import payload is equal to the installed rule.


<!--ONMERGE {"backportTargets":["8.18","8.x","9.0"]} ONMERGE-->


Diff:
diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_objects/__mocks__/prebuilt_rule_objects_client.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_objects/__mocks__/prebuilt_rule_objects_client.ts
new file mode 100644
index 0000000000000..c6077050ed7ca
--- /dev/null
+++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_objects/__mocks__/prebuilt_rule_objects_client.ts
@@ -0,0 +1,15 @@
+/*
+ * 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 const createPrebuiltRuleObjectsClient = () => {
+  return {
+    fetchInstalledRulesByIds: jest.fn(),
+    fetchInstalledRules: jest.fn(),
+    fetchInstalledRuleVersionsByIds: jest.fn(),
+    fetchInstalledRuleVersions: jest.fn(),
+  };
+};
diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/import_rules/route.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/import_rules/route.ts
index de2c05802df1c..e71d891d409b2 100644
--- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/import_rules/route.ts
+++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/import_rules/route.ts
@@ -40,6 +40,7 @@ import {
 } from '../../../utils/utils';
 import { RULE_MANAGEMENT_IMPORT_EXPORT_SOCKET_TIMEOUT_MS } from '../../timeouts';
 import { PrebuiltRulesCustomizationDisabledReason } from '../../../../../../../common/detection_engine/prebuilt_rules/prebuilt_rule_customization_status';
+import { createPrebuiltRuleObjectsClient } from '../../../../prebuilt_rules/logic/rule_objects/prebuilt_rule_objects_client';
 
 const CHUNK_PARSED_OBJECT_SIZE = 50;
 
@@ -86,6 +87,7 @@ export const importRulesRoute = (router: SecuritySolutionPluginRouter, config: C
             'licensing',
           ]);
 
+          const rulesClient = await ctx.alerting.getRulesClient();
           const detectionRulesClient = ctx.securitySolution.getDetectionRulesClient();
           const ruleCustomizationStatus = detectionRulesClient.getRuleCustomizationStatus();
           const actionsClient = ctx.actions.getActionsClient();
@@ -159,6 +161,7 @@ export const importRulesRoute = (router: SecuritySolutionPluginRouter, config: C
             config,
             context: ctx.securitySolution,
             prebuiltRuleAssetsClient: createPrebuiltRuleAssetsClient(savedObjectsClient),
+            prebuiltRuleObjectsClient: createPrebuiltRuleObjectsClient(rulesClient),
             ruleCustomizationStatus: detectionRulesClient.getRuleCustomizationStatus(),
           });
 
diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/bulk_edit_rules.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/bulk_edit_rules.ts
index c907b90bb2ebe..4a440c4b211c2 100644
--- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/bulk_edit_rules.ts
+++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/bulk_edit_rules.ts
@@ -70,8 +70,8 @@ export const bulkEditRules = async ({
   const result = await rulesClient.bulkEdit<RuleParams>({
     ids: rules.map((rule) => rule.id),
     operations,
-    paramsModifier: async (rule) => {
-      const ruleParams = rule.params;
+    paramsModifier: async (currentRule) => {
+      const ruleParams = currentRule.params;
 
       await validateBulkEditRule({
         mlAuthz,
@@ -85,23 +85,23 @@ export const bulkEditRules = async ({
         paramsActions
       );
 
-      // Update rule source
-      const updatedRule = {
-        ...rule,
+      const nextRule = convertAlertingRuleToRuleResponse({
+        ...currentRule,
         params: modifiedParams,
-      };
-      const ruleResponse = convertAlertingRuleToRuleResponse(updatedRule);
+      });
+
       let isCustomized = false;
-      if (ruleResponse.immutable === true) {
+      if (nextRule.immutable === true) {
         isCustomized = calculateIsCustomized({
-          baseRule: baseVersionsMap.get(ruleResponse.rule_id),
-          nextRule: ruleResponse,
+          baseRule: baseVersionsMap.get(nextRule.rule_id),
+          currentRule: convertAlertingRuleToRuleResponse(currentRule),
+          nextRule,
           ruleCustomizationStatus,
         });
       }
 
       const ruleSource =
-        ruleResponse.immutable === true
+        nextRule.immutable === true
           ? {
               type: 'external' as const,
               isCustomized,
diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/apply_rule_patch.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/apply_rule_patch.ts
index 8468eec5dbc64..b8ab25992e0b9 100644
--- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/apply_rule_patch.ts
+++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/apply_rule_patch.ts
@@ -123,7 +123,8 @@ export const applyRulePatch = async ({
   };
 
   nextRule.rule_source = await calculateRuleSource({
-    rule: nextRule,
+    nextRule,
+    currentRule: existingRule,
     prebuiltRuleAssetClient,
     ruleCustomizationStatus,
   });
diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/apply_rule_update.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/apply_rule_update.ts
index 9475a3f5b7f55..886d7e2222664 100644
--- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/apply_rule_update.ts
+++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/apply_rule_update.ts
@@ -47,7 +47,8 @@ export const applyRuleUpdate = async ({
   };
 
   nextRule.rule_source = await calculateRuleSource({
-    rule: nextRule,
+    nextRule,
+    currentRule: existingRule,
     prebuiltRuleAssetClient,
     ruleCustomizationStatus,
   });
diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/rule_source/calculate_is_customized.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/rule_source/calculate_is_customized.ts
index 8b0c5a23a337d..e784c150e83ce 100644
--- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/rule_source/calculate_is_customized.ts
+++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/rule_source/calculate_is_customized.ts
@@ -19,34 +19,68 @@ import {
 interface CalculateIsCustomizedArgs {
   baseRule: PrebuiltRuleAsset | undefined;
   nextRule: RuleResponse;
+  // Current rule can be undefined in case of importing a prebuilt rule that is not installed
+  currentRule: RuleResponse | undefined;
   ruleCustomizationStatus: PrebuiltRulesCustomizationStatus;
 }
 
 export function calculateIsCustomized({
   baseRule,
   nextRule,
+  currentRule,
   ruleCustomizationStatus,
 }: CalculateIsCustomizedArgs) {
   if (
     ruleCustomizationStatus.customizationDisabledReason ===
     PrebuiltRulesCustomizationDisabledReason.FeatureFlag
   ) {
-    // We don't want to accidentally mark rules as customized when customization is disabled.
+    // We don't want to accidentally mark rules as customized when customization
+    // is disabled.
     return false;
   }
 
-  if (baseRule == null) {
-    // If the base version is missing, we consider the rule to be customized
+  if (baseRule) {
+    // Base version is available, so we can determine the customization status
+    // by comparing the base version with the next version
+    return areRulesEqual(convertPrebuiltRuleAssetToRuleResponse(baseRule), nextRule) === false;
+  }
+  // Base version is not available, apply a heuristic to determine the
+  // customization status
+
+  if (currentRule == null) {
+    // Current rule is not installed and base rule is not available, so we can't
+    // determine if the rule is customized. Defaulting to false.
+    return false;
+  }
+
+  if (
+    currentRule.rule_source.type === 'external' &&
+    currentRule.rule_source.is_customized === true
+  ) {
+    // If the rule was previously customized, there's no way to determine
+    // whether the customization remained or was reverted. Keeping it as
+    // customized in this case.
     return true;
   }
 
-  const baseRuleWithDefaults = convertPrebuiltRuleAssetToRuleResponse(baseRule);
+  // If the rule has not been customized before, its customization status can be
+  // determined by comparing the current version with the next version.
+  return areRulesEqual(currentRule, nextRule) === false;
+}
 
+/**
+ * A helper function to determine if two rules are equal
+ *
+ * @param ruleA
+ * @param ruleB
+ * @returns true if all rule fields are equal, false otherwise
+ */
+function areRulesEqual(ruleA: RuleResponse, ruleB: RuleResponse) {
   const fieldsDiff = calculateRuleFieldsDiff({
     base_version: MissingVersion,
-    current_version: convertRuleToDiffable(baseRuleWithDefaults),
-    target_version: convertRuleToDiffable(nextRule),
+    current_version: convertRuleToDiffable(ruleA),
+    target_version: convertRuleToDiffable(ruleB),
   });
 
-  return Object.values(fieldsDiff).some((diff) => diff.has_update);
+  return Object.values(fieldsDiff).every((diff) => diff.has_update === false);
 }
diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/rule_source/calculate_rule_source.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/rule_source/calculate_rule_source.test.ts
index 0a7d51895e4bb..b1b9ff306a68b 100644
--- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/rule_source/calculate_rule_source.test.ts
+++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/rule_source/calculate_rule_source.test.ts
@@ -48,7 +48,8 @@ describe('calculateRuleSource', () => {
 
     const result = await calculateRuleSource({
       prebuiltRuleAssetClient,
-      rule,
+      nextRule: rule,
+      currentRule: undefined,
       ruleCustomizationStatus,
     });
     expect(result).toEqual({
@@ -65,7 +66,8 @@ describe('calculateRuleSource', () => {
 
     const result = await calculateRuleSource({
       prebuiltRuleAssetClient,
-      rule,
+      nextRule: rule,
+      currentRule: rule,
       ruleCustomizationStatus,
     });
     expect(result).toEqual(
@@ -86,7 +88,8 @@ describe('calculateRuleSource', () => {
 
     const result = await calculateRuleSource({
       prebuiltRuleAssetClient,
-      rule,
+      nextRule: rule,
+      currentRule: rule,
       ruleCustomizationStatus,
     });
     expect(result).toEqual(
@@ -109,7 +112,8 @@ describe('calculateRuleSource', () => {
 
     const result = await calculateRuleSource({
       prebuiltRuleAssetClient,
-      rule,
+      nextRule: rule,
+      currentRule: rule,
       ruleCustomizationStatus,
     });
     expect(result).toEqual(
@@ -130,7 +134,8 @@ describe('calculateRuleSource', () => {
 
     const result = await calculateRuleSource({
       prebuiltRuleAssetClient,
-      rule,
+      nextRule: rule,
+      currentRule: rule,
       ruleCustomizationStatus: {
         isRulesCustomizationEnabled: false,
         customizationDisabledReason: PrebuiltRulesCustomizationDisabledReason.FeatureFlag,
@@ -154,7 +159,8 @@ describe('calculateRuleSource', () => {
 
     const result = await calculateRuleSource({
       prebuiltRuleAssetClient,
-      rule,
+      nextRule: rule,
+      currentRule: rule,
       ruleCustomizationStatus: {
         isRulesCustomizationEnabled: false,
         customizationDisabledReason: PrebuiltRulesCustomizationDisabledReason.License,
@@ -167,4 +173,107 @@ describe('calculateRuleSource', () => {
       })
     );
   });
+
+  describe('missing base versions', () => {
+    it('return is_customized false when the base version and current version are missing', async () => {
+      const rule = getSampleRule();
+      rule.immutable = true;
+
+      // No base version
+      prebuiltRuleAssetClient.fetchAssetsByVersion.mockResolvedValueOnce([]);
+
+      const result = await calculateRuleSource({
+        prebuiltRuleAssetClient,
+        nextRule: rule,
+        currentRule: undefined,
+        ruleCustomizationStatus,
+      });
+      expect(result).toEqual(
+        expect.objectContaining({
+          type: 'external',
+          is_customized: false,
+        })
+      );
+    });
+
+    it('returns is_customized true when the current version is already customized', async () => {
+      const rule = getSampleRule();
+      rule.immutable = true;
+      rule.rule_source = {
+        type: 'external',
+        is_customized: true,
+      };
+
+      // No base version
+      prebuiltRuleAssetClient.fetchAssetsByVersion.mockResolvedValueOnce([]);
+
+      const result = await calculateRuleSource({
+        prebuiltRuleAssetClient,
+        nextRule: rule,
+        currentRule: rule,
+        ruleCustomizationStatus,
+      });
+      expect(result).toEqual(
+        expect.objectContaining({
+          type: 'external',
+          is_customized: true,
+        })
+      );
+    });
+
+    it('returns is_customized false when the current version is not customized and the next version has no changes', async () => {
+      const rule = getSampleRule();
+      rule.immutable = true;
+      rule.rule_source = {
+        type: 'external',
+        is_customized: false,
+      };
+
+      // No base version
+      prebuiltRuleAssetClient.fetchAssetsByVersion.mockResolvedValueOnce([]);
+
+      const result = await calculateRuleSource({
+        prebuiltRuleAssetClient,
+        nextRule: rule,
+        currentRule: rule,
+        ruleCustomizationStatus,
+      });
+      expect(result).toEqual(
+        expect.objectContaining({
+          type: 'external',
+          is_customized: false,
+        })
+      );
+    });
+
+    it('returns is_customized true when the current version is not customized and the next version has changes', async () => {
+      const rule = getSampleRule();
+      rule.immutable = true;
+      rule.rule_source = {
+        type: 'external',
+        is_customized: false,
+      };
+
+      const nextRule = {
+        ...rule,
+        name: 'Updated name',
+      };
+
+      // No base version
+      prebuiltRuleAssetClient.fetchAssetsByVersion.mockResolvedValueOnce([]);
+
+      const result = await calculateRuleSource({
+        prebuiltRuleAssetClient,
+        nextRule,
+        currentRule: rule,
+        ruleCustomizationStatus,
+      });
+      expect(result).toEqual(
+        expect.objectContaining({
+          type: 'external',
+          is_customized: true,
+        })
+      );
+    });
+  });
 });
diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/rule_source/calculate_rule_source.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/rule_source/calculate_rule_source.ts
index fe628430ef300..3603a7aa7ff6c 100644
--- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/rule_source/calculate_rule_source.ts
+++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/rule_source/calculate_rule_source.ts
@@ -16,29 +16,32 @@ import { calculateIsCustomized } from './calculate_is_customized';
 
 interface CalculateRuleSourceProps {
   prebuiltRuleAssetClient: IPrebuiltRuleAssetsClient;
-  rule: RuleResponse;
+  nextRule: RuleResponse;
+  currentRule: RuleResponse | undefined;
   ruleCustomizationStatus: PrebuiltRulesCustomizationStatus;
 }
 
 export async function calculateRuleSource({
   prebuiltRuleAssetClient,
-  rule,
+  nextRule,
+  currentRule,
   ruleCustomizationStatus,
 }: CalculateRuleSourceProps): Promise<RuleSource> {
-  if (rule.immutable) {
+  if (nextRule.immutable) {
     // This is a prebuilt rule and, despite the name, they are not immutable. So
     // we need to recalculate `ruleSource.isCustomized` based on the rule's contents.
     const prebuiltRulesResponse = await prebuiltRuleAssetClient.fetchAssetsByVersion([
       {
-        rule_id: rule.rule_id,
-        version: rule.version,
+        rule_id: nextRule.rule_id,
+        version: nextRule.version,
       },
     ]);
     const baseRule: PrebuiltRuleAsset | undefined = prebuiltRulesResponse.at(0);
 
     const isCustomized = calculateIsCustomized({
       baseRule,
-      nextRule: rule,
+      nextRule,
+      currentRule,
       ruleCustomizationStatus,
     });
 
diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/calculate_rule_source_for_import.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/calculate_rule_source_for_import.test.ts
index 734093e44576f..49ab9e1da23fb 100644
--- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/calculate_rule_source_for_import.test.ts
+++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/calculate_rule_source_for_import.test.ts
@@ -17,7 +17,8 @@ const ruleCustomizationStatus: PrebuiltRulesCustomizationStatus = {
 describe('calculateRuleSourceForImport', () => {
   it('calculates as internal if no asset is found', () => {
     const result = calculateRuleSourceForImport({
-      rule: getRulesSchemaMock(),
+      importedRule: getRulesSchemaMock(),
+      currentRule: undefined,
       prebuiltRuleAssetsByRuleId: {},
       isKnownPrebuiltRule: false,
       ruleCustomizationStatus,
@@ -31,12 +32,58 @@ describe('calculateRuleSourceForImport', () => {
     });
   });
 
-  it('calculates as modified external type if an asset is found without a matching version', () => {
+  it('calculates as not modified external type if an asset is found without a matching version and no current rule present', () => {
     const rule = getRulesSchemaMock();
     rule.rule_id = 'rule_id';
 
     const result = calculateRuleSourceForImport({
-      rule,
+      importedRule: rule,
+      currentRule: undefined,
+      prebuiltRuleAssetsByRuleId: {},
+      isKnownPrebuiltRule: true,
+      ruleCustomizationStatus,
+    });
+
+    expect(result).toEqual({
+      ruleSource: {
+        type: 'external',
+        is_customized: false,
+      },
+      immutable: true,
+    });
+  });
+
+  it('calculates as non modified external type if an asset is found without a matching version and current rule present without changes', () => {
+    const rule = getRulesSchemaMock();
+    rule.rule_id = 'rule_id';
+
+    const result = calculateRuleSourceForImport({
+      importedRule: rule,
+      currentRule: rule,
+      prebuiltRuleAssetsByRuleId: {},
+      isKnownPrebuiltRule: true,
+      ruleCustomizationStatus,
+    });
+
+    expect(result).toEqual({
+      ruleSource: {
+        type: 'external',
+        is_customized: false,
+      },
+      immutable: true,
+    });
+  });
+
+  it('calculates as modified external type if an asset is found without a matching version and current rule present with changes', () => {
+    const rule = getRulesSchemaMock();
+    rule.rule_id = 'rule_id';
+
+    const result = calculateRuleSourceForImport({
+      importedRule: rule,
+      currentRule: {
+        ...rule,
+        name: 'new name',
+      },
       prebuiltRuleAssetsByRuleId: {},
       isKnownPrebuiltRule: true,
       ruleCustomizationStatus,
@@ -57,7 +104,8 @@ describe('calculateRuleSourceForImport', () => {
     const prebuiltRuleAssetsByRuleId = { rule_id: getPrebuiltRuleMock({ rule_id: 'rule_id' }) };
 
     const result = calculateRuleSourceForImport({
-      rule,
+      importedRule: rule,
+      currentRule: undefined,
       prebuiltRuleAssetsByRuleId,
       isKnownPrebuiltRule: true,
       ruleCustomizationStatus,
@@ -78,7 +126,8 @@ describe('calculateRuleSourceForImport', () => {
     const prebuiltRuleAssetsByRuleId = { rule_id: getPrebuiltRuleMock(rule) };
 
     const result = calculateRuleSourceForImport({
-      rule,
+      importedRule: rule,
+      currentRule: undefined,
       prebuiltRuleAssetsByRuleId,
       isKnownPrebuiltRule: true,
       ruleCustomizationStatus,
diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/calculate_rule_source_for_import.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/calculate_rule_source_for_import.ts
index 6476c4dbd5779..440bad4f95325 100644
--- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/calculate_rule_source_for_import.ts
+++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/calculate_rule_source_for_import.ts
@@ -6,12 +6,13 @@
  */
 
 import type {
+  RuleResponse,
   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 { calculateIsCustomized } from '../detection_rules_client/mergers/rule_source/calculate_is_customized';
 import { convertRuleToImportToRuleResponse } from './converters/convert_rule_to_import_to_rule_response';
 
 /**
@@ -26,31 +27,44 @@ import { convertRuleToImportToRuleResponse } from './converters/convert_rule_to_
  * @returns The calculated rule_source and immutable fields for the rule
  */
 export const calculateRuleSourceForImport = ({
-  rule,
+  importedRule,
+  currentRule,
   prebuiltRuleAssetsByRuleId,
   isKnownPrebuiltRule,
   ruleCustomizationStatus,
 }: {
-  rule: ValidatedRuleToImport;
+  importedRule: ValidatedRuleToImport;
+  currentRule: RuleResponse | undefined;
   prebuiltRuleAssetsByRuleId: Record<string, PrebuiltRuleAsset>;
   isKnownPrebuiltRule: boolean;
   ruleCustomizationStatus: PrebuiltRulesCustomizationStatus;
 }): { ruleSource: RuleSource; immutable: boolean } => {
-  const assetWithMatchingVersion = prebuiltRuleAssetsByRuleId[rule.rule_id];
+  if (!isKnownPrebuiltRule) {
+    return {
+      ruleSource: { type: 'internal' },
+      immutable: false,
+    };
+  }
+
+  const baseRule = prebuiltRuleAssetsByRuleId[importedRule.rule_id];
   // We convert here so that RuleSource calculation can
   // continue to deal only with RuleResponses. The fields missing from the
   // incoming rule are not actually needed for the calculation, but only to
   // satisfy the type system.
-  const ruleResponseForImport = convertRuleToImportToRuleResponse(rule);
-  const ruleSource = calculateRuleSourceFromAsset({
-    rule: ruleResponseForImport,
-    assetWithMatchingVersion,
-    isKnownPrebuiltRule,
+  const nextRule = convertRuleToImportToRuleResponse(importedRule);
+
+  const isCustomized = calculateIsCustomized({
+    baseRule,
+    nextRule,
+    currentRule,
     ruleCustomizationStatus,
   });
 
   return {
-    ruleSource,
-    immutable: ruleSource.type === 'external',
+    ruleSource: {
+      type: 'external',
+      is_customized: isCustomized,
+    },
+    immutable: true,
   };
 };
diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/calculate_rule_source_from_asset.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/calculate_rule_source_from_asset.test.ts
deleted file mode 100644
index 04074d65eab80..0000000000000
--- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/calculate_rule_source_from_asset.test.ts
+++ /dev/null
@@ -1,85 +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 { 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', () => {
-    const result = calculateRuleSourceFromAsset({
-      rule: getRulesSchemaMock(),
-      assetWithMatchingVersion: undefined,
-      isKnownPrebuiltRule: false,
-      ruleCustomizationStatus,
-    });
-
-    expect(result).toEqual({
-      type: 'internal',
-    });
-  });
-
-  it('calculates as customized external type if an asset is found matching rule_id but not version', () => {
-    const ruleToImport = getRulesSchemaMock();
-    const result = calculateRuleSourceFromAsset({
-      rule: ruleToImport,
-      assetWithMatchingVersion: undefined,
-      isKnownPrebuiltRule: true,
-      ruleCustomizationStatus,
-    });
-
-    expect(result).toEqual({
-      type: 'external',
-      is_customized: true,
-    });
-  });
-
-  describe('matching rule_id and version is found', () => {
-    it('calculates as customized external type if the imported rule has all fields unchanged from the asset', () => {
-      const ruleToImport = getRulesSchemaMock();
-      const result = calculateRuleSourceFromAsset({
-        rule: getRulesSchemaMock(), // version 1
-        assetWithMatchingVersion: getPrebuiltRuleMock({
-          ...ruleToImport,
-          version: 1, // version 1 (same version as imported rule)
-          // no other overwrites -> no differences
-        }),
-        isKnownPrebuiltRule: true,
-        ruleCustomizationStatus,
-      });
-
-      expect(result).toEqual({
-        type: 'external',
-        is_customized: false,
-      });
-    });
-
-    it('calculates as non-customized external type the imported rule has fields which differ from the asset', () => {
-      const ruleToImport = getRulesSchemaMock();
-      const result = calculateRuleSourceFromAsset({
-        rule: getRulesSchemaMock(), // version 1
-        assetWithMatchingVersion: getPrebuiltRuleMock({
-          ...ruleToImport,
-          version: 1, // version 1 (same version as imported rule)
-          name: 'Customized name', // mock a customization
-        }),
-        isKnownPrebuiltRule: true,
-        ruleCustomizationStatus,
-      });
-
-      expect(result).toEqual({
-        type: 'external',
-        is_customized: true,
-      });
-    });
-  });
-});
diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/calculate_rule_source_from_asset.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/calculate_rule_source_from_asset.ts
deleted file mode 100644
index 190c21747b09d..0000000000000
--- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/calculate_rule_source_from_asset.ts
+++ /dev/null
@@ -1,58 +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 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';
-
-/**
- * Calculates rule_source for a rule based on two pieces of information:
- * 1. The prebuilt rule asset that matches the specified rule_id and version
- * 2. Whether a prebuilt rule with the specified rule_id is currently installed
- *
- * @param rule The rule for which rule_source is being calculated
- * @param assetWithMatchingVersion The prebuilt rule asset that matches the specified rule_id and version
- * @param isKnownPrebuiltRule Whether a prebuilt rule with the specified rule_id is currently installed
- *
- * @returns The calculated rule_source
- */
-export const calculateRuleSourceFromAsset = ({
-  rule,
-  assetWithMatchingVersion,
-  isKnownPrebuiltRule,
-  ruleCustomizationStatus,
-}: {
-  rule: RuleResponse;
-  assetWithMatchingVersion: PrebuiltRuleAsset | undefined;
-  isKnownPrebuiltRule: boolean;
-  ruleCustomizationStatus: PrebuiltRulesCustomizationStatus;
-}): RuleSource => {
-  if (!isKnownPrebuiltRule) {
-    return {
-      type: 'internal',
-    };
-  }
-
-  if (assetWithMatchingVersion == null) {
-    return {
-      type: 'external',
-      is_customized: ruleCustomizationStatus.isRulesCustomizationEnabled ? true : false,
-    };
-  }
-
-  const isCustomized = calculateIsCustomized({
-    baseRule: assetWithMatchingVersion,
-    nextRule: rule,
-    ruleCustomizationStatus,
-  });
-
-  return {
-    type: 'external',
-    is_customized: isCustomized,
-  };
-};
diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/rule_source_importer/rule_source_importer.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/rule_source_importer/rule_source_importer.test.ts
index 17d1854d65281..ccc0b062c747f 100644
--- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/rule_source_importer/rule_source_importer.test.ts
+++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/rule_source_importer/rule_source_importer.test.ts
@@ -10,13 +10,14 @@ import type {
   ValidatedRuleToImport,
 } from '../../../../../../../common/api/detection_engine';
 import { createPrebuiltRuleAssetsClient as createPrebuiltRuleAssetsClientMock } from '../../../../prebuilt_rules/logic/rule_assets/__mocks__/prebuilt_rule_assets_client';
+import { createPrebuiltRuleObjectsClient as createPrebuiltRuleObjectsClientMock } from '../../../../prebuilt_rules/logic/rule_objects/__mocks__/prebuilt_rule_objects_client';
 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';
 
 describe('ruleSourceImporter', () => {
   let ruleAssetsClientMock: ReturnType<typeof createPrebuiltRuleAssetsClientMock>;
+  let ruleObjectsClientMock: ReturnType<typeof createPrebuiltRuleObjectsClientMock>;
   let config: ReturnType<typeof createMockConfig>;
   let context: ReturnType<typeof requestContextMock.create>['securitySolution'];
   let ruleToImport: RuleToImport;
@@ -30,12 +31,15 @@ describe('ruleSourceImporter', () => {
     ruleAssetsClientMock.fetchLatestAssets.mockResolvedValue([{}]);
     ruleAssetsClientMock.fetchLatestVersions.mockResolvedValue([]);
     ruleAssetsClientMock.fetchAssetsByVersion.mockResolvedValue([]);
+    ruleObjectsClientMock = createPrebuiltRuleObjectsClientMock();
+    ruleObjectsClientMock.fetchInstalledRulesByIds.mockResolvedValue([]);
     ruleToImport = { rule_id: 'rule-1', version: 1 } as RuleToImport;
 
     subject = createRuleSourceImporter({
       context,
       config,
       prebuiltRuleAssetsClient: ruleAssetsClientMock,
+      prebuiltRuleObjectsClient: ruleObjectsClientMock,
       ruleCustomizationStatus: { isRulesCustomizationEnabled: true },
     });
   });
@@ -112,7 +116,6 @@ describe('ruleSourceImporter', () => {
 
   describe('#calculateRuleSource()', () => {
     let rule: ValidatedRuleToImport;
-    let calculatorSpy: jest.SpyInstance;
 
     beforeEach(() => {
       rule = { rule_id: 'validated-rule', version: 1 } as ValidatedRuleToImport;
@@ -124,23 +127,6 @@ describe('ruleSourceImporter', () => {
         getPrebuiltRuleMock({ rule_id: 'rule-2' }),
         getPrebuiltRuleMock({ rule_id: 'validated-rule' }),
       ]);
-      calculatorSpy = jest
-        .spyOn(calculateRuleSourceModule, 'calculateRuleSourceForImport')
-        .mockReturnValue({ ruleSource: { type: 'internal' }, immutable: false });
-    });
-
-    it('invokes calculateRuleSourceForImport with the correct arguments', async () => {
-      await subject.setup([rule]);
-      await subject.calculateRuleSource(rule);
-
-      expect(calculatorSpy).toHaveBeenCalledTimes(1);
-      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 () => {
@@ -157,23 +143,5 @@ describe('ruleSourceImporter', () => {
         `"Rule validated-rule was not registered during setup."`
       );
     });
-
-    describe('for rules set up without a version', () => {
-      it('invokes the calculator with the correct arguments', async () => {
-        await subject.setup([{ ...rule, version: undefined }]);
-        await subject.calculateRuleSource(rule);
-
-        expect(calculatorSpy).toHaveBeenCalledTimes(1);
-        expect(calculatorSpy).toHaveBeenCalledWith(
-          expect.objectContaining({
-            rule,
-            prebuiltRuleAssetsByRuleId: {
-              'rule-1': expect.objectContaining({ rule_id: 'rule-1' }),
-            },
-            isKnownPrebuiltRule: true,
-          })
-        );
-      });
-    });
   });
 });
diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/rule_source_importer/rule_source_importer.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/rule_source_importer/rule_source_importer.ts
index af80d7f5e67fa..28388ce4d104f 100644
--- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/rule_source_importer/rule_source_importer.ts
+++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/rule_source_importer/rule_source_importer.ts
@@ -15,6 +15,7 @@
 import type { SecuritySolutionApiRequestHandlerContext } from '../../../../../../types';
 import type { ConfigType } from '../../../../../../config';
 import type {
+  RuleResponse,
   RuleToImport,
   ValidatedRuleToImport,
 } from '../../../../../../../common/api/detection_engine';
@@ -24,6 +25,7 @@ import { ensureLatestRulesPackageInstalled } from '../../../../prebuilt_rules/lo
 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';
+import type { IPrebuiltRuleObjectsClient } from '../../../../prebuilt_rules/logic/rule_objects/prebuilt_rule_objects_client';
 
 interface RuleSpecifier {
   rule_id: string;
@@ -96,25 +98,30 @@ export class RuleSourceImporter implements IRuleSourceImporter {
   private context: SecuritySolutionApiRequestHandlerContext;
   private config: ConfigType;
   private ruleAssetsClient: IPrebuiltRuleAssetsClient;
+  private ruleObjectsClient: IPrebuiltRuleObjectsClient;
   private ruleCustomizationStatus: PrebuiltRulesCustomizationStatus;
   private latestPackagesInstalled: boolean = false;
   private matchingAssetsByRuleId: Record<string, PrebuiltRuleAsset> = {};
-  private knownRules: RuleSpecifier[] = [];
+  private currentRulesById: Record<string, RuleResponse> = {};
+  private rulesToImport: RuleSpecifier[] = [];
   private availableRuleAssetIds: Set<string> = new Set();
 
   constructor({
     config,
     context,
     prebuiltRuleAssetsClient,
+    prebuiltRuleObjectsClient,
     ruleCustomizationStatus,
   }: {
     config: ConfigType;
     context: SecuritySolutionApiRequestHandlerContext;
     prebuiltRuleAssetsClient: IPrebuiltRuleAssetsClient;
+    prebuiltRuleObjectsClient: IPrebuiltRuleObjectsClient;
     ruleCustomizationStatus: PrebuiltRulesCustomizationStatus;
   }) {
     this.config = config;
     this.ruleAssetsClient = prebuiltRuleAssetsClient;
+    this.ruleObjectsClient = prebuiltRuleObjectsClient;
     this.context = context;
     this.ruleCustomizationStatus = ruleCustomizationStatus;
   }
@@ -130,9 +137,12 @@ export class RuleSourceImporter implements IRuleSourceImporter {
       this.latestPackagesInstalled = true;
     }
 
-    this.knownRules = rules.map((rule) => ({ rule_id: rule.rule_id, version: rule.version }));
+    this.rulesToImport = rules.map((rule) => ({ rule_id: rule.rule_id, version: rule.version }));
     this.matchingAssetsByRuleId = await this.fetchMatchingAssetsByRuleId();
     this.availableRuleAssetIds = new Set(await this.fetchAvailableRuleAssetIds());
+    this.currentRulesById = await this.fetchInstalledRulesByIds(
+      this.rulesToImport.map((rule) => rule.rule_id)
+    );
   }
 
   public isPrebuiltRule(rule: RuleToImport): boolean {
@@ -145,7 +155,8 @@ export class RuleSourceImporter implements IRuleSourceImporter {
     this.validateRuleInput(rule);
 
     return calculateRuleSourceForImport({
-      rule,
+      importedRule: rule,
+      currentRule: this.currentRulesById[rule.rule_id],
       prebuiltRuleAssetsByRuleId: this.matchingAssetsByRuleId,
       isKnownPrebuiltRule: this.availableRuleAssetIds.has(rule.rule_id),
       ruleCustomizationStatus: this.ruleCustomizationStatus,
@@ -155,7 +166,7 @@ export class RuleSourceImporter implements IRuleSourceImporter {
   private async fetchMatchingAssetsByRuleId(): Promise<Record<string, PrebuiltRuleAsset>> {
     this.validateSetupState();
     const matchingAssets = await fetchMatchingAssets({
-      rules: this.knownRules,
+      rules: this.rulesToImport,
       ruleAssetsClient: this.ruleAssetsClient,
     });
 
@@ -165,11 +176,18 @@ export class RuleSourceImporter implements IRuleSourceImporter {
     }, {});
   }
 
+  private async fetchInstalledRulesByIds(ruleIds: string[]): Promise<Record<string, RuleResponse>> {
+    const currentRules = await this.ruleObjectsClient.fetchInstalledRulesByIds({
+      ruleIds,
+    });
+    return Object.fromEntries(currentRules.map((rule) => [rule.rule_id, rule]));
+  }
+
   private async fetchAvailableRuleAssetIds(): Promise<string[]> {
     this.validateSetupState();
 
     return fetchAvailableRuleAssetIds({
-      rules: this.knownRules,
+      rules: this.rulesToImport,
       ruleAssetsClient: this.ruleAssetsClient,
     });
   }
@@ -185,7 +203,7 @@ export class RuleSourceImporter implements IRuleSourceImporter {
 
   private validateRuleInput(rule: RuleToImport) {
     if (
-      !this.knownRules.some(
+      !this.rulesToImport.some(
         (knownRule) =>
           knownRule.rule_id === rule.rule_id &&
           (knownRule.version === rule.version || knownRule.version == null)
@@ -200,17 +218,20 @@ export const createRuleSourceImporter = ({
   config,
   context,
   prebuiltRuleAssetsClient,
+  prebuiltRuleObjectsClient,
   ruleCustomizationStatus,
 }: {
   config: ConfigType;
   context: SecuritySolutionApiRequestHandlerContext;
   prebuiltRuleAssetsClient: IPrebuiltRuleAssetsClient;
+  prebuiltRuleObjectsClient: IPrebuiltRuleObjectsClient;
   ruleCustomizationStatus: PrebuiltRulesCustomizationStatus;
 }): RuleSourceImporter => {
   return new RuleSourceImporter({
     config,
     context,
     prebuiltRuleAssetsClient,
+    prebuiltRuleObjectsClient,
     ruleCustomizationStatus,
   });
 };
diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/customization_enabled/import_rules.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/customization_enabled/import_rules.ts
index 009a88239df31..38d3a40c7ffe2 100644
--- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/customization_enabled/import_rules.ts
+++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/customization_enabled/import_rules.ts
@@ -271,7 +271,7 @@ export default ({ getService }: FtrProviderContext): void => {
         expect(importedRule).toMatchObject({
           rule_id: rule.rule_id,
           version: 9999,
-          rule_source: { type: 'external', is_customized: true },
+          rule_source: { type: 'external', is_customized: false },
           immutable: true,
         });
       });


Reviews:
user: login: maximpn
body: @xcrzx Thanks for concise implementation 🙏 

I looked through the changes and left non critical comments. Local testing confirmed the changes work as expected.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T17:49:39Z

Review comments:
diff_hunk: @@ -167,4 +173,107 @@ describe('calculateRuleSource', () => {
       })
     );
   });
+
+  describe('missing base versions', () => {
+    it('return is_customized false when the base version and current version are missing', async () => {
+      const rule = getSampleRule();
+      rule.immutable = true;
+
+      // No base version
+      prebuiltRuleAssetClient.fetchAssetsByVersion.mockResolvedValueOnce([]);
path: x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/rule_source/calculate_rule_source.test.ts
user: login: maximpn
body: nit: It feels like this line could potentially be forgotten when adding new tests. Maybe move it to `beforeEach()`?
created_at: 2025-03-06T17:13:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -16,29 +16,32 @@ import { calculateIsCustomized } from './calculate_is_customized';
 
 interface CalculateRuleSourceProps {
   prebuiltRuleAssetClient: IPrebuiltRuleAssetsClient;
-  rule: RuleResponse;
+  nextRule: RuleResponse;
+  currentRule: RuleResponse | undefined;
   ruleCustomizationStatus: PrebuiltRulesCustomizationStatus;
 }
 
 export async function calculateRuleSource({
   prebuiltRuleAssetClient,
-  rule,
+  nextRule,
+  currentRule,
   ruleCustomizationStatus,
 }: CalculateRuleSourceProps): Promise<RuleSource> {
-  if (rule.immutable) {
+  if (nextRule.immutable) {
path: x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/rule_source/calculate_rule_source.ts
user: login: maximpn
body: nit: Inverse logic and early exit will make this function a bit simpler

```
if (!nextRule.immutable) {
  return {
    type: 'internal',
  };
}

...
```
created_at: 2025-03-06T17:14:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -26,31 +27,44 @@ import { convertRuleToImportToRuleResponse } from './converters/convert_rule_to_
  * @returns The calculated rule_source and immutable fields for the rule
  */
 export const calculateRuleSourceForImport = ({
-  rule,
+  importedRule,
+  currentRule,
   prebuiltRuleAssetsByRuleId,
   isKnownPrebuiltRule,
path: x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/calculate_rule_source_for_import.ts
user: login: maximpn
body: nit: It could be simplified by having only `prebuiltRuleAssets: PrebuiltRuleAsset | undefined` matching the rule version instead of  `isKnownPrebuiltRule` and `prebuiltRuleAssetsByRuleId `.
created_at: 2025-03-06T17:19:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -26,31 +27,44 @@ import { convertRuleToImportToRuleResponse } from './converters/convert_rule_to_
  * @returns The calculated rule_source and immutable fields for the rule
  */
 export const calculateRuleSourceForImport = ({
-  rule,
+  importedRule,
path: x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/calculate_rule_source_for_import.ts
user: login: maximpn
body: nit: It doesn't look like a has been imported when this function is invoked. A name like `ruleToImport` or `beingImportedRule` feels like resonating better.
created_at: 2025-03-06T17:22:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -31,12 +32,58 @@ describe('calculateRuleSourceForImport', () => {
     });
   });
 
-  it('calculates as modified external type if an asset is found without a matching version', () => {
+  it('calculates as not modified external type if an asset is found without a matching version and no current rule present', () => {
     const rule = getRulesSchemaMock();
     rule.rule_id = 'rule_id';
 
     const result = calculateRuleSourceForImport({
-      rule,
+      importedRule: rule,
+      currentRule: undefined,
+      prebuiltRuleAssetsByRuleId: {},
+      isKnownPrebuiltRule: true,
+      ruleCustomizationStatus,
+    });
+
+    expect(result).toEqual({
+      ruleSource: {
+        type: 'external',
+        is_customized: false,
+      },
+      immutable: true,
+    });
+  });
+
+  it('calculates as non modified external type if an asset is found without a matching version and current rule present without changes', () => {
path: x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/calculate_rule_source_for_import.test.ts
user: login: maximpn
body: There is a missing scenario covering that `is_customized: true` is preserved when imported rule matches with the current.
created_at: 2025-03-06T17:31:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -271,7 +271,7 @@ export default ({ getService }: FtrProviderContext): void => {
         expect(importedRule).toMatchObject({
           rule_id: rule.rule_id,
           version: 9999,
-          rule_source: { type: 'external', is_customized: true },
+          rule_source: { type: 'external', is_customized: false },
path: x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/customization_enabled/import_rules.ts
user: login: maximpn
body: nit: It doesn't look like something besides `version` should be in that assertion.
created_at: 2025-03-06T17:35:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -96,25 +98,30 @@ export class RuleSourceImporter implements IRuleSourceImporter {
   private context: SecuritySolutionApiRequestHandlerContext;
   private config: ConfigType;
   private ruleAssetsClient: IPrebuiltRuleAssetsClient;
+  private ruleObjectsClient: IPrebuiltRuleObjectsClient;
   private ruleCustomizationStatus: PrebuiltRulesCustomizationStatus;
   private latestPackagesInstalled: boolean = false;
   private matchingAssetsByRuleId: Record<string, PrebuiltRuleAsset> = {};
-  private knownRules: RuleSpecifier[] = [];
+  private currentRulesById: Record<string, RuleResponse> = {};
path: x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/rule_source_importer/rule_source_importer.ts
user: login: maximpn
body: ```suggestion
  private currentRulesById: Record<RuleSignatureId, RuleResponse> = {};
```
created_at: 2025-03-06T17:37:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -165,11 +176,18 @@ export class RuleSourceImporter implements IRuleSourceImporter {
     }, {});
   }
 
+  private async fetchInstalledRulesByIds(ruleIds: string[]): Promise<Record<string, RuleResponse>> {
path: x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/rule_source_importer/rule_source_importer.ts
user: login: maximpn
body: nit: Add `Map` to the name for clarity 

```suggestion
  private async fetchInstalledRulesMapByIds(ruleIds: string[]): Promise<Record<RuleSignatureId, RuleResponse>> {
```
created_at: 2025-03-06T17:37:41Z
author_association: CONTRIBUTOR

PR comments:
user: login: elasticmachine
created_at: 2025-03-06T14:44:07Z
body: Pinging @elastic/security-detections-response (Team:Detections and Resp)
author_association: CONTRIBUTOR
user: login: elasticmachine
created_at: 2025-03-06T14:44:08Z
body: Pinging @elastic/security-solution (Team: SecuritySolution)
author_association: CONTRIBUTOR
user: login: elasticmachine
created_at: 2025-03-06T14:44:10Z
body: Pinging @elastic/security-detection-rule-management (Team:Detection Rule Management)
author_association: CONTRIBUTOR
user: login: kibanamachine
created_at: 2025-03-06T18:22:35Z
body: Starting backport for target branches: 8.18, 8.x, 9.0

https://github.com/elastic/kibana/actions/runs/13705510140
author_association: CONTRIBUTOR
user: login: elasticmachine
created_at: 2025-03-06T18:23:18Z
body: ## :green_heart: Build Succeeded
* [Buildkite Build](https://buildkite.com/elastic/kibana-pull-request/builds/281515)
* Commit: 24cdb9a22a26cd6cd983940194540ab42a556773

### Metrics [[docs](https://www.elastic.co/guide/en/kibana/master/ci-metrics.html)]
> ✅ unchanged

### History
* :broken_heart: [Build #281444](https://buildkite.com/elastic/kibana-pull-request/builds/281444) failed f4f7ff11587a2831612e6ee5b33119ae87c81a57
* :broken_heart: [Build #281129](https://buildkite.com/elastic/kibana-pull-request/builds/281129) failed 3d1975509672107343ccf1a0bb00e48326db4fe8

cc @xcrzx

<!--buildkite-pr-comment-kibana-pull-request
{"builds":[{"buildStatus":{"state":"passed","success":true,"hasRetries":true,"hasNonPreemptionRetries":false},"url":"https://buildkite.com/elastic/kibana-pull-request/builds/281515","number":281515,"commit":"24cdb9a22a26cd6cd983940194540ab42a556773"},{"buildStatus":{"state":"failing","success":false,"hasRetries":true,"hasNonPreemptionRetries":true},"url":"https://buildkite.com/elastic/kibana-pull-request/builds/281444","number":281444,"commit":"f4f7ff11587a2831612e6ee5b33119ae87c81a57"},{"buildStatus":{"state":"failed","success":false,"hasRetries":false,"hasNonPreemptionRetries":false},"url":"https://buildkite.com/elastic/kibana-pull-request/builds/281129","number":281129,"commit":"3d1975509672107343ccf1a0bb00e48326db4fe8"}],"number":281515}
buildkite-pr-comment-->
author_association: CONTRIBUTOR
user: login: kibanamachine
created_at: 2025-03-06T18:28:28Z
body: ## 💔 Some backports could not be created

| Status | Branch | Result |
|:------:|:------:|:------|
|✅|8.18|[<img src="https://img.shields.io/github/pulls/detail/state/elastic/kibana/213460">](https://github.com/elastic/kibana/pull/213460)|
|✅|8.x|[<img src="https://img.shields.io/github/pulls/detail/state/elastic/kibana/213462">](https://github.com/elastic/kibana/pull/213462)|
|❌|9.0|**Backport failed because of merge conflicts**<br><br>You might need to backport the following PRs to 9.0:<br> - [[Security Solution] Allow prebuilt rules import and export (#212509)](https://github.com/elastic/kibana/pull/212509)|

Note: Successful backport PRs will be merged automatically after passing CI.

### Manual backport
To create the backport manually run:
```
node scripts/backport --pr 213250
```

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

<!--- Backport version: 9.6.6 -->
author_association: CONTRIBUTOR
user: login: banderror
created_at: 2025-03-06T19:32:35Z
body: ## 💚 All backports created successfully

| Status | Branch | Result |
|:------:|:------:|:------|
|✅|9.0|[<img src="https://img.shields.io/github/pulls/detail/state/elastic/kibana/213466">](https://github.com/elastic/kibana/pull/213466)|

Note: Successful backport PRs will be merged automatically after passing CI.

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

<!--- Backport version: 9.6.6 -->
author_association: CONTRIBUTOR