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 255b56c485b9b..295ee46f7bbb8 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 @@ -292,6 +292,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, @@ -1899,6 +1903,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 @@ -2229,6 +2249,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..95d14208d673e --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/siem_migrations/splunk/rules/constants.ts @@ -0,0 +1,13 @@ +/* + * 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; 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..f0f2bc4ab520f --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/siem_migrations/splunk/rules/splunk_rule.gen.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. + */ + +/* + * 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 name. + */ + title: z.string().min(1), + /** + * The Splunk rule search query. + */ + search: z.string().min(1), + /** + * The Splunk rule description. + */ + description: z.string().min(1), + /** + * String array containing the rule Mitre Attack technique IDs. + */ + mitreAttackIds: z.array(z.string()).optional(), +}); 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..92d7b049f6179 --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/siem_migrations/splunk/rules/splunk_rule.schema.yaml @@ -0,0 +1,32 @@ +openapi: 3.0.3 +info: + title: Common Splunk Rules Attributes + version: 'not applicable' +paths: {} +components: + x-codegen-enabled: true + schemas: + SplunkRule: + type: object + required: + - title + - description + - search + properties: + title: + type: string + minLength: 1 + description: The Splunk rule name. + search: + type: string + minLength: 1 + description: The Splunk rule search query. + description: + type: string + minLength: 1 + description: The Splunk rule description. + mitreAttackIds: + type: array + items: + type: string + description: String array containing the rule Mitre Attack technique IDs. diff --git a/x-pack/plugins/security_solution/common/experimental_features.ts b/x-pack/plugins/security_solution/common/experimental_features.ts index 88ca097e09b18..3da35bb238e48 100644 --- a/x-pack/plugins/security_solution/common/experimental_features.ts +++ b/x-pack/plugins/security_solution/common/experimental_features.ts @@ -259,6 +259,11 @@ export const allowedExperimentalValues = Object.freeze({ * Enables the new Entity Store engine routes */ entityStoreEnabled: false, + + /** + * Enables the siem migrations feature + */ + siemMigrationsEnabled: false, }); type ExperimentalConfigKeys = Array; 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..94d98ee8078b1 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/actions_client_chat.ts @@ -0,0 +1,87 @@ +/* + * 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 { + ActionsClientBedrockChatModel, + ActionsClientChatOpenAI, + ActionsClientGeminiChatModel, + ActionsClientSimpleChatModel, +} 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 ActionsClientGeminiChatModelParams } 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 + | ActionsClientGeminiChatModel; + +export type ActionsClientChatModelClass = + | typeof ActionsClientSimpleChatModel + | typeof ActionsClientChatOpenAI + | typeof ActionsClientBedrockChatModel + | typeof ActionsClientGeminiChatModel; + +export type ChatModelParams = Partial & + Partial & + Partial & + Partial & { + /** Enables the streaming mode of the response, disabled by default */ + streaming?: boolean; + }; + +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 { + const llmTypeDictionary: Record = { + [`.gen-ai`]: `openai`, + [`.bedrock`]: `bedrock`, + [`.gemini`]: `gemini`, + }; + return llmTypeDictionary[actionTypeId]; + } + + private getLLMClass(llmType?: string): ActionsClientChatModelClass { + return llmType === 'openai' + ? ActionsClientChatOpenAI + : llmType === 'bedrock' + ? ActionsClientBedrockChatModel + : llmType === 'gemini' + ? ActionsClientGeminiChatModel + : ActionsClientSimpleChatModel; + } +} 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..02a22222d443d --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/splunk/rules/index.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 { registerSplunkMatchPrebuiltRuleRoute } from './match_prebuilt_rule/api/match_prebuilt_rule'; + +export const registerSplunkMigrationRoutes = ( + router: SecuritySolutionPluginRouter, + logger: Logger +) => { + registerSplunkMatchPrebuiltRuleRoute(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/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/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/test/api_integration/services/security_solution_api.gen.ts b/x-pack/test/api_integration/services/security_solution_api.gen.ts index aba6550ea9c3b..cdb69327e1bf5 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'; @@ -1266,6 +1267,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( @@ -1574,6 +1591,9 @@ export interface SetAlertsStatusProps { export interface SetAlertTagsProps { body: SetAlertTagsRequestBodyInput; } +export interface SplunkRuleMigrationMatchPrebuiltRuleProps { + body: SplunkRuleMigrationMatchPrebuiltRuleRequestBodyInput; +} export interface StartEntityEngineProps { params: StartEntityEngineRequestParamsInput; }