Skip to content
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
b4cbaf9
- (temp) enable in mgmt
andrew-goldstein Feb 7, 2025
593512a
beta version and feature flag
stephmilovic Mar 26, 2025
a49e283
rm pirate talk
stephmilovic Mar 26, 2025
f10e13a
Merge branch 'main' into security_ai_prompts_integration
stephmilovic Mar 28, 2025
e66000e
Merge branch 'main' into security_ai_prompts_integration
stephmilovic Apr 29, 2025
987e9fa
more fix
stephmilovic Apr 29, 2025
ce93e88
fix test
stephmilovic Apr 29, 2025
1bff213
fix types
stephmilovic Apr 29, 2025
bd13130
Merge branch 'main' into security_ai_prompts_integration
stephmilovic Apr 30, 2025
c37ebf4
test fixing
stephmilovic Apr 30, 2025
77e9927
type
stephmilovic May 1, 2025
ab70229
rm tagging
stephmilovic May 1, 2025
cb06cb2
Merge branch 'main' into security_ai_prompts_integration
stephmilovic May 1, 2025
fc38ea6
maybe fixing?
stephmilovic May 1, 2025
21097de
fix install remove assets test
stephmilovic May 1, 2025
43d11cf
rm tag assets test
stephmilovic May 1, 2025
d0c4af3
fix epm test
stephmilovic May 1, 2025
9b5f519
fix snapshot
stephmilovic May 1, 2025
25a997a
Merge branch 'main' into security_ai_prompts_integration
stephmilovic May 6, 2025
b4b0b43
update comment
stephmilovic May 6, 2025
4832e69
Merge branch 'main' into security_ai_prompts_integration
elasticmachine May 7, 2025
5bb9f94
exclude package from tests
stephmilovic May 7, 2025
b7d574e
Merge branch 'security_ai_prompts_integration' of github.com:stephmil…
stephmilovic May 7, 2025
596ee6c
rm from bundled packages
stephmilovic May 7, 2025
49a237d
Merge branch 'main' into security_ai_prompts_integration
elasticmachine May 7, 2025
90e3f69
fix error handling
stephmilovic May 8, 2025
02eefe4
Merge branch 'main' into security_ai_prompts_integration
elasticmachine May 8, 2025
bb76dc3
Merge branch 'main' into security_ai_prompts_integration
elasticmachine May 9, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion fleet_packages.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,4 @@
"name": "security_detection_engine",
"version": "9.0.3"
}
]
]
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export const promptType: SavedObjectsType = {
hidden: false,
management: {
importableAndExportable: true,
visibleInManagement: false,
visibleInManagement: true, // <--show in management
},
namespaceType: 'agnostic',
mappings: promptSavedObjectMappings,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"license": "Elastic License 2.0",
"scripts": {
"evaluate-model": "node ./scripts/model_evaluator",
"generate-security-ai-prompts": "node ./scripts/generate_security_ai_prompts",
"draw-graph": "node ./scripts/draw_graph"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
* 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.
*/

require('../../../../../../src/setup_node_env');
require('./generate_security_ai_prompts_script').generateSecurityAiPrompts();
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/*
* 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.
*/

/* eslint-disable no-console */

import { Prompt } from '@kbn/security-ai-prompts';
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';

export const OUTPUT_DIR = '../../../../../target/security_ai_prompts';
export const DELETE_FILES_PATTERN = '*.json';
export const SAVED_OBJECT_ID_PREFIX = 'security_ai_prompts-';

interface SecurityAiPromptSavedObject {
attributes: Prompt;
id: string;
type: 'security-ai-prompt';
}

export const createOutputDir = () => {
if (!existsSync(OUTPUT_DIR)) {
console.log(`Creating output directory: ${OUTPUT_DIR}`);
mkdirSync(OUTPUT_DIR);
}
};

export const deleteFilesByPattern = async ({
directoryPath,
pattern,
}: {
directoryPath: string;
pattern: string;
}): Promise<void> => {
try {
console.log(`Deleting files matching pattern "${pattern}" in directory "${directoryPath}"`);
const files = await globby(pattern, { cwd: directoryPath });

if (files.length === 0) {
console.log(`No files found matching pattern "${pattern}" in directory "${directoryPath}".`);
return;
}

for (const file of files) {
const filePath = path.join(directoryPath, file);
await fs.unlink(filePath);
console.log(`Deleted file: "${filePath}"`);
}
} catch (error) {
console.error(`Error deleting files: ${error.message}`);
throw error;
}
};

export const writeSavedObjects = async ({
direcotryPath,
savedObjects,
}: {
direcotryPath: string;
savedObjects: SecurityAiPromptSavedObject[];
}) => {
for (const savedObject of savedObjects) {
const filePath = path.join(direcotryPath, `${savedObject.id}.json`);

await fs.writeFile(filePath, `${JSON.stringify(savedObject, null, 2)}\n`);
console.log(`Wrote saved object to file: "${filePath}"`);
}
};

export const generateSavedObject = (prompt: Prompt): SecurityAiPromptSavedObject => ({
attributes: {
...prompt,
prompt: {
default: `${prompt.prompt.default}`,
},
},
id: `${SAVED_OBJECT_ID_PREFIX}${uuidv4()}`,
type: 'security-ai-prompt',
});

export const generateSavedObjects = (prompts: Prompt[]): SecurityAiPromptSavedObject[] =>
prompts.map(generateSavedObject);

export const generateSecurityAiPrompts = async () => {
console.log('Generating Security AI prompts');

createOutputDir();

await deleteFilesByPattern({
directoryPath: OUTPUT_DIR,
pattern: DELETE_FILES_PATTERN,
});

const prompts = [...localPrompts, ...localToolPrompts];

console.log('--> prompts', JSON.stringify(prompts, null, 2));

const savedObjects = generateSavedObjects(prompts);

console.log('--> savedObjects', JSON.stringify(savedObjects, null, 2));

await writeSavedObjects({
direcotryPath: OUTPUT_DIR,
savedObjects,
});

console.log('Done');
};
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"esModuleInterop": true,
"extends": "../../../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "target/types",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export enum RULE_PREVIEW_FROM {

export const PREBUILT_RULES_PACKAGE_NAME = 'security_detection_engine';
export const ENDPOINT_PACKAGE_NAME = 'endpoint';
export const SECURITY_AI_PROMPTS_PACKAGE_NAME = 'security_ai_prompts';

/**
* Rule signature id (`rule.rule_id`) of the prebuilt "Endpoint Security" rule.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,11 @@ export const allowedExperimentalValues = Object.freeze({

/** Enables new Data View Picker */
newDataViewPickerEnabled: false,

/**
* Automatically installs the security AI prompts package
*/
securityAIPromptsEnabled: false,
});

