From 8e911cd1c430d403c95402e5e1797aa9aba7dd75 Mon Sep 17 00:00:00 2001 From: Kenneth Kreindler <42113355+KDKHD@users.noreply.github.com> Date: Mon, 27 Apr 2026 17:23:37 +0200 Subject: [PATCH] [Security GenAI] Use stable IDs for Security AI Prompt saved object filenames (#265301) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Resolves #237178. The `generate_security_ai_prompts_script.ts` script previously generated saved object filenames using a random UUID on every invocation (e.g. `security_ai_prompts-022f0559-929f-4b06-ad08-571fc9b768ca.json`). This made PR reviews noisy and required a `rm ./*.json` step when copying files into the integration package. This PR replaces `uuidv4()` with a deterministic `generateStableId` function that builds the ID from `promptGroupId`, `promptId`, and optionally `provider` and `model` — all lowercased. Example stable filename: ``` security_ai_prompts-aiassistant-systemprompt-openai.json ``` ### Changes - **Script**: replaced `uuidv4()` with `generateStableId(prompt)` that derives a stable, lowercase ID from the prompt's identifying fields. Removed `uuid` import. - **Tests**: added 13 Jest unit tests for `generateStableId`, `generateSavedObject`, and `generateSavedObjects`. - **README**: removed the `rm ./*.json` step from the developer update flow — with stable filenames, `cp` overwrites files in place without needing to clear them first. ### Migration note The first time the script runs after this change, the integration repo (`elastic/integrations`) will still contain the old UUID-named files. A one-time `rm ./*.json` followed by `cp` in that repo is needed to fully replace them with stable-named files. After that, the `rm` step is no longer required. ## Test plan - [x] `node scripts/jest x-pack/solutions/security/plugins/elastic_assistant/scripts/generate_security_ai_prompts_script.test.ts` — 13 tests pass - [x] `node scripts/check_changes.ts` — lint and pre-commit checks pass - [x] Run `yarn generate-security-ai-prompts` in `x-pack/solutions/security/plugins/elastic_assistant` and confirm output filenames are stable across multiple invocations --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> (cherry picked from commit 6f23495f1b94f6a1fa1e5735ae00444164d770a4) --- .../packages/security-ai-prompts/README.md | 1 - ...enerate_security_ai_prompts_script.test.ts | 119 ++++++++++++++++++ .../generate_security_ai_prompts_script.ts | 18 ++- 3 files changed, 135 insertions(+), 3 deletions(-) create mode 100644 x-pack/solutions/security/plugins/elastic_assistant/scripts/generate_security_ai_prompts_script.test.ts diff --git a/x-pack/solutions/security/packages/security-ai-prompts/README.md b/x-pack/solutions/security/packages/security-ai-prompts/README.md index cff87d28dcf9a..6f81f27aaa458 100644 --- a/x-pack/solutions/security/packages/security-ai-prompts/README.md +++ b/x-pack/solutions/security/packages/security-ai-prompts/README.md @@ -64,7 +64,6 @@ When updating Security AI Prompts saved objects in the `elastic/integrations` re ```bash cd $INTEGRATIONS_HOME/packages/security_ai_prompts/kibana/security_ai_prompt - rm ./*.json cp $KIBANA_HOME/target/security_ai_prompts/*.json . ``` diff --git a/x-pack/solutions/security/plugins/elastic_assistant/scripts/generate_security_ai_prompts_script.test.ts b/x-pack/solutions/security/plugins/elastic_assistant/scripts/generate_security_ai_prompts_script.test.ts new file mode 100644 index 0000000000000..cfb30eb3ff098 --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/scripts/generate_security_ai_prompts_script.test.ts @@ -0,0 +1,119 @@ +/* + * 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 { Prompt } from '@kbn/security-ai-prompts'; +import { + SAVED_OBJECT_ID_PREFIX, + generateSavedObject, + generateSavedObjects, + generateStableId, +} from './generate_security_ai_prompts_script'; + +const basePrompt: Prompt = { + promptGroupId: 'aiAssistant', + promptId: 'systemPrompt', + prompt: { default: 'You are a helpful assistant.' }, +}; + +describe('generateStableId', () => { + it('converts camelCase to kebab-case', () => { + const id = generateStableId(basePrompt); + expect(id).toBe(`${SAVED_OBJECT_ID_PREFIX}ai-assistant-system-prompt`); + }); + + it('handles acronyms correctly (e.g. ESQL stays grouped)', () => { + const id = generateStableId({ ...basePrompt, promptId: 'NaturalLanguageESQLTool' }); + expect(id).toBe(`${SAVED_OBJECT_ID_PREFIX}ai-assistant-natural-language-esql-tool`); + }); + + it('appends provider when present', () => { + const id = generateStableId({ ...basePrompt, provider: 'openai' }); + expect(id).toBe(`${SAVED_OBJECT_ID_PREFIX}ai-assistant-system-prompt-openai`); + }); + + it('appends model when present', () => { + const id = generateStableId({ ...basePrompt, provider: 'openai', model: 'gpt-4o' }); + expect(id).toBe(`${SAVED_OBJECT_ID_PREFIX}ai-assistant-system-prompt-openai-gpt-4o`); + }); + + it('omits model segment when provider is absent', () => { + const id = generateStableId({ ...basePrompt, model: 'gpt-4o' }); + expect(id).toBe(`${SAVED_OBJECT_ID_PREFIX}ai-assistant-system-prompt`); + }); + + it('does not collide when provider-only vs model-only have the same value', () => { + const providerOnly = generateStableId({ ...basePrompt, provider: 'openai' }); + const modelOnly = generateStableId({ ...basePrompt, model: 'openai' }); + expect(providerOnly).not.toBe(modelOnly); + }); + + it('is fully lowercase', () => { + const id = generateStableId({ + ...basePrompt, + promptGroupId: 'AttackDiscovery', + promptId: 'SystemPrompt', + provider: 'OpenAI', + }); + expect(id).toBe(id.toLowerCase()); + }); + + it('returns the same id on repeated calls (stable)', () => { + expect(generateStableId(basePrompt)).toBe(generateStableId(basePrompt)); + }); + + it('produces distinct ids for different providers', () => { + const openai = generateStableId({ ...basePrompt, provider: 'openai' }); + const bedrock = generateStableId({ ...basePrompt, provider: 'bedrock' }); + expect(openai).not.toBe(bedrock); + }); + + it('produces distinct ids for different promptGroupIds', () => { + const a = generateStableId({ ...basePrompt, promptGroupId: 'aiAssistant' }); + const b = generateStableId({ ...basePrompt, promptGroupId: 'attackDiscovery' }); + expect(a).not.toBe(b); + }); + + it('produces distinct ids for different promptIds', () => { + const a = generateStableId({ ...basePrompt, promptId: 'systemPrompt' }); + const b = generateStableId({ ...basePrompt, promptId: 'userPrompt' }); + expect(a).not.toBe(b); + }); +}); + +describe('generateSavedObject', () => { + it('sets a stable id', () => { + const result = generateSavedObject(basePrompt); + expect(result.id).toBe(generateStableId(basePrompt)); + }); + + it('sets type to security-ai-prompt', () => { + expect(generateSavedObject(basePrompt).type).toBe('security-ai-prompt'); + }); + + it('preserves prompt attributes', () => { + const prompt: Prompt = { ...basePrompt, provider: 'bedrock', description: 'My prompt' }; + const { attributes } = generateSavedObject(prompt); + expect(attributes.promptId).toBe(prompt.promptId); + expect(attributes.promptGroupId).toBe(prompt.promptGroupId); + expect(attributes.provider).toBe('bedrock'); + expect(attributes.description).toBe('My prompt'); + expect(attributes.prompt.default).toBe(prompt.prompt.default); + }); +}); + +describe('generateSavedObjects', () => { + it('maps each prompt to a saved object with a unique stable id', () => { + const prompts: Prompt[] = [ + { ...basePrompt, promptId: 'systemPrompt' }, + { ...basePrompt, promptId: 'userPrompt' }, + { ...basePrompt, promptId: 'systemPrompt', provider: 'openai' }, + ]; + const results = generateSavedObjects(prompts); + const ids = results.map((r) => r.id); + expect(new Set(ids).size).toBe(ids.length); + }); +}); diff --git a/x-pack/solutions/security/plugins/elastic_assistant/scripts/generate_security_ai_prompts_script.ts b/x-pack/solutions/security/plugins/elastic_assistant/scripts/generate_security_ai_prompts_script.ts index 1853dd5fda922..1cbbc321af8b6 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/scripts/generate_security_ai_prompts_script.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/scripts/generate_security_ai_prompts_script.ts @@ -12,7 +12,6 @@ import * as fs from 'fs/promises'; import { existsSync, mkdirSync } from 'fs'; import * as path from 'path'; import globby from 'globby'; -import { v4 as uuidv4 } from 'uuid'; import { localPrompts } from '../server/lib/prompt/local_prompt_object'; import { localToolPrompts } from '../server/lib/prompt/tool_prompts'; @@ -76,6 +75,21 @@ export const writeSavedObjects = async ({ } }; +const toKebabCase = (str: string): string => + str + .replace(/([a-z])([A-Z])/g, '$1-$2') + .replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2') + .toLowerCase(); + +export const generateStableId = ({ promptGroupId, promptId, provider, model }: Prompt): string => { + const parts = [SAVED_OBJECT_ID_PREFIX + toKebabCase(promptGroupId), toKebabCase(promptId)]; + if (provider) { + parts.push(toKebabCase(provider)); + if (model) parts.push(toKebabCase(model)); + } + return parts.join('-'); +}; + export const generateSavedObject = (prompt: Prompt): SecurityAiPromptSavedObject => ({ attributes: { ...prompt, @@ -83,7 +97,7 @@ export const generateSavedObject = (prompt: Prompt): SecurityAiPromptSavedObject default: `${prompt.prompt.default}`, }, }, - id: `${SAVED_OBJECT_ID_PREFIX}${uuidv4()}`, + id: generateStableId(prompt), type: 'security-ai-prompt', });