Repository: elastic/kibana
Pull request #219006: [AI4DSOC] Disable NLPCleanupTask for `search_ai_lake` tier
Author: login: spong
Labels: name: release_note:skip
description: Skip the PR/issue when compiling release notes
name: backport:skip
description: This PR does not require backporting
name: Team:Security Generative AI
description: Security Generative AI
name: v9.1.0
Created: 2025-04-23T18:16:07Z | Merged: 2025-04-24T20:03:05Z

## Summary

Updates the `NLPCleanupTask` added in https://github.com/elastic/kibana/pull/175358 to behave the same as `complete` for the new `search_ai_lake` tier as we don't want it periodically running and cleaning up ELSER. The task will run once, and disable itself if not in the correct tier.

An FTR test was also added to ensure expected behavior in the `search_ai_lake` tier.

Note: we may want to update this task to still cleanup extraneous NLP models that _are not_ ELSER, but this can be done in a future PR if desired.







Diff:
diff --git a/.buildkite/ftr_security_serverless_configs.yml b/.buildkite/ftr_security_serverless_configs.yml
index eb04c7697a775..db74a3b6f606a 100644
--- a/.buildkite/ftr_security_serverless_configs.yml
+++ b/.buildkite/ftr_security_serverless_configs.yml
@@ -134,6 +134,7 @@ enabled:
   - x-pack/test/security_solution_endpoint/configs/serverless.endpoint.config.ts
   - x-pack/test/security_solution_endpoint/configs/serverless.integrations.config.ts
   - x-pack/test/security_solution_api_integration/test_suites/ai4dsoc/cases/search_ai_lake_tier/configs/serverless.config.ts
+  - x-pack/test/security_solution_api_integration/test_suites/ai4dsoc/nlp_cleanup_task/search_ai_lake_tier/configs/serverless.config.ts
   # serverless config files that run deployment-agnostic tests
   - x-pack/test/api_integration/deployment_agnostic/configs/serverless/security.serverless.config.ts
   - x-pack/test/spaces_api_integration/deployment_agnostic/security_and_spaces/serverless.config.ts
diff --git a/x-pack/solutions/security/plugins/security_solution_serverless/server/plugin.ts b/x-pack/solutions/security/plugins/security_solution_serverless/server/plugin.ts
index 1ff3b29af4422..be9a1b14846ff 100644
--- a/x-pack/solutions/security/plugins/security_solution_serverless/server/plugin.ts
+++ b/x-pack/solutions/security/plugins/security_solution_serverless/server/plugin.ts
@@ -29,7 +29,7 @@ import type {
 } from './types';
 import { SecurityUsageReportingTask } from './task_manager/usage_reporting_task';
 import { cloudSecurityMetringTaskProperties } from './cloud_security/cloud_security_metering_task_config';