type ExperimentalConfigKeys = Array<keyof ExperimentalFeatures>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import type { IKibanaResponse, KibanaRequest, KibanaResponseFactory } from '@kbn/core/server';
import { ProductFeatureSecurityKey } from '@kbn/security-solution-features/keys';
import { transformError } from '@kbn/securitysolution-es-utils';
import { installSecurityAiPromptsPackage } from '../../logic/integrations/install_ai_prompts';
import type {
BootstrapPrebuiltRulesResponse,
PackageInstallStatus,
Expand All @@ -32,6 +33,7 @@ export const bootstrapPrebuiltRulesHandler = async (
const ctx = await context.resolve(['securitySolution', 'alerting', 'core']);
const securityContext = ctx.securitySolution;
const config = securityContext.getConfig();
const securityAIPromptsEnabled = config.experimentalFeatures.securityAIPromptsEnabled;

const savedObjectsClient = ctx.core.savedObjects.client;
const detectionRulesClient = securityContext.getDetectionRulesClient();
Expand Down Expand Up @@ -72,6 +74,18 @@ export const bootstrapPrebuiltRulesHandler = async (
});
}

const securityAiPromptsResult = securityAIPromptsEnabled
? await installSecurityAiPromptsPackage(config, securityContext)
: null;

if (securityAiPromptsResult !== null) {
packageResults.push({
name: securityAiPromptsResult.package.name,
version: securityAiPromptsResult.package.version,
status: securityAiPromptsResult.status,
});
}

