diff --git a/x-pack/plugins/security_solution/common/api/quickstart_client.gen.ts b/x-pack/plugins/security_solution/common/api/quickstart_client.gen.ts index 19fbc38072c14..3f1ca654c1d47 100644 --- a/x-pack/plugins/security_solution/common/api/quickstart_client.gen.ts +++ b/x-pack/plugins/security_solution/common/api/quickstart_client.gen.ts @@ -293,6 +293,10 @@ import type { PreviewRiskScoreRequestBodyInput, PreviewRiskScoreResponse, } from './entity_analytics/risk_engine/preview_route.gen'; +import type { + SplunkRuleMigrationMatchPrebuiltRuleRequestBodyInput, + SplunkRuleMigrationMatchPrebuiltRuleResponse, +} from './siem_migrations/splunk/rules/match_prebuilt_rule.gen'; import type { CleanDraftTimelinesRequestBodyInput, CleanDraftTimelinesResponse, @@ -1912,6 +1916,22 @@ detection engine rules. }) .catch(catchAxiosErrorFormatAndThrow); } + /** + * Perform Elastic prebuilt rule matching from Splunk Security rule + */ + async splunkRuleMigrationMatchPrebuiltRule(props: SplunkRuleMigrationMatchPrebuiltRuleProps) { + this.log.info(`${new Date().toISOString()} Calling API SplunkRuleMigrationMatchPrebuiltRule`); + return this.kbnClient + .request({ + path: '/internal/migrations/splunk/rules/match_prebuilt_rule', + headers: { + [ELASTIC_HTTP_VERSION_HEADER]: '1', + }, + method: 'POST', + body: props.body, + }) + .catch(catchAxiosErrorFormatAndThrow); + } async startEntityEngine(props: StartEntityEngineProps) { this.log.info(`${new Date().toISOString()} Calling API StartEntityEngine`); return this.kbnClient @@ -2242,6 +2262,9 @@ export interface SetAlertsStatusProps { export interface SetAlertTagsProps { body: SetAlertTagsRequestBodyInput; } +export interface SplunkRuleMigrationMatchPrebuiltRuleProps { + body: SplunkRuleMigrationMatchPrebuiltRuleRequestBodyInput; +} export interface StartEntityEngineProps { params: StartEntityEngineRequestParamsInput; } diff --git a/x-pack/plugins/security_solution/common/api/siem_migrations/common.gen.ts b/x-pack/plugins/security_solution/common/api/siem_migrations/common.gen.ts new file mode 100644 index 0000000000000..db43041787722 --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/siem_migrations/common.gen.ts @@ -0,0 +1,38 @@ +/* + * 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. + */ + +/* + * NOTICE: Do not edit this file manually. + * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator. + * + * info: + * title: Common SIEM Migrations Attributes + * version: not applicable + */ + +import { z } from '@kbn/zod'; + +/** + * The GenAI connector id to use. + */ +export type ConnectorId = z.infer; +export const ConnectorId = z.string(); + +/** + * The LangSmith options object. + */ +export type LangSmithOptions = z.infer; +export const LangSmithOptions = z.object({ + /** + * The project name. + */ + projectName: z.string(), + /** + * The apiKey to use for tracing. + */ + apiKey: z.string(), +}); diff --git a/x-pack/plugins/security_solution/common/api/siem_migrations/common.schema.yaml b/x-pack/plugins/security_solution/common/api/siem_migrations/common.schema.yaml new file mode 100644 index 0000000000000..334a36eba91ba --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/siem_migrations/common.schema.yaml @@ -0,0 +1,24 @@ +openapi: 3.0.3 +info: + title: Common SIEM Migrations Attributes + version: 'not applicable' +paths: {} +components: + x-codegen-enabled: true + schemas: + ConnectorId: + type: string + description: The GenAI connector id to use. + LangSmithOptions: + type: object + description: The LangSmith options object. + required: + - projectName + - apiKey + properties: + projectName: + type: string + description: The project name. + apiKey: + type: string + description: The apiKey to use for tracing. diff --git a/x-pack/plugins/security_solution/common/api/siem_migrations/constants.ts b/x-pack/plugins/security_solution/common/api/siem_migrations/constants.ts new file mode 100644 index 0000000000000..8dd2a22b859e5 --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/siem_migrations/constants.ts @@ -0,0 +1,8 @@ +/* + * 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 INTERNAL_SIEM_MIGRATIONS_PATH = '/internal/siem_migrations' as const; diff --git a/x-pack/plugins/security_solution/common/api/siem_migrations/splunk/constants.ts b/x-pack/plugins/security_solution/common/api/siem_migrations/splunk/constants.ts new file mode 100644 index 0000000000000..3cb4ff0083e4e --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/siem_migrations/splunk/constants.ts @@ -0,0 +1,10 @@ +/* + * 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 { INTERNAL_SIEM_MIGRATIONS_PATH } from '../constants'; + +export const SPLUNK_MIGRATIONS_PATH = `${INTERNAL_SIEM_MIGRATIONS_PATH}/splunk` as const; diff --git a/x-pack/plugins/security_solution/common/api/siem_migrations/splunk/rules/constants.ts b/x-pack/plugins/security_solution/common/api/siem_migrations/splunk/rules/constants.ts new file mode 100644 index 0000000000000..67794c0eca73e --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/siem_migrations/splunk/rules/constants.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 { SPLUNK_MIGRATIONS_PATH } from '../constants'; + +const SPLUNK_RULE_MIGRATIONS_PATH = `${SPLUNK_MIGRATIONS_PATH}/rules` as const; + +export const SPLUNK_MATCH_PREBUILT_RULE_PATH = + `${SPLUNK_RULE_MIGRATIONS_PATH}/match_prebuilt_rule` as const; + +export const SPLUNK_TRANSLATE_RULE_PATH = `${SPLUNK_RULE_MIGRATIONS_PATH}/translate` as const; diff --git a/x-pack/plugins/security_solution/common/api/siem_migrations/splunk/rules/match_prebuilt_rule.gen.ts b/x-pack/plugins/security_solution/common/api/siem_migrations/splunk/rules/match_prebuilt_rule.gen.ts new file mode 100644 index 0000000000000..4e245673bd17d --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/siem_migrations/splunk/rules/match_prebuilt_rule.gen.ts @@ -0,0 +1,46 @@ +/* + * 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. + */ + +/* + * NOTICE: Do not edit this file manually. + * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator. + * + * info: + * title: Splunk Migration Match Prebuilt Rule API endpoint + * version: 1 + */ + +import { z } from '@kbn/zod'; + +import { SplunkRule } from './splunk_rule.gen'; +import { ConnectorId, LangSmithOptions } from '../../common.gen'; + +export type SplunkRuleMigrationMatchPrebuiltRuleRequestBody = z.infer< + typeof SplunkRuleMigrationMatchPrebuiltRuleRequestBody +>; +export const SplunkRuleMigrationMatchPrebuiltRuleRequestBody = z.object({ + splunkRule: SplunkRule, + connectorId: ConnectorId, + langSmithOptions: LangSmithOptions.optional(), +}); +export type SplunkRuleMigrationMatchPrebuiltRuleRequestBodyInput = z.input< + typeof SplunkRuleMigrationMatchPrebuiltRuleRequestBody +>; + +export type SplunkRuleMigrationMatchPrebuiltRuleResponse = z.infer< + typeof SplunkRuleMigrationMatchPrebuiltRuleResponse +>; +export const SplunkRuleMigrationMatchPrebuiltRuleResponse = z.object({ + /** + * The Elastic prebuilt rule information. + */ + rule: z.object({}), + /** + * Flag indicating if the rule is already installed. + */ + isInstalled: z.boolean(), +}); diff --git a/x-pack/plugins/security_solution/common/api/siem_migrations/splunk/rules/match_prebuilt_rule.schema.yaml b/x-pack/plugins/security_solution/common/api/siem_migrations/splunk/rules/match_prebuilt_rule.schema.yaml new file mode 100644 index 0000000000000..73325e8d13299 --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/siem_migrations/splunk/rules/match_prebuilt_rule.schema.yaml @@ -0,0 +1,48 @@ +openapi: 3.0.3 +info: + title: Splunk Migration Match Prebuilt Rule API endpoint + version: '1' +paths: + /internal/migrations/splunk/rules/match_prebuilt_rule: + post: + summary: Matches Splunk Security rules to Elastic prebuilt detection rules + operationId: SplunkRuleMigrationMatchPrebuiltRule + x-codegen-enabled: true + description: Perform Elastic prebuilt rule matching from Splunk Security rule + tags: + - Splunk Rule Migration API + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - splunkRule + - connectorId + properties: + splunkRule: + $ref: './splunk_rule.schema.yaml#/components/schemas/SplunkRule' + connectorId: + $ref: '../../common.schema.yaml#/components/schemas/ConnectorId' + langSmithOptions: + $ref: '../../common.schema.yaml#/components/schemas/LangSmithOptions' + responses: + 200: + description: Indicates a successful match with a prebuilt rule. + content: + application/json: + schema: + type: object + required: + - rule + - isInstalled + properties: + rule: + type: object + description: The Elastic prebuilt rule information. + isInstalled: + type: boolean + description: Flag indicating if the rule is already installed. + 204: + description: Indicates no prebuilt rule was matched. diff --git a/x-pack/plugins/security_solution/common/api/siem_migrations/splunk/rules/splunk_rule.gen.ts b/x-pack/plugins/security_solution/common/api/siem_migrations/splunk/rules/splunk_rule.gen.ts new file mode 100644 index 0000000000000..219eb3601e138 --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/siem_migrations/splunk/rules/splunk_rule.gen.ts @@ -0,0 +1,111 @@ +/* + * 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. + */ + +/* + * NOTICE: Do not edit this file manually. + * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator. + * + * info: + * title: Common Splunk Rules Attributes + * version: not applicable + */ + +import { z } from '@kbn/zod'; + +export type SplunkRule = z.infer; +export const SplunkRule = z.object({ + /** + * The Splunk rule id. + */ + id: z.string(), + /** + * The Splunk rule name. + */ + title: z.string(), + /** + * The Splunk rule splSearch query. + */ + splSearch: z.string(), + /** + * The Splunk rule description. + */ + description: z.string(), + /** + * String array containing the rule Mitre Attack technique IDs. + */ + mitreAttackIds: z.array(z.string()).optional(), +}); + +export type SplunkRuleMigration = z.infer; +export const SplunkRuleMigration = SplunkRule.merge( + z.object({ + /** + * The Splunk rule migration document uuid. + */ + uuid: z.string(), + /** + * The translated elastic query. + */ + elasticQuery: z.string(), + /** + * The translated elastic query language. + */ + elasticQueryLanguage: z.literal('esql').default('esql'), + /** + * The translation state. + */ + translationState: z.enum([ + 'untranslated', + 'matched', + 'translated:complete', + 'translated:partial', + 'translated:missing', + 'translated:failed', + ]), + /** + * The resources needed to complete the translation. + */ + resources: z + .array( + z.object({ + /** + * The resource type. + */ + type: z.enum(['macro', 'lookup']).optional(), + /** + * The resource name. + */ + name: z.string().optional(), + /** + * The resource value. + */ + value: z.string().optional(), + }) + ) + .optional(), + /** + * The missing resources needed to complete the translation. The format is `:`. E.g. `macro:my_macro`. + */ + missingResources: z.array(z.string()).optional(), + /** + * The status of the rule migration. + */ + status: z.enum(['pending', 'processing', 'finished', 'error']).default('pending'), + /** + * The migrated Elastic rule id. + */ + ruleId: z.string().optional(), + /** + * The summary of the migration in markdown. + */ + summary: z.string().optional(), + /** + * The agent messages list from the migration. + */ + messages: z.array(z.string()), + }) +); diff --git a/x-pack/plugins/security_solution/common/api/siem_migrations/splunk/rules/splunk_rule.schema.yaml b/x-pack/plugins/security_solution/common/api/siem_migrations/splunk/rules/splunk_rule.schema.yaml new file mode 100644 index 0000000000000..e403c14bbe5ce --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/siem_migrations/splunk/rules/splunk_rule.schema.yaml @@ -0,0 +1,112 @@ +openapi: 3.0.3 +info: + title: Common Splunk Rules Attributes + version: 'not applicable' +paths: {} +components: + x-codegen-enabled: true + schemas: + SplunkRule: + type: object + required: + - id + - title + - description + - splSearch + properties: + id: + type: string + description: The Splunk rule id. + title: + type: string + description: The Splunk rule name. + splSearch: + type: string + description: The Splunk rule splSearch query. + description: + type: string + description: The Splunk rule description. + mitreAttackIds: + type: array + items: + type: string + description: String array containing the rule Mitre Attack technique IDs. + + SplunkRuleMigration: + allOf: # Combines the SplunkRule and the inline model + - $ref: '#/components/schemas/SplunkRule' + - type: object + description: The Splunk rule migration document object. + required: + - uuid + - elasticQuery + - elasticQueryLanguage + - translationState + - status + - messages + properties: + uuid: + type: string + description: The Splunk rule migration document uuid. + elasticQuery: + type: string + description: The translated elastic query. + elasticQueryLanguage: + type: string + description: The translated elastic query language. + enum: + - esql + default: esql + translationState: + type: string + description: The translation state. + enum: + - untranslated + - matched + - translated:complete + - translated:partial + - translated:missing + - translated:failed + resources: + type: array + description: The resources needed to complete the translation. + items: + type: object + properties: + type: + type: string + description: The resource type. + enum: + - macro + - lookup + name: + type: string + description: The resource name. + value: + type: string + description: The resource value. + missingResources: + type: array + description: The missing resources needed to complete the translation. The format is `:`. E.g. `macro:my_macro`. + items: + type: string + status: + type: string + description: The status of the rule migration. + enum: + - pending + - processing + - finished + - error + default: pending + ruleId: + type: string + description: The migrated Elastic rule id. + summary: + type: string + description: The summary of the migration in markdown. + messages: + type: array + description: The agent messages list from the migration. + items: + type: string diff --git a/x-pack/plugins/security_solution/common/api/siem_migrations/splunk/rules/translate_rule.gen.ts b/x-pack/plugins/security_solution/common/api/siem_migrations/splunk/rules/translate_rule.gen.ts new file mode 100644 index 0000000000000..7b11a6fec34ab --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/siem_migrations/splunk/rules/translate_rule.gen.ts @@ -0,0 +1,39 @@ +/* + * 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. + */ + +/* + * NOTICE: Do not edit this file manually. + * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator. + * + * info: + * title: Splunk Migration Translation Rule API endpoint + * version: 1 + */ + +import { z } from '@kbn/zod'; + +import { SplunkRule, SplunkRuleMigration } from './splunk_rule.gen'; +import { ConnectorId, LangSmithOptions } from '../../common.gen'; + +export type SplunkRuleMigrationTranslateRuleRequestBody = z.infer< + typeof SplunkRuleMigrationTranslateRuleRequestBody +>; +export const SplunkRuleMigrationTranslateRuleRequestBody = z.object({ + splunkRule: SplunkRule, + connectorId: ConnectorId, + langSmithOptions: LangSmithOptions.optional(), +}); +export type SplunkRuleMigrationTranslateRuleRequestBodyInput = z.input< + typeof SplunkRuleMigrationTranslateRuleRequestBody +>; + +export type SplunkRuleMigrationTranslateRuleResponse = z.infer< + typeof SplunkRuleMigrationTranslateRuleResponse +>; +export const SplunkRuleMigrationTranslateRuleResponse = z.object({ + migration: SplunkRuleMigration, +}); diff --git a/x-pack/plugins/security_solution/common/api/siem_migrations/splunk/rules/translate_rule.schema.yaml b/x-pack/plugins/security_solution/common/api/siem_migrations/splunk/rules/translate_rule.schema.yaml new file mode 100644 index 0000000000000..c0c736c3a50b2 --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/siem_migrations/splunk/rules/translate_rule.schema.yaml @@ -0,0 +1,41 @@ +openapi: 3.0.3 +info: + title: Splunk Migration Translation Rule API endpoint + version: '1' +paths: + /internal/migrations/splunk/rules/translate: + post: + summary: Translates Splunk rules to Elastic custom rules + operationId: SplunkRuleMigrationTranslateRule + x-codegen-enabled: true + description: Perform Elastic custom rule rule translation from Splunk Security rule + tags: + - Splunk Rule Migration API + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - splunkRule + - connectorId + properties: + splunkRule: + $ref: './splunk_rule.schema.yaml#/components/schemas/SplunkRule' + connectorId: + $ref: '../../common.schema.yaml#/components/schemas/ConnectorId' + langSmithOptions: + $ref: '../../common.schema.yaml#/components/schemas/LangSmithOptions' + responses: + 200: + description: Indicates the translation of the Splunk rule to Elastic custom rule has been processed. + content: + application/json: + schema: + type: object + required: + - migration + properties: + migration: + $ref: './splunk_rule.schema.yaml#/components/schemas/SplunkRuleMigration' diff --git a/x-pack/plugins/security_solution/common/experimental_features.ts b/x-pack/plugins/security_solution/common/experimental_features.ts index 5e438669916c6..a16b88f649618 100644 --- a/x-pack/plugins/security_solution/common/experimental_features.ts +++ b/x-pack/plugins/security_solution/common/experimental_features.ts @@ -235,6 +235,11 @@ export const allowedExperimentalValues = Object.freeze({ * can be disabled if necessary in a given environment. */ entityStoreDisabled: false, + + /** + * Enables the siem migrations feature + */ + siemMigrationsEnabled: false, }); type ExperimentalConfigKeys = Array; diff --git a/x-pack/plugins/security_solution/kibana.jsonc b/x-pack/plugins/security_solution/kibana.jsonc index e48a9794b7e5c..8c8b77d48bc9f 100644 --- a/x-pack/plugins/security_solution/kibana.jsonc +++ b/x-pack/plugins/security_solution/kibana.jsonc @@ -54,7 +54,8 @@ "savedSearch", "unifiedDocViewer", "charts", - "entityManager" + "entityManager", + "inference" ], "optionalPlugins": [ "encryptedSavedObjects", diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/actions_client_chat.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/actions_client_chat.ts new file mode 100644 index 0000000000000..204978c901df6 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/actions_client_chat.ts @@ -0,0 +1,93 @@ +/* + * 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 { ActionsClientSimpleChatModel } from '@kbn/langchain/server'; +import { + ActionsClientBedrockChatModel, + ActionsClientChatOpenAI, + ActionsClientChatVertexAI, +} from '@kbn/langchain/server'; +import type { Logger } from '@kbn/core/server'; +import type { ActionsClient } from '@kbn/actions-plugin/server'; +import type { ActionsClientChatOpenAIParams } from '@kbn/langchain/server/language_models/chat_openai'; +import type { CustomChatModelInput as ActionsClientBedrockChatModelParams } from '@kbn/langchain/server/language_models/bedrock_chat'; +import type { CustomChatModelInput as ActionsClientChatVertexAIParams } from '@kbn/langchain/server/language_models/gemini_chat'; +import type { CustomChatModelInput as ActionsClientSimpleChatModelParams } from '@kbn/langchain/server/language_models/simple_chat_model'; + +export type ChatModel = + | ActionsClientSimpleChatModel + | ActionsClientChatOpenAI + | ActionsClientBedrockChatModel + | ActionsClientChatVertexAI; + +export type ActionsClientChatModelClass = + | typeof ActionsClientSimpleChatModel + | typeof ActionsClientChatOpenAI + | typeof ActionsClientBedrockChatModel + | typeof ActionsClientChatVertexAI; + +export type ChatModelParams = Partial & + Partial & + Partial & + Partial & { + /** Enables the streaming mode of the response, disabled by default */ + streaming?: boolean; + }; + +const llmTypeDictionary: Record = { + [`.gen-ai`]: `openai`, + [`.bedrock`]: `bedrock`, + [`.gemini`]: `gemini`, +}; + +export class ActionsClientChat { + constructor( + private readonly connectorId: string, + private readonly actionsClient: ActionsClient, + private readonly logger: Logger + ) {} + + public async createModel(params?: ChatModelParams): Promise { + const connector = await this.actionsClient.get({ id: this.connectorId }); + if (!connector) { + throw new Error(`Connector not found: ${this.connectorId}`); + } + + const llmType = this.getLLMType(connector.actionTypeId); + const ChatModelClass = this.getLLMClass(llmType); + + const model = new ChatModelClass({ + actionsClient: this.actionsClient, + connectorId: this.connectorId, + logger: this.logger, + llmType, + model: connector.config?.defaultModel, + ...params, + streaming: params?.streaming ?? false, // disabling streaming by default, for some reason is enabled when omitted + }); + return model; + } + + private getLLMType(actionTypeId: string): string | undefined { + if (llmTypeDictionary[actionTypeId]) { + return llmTypeDictionary[actionTypeId]; + } + throw new Error(`Unknown LLM type for action type ID: ${actionTypeId}`); + } + + private getLLMClass(llmType?: string): ActionsClientChatModelClass { + switch (llmType) { + case 'bedrock': + return ActionsClientBedrockChatModel; + case 'gemini': + return ActionsClientChatVertexAI; + case 'openai': + default: + return ActionsClientChatOpenAI; + } + } +} diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/routes.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/routes.ts new file mode 100644 index 0000000000000..f5e1be27e0e09 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/routes.ts @@ -0,0 +1,17 @@ +/* + * 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 { Logger } from '@kbn/core/server'; +import type { SecuritySolutionPluginRouter } from '../../types'; +import { registerSplunkMigrationRoutes } from './splunk/rules'; + +export const registerSiemMigrationsRoutes = ( + router: SecuritySolutionPluginRouter, + logger: Logger +) => { + registerSplunkMigrationRoutes(router, logger); +}; diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/splunk/rules/index.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/splunk/rules/index.ts new file mode 100644 index 0000000000000..ed620623e9e5a --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/splunk/rules/index.ts @@ -0,0 +1,19 @@ +/* + * 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 { Logger } from '@kbn/core/server'; +import type { SecuritySolutionPluginRouter } from '../../../../types'; +import { registerSplunkMatchPrebuiltRuleRoute } from './match_prebuilt_rule'; +import { registerSplunkTranslateRuleRoute } from './translate_rule'; + +export const registerSplunkMigrationRoutes = ( + router: SecuritySolutionPluginRouter, + logger: Logger +) => { + registerSplunkMatchPrebuiltRuleRoute(router, logger); + registerSplunkTranslateRuleRoute(router, logger); +}; diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/splunk/rules/match_prebuilt_rule/agent/graph.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/splunk/rules/match_prebuilt_rule/agent/graph.ts new file mode 100644 index 0000000000000..cb17f46ea21b8 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/splunk/rules/match_prebuilt_rule/agent/graph.ts @@ -0,0 +1,55 @@ +/* + * 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 { END, START, StateGraph } from '@langchain/langgraph'; +import { StringOutputParser } from '@langchain/core/output_parsers'; +import { matchPrebuiltRuleState } from './state'; +import type { + MatchPrebuiltRuleGraphParams, + MatchPrebuiltRuleNodeParams, + MatchPrebuiltRuleState, +} from './types'; +import { MATCH_PREBUILT_RULE_PROMPT } from './prompts'; + +const callModel = async ({ + state, + model, +}: MatchPrebuiltRuleNodeParams): Promise> => { + const outputParser = new StringOutputParser(); + const matchPrebuiltRule = MATCH_PREBUILT_RULE_PROMPT.pipe(model).pipe(outputParser); + + const elasticSecurityRules = Array(state.prebuiltRulesMap.keys()).join('\n'); + const response = await matchPrebuiltRule.invoke({ + elasticSecurityRules, + splunkRuleTitle: state.splunkRuleTitle, + splunkRuleDescription: state.splunkRuleDescription, + }); + return { response }; +}; + +const processResponse = (state: MatchPrebuiltRuleState): Partial => { + const cleanResponse = state.response.trim(); + if (cleanResponse === 'no_match') { + return { matched: false }; + } + const result = state.prebuiltRulesMap.get(cleanResponse); + if (result != null) { + return { matched: true, result }; + } + return { matched: false }; +}; + +export async function getMatchPrebuiltRuleGraph({ model }: MatchPrebuiltRuleGraphParams) { + const matchPrebuiltRuleGraph = new StateGraph(matchPrebuiltRuleState) + .addNode('callModel', (state: MatchPrebuiltRuleState) => callModel({ state, model })) + .addNode('processResponse', processResponse) + .addEdge(START, 'callModel') + .addEdge('callModel', 'processResponse') + .addEdge('processResponse', END); + + return matchPrebuiltRuleGraph.compile(); +} diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/splunk/rules/match_prebuilt_rule/agent/prompts.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/splunk/rules/match_prebuilt_rule/agent/prompts.ts new file mode 100644 index 0000000000000..4cae08c1da6ff --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/splunk/rules/match_prebuilt_rule/agent/prompts.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ChatPromptTemplate } from '@langchain/core/prompts'; +export const MATCH_PREBUILT_RULE_PROMPT = ChatPromptTemplate.fromMessages([ + [ + 'system', + `You are an expert assistant in Cybersecurity, your task is to help migrating a SIEM detection rule, from Splunk Security to Elastic Security. +You will be provided with a Splunk Detection Rule name by the user, your goal is to try find an Elastic Detection Rule that covers the same threat, if any. +The list of Elastic Detection Rules suggested is provided in the context below. + + +If there is no Elastic rule in the list that covers the same threat, answer only with the string: no_match +If there is one Elastic rule in the list that covers the same threat, answer only with its name without any further explanation. +If there are multiple rules in the list that cover the same threat, answer with the most specific of them, for example: "Linux User Account Creation" is more specific than "User Account Creation". + + + + +{elasticSecurityRules} + +`, + ], + [ + 'human', + `The Splunk Detection Rule is: + +{splunkRuleTitle} + +`, + ], + ['ai', 'Please find the answer below:'], +]); diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/splunk/rules/match_prebuilt_rule/agent/state.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/splunk/rules/match_prebuilt_rule/agent/state.ts new file mode 100644 index 0000000000000..dc93281bac3c8 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/splunk/rules/match_prebuilt_rule/agent/state.ts @@ -0,0 +1,18 @@ +/* + * 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 { Annotation } from '@langchain/langgraph'; +import type { PrebuiltRuleMapped, PrebuiltRulesMapByName } from '../types'; + +export const matchPrebuiltRuleState = Annotation.Root({ + splunkRuleTitle: Annotation(), + splunkRuleDescription: Annotation, + prebuiltRulesMap: Annotation, + response: Annotation, + matched: Annotation, + result: Annotation, +}); diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/splunk/rules/match_prebuilt_rule/agent/types.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/splunk/rules/match_prebuilt_rule/agent/types.ts new file mode 100644 index 0000000000000..40e8ca6f9a0fd --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/splunk/rules/match_prebuilt_rule/agent/types.ts @@ -0,0 +1,20 @@ +/* + * 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 { ChatModel } from '../../../../actions_client_chat'; +import type { matchPrebuiltRuleState } from './state'; + +export type MatchPrebuiltRuleState = typeof matchPrebuiltRuleState.State; + +export interface MatchPrebuiltRuleGraphParams { + model: ChatModel; +} + +export interface MatchPrebuiltRuleNodeParams { + model: ChatModel; + state: MatchPrebuiltRuleState; +} diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/splunk/rules/match_prebuilt_rule/api/match_prebuilt_rule.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/splunk/rules/match_prebuilt_rule/api/match_prebuilt_rule.ts new file mode 100644 index 0000000000000..1c3f384eec30a --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/splunk/rules/match_prebuilt_rule/api/match_prebuilt_rule.ts @@ -0,0 +1,91 @@ +/* + * 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 { IKibanaResponse, Logger } from '@kbn/core/server'; +import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; +import { getRequestAbortedSignal } from '@kbn/data-plugin/server'; +import { getLangSmithTracer } from '@kbn/langchain/server/tracers/langsmith'; +import { APMTracer } from '@kbn/langchain/server/tracers/apm'; +import { SplunkRuleMigrationMatchPrebuiltRuleRequestBody } from '../../../../../../../common/api/siem_migrations/splunk/rules/match_prebuilt_rule.gen'; +import type { SecuritySolutionPluginRouter } from '../../../../../../types'; +import { SPLUNK_MATCH_PREBUILT_RULE_PATH } from '../../../../../../../common/api/siem_migrations/splunk/rules/constants'; +import { retrievePrebuiltRulesMap } from './util/prebuilt_rules'; +import { getMatchPrebuiltRuleGraph } from '../agent/graph'; +import type { MatchPrebuiltRuleState } from '../agent/types'; +import type { PrebuiltRuleMapped } from '../types'; +import { ActionsClientChat } from '../../../../actions_client_chat'; + +export const registerSplunkMatchPrebuiltRuleRoute = ( + router: SecuritySolutionPluginRouter, + logger: Logger +) => { + router.versioned + .post({ + path: SPLUNK_MATCH_PREBUILT_RULE_PATH, + access: 'internal', + options: { + tags: ['access:securitySolution'], + }, + }) + .addVersion( + { + version: '1', + validate: { + request: { + body: buildRouteValidationWithZod(SplunkRuleMigrationMatchPrebuiltRuleRequestBody), + }, + }, + }, + async (context, request, response): Promise> => { + const { langSmithOptions, splunkRule, connectorId } = request.body; + try { + const ctx = await context.resolve(['core', 'actions', 'alerting']); + + const actionsClient = ctx.actions.getActionsClient(); + const soClient = ctx.core.savedObjects.client; + const rulesClient = ctx.alerting.getRulesClient(); + + const prebuiltRulesMap = await retrievePrebuiltRulesMap({ + soClient, + rulesClient, + splunkRule, + }); + + const actionsClientChat = new ActionsClientChat(connectorId, actionsClient, logger); + const model = await actionsClientChat.createModel({ + signal: getRequestAbortedSignal(request.events.aborted$), + temperature: 0.05, + }); + + const parameters: Partial = { + splunkRuleTitle: splunkRule.title, + splunkRuleDescription: splunkRule.description, + prebuiltRulesMap, + }; + + const options = { + callbacks: [ + new APMTracer({ projectName: langSmithOptions?.projectName ?? 'default' }, logger), + ...getLangSmithTracer({ ...langSmithOptions, logger }), + ], + }; + const graph = await getMatchPrebuiltRuleGraph({ model }); + const matchPrebuiltRuleState = await graph.invoke(parameters, options); + + const { matched, result } = matchPrebuiltRuleState as MatchPrebuiltRuleState; + if (!matched) { + return response.noContent(); + } + return response.ok({ body: result }); + } catch (err) { + return response.badRequest({ + body: err.message, + }); + } + } + ); +}; diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/splunk/rules/match_prebuilt_rule/api/util/prebuilt_rules.test.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/splunk/rules/match_prebuilt_rule/api/util/prebuilt_rules.test.ts new file mode 100644 index 0000000000000..dd3801dde0e33 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/splunk/rules/match_prebuilt_rule/api/util/prebuilt_rules.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 { savedObjectsClientMock } from '@kbn/core-saved-objects-api-server-mocks'; +import { retrievePrebuiltRulesMap } from './prebuilt_rules'; +import { rulesClientMock } from '@kbn/alerting-plugin/server/mocks'; +import type { SplunkRule } from '../../../../../../../../common/api/siem_migrations/splunk/rules/splunk_rule.gen'; + +jest.mock( + '../../../../../../detection_engine/prebuilt_rules/logic/rule_objects/prebuilt_rule_objects_client', + () => ({ createPrebuiltRuleObjectsClient: jest.fn() }) +); +jest.mock( + '../../../../../../detection_engine/prebuilt_rules/logic/rule_assets/prebuilt_rule_assets_client', + () => ({ createPrebuiltRuleAssetsClient: jest.fn() }) +); + +const rule1 = { + name: 'rule one', + id: 'rule1', + threat: [ + { + framework: 'MITRE ATT&CK', + technique: [{ id: 'T1234', name: 'tactic one' }], + }, + ], +}; +const rule2 = { + name: 'rule two', + id: 'rule2', +}; + +const defaultRuleVersionsTriad = new Map([ + ['rule1', { target: rule1 }], + ['rule2', { target: rule2, current: rule2 }], +]); +const mockFetchRuleVersionsTriad = jest.fn().mockResolvedValue(defaultRuleVersionsTriad); +jest.mock( + '../../../../../../detection_engine/prebuilt_rules/logic/rule_versions/fetch_rule_versions_triad', + () => ({ + fetchRuleVersionsTriad: () => mockFetchRuleVersionsTriad(), + }) +); + +const splunkRule: SplunkRule = { + title: 'splunk rule', + description: 'splunk rule description', + search: 'index=*', + mitreAttackIds: ['T1234'], +}; + +const defaultParams = { + soClient: savedObjectsClientMock.create(), + rulesClient: rulesClientMock.create(), + splunkRule, +}; + +describe('retrievePrebuiltRulesMap', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('when prebuilt rule is installed', () => { + it('should return isInstalled flag', async () => { + const prebuiltRulesMap = await retrievePrebuiltRulesMap(defaultParams); + expect(prebuiltRulesMap.size).toBe(2); + expect(prebuiltRulesMap.get('rule one')).toEqual( + expect.objectContaining({ isInstalled: false }) + ); + expect(prebuiltRulesMap.get('rule two')).toEqual( + expect.objectContaining({ isInstalled: true }) + ); + }); + }); + + describe('when splunk rule does not contain mitreAttackIds', () => { + it('should return the full rules map', async () => { + const prebuiltRulesMap = await retrievePrebuiltRulesMap({ + ...defaultParams, + splunkRule: { ...splunkRule, mitreAttackIds: undefined }, + }); + expect(prebuiltRulesMap.size).toBe(2); + }); + }); + + describe('when splunk rule contains empty mitreAttackIds', () => { + it('should return the full rules map', async () => { + const prebuiltRulesMap = await retrievePrebuiltRulesMap({ + ...defaultParams, + splunkRule: { ...splunkRule, mitreAttackIds: [] }, + }); + expect(prebuiltRulesMap.size).toBe(2); + }); + }); + + describe('when splunk rule contains non matching mitreAttackIds', () => { + it('should return the full rules map', async () => { + const prebuiltRulesMap = await retrievePrebuiltRulesMap({ + ...defaultParams, + splunkRule: { ...splunkRule, mitreAttackIds: ['T2345'] }, + }); + expect(prebuiltRulesMap.size).toBe(1); + expect(prebuiltRulesMap.get('rule two')).toEqual(expect.objectContaining({ rule: rule2 })); + }); + }); + + describe('when splunk rule contains matching mitreAttackIds', () => { + it('should return the filtered rules map', async () => { + const prebuiltRulesMap = await retrievePrebuiltRulesMap(defaultParams); + expect(prebuiltRulesMap.size).toBe(2); + expect(prebuiltRulesMap.get('rule one')).toEqual(expect.objectContaining({ rule: rule1 })); + expect(prebuiltRulesMap.get('rule two')).toEqual(expect.objectContaining({ rule: rule2 })); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/splunk/rules/match_prebuilt_rule/api/util/prebuilt_rules.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/splunk/rules/match_prebuilt_rule/api/util/prebuilt_rules.ts new file mode 100644 index 0000000000000..5d209928ec7fe --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/splunk/rules/match_prebuilt_rule/api/util/prebuilt_rules.ts @@ -0,0 +1,67 @@ +/* + * 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 { RulesClient } from '@kbn/alerting-plugin/server'; +import type { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server'; +import type { SplunkRule } from '../../../../../../../../common/api/siem_migrations/splunk/rules/splunk_rule.gen'; +import { fetchRuleVersionsTriad } from '../../../../../../detection_engine/prebuilt_rules/logic/rule_versions/fetch_rule_versions_triad'; +import { createPrebuiltRuleObjectsClient } from '../../../../../../detection_engine/prebuilt_rules/logic/rule_objects/prebuilt_rule_objects_client'; +import { createPrebuiltRuleAssetsClient } from '../../../../../../detection_engine/prebuilt_rules/logic/rule_assets/prebuilt_rule_assets_client'; +import type { PrebuiltRulesMapByName } from '../../types'; + +interface RetrievePrebuiltRulesParams { + soClient: SavedObjectsClientContract; + rulesClient: RulesClient; + splunkRule: SplunkRule; +} + +export const retrievePrebuiltRulesMap = async ({ + soClient, + rulesClient, + splunkRule, +}: RetrievePrebuiltRulesParams): Promise => { + const ruleAssetsClient = createPrebuiltRuleAssetsClient(soClient); + const ruleObjectsClient = createPrebuiltRuleObjectsClient(rulesClient); + + const prebuiltRulesMap = await fetchRuleVersionsTriad({ + ruleAssetsClient, + ruleObjectsClient, + }); + const prebuiltRulesByName: PrebuiltRulesMapByName = new Map(); + prebuiltRulesMap.forEach((ruleVersions) => { + const rule = ruleVersions.target || ruleVersions.current; + if (rule) { + prebuiltRulesByName.set(rule.name, { + isInstalled: !!ruleVersions.current, + rule, + }); + } + }); + + if (splunkRule.mitreAttackIds?.length) { + // If this rule has MITRE ATT&CK IDs, remove unrelated prebuilt rules + prebuiltRulesByName.forEach(({ rule }, ruleName) => { + const mitreAttackThreat = rule.threat?.filter( + ({ framework }) => framework === 'MITRE ATT&CK' + ); + if (!mitreAttackThreat) { + // If this rule has no MITRE ATT&CK reference we can not ensure it is not related to the rule, keep it + return; + } + + const sameTechnique = mitreAttackThreat.find((threat) => + threat.technique?.some(({ id }) => splunkRule.mitreAttackIds?.includes(id)) + ); + + if (!sameTechnique) { + prebuiltRulesByName.delete(ruleName); + } + }); + } + + return prebuiltRulesByName; +}; diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/splunk/rules/match_prebuilt_rule/index.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/splunk/rules/match_prebuilt_rule/index.ts new file mode 100644 index 0000000000000..beef0eef8c0fa --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/splunk/rules/match_prebuilt_rule/index.ts @@ -0,0 +1,8 @@ +/* + * 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 { registerSplunkMatchPrebuiltRuleRoute } from './api/match_prebuilt_rule'; diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/splunk/rules/match_prebuilt_rule/types.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/splunk/rules/match_prebuilt_rule/types.ts new file mode 100644 index 0000000000000..bffc7c2714f4f --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/splunk/rules/match_prebuilt_rule/types.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 type { PrebuiltRuleAsset } from '../../../../detection_engine/prebuilt_rules'; + +export interface PrebuiltRuleMapped { + isInstalled: boolean; + rule: PrebuiltRuleAsset; +} + +export type PrebuiltRulesMapByName = Map; diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/splunk/rules/translate_rule/agent/esql_knowledge_base_caller.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/splunk/rules/translate_rule/agent/esql_knowledge_base_caller.ts new file mode 100644 index 0000000000000..963eec6ace830 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/splunk/rules/translate_rule/agent/esql_knowledge_base_caller.ts @@ -0,0 +1,36 @@ +/* + * 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 { naturalLanguageToEsql, type InferenceClient } from '@kbn/inference-plugin/server'; +import type { Logger } from '@kbn/core/server'; +import { lastValueFrom } from 'rxjs'; + +export type EsqlKnowledgeBaseCaller = (input: string) => Promise; + +type GetEsqlTranslatorToolParams = (params: { + inferenceClient: InferenceClient; + connectorId: string; + logger: Logger; +}) => EsqlKnowledgeBaseCaller; + +export const getEsqlKnowledgeBase: GetEsqlTranslatorToolParams = + ({ inferenceClient: client, connectorId, logger }) => + async (input: string) => { + const { content } = await lastValueFrom( + naturalLanguageToEsql({ + client, + connectorId, + input, + logger: { + debug: (source) => { + logger.debug(typeof source === 'function' ? source() : source); + }, + }, + }) + ); + return content; + }; diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/splunk/rules/translate_rule/agent/graph.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/splunk/rules/translate_rule/agent/graph.ts new file mode 100644 index 0000000000000..86fddb701e75c --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/splunk/rules/translate_rule/agent/graph.ts @@ -0,0 +1,104 @@ +/* + * 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 { END, START, StateGraph } from '@langchain/langgraph'; +import { ToolNode } from '@langchain/langgraph/prebuilt'; +// import { StringOutputParser } from '@langchain/core/output_parsers'; +import { AIMessage } from '@langchain/core/messages'; +import type { Runnable } from '@langchain/core/runnables'; +import { translateRuleState } from './state'; +import type { TranslateRuleGraphParams, TranslateRuleState } from './types'; +import { TRANSLATE_RULE_MAIN_PROMPT, getEsqlTranslationPrompt } from './prompts'; +import { getEsqlKnowledgeBase, type EsqlKnowledgeBaseCaller } from './esql_knowledge_base_caller'; + +type GraphNode = (state: TranslateRuleState) => Promise>; + +const initMessages: GraphNode = async (state: TranslateRuleState) => { + return { + messages: await TRANSLATE_RULE_MAIN_PROMPT.formatMessages({ + splunkRuleTitle: state.splunkRuleTitle, + splunkRuleDescription: state.splunkRuleDescription, + splunkRuleQuery: state.splunkRuleQuery, + }), + }; +}; + +const createCallModelNode = + (model: Runnable): GraphNode => + async (state) => { + const response = await model.invoke(state.messages); + return { messages: [response] }; + }; + +const createTranslationNode = (esqlKnowledgeBaseCaller: EsqlKnowledgeBaseCaller): GraphNode => { + return async (state) => { + const input = getEsqlTranslationPrompt({ + splunkRuleTitle: state.splunkRuleTitle, + splunkRuleDescription: state.splunkRuleDescription, + splunkRuleQuery: state.splunkRuleQuery, + }); + const response = await esqlKnowledgeBaseCaller(input); + return { messages: [new AIMessage(response)] }; + }; +}; + +const responseNode: GraphNode = async (state) => { + const messages = state.messages; + const lastMessage = messages[messages.length - 1] as AIMessage; + return { response: lastMessage.content as string }; +}; + +function toolConditionalEdge(state: TranslateRuleState) { + const messages = state.messages; + const lastMessage = messages[messages.length - 1] as AIMessage; + if (lastMessage.tool_calls?.length) { + return 'tools'; + } + return 'processResponse'; +} + +// export function getTranslateRuleGraph({ model, tools }: TranslateRuleGraphParams) { +// if (model.bindTools === undefined) { +// throw new Error(`The ${model.name} model does not support tools`); +// } +// const modelWithTools: Runnable = model.bindTools(tools); +// const callModel = createCallModelNode(modelWithTools); +// const toolsNode = new ToolNode(tools); + +// const translateRuleGraph = new StateGraph(translateRuleState) +// .addNode('initMessages', initMessages) +// .addNode('callModel', callModel) +// .addNode('tools', toolsNode) +// .addNode('processResponse', responseNode) + +// .addEdge(START, 'initMessages') +// .addEdge('initMessages', 'callModel') +// .addConditionalEdges('callModel', toolConditionalEdge) +// .addEdge('tools', 'callModel') +// .addEdge('processResponse', END); + +// return translateRuleGraph.compile(); +// } + +export function getTranslateRuleGraph({ + inferenceClient, + connectorId, + logger, +}: TranslateRuleGraphParams) { + const esqlKnowledgeBaseCaller = getEsqlKnowledgeBase({ inferenceClient, connectorId, logger }); + const translationNode = createTranslationNode(esqlKnowledgeBaseCaller); + + const translateRuleGraph = new StateGraph(translateRuleState) + .addNode('translation', translationNode) + .addNode('processResponse', responseNode) + + .addEdge(START, 'translation') + .addEdge('translation', 'processResponse') + .addEdge('processResponse', END); + + return translateRuleGraph.compile(); +} diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/splunk/rules/translate_rule/agent/prompts.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/splunk/rules/translate_rule/agent/prompts.ts new file mode 100644 index 0000000000000..c5e7895ee5f59 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/splunk/rules/translate_rule/agent/prompts.ts @@ -0,0 +1,72 @@ +/* + * 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 { ChatPromptTemplate } from '@langchain/core/prompts'; +import type { TranslateRuleState } from './types'; + +export const TRANSLATE_RULE_MAIN_PROMPT = ChatPromptTemplate.fromMessages([ + [ + 'system', + `You are a helpful cybersecurity (SIEM) expert agent. Your task is to translate "detection rules" from Splunk to Elastic Security. +You will be provided with a Splunk rule information: the title, description and the SPL (Search Processing Language) query. +Your goal is to translate the SPL query into an equivalent Elastic Security Query Language (ES|QL) query. + +IMPORTANT: Always use the tools provided to translate the ES|QL query and summarize the translation, rather than making assumptions about how the ES|QL language works. + +The output should contain: +- First, the ES|QL query inside an \`\`\`esql code block. +- At the end, the summary of the translation process followed in markdown, starting with "## Translation Summary". +`, + ], + [ + 'human', + `Translate this Splunk rule into an Elastic ES|QL query rule: +<> +{splunkRuleTitle} +<> + +<> +{splunkRuleDescription} +<> + +<> +{splunkRuleQuery} +<> + +`, + ], +]); + +export const getEsqlTranslationPrompt = ({ + splunkRuleTitle, + splunkRuleDescription, + splunkRuleQuery, +}: Pick< + TranslateRuleState, + 'splunkRuleTitle' | 'splunkRuleDescription' | 'splunkRuleQuery' +>): string => `You are a helpful cybersecurity (SIEM) expert agent. Your task is to migrate "detection rules" from Splunk to Elastic Security. +Below you will find Splunk rule information: the title, description and the SPL (Search Processing Language) query. +Your goal is to translate the SPL query into an equivalent Elastic Security Query Language (ES|QL) query. + +The output will be parsed and should contain: +- First, the ES|QL query inside an \`\`\`esql code block. +- At the end, the summary of the translation process followed in markdown, starting with "## Migration Summary". + +This is the Splunk rule information: + +<> +${splunkRuleTitle} +<> + +<> +${splunkRuleDescription} +<> + +<> +${splunkRuleQuery} +<> +`; diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/splunk/rules/translate_rule/agent/state.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/splunk/rules/translate_rule/agent/state.ts new file mode 100644 index 0000000000000..29ba9a1433d2b --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/splunk/rules/translate_rule/agent/state.ts @@ -0,0 +1,20 @@ +/* + * 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 { BaseMessage } from '@langchain/core/messages'; +import { Annotation, messagesStateReducer } from '@langchain/langgraph'; + +export const translateRuleState = Annotation.Root({ + messages: Annotation({ + reducer: messagesStateReducer, + default: () => [], + }), + splunkRuleTitle: Annotation(), + splunkRuleDescription: Annotation(), + splunkRuleQuery: Annotation(), + response: Annotation(), +}); diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/splunk/rules/translate_rule/agent/system_prompt.txt b/x-pack/plugins/security_solution/server/lib/siem_migrations/splunk/rules/translate_rule/agent/system_prompt.txt new file mode 100644 index 0000000000000..8691f4b732d3a --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/splunk/rules/translate_rule/agent/system_prompt.txt @@ -0,0 +1,105 @@ +You are a helpful assistant for translating queries from SPL (Splunk Search Processing Language) to Elastic ES|QL queries. +Your goal is to construct the equivalent ES|QL query given a SPL query. + +VERY IMPORTANT: Use the provided tools to construct and execute ES|QL query, do not make assumptions about the ES|QL queries. + +The final response should contain the ES|QL query inside a esql code block like: +```esql + +``` + +# Usage examples + +Here are some examples of ES|QL queries: + +```esql +FROM employees +| WHERE country == "NL" AND gender == "M" +| STATS COUNT(*) +``` + +```esql +FROM employees +| EVAL trunk_worked_seconds = avg_worked_seconds / 100000000 * 100000000 +| STATS c = count(languages.long) BY languages.long, trunk_worked_seconds +| SORT c desc, languages.long, trunk_worked_seconds +``` + +*Extracting structured data from logs using DISSECT* +```esql +ROW a = "2023-01-23T12:15:00.000Z - some text - 127.0.0.1" +| DISSECT a "%{date} - %{msg} - %{ip}" +| KEEP date, msg, ip +| EVAL date = TO_DATETIME(date) +``` + +```esql +FROM employees +| WHERE first_name LIKE "?b*" +| STATS doc_count = COUNT(*) by first_name, last_name +| SORT doc_count DESC +| KEEP first_name, last_name +``` + +**Returning average salary per hire date with 20 buckets** +```esql +FROM employees +| WHERE hire_date >= "1985-01-01T00:00:00Z" AND hire_date < "1986-01-01T00:00:00Z" +| STATS avg_salary = AVG(salary) BY date_bucket = BUCKET(hire_date, 20, "1985-01-01T00:00:00Z", "1986-01-01T00:00:00Z") +| SORT bucket +``` + +**Returning number of employees grouped by buckets of salary** +```esql +FROM employees +| WHERE hire_date >= "1985-01-01T00:00:00Z" AND hire_date < "1986-01-01T00:00:00Z" +| STATS c = COUNT(1) BY b = BUCKET(salary, 5000.) +| SORT b +``` + +```esql +FROM employees +| EVAL is_recent_hire = CASE(hire_date <= "2023-01-01T00:00:00Z", 1, 0) +| STATS total_recent_hires = SUM(is_recent_hire), total_hires = COUNT(*) BY country +| EVAL recent_hiring_rate = total_recent_hires / total_hires +``` + +```esql +FROM logs-* +| WHERE @timestamp <= NOW() - 24 hours +// convert a keyword field into a numeric field to aggregate over it +| EVAL is_5xx = CASE(http.response.status_code >= 500, 1, 0) +// count total events and failed events to calculate a rate +| STATS total_events = COUNT(*), total_failures = SUM(is_5xx) BY host.hostname, bucket = BUCKET(@timestamp, 1 hour) +| EVAL failure_rate_per_host = total_failures / total_events +| DROP total_events, total_failures +``` + +```esql +FROM logs-* +| WHERE @timestamp <= NOW() - 24 hours +| STATS count = COUNT(*) BY log.level +| SORT count DESC +``` + +**Returning all first names for each first letter** +```esql +FROM employees +| EVAL first_letter = SUBSTRING(first_name, 0, 1) +| STATS first_name = MV_SORT(VALUES(first_name)) BY first_letter +| SORT first_letter +``` + +```esql +FROM employees +| WHERE still_hired == true +| EVAL hired = DATE_FORMAT("YYYY", hire_date) +| STATS avg_salary = AVG(salary) BY languages +| EVAL avg_salary = ROUND(avg_salary) +| EVAL lang_code = TO_STRING(languages) +| ENRICH languages_policy ON lang_code WITH lang = language_name +| WHERE lang IS NOT NULL +| KEEP avg_salary, lang +| SORT avg_salary ASC +| LIMIT 3 +``` diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/splunk/rules/translate_rule/agent/tools/esql_translator_tool.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/splunk/rules/translate_rule/agent/tools/esql_translator_tool.ts new file mode 100644 index 0000000000000..588437e7d2a4a --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/splunk/rules/translate_rule/agent/tools/esql_translator_tool.ts @@ -0,0 +1,115 @@ +/* + * 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 { Logger } from '@kbn/core/server'; +import { naturalLanguageToEsql, type InferenceClient } from '@kbn/inference-plugin/server'; +import type { StructuredTool } from '@langchain/core/tools'; +import { DynamicStructuredTool } from '@langchain/core/tools'; +import { lastValueFrom } from 'rxjs'; +import { z } from '@kbn/zod'; + +const TOOL_NAME = 'esql_translator'; +const schema = z.object({ + splQuery: z.string().describe(`The exact SPL query to translate to ES|QL`), + title: z.string().describe(`The title of the splunk rule`), + description: z.string().describe(`The description of the splunk rule`), +}); +type Schema = typeof schema; +type SchemaInput = z.output; + +const toolParams = { + name: TOOL_NAME, + description: `ALWAYS use the "${TOOL_NAME}" tool to convert the Splunk detection rule (SPL) to an Elastic ES|QL query.`, + schema, + tags: ['esql', 'query-translation', 'knowledge-base'], +}; + +const createInputPrompt = ({ + splQuery, + title, + description, +}: SchemaInput) => `Translate the following Splunk SPL (Search Processing Language) query rule to an ES|QL query, in order to be used as an Elastic Security detection rule: + +Splunk rule title: ${title} + +Splunk rule description: ${description} + +Splunk rule SPL query: +\`\`\`spl +${splQuery} +\`\`\` + +Along with the translated ES|QL query, you should also provide a summary of the translation process you followed, in markdown format. +The output should contain: +- First, the ES|QL query inside an \`\`\`esql code block. +- At the end, the summary of the translation process followed in markdown, starting with "## Translation Summary". +`; + +export type EsqlTranslatorTool = StructuredTool; + +interface GetEsqlTranslatorToolParams { + inferenceClient: InferenceClient; + connectorId: string; + logger: Logger; +} +export const getEsqlTranslatorTool = ({ + inferenceClient: client, + connectorId, + logger, +}: GetEsqlTranslatorToolParams): EsqlTranslatorTool => { + const callNaturalLanguageToEsql = async (input: SchemaInput) => { + return lastValueFrom( + naturalLanguageToEsql({ + client, + connectorId, + input: createInputPrompt(input), + logger: { + debug: (source) => { + logger.debug(typeof source === 'function' ? source() : source); + }, + }, + }) + ); + }; + + const esqlTool = new DynamicStructuredTool({ + ...toolParams, + responseFormat: 'markdown', + func: async (input) => { + const generateEvent = await callNaturalLanguageToEsql(input); + const answer = generateEvent.content ?? 'An error occurred in the tool'; + + logger.debug(`Received response from NL to ESQL tool: ${answer}`); + return answer; + }, + }); + + return esqlTool; +}; + +export type EsqlKnowledgeBaseCaller = (input: string) => Promise; +export const getEsqlKnowledgeBase = + ({ + inferenceClient: client, + connectorId, + logger, + }: GetEsqlTranslatorToolParams): EsqlKnowledgeBaseCaller => + async (input: string) => { + const { content } = await lastValueFrom( + naturalLanguageToEsql({ + client, + connectorId, + input, + logger: { + debug: (source) => { + logger.debug(typeof source === 'function' ? source() : source); + }, + }, + }) + ); + return content; + }; diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/splunk/rules/translate_rule/agent/types.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/splunk/rules/translate_rule/agent/types.ts new file mode 100644 index 0000000000000..16eec347d57fc --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/splunk/rules/translate_rule/agent/types.ts @@ -0,0 +1,22 @@ +/* + * 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 { Logger } from '@kbn/core/server'; +import type { StructuredTool } from '@langchain/core/tools'; +import type { InferenceClient } from '@kbn/inference-plugin/server'; +import type { ChatModel } from '../../../../actions_client_chat'; +import type { translateRuleState } from './state'; + +export type TranslateRuleState = typeof translateRuleState.State; + +export interface TranslateRuleGraphParams { + model: ChatModel; + tools: StructuredTool[]; + inferenceClient: InferenceClient; + connectorId: string; + logger: Logger; +} diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/splunk/rules/translate_rule/api/translate_rule.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/splunk/rules/translate_rule/api/translate_rule.ts new file mode 100644 index 0000000000000..d595e906cf2ad --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/splunk/rules/translate_rule/api/translate_rule.ts @@ -0,0 +1,112 @@ +/* + * 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 { IKibanaResponse, Logger } from '@kbn/core/server'; +import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; +import { getRequestAbortedSignal } from '@kbn/data-plugin/server'; +import { getLangSmithTracer } from '@kbn/langchain/server/tracers/langsmith'; +import { APMTracer } from '@kbn/langchain/server/tracers/apm'; +import type { SplunkRuleMigrationTranslateRuleResponse } from '../../../../../../../common/api/siem_migrations/splunk/rules/translate_rule.gen'; +import { SplunkRuleMigrationTranslateRuleRequestBody } from '../../../../../../../common/api/siem_migrations/splunk/rules/translate_rule.gen'; +import type { SecuritySolutionPluginRouter } from '../../../../../../types'; +import { SPLUNK_TRANSLATE_RULE_PATH } from '../../../../../../../common/api/siem_migrations/splunk/rules/constants'; +import { getTranslateRuleGraph, getTranslateRuleGraphNew } from '../agent/graph'; +import type { TranslateRuleState } from '../agent/types'; +import { ActionsClientChat } from '../../../../actions_client_chat'; +import { getEsqlTranslatorTool } from '../agent/tools/esql_translator_tool'; + +type SplunkTranslateRuleRouteResponse = IKibanaResponse; + +export const registerSplunkTranslateRuleRoute = ( + router: SecuritySolutionPluginRouter, + logger: Logger +) => { + router.versioned + .post({ + path: SPLUNK_TRANSLATE_RULE_PATH, + access: 'internal', + options: { + tags: ['access:securitySolution'], + }, + }) + .addVersion( + { + version: '1', + validate: { + request: { + body: buildRouteValidationWithZod(SplunkRuleMigrationTranslateRuleRequestBody), + }, + }, + }, + async (context, req, res): Promise => { + const { langSmithOptions, splunkRule, connectorId } = req.body; + try { + const ctx = await context.resolve(['core', 'actions', 'securitySolution']); + + const inferenceClient = ctx.securitySolution.getInferenceClient({ request: req }); + const esqlTranslatorTool = getEsqlTranslatorTool({ + inferenceClient, + connectorId, + logger, + }); + + const actionsClient = ctx.actions.getActionsClient(); + const actionsClientChat = new ActionsClientChat(connectorId, actionsClient, logger); + const model = await actionsClientChat.createModel({ + signal: getRequestAbortedSignal(req.events.aborted$), + temperature: 0.05, + }); + + const parameters: Partial = { + splunkRuleTitle: splunkRule.title, + splunkRuleDescription: splunkRule.description, + splunkRuleQuery: splunkRule.splSearch, + }; + + const options = { + callbacks: [ + new APMTracer({ projectName: langSmithOptions?.projectName ?? 'default' }, logger), + ...getLangSmithTracer({ ...langSmithOptions, logger }), + ], + }; + // const graph = getTranslateRuleGraph({ + // model, + // tools: [esqlTranslatorTool], + // inferenceClient, + // connectorId, + // logger, + // }); + const graph = getTranslateRuleGraph({ + model, + tools: [esqlTranslatorTool], + inferenceClient, + connectorId, + logger, + }); + const translateRuleState = await graph.invoke(parameters, options); + + // const { response, messages } = translateRuleState as TranslateRuleState; + // const migration: SplunkRuleMigration = { + // ...splunkRule, + // uuid: '1234', + // elasticQuery: response, + // elasticQueryLanguage: 'esql', + // status: 'finished', + // translationState: 'translated:complete', + // summary: 'This is a summary', + // messages, + // }; + + return res.ok({ body: translateRuleState }); + } catch (err) { + return res.badRequest({ + body: err.message, + }); + } + } + ); +}; diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/splunk/rules/translate_rule/api/util/find_missing_resources.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/splunk/rules/translate_rule/api/util/find_missing_resources.ts new file mode 100644 index 0000000000000..7d918f0ac3929 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/splunk/rules/translate_rule/api/util/find_missing_resources.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +const lookupRegex = /\b(?:lookup|inputlookup)\s+([^\s|]+)/g; +const macroRegex = /[\$`](\w+)[\$`]/g; + +export const findMissingResources = (splSearch: string): string[] => { + const missingResources: string[] = []; + + // lookups + let lookupMatch; + while ((lookupMatch = lookupRegex.exec(splSearch)) !== null) { + missingResources.push(`lookup:${lookupMatch[1]}`); + } + + // macros + let macroMatch; + while ((macroMatch = macroRegex.exec(splSearch)) !== null) { + missingResources.push(`macro:${macroMatch[0]}`); + } + + return missingResources; +}; diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/splunk/rules/translate_rule/index.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/splunk/rules/translate_rule/index.ts new file mode 100644 index 0000000000000..ebf1ece565098 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/splunk/rules/translate_rule/index.ts @@ -0,0 +1,8 @@ +/* + * 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 { registerSplunkTranslateRuleRoute } from './api/translate_rule'; diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/splunk/rules/translate_rule/types.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/splunk/rules/translate_rule/types.ts new file mode 100644 index 0000000000000..1fec1c76430eb --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/splunk/rules/translate_rule/types.ts @@ -0,0 +1,6 @@ +/* + * 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. + */ diff --git a/x-pack/plugins/security_solution/server/plugin_contract.ts b/x-pack/plugins/security_solution/server/plugin_contract.ts index c7ec67c1b07fc..c178f0654d9bd 100644 --- a/x-pack/plugins/security_solution/server/plugin_contract.ts +++ b/x-pack/plugins/security_solution/server/plugin_contract.ts @@ -45,6 +45,7 @@ import type { SharePluginStart } from '@kbn/share-plugin/server'; import type { GuidedOnboardingPluginSetup } from '@kbn/guided-onboarding-plugin/server'; import type { PluginSetup as UnifiedSearchServerPluginSetup } from '@kbn/unified-search-plugin/server'; import type { ElasticAssistantPluginStart } from '@kbn/elastic-assistant-plugin/server'; +import type { InferenceServerStart } from '@kbn/inference-plugin/server'; import type { ProductFeaturesService } from './lib/product_features_service/product_features_service'; import type { ExperimentalFeatures } from '../common'; @@ -88,6 +89,7 @@ export interface SecuritySolutionPluginStartDependencies { telemetry?: TelemetryPluginStart; share: SharePluginStart; actions: ActionsPluginStartContract; + inference: InferenceServerStart; } export interface SecuritySolutionPluginSetup { diff --git a/x-pack/plugins/security_solution/server/request_context_factory.ts b/x-pack/plugins/security_solution/server/request_context_factory.ts index 0782fa25c71eb..9e972d00c9285 100644 --- a/x-pack/plugins/security_solution/server/request_context_factory.ts +++ b/x-pack/plugins/security_solution/server/request_context_factory.ts @@ -214,6 +214,7 @@ export class RequestContextFactory implements IRequestContextFactory { kibanaVersion: options.kibanaVersion, }); }), + getInferenceClient: startPlugins.inference.getClient, }; } } diff --git a/x-pack/plugins/security_solution/server/routes/index.ts b/x-pack/plugins/security_solution/server/routes/index.ts index 6f245bd04a02b..9a8e4bf157859 100644 --- a/x-pack/plugins/security_solution/server/routes/index.ts +++ b/x-pack/plugins/security_solution/server/routes/index.ts @@ -61,6 +61,7 @@ import { suggestUserProfilesRoute } from '../lib/detection_engine/routes/users/s import { registerTimelineRoutes } from '../lib/timeline/routes'; import { getFleetManagedIndexTemplatesRoute } from '../lib/security_integrations/cribl/routes'; import { registerEntityAnalyticsRoutes } from '../lib/entity_analytics/register_entity_analytics_routes'; +import { registerSiemMigrationsRoutes } from '../lib/siem_migrations/routes'; export const initRoutes = ( router: SecuritySolutionPluginRouter, @@ -138,13 +139,18 @@ export const initRoutes = ( // Dashboards registerDashboardsRoutes(router, logger); registerTagsRoutes(router, logger); - const { previewTelemetryUrlEnabled } = config.experimentalFeatures; + const { previewTelemetryUrlEnabled, siemMigrationsEnabled } = config.experimentalFeatures; if (previewTelemetryUrlEnabled) { // telemetry preview endpoint for e2e integration tests only at the moment. telemetryDetectionRulesPreviewRoute(router, logger, previewTelemetryReceiver, telemetrySender); } registerEntityAnalyticsRoutes({ router, config, getStartServices, logger }); + + if (siemMigrationsEnabled) { + registerSiemMigrationsRoutes(router, logger); + } + // Security Integrations getFleetManagedIndexTemplatesRoute(router); }; diff --git a/x-pack/plugins/security_solution/server/types.ts b/x-pack/plugins/security_solution/server/types.ts index 31e10b70adbcf..4e4ecf4b446fa 100644 --- a/x-pack/plugins/security_solution/server/types.ts +++ b/x-pack/plugins/security_solution/server/types.ts @@ -20,6 +20,7 @@ import type { AlertsClient, IRuleDataService } from '@kbn/rule-registry-plugin/s import type { Readable } from 'stream'; import type { AuditLogger } from '@kbn/security-plugin-types-server'; +import type { InferenceServerStart } from '@kbn/inference-plugin/server'; import type { Immutable } from '../common/endpoint/types'; import { AppClient } from './client'; import type { ConfigType } from './config'; @@ -57,6 +58,7 @@ export interface SecuritySolutionApiRequestHandlerContext { getRiskScoreDataClient: () => RiskScoreDataClient; getAssetCriticalityDataClient: () => AssetCriticalityDataClient; getEntityStoreDataClient: () => EntityStoreDataClient; + getInferenceClient: InferenceServerStart['getClient']; } export type SecuritySolutionRequestHandlerContext = CustomRequestHandlerContext<{ diff --git a/x-pack/test/api_integration/services/security_solution_api.gen.ts b/x-pack/test/api_integration/services/security_solution_api.gen.ts index 7e1e532806a6c..f859043d2bb48 100644 --- a/x-pack/test/api_integration/services/security_solution_api.gen.ts +++ b/x-pack/test/api_integration/services/security_solution_api.gen.ts @@ -125,6 +125,7 @@ import { SearchAlertsRequestBodyInput } from '@kbn/security-solution-plugin/comm import { SetAlertAssigneesRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/alert_assignees/set_alert_assignees_route.gen'; import { SetAlertsStatusRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/signals/set_signal_status/set_signals_status_route.gen'; import { SetAlertTagsRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/alert_tags/set_alert_tags/set_alert_tags.gen'; +import { SplunkRuleMigrationMatchPrebuiltRuleRequestBodyInput } from '@kbn/security-solution-plugin/common/api/siem_migrations/splunk/rules/match_prebuilt_rule.gen'; import { StartEntityEngineRequestParamsInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/entity_store/engine/start.gen'; import { StopEntityEngineRequestParamsInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/entity_store/engine/stop.gen'; import { SuggestUserProfilesRequestQueryInput } from '@kbn/security-solution-plugin/common/api/detection_engine/users/suggest_user_profiles_route.gen'; @@ -1273,6 +1274,22 @@ detection engine rules. .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .send(props.body as object); }, + /** + * Perform Elastic prebuilt rule matching from Splunk Security rule + */ + splunkRuleMigrationMatchPrebuiltRule( + props: SplunkRuleMigrationMatchPrebuiltRuleProps, + kibanaSpace: string = 'default' + ) { + return supertest + .post( + routeWithNamespace('/internal/migrations/splunk/rules/match_prebuilt_rule', kibanaSpace) + ) + .set('kbn-xsrf', 'true') + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') + .send(props.body as object); + }, startEntityEngine(props: StartEntityEngineProps, kibanaSpace: string = 'default') { return supertest .post( @@ -1581,6 +1598,9 @@ export interface SetAlertsStatusProps { export interface SetAlertTagsProps { body: SetAlertTagsRequestBodyInput; } +export interface SplunkRuleMigrationMatchPrebuiltRuleProps { + body: SplunkRuleMigrationMatchPrebuiltRuleRequestBodyInput; +} export interface StartEntityEngineProps { params: StartEntityEngineRequestParamsInput; }