-import { registerProductFeatures, getSecurityProductTier } from './product_features';
+import { registerProductFeatures, getSecurityAiSocProductTier } from './product_features';
 import { METERING_TASK as ENDPOINT_METERING_TASK } from './endpoint/constants/metering';
 import { METERING_TASK as AI4SOC_METERING_TASK } from './ai4soc/constants/metering';
 import {
@@ -160,7 +160,7 @@ export class SecuritySolutionServerlessPlugin
     this.nlpCleanupTask = new NLPCleanupTask({
       core: coreSetup,
       logFactory: this.initializerContext.logger,
-      productTier: getSecurityProductTier(this.config, this.logger),
+      productTier: getSecurityAiSocProductTier(this.config, this.logger),
       taskManager: pluginsSetup.taskManager,
     });
 
diff --git a/x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/index.ts b/x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/index.ts
index f6eec1a11e0d3..43ba3a75edf72 100644
--- a/x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/index.ts
+++ b/x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/index.ts
@@ -61,15 +61,26 @@ export const registerProductFeatures = (
 };
 
 /**
- * Get the security product tier from the security product type in the config
+ * Get the product tier from the security/ai_soc product type in the config. This is used to determine if AI features
+ * are available in the given configuration.
  */
-export const getSecurityProductTier = (config: ServerlessSecurityConfig, logger: Logger): Tier => {
+export const getSecurityAiSocProductTier = (
+  config: ServerlessSecurityConfig,
+  logger: Logger
+): Tier => {
   const securityProductType = config.productTypes.find(
     (productType) => productType.product_line === ProductLine.security
   );
-  const tier = securityProductType ? securityProductType.product_tier : 'none';
+  const ai4SocProductType = config.productTypes.find(
+    (productType) => productType.product_line === ProductLine.aiSoc
+  );
+  const tier = securityProductType
+    ? securityProductType.product_tier
+    : ai4SocProductType
+    ? ai4SocProductType.product_tier
+    : 'none';
   if (tier === 'none') {
-    logger.error(`Failed to fetch security product tier, config: ${JSON.stringify(config)}`);
+    logger.error(`Failed to fetch security/aiSoc product tier, config: ${JSON.stringify(config)}`);
   }
 
   return tier;
diff --git a/x-pack/solutions/security/plugins/security_solution_serverless/server/task_manager/nlp_cleanup_task/nlp_cleanup_task.ts b/x-pack/solutions/security/plugins/security_solution_serverless/server/task_manager/nlp_cleanup_task/nlp_cleanup_task.ts
index 011e80bf02824..17c058a0b9677 100644
--- a/x-pack/solutions/security/plugins/security_solution_serverless/server/task_manager/nlp_cleanup_task/nlp_cleanup_task.ts
+++ b/x-pack/solutions/security/plugins/security_solution_serverless/server/task_manager/nlp_cleanup_task/nlp_cleanup_task.ts
@@ -40,10 +40,10 @@ export interface NLPCleanupTaskStartContract {
  * This task is responsible for periodically cleaning up all NLP model deployments, specifically, trained models where
  * the `model_type` is `pytorch`.
  *
- * NLP is only intended to be enabled for the Security `complete` productTier, however due to current capabilities
- * (see: https://github.com/elastic/ml-team/issues/1139), it will remain enabled at the ES level for all productTiers,
- * so we must periodically check for any NLP models that may have been manually deployed via the ES API and clean them
- * up.
+ * NLP is only intended to be enabled for the Security `complete` and `searchAiLake` productTiers, however due to
+ * current capabilities (see: https://github.com/elastic/ml-team/issues/1139), it will remain enabled at the ES level
+ * for all productTiers, so we must periodically check for any NLP models that may have been manually deployed via the
+ * ES API and clean them up.
  *
  * Note: Models cannot be deployed via the Kibana UI as NLP will be disabled on the Kibana side by project-controller
  * based on the productTier, so no additional UI gating is necessary.
@@ -51,7 +51,7 @@ export interface NLPCleanupTaskStartContract {
  * See https://github.com/elastic/security-team/issues/7995 for further details.
  *
  * Task Details: after discussion with ResponseOps, it is preferred for the task to always be registered regardless of
- * the productTier, and to only schedule it if the productTier is `complete`.
+ * the productTier, and to only schedule it if the productTier is `complete` or `searchAiLake`.
  *
  * See `x-pack/test/security_solution_api_integration/test_suites/genai/nlp_cleanup_task` for API integration tests.
  */
@@ -77,7 +77,10 @@ export class NLPCleanupTask {
           createTaskRunner: ({ taskInstance }: { taskInstance: ConcreteTaskInstance }) => {
             return {
               run: async () => {
-                if (this.productTier === ProductTier.complete) {
+                if (
+                  this.productTier === ProductTier.complete ||
+                  this.productTier === ProductTier.searchAiLake
+                ) {
                   this.logger.info(
                     `Task ${taskInstance.id} no longer needed for current productTier, disabling...`
                   );
@@ -113,7 +116,10 @@ export class NLPCleanupTask {
         params: { version: NLPCleanupTaskConstants.VERSION },
       });
 
-      if (this.productTier !== ProductTier.complete) {
+      if (
+        this.productTier !== ProductTier.complete &&
+        this.productTier !== ProductTier.searchAiLake
+      ) {
         // In case the task was previously ran and is scheduled for later in the future, run now
         this.logger.info('Scheduling for immediate run');
         await taskManager.runSoon(this.taskId);
diff --git a/x-pack/test/security_solution_api_integration/package.json b/x-pack/test/security_solution_api_integration/package.json
index 1784aea8b51cd..def2ae9505966 100644
--- a/x-pack/test/security_solution_api_integration/package.json
+++ b/x-pack/test/security_solution_api_integration/package.json
@@ -67,6 +67,9 @@
     "ai4dsoc_cases:server:serverless": "npm run initialize-server:ai4dsoc cases serverless",
     "ai4dsoc_cases:runner:serverless": "npm run run-tests:ai4dsoc cases serverless serverlessEnv",
 
+    "ai4dsoc_nlp_cleanup_task:server:serverless": "npm run initialize-server:ai4dsoc nlp_cleanup_task serverless",
+    "ai4dsoc_nlp_cleanup_task:runner:serverless": "npm run run-tests:ai4dsoc nlp_cleanup_task serverless serverlessEnv",
+
     "genai_evals:server:ess": "npm run initialize-server:genai:trial_complete evaluations ess",
     "genai_evals:runner:ess": "npm run run-tests:genai:trial_complete evaluations ess essEnv",
 
diff --git a/x-pack/test/security_solution_api_integration/test_suites/ai4dsoc/nlp_cleanup_task/search_ai_lake_tier/configs/serverless.config.ts b/x-pack/test/security_solution_api_integration/test_suites/ai4dsoc/nlp_cleanup_task/search_ai_lake_tier/configs/serverless.config.ts
new file mode 100644
index 0000000000000..497a3a445db6f
--- /dev/null
+++ b/x-pack/test/security_solution_api_integration/test_suites/ai4dsoc/nlp_cleanup_task/search_ai_lake_tier/configs/serverless.config.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.
+ */
+
+import { createTestConfig } from '../../../../../config/serverless/config.base.ai4dsoc';
+
+export default createTestConfig({
+  testFiles: [require.resolve('..')],
+  junit: {
+    reportName: 'AI4DSOC - NLP Cleanup Task',
+  },
+});
diff --git a/x-pack/test/security_solution_api_integration/test_suites/ai4dsoc/nlp_cleanup_task/search_ai_lake_tier/index.ts b/x-pack/test/security_solution_api_integration/test_suites/ai4dsoc/nlp_cleanup_task/search_ai_lake_tier/index.ts
new file mode 100644
index 0000000000000..3172fc268b5aa
--- /dev/null
+++ b/x-pack/test/security_solution_api_integration/test_suites/ai4dsoc/nlp_cleanup_task/search_ai_lake_tier/index.ts
@@ -0,0 +1,14 @@
+/*
+ * 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 { FtrProviderContext } from '../../../../ftr_provider_context';
+
+export default function ({ loadTestFile }: FtrProviderContext) {
+  describe('AI4DSOC - NLP Cleanup Task', function () {
+    loadTestFile(require.resolve('./task_execution'));
+  });
+}
diff --git a/x-pack/test/security_solution_api_integration/test_suites/ai4dsoc/nlp_cleanup_task/search_ai_lake_tier/task_execution.ts b/x-pack/test/security_solution_api_integration/test_suites/ai4dsoc/nlp_cleanup_task/search_ai_lake_tier/task_execution.ts
new file mode 100644
index 0000000000000..e88b42c405645
--- /dev/null
+++ b/x-pack/test/security_solution_api_integration/test_suites/ai4dsoc/nlp_cleanup_task/search_ai_lake_tier/task_execution.ts
@@ -0,0 +1,45 @@
+/*
+ * 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 expect from '@kbn/expect';
+
+import { FtrProviderContext } from '../../../../ftr_provider_context';
+import { waitFor } from '../../../../../common/utils/security_solution';
+
+export default ({ getService }: FtrProviderContext) => {
+  const kibanaServer = getService('kibanaServer');
+  const logger = getService('log');
+
+  const TASK_ID = 'serverless-security:nlp-cleanup-task:1.0.0';
+
+  describe('@serverless NLP Cleanup Task in SearchAILake Tier', () => {
+    describe('New SearchAILake Deployment', () => {
+      it('registers, runs and immediately deletes NLP Cleanup Task', async () => {
+        try {
+          // When running on CI, the task may not have run and been disabled, so poll for it to be cleaned up
+          await waitFor(
+            async () => {
+              const task = await kibanaServer.savedObjects.get({
+                type: 'task',
+                id: TASK_ID,
+              });
+
+              return task == null;
+            },
+            'waitForTaskToBeDeleted',
+            logger
+          );
+        } catch (e) {
+          expect(e.message).to.eql('Request failed with status code 404');
+
+          return;
+        }
+        throw new Error('Task should not exist!');
+      });
+    });
+  });
+};


Reviews:
user: login: Ikuni17
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T20:12:53Z
user: login: tomsonpl
body: LGTM 👍 
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T07:03:02Z
user: login: vgomez-el
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T15:32:24Z
user: login: vgomez-el
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T15:44:26Z
user: login: spong
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-24T15:54:33Z
user: login: spong
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-24T15:55:45Z
user: login: spong
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-24T16:02:21Z
user: login: spong
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-24T16:06:36Z
user: login: tomsonpl
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T16:07:18Z
user: login: tomsonpl
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T16:14:31Z
user: login: spong
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-24T16:19:31Z
user: login: spong
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-24T16:23:48Z
user: login: vgomez-el
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T16:28:19Z
user: login: vgomez-el
body: LGTM!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T16:28:37Z
user: login: tomsonpl
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T16:29:23Z

Review comments:
diff_hunk: @@ -61,15 +61,23 @@ export const registerProductFeatures = (
 };
 
 /**
- * Get the security product tier from the security product type in the config
+ * Get the security product tier from the security product type in the config. If not available, will check aiSoc
+ * product type and return the search_ai_lake tier.
  */
 export const getSecurityProductTier = (config: ServerlessSecurityConfig, logger: Logger): Tier => {
path: x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/index.ts
user: login: tomsonpl
body: Consider renaming this, I don't think it's 'security' specific anymore. 
created_at: 2025-04-24T06:55:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -61,15 +61,23 @@ export const registerProductFeatures = (
 };
 
 /**
- * Get the security product tier from the security product type in the config
+ * Get the security product tier from the security product type in the config. If not available, will check aiSoc
+ * product type and return the search_ai_lake tier.
  */
 export const getSecurityProductTier = (config: ServerlessSecurityConfig, logger: Logger): Tier => {
   const securityProductType = config.productTypes.find(
     (productType) => productType.product_line === ProductLine.security
   );
-  const tier = securityProductType ? securityProductType.product_tier : 'none';
+  const ai4SocProductType = config.productTypes.find(
+    (productType) => productType.product_line === ProductLine.aiSoc
+  );
+  const tier = securityProductType
+    ? securityProductType.product_tier
path: x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/index.ts
user: login: tomsonpl
body: I think that the tier will be always the same, so we might actually just get the first config, right? 
created_at: 2025-04-24T06:57:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,45 @@
+/*
+ * 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 expect from '@kbn/expect';
+
+import { FtrProviderContext } from '../../../../ftr_provider_context';
+import { waitFor } from '../../../../../common/utils/security_solution';
+
+export default ({ getService }: FtrProviderContext) => {
+  const kibanaServer = getService('kibanaServer');
+  const logger = getService('log');
+
+  const TASK_ID = 'serverless-security:nlp-cleanup-task:1.0.0';
+
+  describe('@serverless NLP Cleanup Task in SearchAILake Tier', () => {
+    describe('New SearchAILake Deployment', () => {
+      it('registers, runs and immediately deletes NLP Cleanup Task', async () => {
+        try {
+          // When running on CI, the task may not have run and been disabled, so poll for it to be cleaned up
+          await waitFor(
+            async () => {
+              const task = await kibanaServer.savedObjects.get({
+                type: 'task',
+                id: TASK_ID,
+              });
+
+              return task == null;
+            },
+            'waitForTaskToBeDeleted',
+            logger
+          );
+        } catch (e) {
+          expect(e.message).to.eql('Request failed with status code 404');
+
+          return;
+        }
+        throw new Error('Task should not exist!');
path: x-pack/test/security_solution_api_integration/test_suites/ai4dsoc/nlp_cleanup_task/search_ai_lake_tier/task_execution.ts
user: login: vgomez-el
body: nit: Review the error handling logic - if a 404 is expected because the task was deleted, perhaps throwing another error with 'Task should not exist!' seems to be redundant. Maybe I am missing something here and not understanding properly the error throwing/catching logic.
created_at: 2025-04-24T15:32:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,14 @@
+/*
+ * 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 { FtrProviderContext } from '../../../../ftr_provider_context';
+
+export default function ({ loadTestFile }: FtrProviderContext) {
+  describe('GenAI - NLP Cleanup Task', function () {
path: x-pack/test/security_solution_api_integration/test_suites/ai4dsoc/nlp_cleanup_task/search_ai_lake_tier/index.ts
user: login: vgomez-el
body: In serverless.config.ts file this is called AI4DSOC - NLP Cleanup Task, shouldn't it be called equally here?
created_at: 2025-04-24T15:44:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,45 @@
+/*
+ * 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 expect from '@kbn/expect';
+
+import { FtrProviderContext } from '../../../../ftr_provider_context';
+import { waitFor } from '../../../../../common/utils/security_solution';
+
+export default ({ getService }: FtrProviderContext) => {
+  const kibanaServer = getService('kibanaServer');
+  const logger = getService('log');
+
+  const TASK_ID = 'serverless-security:nlp-cleanup-task:1.0.0';
+
+  describe('@serverless NLP Cleanup Task in SearchAILake Tier', () => {
+    describe('New SearchAILake Deployment', () => {
+      it('registers, runs and immediately deletes NLP Cleanup Task', async () => {
+        try {
+          // When running on CI, the task may not have run and been disabled, so poll for it to be cleaned up
+          await waitFor(
+            async () => {
+              const task = await kibanaServer.savedObjects.get({
+                type: 'task',
+                id: TASK_ID,
+              });
+
+              return task == null;
+            },
+            'waitForTaskToBeDeleted',
+            logger
+          );
+        } catch (e) {
+          expect(e.message).to.eql('Request failed with status code 404');
+
+          return;
+        }
+        throw new Error('Task should not exist!');
path: x-pack/test/security_solution_api_integration/test_suites/ai4dsoc/nlp_cleanup_task/search_ai_lake_tier/task_execution.ts
user: login: spong
body: If the `404` is thrown it'll end up `return`'ing on line `39`, and so be a success. This is essentially just polling till the task doesn't exist anymore, and if the polling times out then it'll throw the new error and fail the test. 

This mirrors [the same test](https://github.com/elastic/kibana/blob/8436f45fd1e98deb4c9649c826635c41c9484892/x-pack/test/security_solution_api_integration/test_suites/genai/nlp_cleanup_task/trial_license_complete_tier/task_execution.ts#L39-L41) that was introduced for testing the `complete` tier last year, and that seems to have been working as expected from what I can tell.
created_at: 2025-04-24T15:54:33Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,14 @@
+/*
+ * 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 { FtrProviderContext } from '../../../../ftr_provider_context';
+
+export default function ({ loadTestFile }: FtrProviderContext) {
+  describe('GenAI - NLP Cleanup Task', function () {
path: x-pack/test/security_solution_api_integration/test_suites/ai4dsoc/nlp_cleanup_task/search_ai_lake_tier/index.ts
user: login: spong
body: Ty, will update!
created_at: 2025-04-24T15:55:45Z
author_association: MEMBER
diff_hunk: @@ -61,15 +61,23 @@ export const registerProductFeatures = (
 };
 
 /**
- * Get the security product tier from the security product type in the config
+ * Get the security product tier from the security product type in the config. If not available, will check aiSoc
+ * product type and return the search_ai_lake tier.
  */
 export const getSecurityProductTier = (config: ServerlessSecurityConfig, logger: Logger): Tier => {
   const securityProductType = config.productTypes.find(
     (productType) => productType.product_line === ProductLine.security
   );
-  const tier = securityProductType ? securityProductType.product_tier : 'none';
+  const ai4SocProductType = config.productTypes.find(
+    (productType) => productType.product_line === ProductLine.aiSoc
+  );
+  const tier = securityProductType
+    ? securityProductType.product_tier
path: x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/index.ts
user: login: spong
body: I was under the impression that there is no `security` ProductLine provided when it's `ai_soc`, is this not the case?

E.g. I thought the different configs would be as follows:

Essentials/Complete
```
xpack.securitySolutionServerless.productTypes:
  [
    { product_line: 'security', product_tier: 'complete' },
    { product_line: 'endpoint', product_tier: 'complete' },
    { product_line: 'cloud', product_tier: 'complete' },
  ]
```

AI4DSOC
```
xpack.securitySolutionServerless.productTypes:
  [
    { product_line: 'ai_soc', product_tier: 'search_ai_lake' },
  ]
```

And so we have to check both ProductLines to confirm if we're `complete`/`essentials`/`search_ai_lake`.


created_at: 2025-04-24T16:02:21Z
author_association: MEMBER
diff_hunk: @@ -61,15 +61,23 @@ export const registerProductFeatures = (
 };
 
 /**
- * Get the security product tier from the security product type in the config
+ * Get the security product tier from the security product type in the config. If not available, will check aiSoc
+ * product type and return the search_ai_lake tier.
  */
 export const getSecurityProductTier = (config: ServerlessSecurityConfig, logger: Logger): Tier => {
   const securityProductType = config.productTypes.find(
     (productType) => productType.product_line === ProductLine.security
   );
-  const tier = securityProductType ? securityProductType.product_tier : 'none';
+  const ai4SocProductType = config.productTypes.find(
+    (productType) => productType.product_line === ProductLine.aiSoc
+  );
+  const tier = securityProductType
+    ? securityProductType.product_tier
path: x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/index.ts
user: login: spong
body: Yeah, I believe this is the case, this is what the `ai_soc` deployment logs on startup:

<img width="1074" alt="image" src="https://github.com/user-attachments/assets/3aec5fa6-a85e-4da7-8183-7f70bc93ce23" />

created_at: 2025-04-24T16:06:36Z
author_association: MEMBER
diff_hunk: @@ -61,15 +61,23 @@ export const registerProductFeatures = (
 };
 
 /**
- * Get the security product tier from the security product type in the config
+ * Get the security product tier from the security product type in the config. If not available, will check aiSoc
+ * product type and return the search_ai_lake tier.
  */
 export const getSecurityProductTier = (config: ServerlessSecurityConfig, logger: Logger): Tier => {
   const securityProductType = config.productTypes.find(
     (productType) => productType.product_line === ProductLine.security
   );
-  const tier = securityProductType ? securityProductType.product_tier : 'none';
+  const ai4SocProductType = config.productTypes.find(
+    (productType) => productType.product_line === ProductLine.aiSoc
+  );
+  const tier = securityProductType
+    ? securityProductType.product_tier
path: x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/index.ts
user: login: tomsonpl
body: You're right - this is the case for MVP.

But post MVP we're going to be more flexible - this post shows examples of the configurations: https://github.com/elastic/security-team/issues/11781#issuecomment-2701518004

However, no matter how many product_lines we will choose - we always will have one `tier`. Which in the end means we can use `tier` of any product_line in the array, right? 

Does it make any sense? 
created_at: 2025-04-24T16:07:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -61,15 +61,23 @@ export const registerProductFeatures = (
 };
 
 /**
- * Get the security product tier from the security product type in the config
+ * Get the security product tier from the security product type in the config. If not available, will check aiSoc
+ * product type and return the search_ai_lake tier.
  */
 export const getSecurityProductTier = (config: ServerlessSecurityConfig, logger: Logger): Tier => {
   const securityProductType = config.productTypes.find(
     (productType) => productType.product_line === ProductLine.security
   );
-  const tier = securityProductType ? securityProductType.product_tier : 'none';
+  const ai4SocProductType = config.productTypes.find(
+    (productType) => productType.product_line === ProductLine.aiSoc
+  );
+  const tier = securityProductType
+    ? securityProductType.product_tier
path: x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/index.ts
user: login: tomsonpl
body: Like in this logic:
 https://github.com/elastic/kibana/blob/f833e09121d6235a22ec377ab17aa2b23b6191fa/src/cli/serve/compile_config_stack.js#L94
created_at: 2025-04-24T16:14:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -61,15 +61,23 @@ export const registerProductFeatures = (
 };
 
 /**
- * Get the security product tier from the security product type in the config
+ * Get the security product tier from the security product type in the config. If not available, will check aiSoc
+ * product type and return the search_ai_lake tier.
  */
 export const getSecurityProductTier = (config: ServerlessSecurityConfig, logger: Logger): Tier => {
   const securityProductType = config.productTypes.find(
     (productType) => productType.product_line === ProductLine.security
   );
-  const tier = securityProductType ? securityProductType.product_tier : 'none';
+  const ai4SocProductType = config.productTypes.find(
+    (productType) => productType.product_line === ProductLine.aiSoc
+  );
+  const tier = securityProductType
+    ? securityProductType.product_tier
path: x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/index.ts
user: login: spong
body: > But post MVP we're going to be more flexible - this post shows examples of the configurations: https://github.com/elastic/security-team/issues/11781#issuecomment-2701518004

Yeah, in that case if the `security` ProductLine will always be provided and be consistent we can later switch to use it. But I don't think we can do that now as it's missing, and it seems there's a difference between the post-MVP and future proposed configs as well, no?

I would say let's leave it as-is for now, and we can update later when the config is updated. Does that sound okay? This test will fail once the config changes, so it should be pretty easy to bundle those changes at the same time.
created_at: 2025-04-24T16:19:31Z
author_association: MEMBER
diff_hunk: @@ -61,15 +61,23 @@ export const registerProductFeatures = (
 };
 
 /**
- * Get the security product tier from the security product type in the config
+ * Get the security product tier from the security product type in the config. If not available, will check aiSoc
+ * product type and return the search_ai_lake tier.
  */
 export const getSecurityProductTier = (config: ServerlessSecurityConfig, logger: Logger): Tier => {
   const securityProductType = config.productTypes.find(
     (productType) => productType.product_line === ProductLine.security
   );
-  const tier = securityProductType ? securityProductType.product_tier : 'none';
+  const ai4SocProductType = config.productTypes.find(
+    (productType) => productType.product_line === ProductLine.aiSoc
+  );
+  const tier = securityProductType
+    ? securityProductType.product_tier
path: x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/index.ts
user: login: spong
body: > Like in this logic:
> 
> https://github.com/elastic/kibana/blob/f833e09121d6235a22ec377ab17aa2b23b6191fa/src/cli/serve/compile_config_stack.js#L94

Ahhh, I see. I didn't want to make any assumptions that this list would be sorted or wouldn't have arbitrary items added at any moment, so that's why I was trying to explicitly check.

If you're not concerned about those cases then I can go ahead and switch to just grabbing the first config like the code you linked.
created_at: 2025-04-24T16:23:48Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,45 @@
+/*
+ * 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 expect from '@kbn/expect';
+
+import { FtrProviderContext } from '../../../../ftr_provider_context';
+import { waitFor } from '../../../../../common/utils/security_solution';
+
+export default ({ getService }: FtrProviderContext) => {
+  const kibanaServer = getService('kibanaServer');
+  const logger = getService('log');
+
+  const TASK_ID = 'serverless-security:nlp-cleanup-task:1.0.0';
+
+  describe('@serverless NLP Cleanup Task in SearchAILake Tier', () => {
+    describe('New SearchAILake Deployment', () => {
+      it('registers, runs and immediately deletes NLP Cleanup Task', async () => {
+        try {
+          // When running on CI, the task may not have run and been disabled, so poll for it to be cleaned up
+          await waitFor(
+            async () => {
+              const task = await kibanaServer.savedObjects.get({
+                type: 'task',
+                id: TASK_ID,
+              });
+
+              return task == null;
+            },
+            'waitForTaskToBeDeleted',
+            logger
+          );
+        } catch (e) {
+          expect(e.message).to.eql('Request failed with status code 404');
+
+          return;
+        }
+        throw new Error('Task should not exist!');
path: x-pack/test/security_solution_api_integration/test_suites/ai4dsoc/nlp_cleanup_task/search_ai_lake_tier/task_execution.ts
user: login: vgomez-el
body: Now I got it, thanks for the clarification! 
created_at: 2025-04-24T16:28:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -61,15 +61,23 @@ export const registerProductFeatures = (
 };
 
 /**
- * Get the security product tier from the security product type in the config
+ * Get the security product tier from the security product type in the config. If not available, will check aiSoc
+ * product type and return the search_ai_lake tier.
  */
 export const getSecurityProductTier = (config: ServerlessSecurityConfig, logger: Logger): Tier => {
   const securityProductType = config.productTypes.find(
     (productType) => productType.product_line === ProductLine.security
   );
-  const tier = securityProductType ? securityProductType.product_tier : 'none';
+  const ai4SocProductType = config.productTypes.find(
+    (productType) => productType.product_line === ProductLine.aiSoc
+  );
+  const tier = securityProductType
+    ? securityProductType.product_tier
path: x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/index.ts
user: login: tomsonpl
body: I think as long as this works this way in compile_config_stack.js - we can have the same logic in code you're changing. 
But I am also ok with leaving it as it is now as it's not a big deal 👍 The behavior is the same :) 

Thanks for taking my suggestion under consideration! 
created_at: 2025-04-24T16:29:23Z
author_association: CONTRIBUTOR

PR comments:
user: login: elasticmachine
created_at: 2025-04-24T18:50:58Z
body: ## :yellow_heart: Build succeeded, but was flaky
* [Buildkite Build](https://buildkite.com/elastic/kibana-pull-request/builds/295937)
* Commit: c63337e342fbbeb9d9bfa5d1a683d909d2e51495

### Failed CI Steps
* [Defend Workflows Cypress Tests #3](https://buildkite.com/elastic/kibana-pull-request/builds/295937#019668ef-3dc3-487e-b379-58c9345d659a)

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

### History
* :green_heart: [Build #295910](https://buildkite.com/elastic/kibana-pull-request/builds/295910) succeeded ee3c319c2a785e8f2e596b662273b4a026fca046
* :green_heart: [Build #295590](https://buildkite.com/elastic/kibana-pull-request/builds/295590) succeeded 27f4721602e4e2b84c51827d108fd4783bfcd1dc
* :broken_heart: [Build #295569](https://buildkite.com/elastic/kibana-pull-request/builds/295569) failed 116bd7a7c0e8520027a716922d5dedb5f7c85a8a

cc @spong

<!--buildkite-pr-comment-kibana-pull-request
{"builds":[{"buildStatus":{"state":"passed","success":true,"hasRetries":true,"hasNonPreemptionRetries":true},"url":"https://buildkite.com/elastic/kibana-pull-request/builds/295937","number":295937,"commit":"c63337e342fbbeb9d9bfa5d1a683d909d2e51495"},{"buildStatus":{"state":"passed","success":true,"hasRetries":true,"hasNonPreemptionRetries":false},"url":"https://buildkite.com/elastic/kibana-pull-request/builds/295910","number":295910,"commit":"ee3c319c2a785e8f2e596b662273b4a026fca046"},{"buildStatus":{"state":"passed","success":true,"hasRetries":false,"hasNonPreemptionRetries":false},"url":"https://buildkite.com/elastic/kibana-pull-request/builds/295590","number":295590,"commit":"27f4721602e4e2b84c51827d108fd4783bfcd1dc"},{"buildStatus":{"state":"failing","success":false,"hasRetries":true,"hasNonPreemptionRetries":true},"url":"https://buildkite.com/elastic/kibana-pull-request/builds/295569","number":295569,"commit":"116bd7a7c0e8520027a716922d5dedb5f7c85a8a"}],"number":295937}
buildkite-pr-comment-->
author_association: CONTRIBUTOR