const responseBody: BootstrapPrebuiltRulesResponse = {
packages: packageResults,
rules: ruleResults,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* 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 { SECURITY_AI_PROMPTS_PACKAGE_NAME } from '../../../../../../common/detection_engine/constants';
import type { SecuritySolutionApiRequestHandlerContext } from '../../../../../types';
import type { ConfigType } from '../../../../../config';
import { findLatestPackageVersion } from './find_latest_package_version';

export async function installSecurityAiPromptsPackage(
config: ConfigType,
context: SecuritySolutionApiRequestHandlerContext
) {
try {
await findLatestPackageVersion(context, SECURITY_AI_PROMPTS_PACKAGE_NAME);
} catch (e) {
// fail silently
return null;
}

const pkgVersion = await findLatestPackageVersion(context, SECURITY_AI_PROMPTS_PACKAGE_NAME);

return context.getInternalFleetServices().packages.ensureInstalledPackage({
pkgName: SECURITY_AI_PROMPTS_PACKAGE_NAME,
pkgVersion,
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error should probably be handled like this to avoid a double findLatestPackageVersion call when there's no error:

Suggested change
try {
await findLatestPackageVersion(context, SECURITY_AI_PROMPTS_PACKAGE_NAME);
} catch (e) {
// fail silently
return null;
}
const pkgVersion = await findLatestPackageVersion(context, SECURITY_AI_PROMPTS_PACKAGE_NAME);
return context.getInternalFleetServices().packages.ensureInstalledPackage({
pkgName: SECURITY_AI_PROMPTS_PACKAGE_NAME,
pkgVersion,
});
try {
const pkgVersion = await findLatestPackageVersion(context, SECURITY_AI_PROMPTS_PACKAGE_NAME);
return context.getInternalFleetServices().packages.ensureInstalledPackage({
pkgName: SECURITY_AI_PROMPTS_PACKAGE_NAME,
pkgVersion,
});
} catch (e) {
// fail silently
return null;
}

Another good option would be to adjust findLatestPackageVersion to return null when no package is found, instead of throwing an error. That would help avoid using a clumsy empty catch block, which can backfire in some cases.

}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,17 @@ export default function (providerContext: FtrProviderContext) {
resOsquerySavedQuery = err;
}
expect(resOsquerySavedQuery.response.data.statusCode).equal(404);
let securityAiPrompt;
try {
securityAiPrompt = await kibanaServer.savedObjects.get({
type: 'security-ai-prompt',
id: 'sample_security_ai_prompt',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to have conflicts if multiple packages have prompts with the same id?

I think that for other assets we prefix their ids with the name of the package to avoid these issues.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ids will be UUID autogenerated by saved object client, so we will not have this issue. this id was just for tests following other examples

});
} catch (err) {
checkErrorWithResponseDataOrThrow(err);
securityAiPrompt = err;
}
expect(securityAiPrompt.response.data.statusCode).equal(404);
});
it('should have removed the saved object', async function () {
let res;
Expand Down Expand Up @@ -483,6 +494,12 @@ const expectAssetsInstalled = ({
type: 'csp-rule-template',
id: 'sample_csp_rule_template',
});
const resSecurityAiPrompt = await kibanaServer.savedObjects.get({
type: 'security-ai-prompt',
id: 'sample_security_ai_prompt',
});
expect(resSecurityAiPrompt.id).equal('sample_security_ai_prompt');
expect(resSecurityAiPrompt.managed).be(true);
expect(resCloudSecurityPostureRuleTemplate.id).equal('sample_csp_rule_template');
expect(resCloudSecurityPostureRuleTemplate.managed).be(true);
const resTag = await kibanaServer.savedObjects.get({
Expand Down Expand Up @@ -581,6 +598,10 @@ const expectAssetsInstalled = ({
id: 'sample_search',
type: 'search',
},
{
id: 'sample_security_ai_prompt',
type: 'security-ai-prompt',
},
{
id: 'sample_security_rule',
type: 'security-rule',
Expand Down Expand Up @@ -786,6 +807,11 @@ const expectAssetsInstalled = ({
path: 'all_assets-0.1.0/kibana/search/sample_search.json',
type: 'epm-packages-assets',
},
{
id: '5d12ad91-0624-5dce-800d-b1f9a7732f7c',
path: 'all_assets-0.1.0/kibana/security_ai_prompt/sample_security_ai_prompts.json',
type: 'epm-packages-assets',
},
{
id: 'd8b175c3-0d42-5ec7-90c1-d1e4b307a4c2',
path: 'all_assets-0.1.0/kibana/security_rule/sample_security_rule.json',
Expand Down
9 changes: 9 additions & 0 deletions x-pack/test/fleet_api_integration/apis/epm/update_assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,10 @@ export default function (providerContext: FtrProviderContext) {
id: 'sample_osquery_saved_query',
type: 'osquery-saved-query',
},
{
id: 'sample_security_ai_prompt',
type: 'security-ai-prompt',
},
{
id: 'sample_tag',
type: 'tag',
Expand Down Expand Up @@ -552,6 +556,11 @@ export default function (providerContext: FtrProviderContext) {
path: 'all_assets-0.2.0/kibana/csp_rule_template/sample_csp_rule_template.json',
type: 'epm-packages-assets',
},
{
id: '848d7b69-26d1-52c1-8afc-65e627b34812',
path: 'all_assets-0.2.0/kibana/security_ai_prompt/sample_security_ai_prompts.json',
type: 'epm-packages-assets',
},
{
id: '8c665f28-a439-5f43-b5fd-8fda7b576735',
path: 'all_assets-0.2.0/manifest.yml',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"attributes": {
"promptId": "systemPrompt",
"promptGroupId": "aiAssistant",
"provider": "openai",
"prompt": {
"default": "You always talk like a pirate."
}
},
"id": "sample_security_ai_prompt",
"type": "security-ai-prompt"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"attributes": {
"promptId": "systemPrompt",
"promptGroupId": "aiAssistant",
"provider": "openai",
"prompt": {
"default": "You always talk like a pirate."
}
},
"id": "sample_security_ai_prompt",
"type": "security-ai-prompt"
}