From c1d976b470bf08d472165f7751a444d6968c48d2 Mon Sep 17 00:00:00 2001 From: Marius Iversen Date: Tue, 3 Dec 2024 11:51:19 +0100 Subject: [PATCH] [Rule Migration] Adding CIM to ECS mapping and ESQL validation (#202331) ## Summary This PR adds the initial context to map CIM fields to ECS and two new nodes validation and a node to handle esql validation issues, fixing itself. This is how the graph looks compared to its old one: image Validation always runs last, and if validation returns any errors it will run the appropriate node depending on what validation failed. Once it is resolved it will validate again and then END when its successful. Currently 5 error iterations is max, which is just an arbitrary number. The default Langgraph configuration is 25 nodes executed in total for a specific graph before it errors with a recursion limit (main and sub graphs are not combined in that count). A few things are included in this PR: - Moved ESQL KB caller to util(any better place?), as it is now used in multiple nodes. - New Validation node, where any sort of validation takes place, usually the last step before ending the graph (on success). - New ESQL Error node, to resolve any ESQL validation errors and trigger a re-validation. - Fix a small bug in the main graph on the conditional edges, added a map for the allowed return values. --- .../docs/siem_migration/img/agent_graph.png | Bin 23303 -> 42382 bytes .../siem_migrations/rules/task/agent/graph.ts | 2 +- .../agent/sub_graphs/translate_rule/graph.ts | 29 ++- .../fix_query_errors/fix_query_errors.ts | 38 ++++ .../nodes/fix_query_errors/index.ts | 7 + .../nodes/fix_query_errors/prompts.ts | 42 ++++ .../nodes/process_query/process_query.ts | 6 +- .../nodes/process_query/prompts.ts | 10 +- .../nodes/translate_rule/cim_ecs_map.ts | 181 ++++++++++++++++++ .../nodes/translate_rule/prompts.ts | 27 +-- .../nodes/translate_rule/translate_rule.ts | 21 +- .../nodes/validation/esql_query.ts | 62 ++++++ .../translate_rule/nodes/validation/index.ts | 7 + .../nodes/validation/validation.ts | 43 +++++ .../agent/sub_graphs/translate_rule/state.ts | 5 + .../agent/sub_graphs/translate_rule/types.ts | 5 + .../esql_knowledge_base_caller.ts | 0 17 files changed, 461 insertions(+), 24 deletions(-) create mode 100644 x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/fix_query_errors/fix_query_errors.ts create mode 100644 x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/fix_query_errors/index.ts create mode 100644 x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/fix_query_errors/prompts.ts create mode 100644 x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/translate_rule/cim_ecs_map.ts create mode 100644 x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/validation/esql_query.ts create mode 100644 x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/validation/index.ts create mode 100644 x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/validation/validation.ts rename x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/{agent/sub_graphs/translate_rule/nodes/translate_rule => util}/esql_knowledge_base_caller.ts (100%) diff --git a/x-pack/plugins/security_solution/docs/siem_migration/img/agent_graph.png b/x-pack/plugins/security_solution/docs/siem_migration/img/agent_graph.png index a4039ef4a74461e37c9be915583e62af447d3a1c..ecccb602e20168b077eb3fef1278259c452d02ef 100644 GIT binary patch literal 42382 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 { if (state.elastic_rule?.prebuilt_rule_id) { return END; } - return 'translation'; + return 'translationSubGraph'; }; diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/graph.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/graph.ts index f986098e9deb0..32f41e54619be 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/graph.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/graph.ts @@ -6,11 +6,18 @@ */ import { END, START, StateGraph } from '@langchain/langgraph'; +import { isEmpty } from 'lodash/fp'; +import { SiemMigrationRuleTranslationResult } from '../../../../../../../../common/siem_migrations/constants'; +import { getFixQueryErrorsNode } from './nodes/fix_query_errors'; import { getProcessQueryNode } from './nodes/process_query'; import { getRetrieveIntegrationsNode } from './nodes/retrieve_integrations'; import { getTranslateRuleNode } from './nodes/translate_rule'; +import { getValidationNode } from './nodes/validation'; import { translateRuleState } from './state'; -import type { TranslateRuleGraphParams } from './types'; +import type { TranslateRuleGraphParams, TranslateRuleState } from './types'; + +// How many times we will try to self-heal when validation fails, to prevent infinite graph recursions +const MAX_VALIDATION_ITERATIONS = 3; export function getTranslateRuleGraph({ model, @@ -35,19 +42,37 @@ export function getTranslateRuleGraph({ model, integrationRetriever, }); + const validationNode = getValidationNode({ logger }); + const fixQueryErrorsNode = getFixQueryErrorsNode({ inferenceClient, connectorId, logger }); const translateRuleGraph = new StateGraph(translateRuleState) // Nodes .addNode('processQuery', processQueryNode) .addNode('retrieveIntegrations', retrieveIntegrationsNode) .addNode('translateRule', translateRuleNode) + .addNode('validation', validationNode) + .addNode('fixQueryErrors', fixQueryErrorsNode) // Edges .addEdge(START, 'processQuery') .addEdge('processQuery', 'retrieveIntegrations') .addEdge('retrieveIntegrations', 'translateRule') - .addEdge('translateRule', END); + .addEdge('translateRule', 'validation') + .addEdge('fixQueryErrors', 'validation') + .addConditionalEdges('validation', validationRouter); const graph = translateRuleGraph.compile(); graph.name = 'Translate Rule Graph'; return graph; } + +const validationRouter = (state: TranslateRuleState) => { + if ( + state.validation_errors.iterations <= MAX_VALIDATION_ITERATIONS && + state.translation_result === SiemMigrationRuleTranslationResult.FULL + ) { + if (!isEmpty(state.validation_errors?.esql_errors)) { + return 'fixQueryErrors'; + } + } + return END; +}; diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/fix_query_errors/fix_query_errors.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/fix_query_errors/fix_query_errors.ts new file mode 100644 index 0000000000000..a21aceee70f95 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/fix_query_errors/fix_query_errors.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. + */ + +import type { Logger } from '@kbn/core/server'; +import type { InferenceClient } from '@kbn/inference-plugin/server'; +import { getEsqlKnowledgeBase } from '../../../../../util/esql_knowledge_base_caller'; +import type { GraphNode } from '../../types'; +import { RESOLVE_ESQL_ERRORS_TEMPLATE } from './prompts'; + +interface GetFixQueryErrorsNodeParams { + inferenceClient: InferenceClient; + connectorId: string; + logger: Logger; +} + +export const getFixQueryErrorsNode = ({ + inferenceClient, + connectorId, + logger, +}: GetFixQueryErrorsNodeParams): GraphNode => { + const esqlKnowledgeBaseCaller = getEsqlKnowledgeBase({ inferenceClient, connectorId, logger }); + return async (state) => { + const rule = state.elastic_rule; + const prompt = await RESOLVE_ESQL_ERRORS_TEMPLATE.format({ + esql_errors: state.validation_errors.esql_errors, + esql_query: rule.query, + }); + const response = await esqlKnowledgeBaseCaller(prompt); + + const esqlQuery = response.match(/```esql\n([\s\S]*?)\n```/)?.[1] ?? ''; + rule.query = esqlQuery; + return { elastic_rule: rule }; + }; +}; diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/fix_query_errors/index.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/fix_query_errors/index.ts new file mode 100644 index 0000000000000..a805331675389 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/fix_query_errors/index.ts @@ -0,0 +1,7 @@ +/* + * 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 { getFixQueryErrorsNode } from './fix_query_errors'; diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/fix_query_errors/prompts.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/fix_query_errors/prompts.ts new file mode 100644 index 0000000000000..ca5fe67097d12 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/fix_query_errors/prompts.ts @@ -0,0 +1,42 @@ +/* + * 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 RESOLVE_ESQL_ERRORS_TEMPLATE = + ChatPromptTemplate.fromTemplate(`You are a helpful cybersecurity (SIEM) expert agent. Your task is to resolve the errors in the Elasticsearch Query Language (ES|QL) query provided by the user. + +Below is the relevant errors related to the ES|SQL query: + + + +{esql_errors} + + +{esql_query} + + + + +- You will be provided with the currentl ES|QL query and its related errors. +- Try to resolve the errors in the ES|QL query as best as you can to make it work. +- You must respond only with the modified query inside a \`\`\`esql code block, nothing else similar to the example response below. + + + +A: Please find the modified ES|QL query below: +\`\`\`esql +FROM logs-endpoint.events.process-* +| WHERE process.executable LIKE \"%chown root%\" +| STATS count = COUNT(*), firstTime = MIN(@timestamp), lastTime = MAX(@timestamp) BY process.executable, + process.command_line, + host.name +| EVAL firstTime = TO_DATETIME(firstTime), lastTime = TO_DATETIME(lastTime) +\`\`\` + + +`); diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/process_query/process_query.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/process_query/process_query.ts index 97d4168f1283e..ae0e93ee0c4bb 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/process_query/process_query.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/process_query/process_query.ts @@ -29,11 +29,15 @@ export const getProcessQueryNode = ({ const replaceQueryResourcePrompt = REPLACE_QUERY_RESOURCE_PROMPT.pipe(model).pipe(replaceQueryParser); const resourceContext = getResourcesContext(resources); - query = await replaceQueryResourcePrompt.invoke({ + const response = await replaceQueryResourcePrompt.invoke({ query: state.original_rule.query, macros: resourceContext.macros, lookup_tables: resourceContext.lists, }); + const splQuery = response.match(/```spl\n([\s\S]*?)\n```/)?.[1] ?? ''; + if (splQuery) { + query = splQuery; + } } return { inline_query: query }; }; diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/process_query/prompts.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/process_query/prompts.ts index f074da6b27d1a..b4c6b0e74aaa9 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/process_query/prompts.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/process_query/prompts.ts @@ -140,8 +140,14 @@ Divide the query up into separate section and go through each section one at a t A: Please find the modified SPL query below: -\`\`\`json -{{"match": "Linux User Account Creation"}} +\`\`\`spl +sourcetype="linux:audit" \`linux_auditd_normalized_proctitle_process\` +| rename host as dest +| where LIKE (process_exec, "%chown root%") +| stats count min(_time) as firstTime max(_time) as lastTime by process_exec proctitle normalized_proctitle_delimiter dest +| convert timeformat="%Y-%m-%dT%H:%M:%S" ctime(firstTime) +| convert timeformat="%Y-%m-%dT%H:%M:%S" ctime(lastTime) +| search * \`\`\` diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/translate_rule/cim_ecs_map.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/translate_rule/cim_ecs_map.ts new file mode 100644 index 0000000000000..3bafaf2fc6518 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/translate_rule/cim_ecs_map.ts @@ -0,0 +1,181 @@ +/* + * 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 SIEM_RULE_MIGRATION_CIM_ECS_MAP = ` +datamodel,object,source_field,ecs_field,data_type +Application_State,All_Application_State,dest,service.node.name,string +Application_State,All_Application_State,process,process.title,string +Application_State,All_Application_State,user,user.name,string +Application_State,Ports,dest_port,destination.port,number +Application_State,Ports,transport,network.transport,string +Application_State,Ports,transport_dest_port,destination.port,string +Application_State,Services,service,service.name,string +Application_State,Services,service_id,service.id,string +Application_State,Services,status,service.state,string +Authentication,Authentication,action,event.action,string +Authentication,Authentication,app,process.name,string +Authentication,Authentication,dest,host.name,string +Authentication,Authentication,duration,event.duration,number +Authentication,Authentication,signature,event.code,string +Authentication,Authentication,signature_id,event.reason,string +Authentication,Authentication,src,source.address,string +Authentication,Authentication,src_nt_domain,source.domain,string +Authentication,Authentication,user,user.name,string +Certificates,All_Certificates,dest_port,destination.port,number +Certificates,All_Certificates,duration,event.duration,number +Certificates,All_Certificates,src,source.address,string +Certificates,All_Certificates,src_port,source.port,number +Certificates,All_Certificates,transport,network.protocol,string +Certificates,SSL,ssl_end_time,tls.server.not_after,time +Certificates,SSL,ssl_hash,tls.server.hash,string +Certificates,SSL,ssl_issuer_common_name,tls.server.issuer,string +Certificates,SSL,ssl_issuer_locality,x509.issuer.locality,string +Certificates,SSL,ssl_issuer_organization,x509.issuer.organization,string +Certificates,SSL,ssl_issuer_state,x509.issuer.state_or_province,string +Certificates,SSL,ssl_issuer_unit,x509.issuer.organizational_unit,string +Certificates,SSL,ssl_publickey_algorithm,x509.public_key_algorithm,string +Certificates,SSL,ssl_serial,x509.serial_number,string +Certificates,SSL,ssl_signature_algorithm,x509.signature_algorithm,string +Certificates,SSL,ssl_start_time,x509.not_before,time +Certificates,SSL,ssl_subject,x509.subject.distinguished_name,string +Certificates,SSL,ssl_subject_common_name,x509.subject.common_name,string +Certificates,SSL,ssl_subject_locality,x509.subject.locality,string +Certificates,SSL,ssl_subject_organization,x509.subject.organization,string +Certificates,SSL,ssl_subject_state,x509.subject.state_or_province,string +Certificates,SSL,ssl_subject_unit,x509.subject.organizational_unit,string +Certificates,SSL,ssl_version,tls.version,string +Change,All_Changes,action,event.action,string +Change,Account_Management,dest_nt_domain,destination.domain,string +Change,Account_Management,src_nt_domain,source.domain,string +Change,Account_Management,src_user,source.user,string +Intrusion_Detection,IDS_Attacks,action,event.action,string +Intrusion_Detection,IDS_Attacks,dest,destination.address,string +Intrusion_Detection,IDS_Attacks,dest_port,destination.port,number +Intrusion_Detection,IDS_Attacks,dvc,observer.hostname,string +Intrusion_Detection,IDS_Attacks,severity,event.severity,string +Intrusion_Detection,IDS_Attacks,src,source.ip,string +Intrusion_Detection,IDS_Attacks,user,source.user,string +JVM,OS,os,host.os.name,string +JVM,OS,os_architecture,host.architecture,string +JVM,OS,os_version,host.os.version,string +Malware,Malware_Attacks,action,event.action,string +Malware,Malware_Attacks,date,event.created,string +Malware,Malware_Attacks,dest,host.hostname,string +Malware,Malware_Attacks,file_hash,file.hash.*,string +Malware,Malware_Attacks,file_name,file.name,string +Malware,Malware_Attacks,file_path,file.path,string +Malware,Malware_Attacks,Sender,source.user.email,string +Malware,Malware_Attacks,src,source.ip,string +Malware,Malware_Attacks,user,related.user,string +Malware,Malware_Attacks,url,rule.reference,string +Network_Resolution,DNS,answer,dns.answers,string +Network_Resolution,DNS,dest,destination.address,string +Network_Resolution,DNS,dest_port,destination.port,number +Network_Resolution,DNS,duration,event.duration,number +Network_Resolution,DNS,message_type,dns.type,string +Network_Resolution,DNS,name,dns.question.name,string +Network_Resolution,DNS,query,dns.question.name,string +Network_Resolution,DNS,query_type,dns.op_code,string +Network_Resolution,DNS,record_type,dns.question.type,string +Network_Resolution,DNS,reply_code,dns.response_code,string +Network_Resolution,DNS,reply_code_id,dns.id,number +Network_Resolution,DNS,response_time,event.duration,number +Network_Resolution,DNS,src,source.address,string +Network_Resolution,DNS,src_port,source.port,number +Network_Resolution,DNS,transaction_id,dns.id,number +Network_Resolution,DNS,transport,network.transport,string +Network_Resolution,DNS,ttl,dns.answers.ttl,number +Network_Sessions,All_Sessions,action,event.action,string +Network_Sessions,All_Sessions,dest_ip,destination.ip,string +Network_Sessions,All_Sessions,dest_mac,destination.mac,string +Network_Sessions,All_Sessions,duration,event.duration,number +Network_Sessions,All_Sessions,src_dns,source.registered_domain,string +Network_Sessions,All_Sessions,src_ip,source.ip,string +Network_Sessions,All_Sessions,src_mac,source.mac,string +Network_Sessions,All_Sessions,user,user.name,string +Network_Traffic,All_Traffic,action,event.action,string +Network_Traffic,All_Traffic,app,network.protocol,string +Network_Traffic,All_Traffic,bytes,network.bytes,number +Network_Traffic,All_Traffic,dest,destination.ip,string +Network_Traffic,All_Traffic,dest_ip,destination.ip,string +Network_Traffic,All_Traffic,dest_mac,destination.mac,string +Network_Traffic,All_Traffic,dest_port,destination.port,number +Network_Traffic,All_Traffic,dest_translated_ip,destination.nat.ip,string +Network_Traffic,All_Traffic,dest_translated_port,destination.nat.port,number +Network_Traffic,All_Traffic,direction,network.direction,string +Network_Traffic,All_Traffic,duration,event.duration,number +Network_Traffic,All_Traffic,dvc,observer.name,string +Network_Traffic,All_Traffic,dvc_ip,observer.ip,string +Network_Traffic,All_Traffic,dvc_mac,observer.mac,string +Network_Traffic,All_Traffic,dvc_zone,observer.egress.zone,string +Network_Traffic,All_Traffic,packets,network.packets,number +Network_Traffic,All_Traffic,packets_in,source.packets,number +Network_Traffic,All_Traffic,packets_out,destination.packets,number +Network_Traffic,All_Traffic,protocol,network.protocol,string +Network_Traffic,All_Traffic,rule,rule.name,string +Network_Traffic,All_Traffic,src,source.address,string +Network_Traffic,All_Traffic,src_ip,source.ip,string +Network_Traffic,All_Traffic,src_mac,source.mac,string +Network_Traffic,All_Traffic,src_port,source.port,number +Network_Traffic,All_Traffic,src_translated_ip,source.nat.ip,string +Network_Traffic,All_Traffic,src_translated_port,source.nat.port,number +Network_Traffic,All_Traffic,transport,network.transport,string +Network_Traffic,All_Traffic,vlan,vlan.name,string +Vulnerabilities,Vulnerabilities,category,vulnerability.category,string +Vulnerabilities,Vulnerabilities,cve,vulnerability.id,string +Vulnerabilities,Vulnerabilities,cvss,vulnerability.score.base,number +Vulnerabilities,Vulnerabilities,dest,host.name,string +Vulnerabilities,Vulnerabilities,dvc,vulnerability.scanner.vendor,string +Vulnerabilities,Vulnerabilities,severity,vulnerability.severity,string +Vulnerabilities,Vulnerabilities,url,vulnerability.reference,string +Vulnerabilities,Vulnerabilities,user,related.user,string +Vulnerabilities,Vulnerabilities,vendor_product,vulnerability.scanner.vendor,string +Endpoint,Ports,creation_time,@timestamp,timestamp +Endpoint,Ports,dest_port,destination.port,number +Endpoint,Ports,process_id,process.pid,string +Endpoint,Ports,transport,network.transport,string +Endpoint,Ports,transport_dest_port,destination.port,string +Endpoint,Processes,action,event.action,string +Endpoint,Processes,os,os.full,string +Endpoint,Processes,parent_process_exec,process.parent.name,string +Endpoint,Processes,parent_process_id,process.ppid,number +Endpoint,Processes,parent_process_guid,process.parent.entity_id,string +Endpoint,Processes,parent_process_path,process.parent.executable,string +Endpoint,Processes,process_current_directory,process.parent.working_directory, +Endpoint,Processes,process_exec,process.name,string +Endpoint,Processes,process_hash,process.hash.*,string +Endpoint,Processes,process_guid,process.entity_id,string +Endpoint,Processes,process_id,process.pid,number +Endpoint,Processes,process_path,process.executable,string +Endpoint,Processes,user_id,related.user,string +Endpoint,Services,description,service.name,string +Endpoint,Services,process_id,service.id,string +Endpoint,Services,service_dll,dll.name,string +Endpoint,Services,service_dll_path,dll.path,string +Endpoint,Services,service_dll_hash,dll.hash.*,string +Endpoint,Services,service_dll_signature_exists,dll.code_signature.exists,boolean +Endpoint,Services,service_dll_signature_verified,dll.code_signature.valid,boolean +Endpoint,Services,service_exec,service.name,string +Endpoint,Services,service_hash,hash.*,string +Endpoint,Filesystem,file_access_time,file.accessed,timestamp +Endpoint,Filesystem,file_create_time,file.created,timestamp +Endpoint,Filesystem,file_modify_time,file.mtime,timestamp +Endpoint,Filesystem,process_id,process.pid,string +Endpoint,Registry,process_id,process.id,string +Web,Web,action,event.action,string +Web,Web,app,observer.product,string +Web,Web,bytes_in,http.request.bytes,number +Web,Web,bytes_out,http.response.bytes,number +Web,Web,dest,destination.ip,string +Web,Web,duration,event.duration,number +Web,Web,http_method,http.request.method,string +Web,Web,http_referrer,http.request.referrer,string +Web,Web,http_user_agent,user_agent.name,string +Web,Web,status,http.response.status_code,string +Web,Web,url,url.full,string +Web,Web,user,url.username,string +Web,Web,vendor_product,observer.product,string`; diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/translate_rule/prompts.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/translate_rule/prompts.ts index 3e77353bba8b1..9749dfd96efba 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/translate_rule/prompts.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/translate_rule/prompts.ts @@ -5,14 +5,18 @@ * 2.0. */ -import type { TranslateRuleState } from '../../types'; +import { ChatPromptTemplate } from '@langchain/core/prompts'; -export const getEsqlTranslationPrompt = ( - state: TranslateRuleState, - indexPatterns: string -): string => { - return `You are a helpful cybersecurity (SIEM) expert agent. Your task is to migrate "detection rules" from Splunk to Elastic Security. +export const ESQL_TRANSLATION_PROMPT = + ChatPromptTemplate.fromTemplate(`You are a helpful cybersecurity (SIEM) expert agent. Your task is to migrate "detection rules" from Splunk to Elastic Security. Your goal is to translate the SPL query into an equivalent Elastic Security Query Language (ES|QL) query. +Below is the relevant context used when deciding which Elastic Common Schema field to use when translating from Splunk CIM fields: + + + +{field_mapping} + + ## Splunk rule Information provided: - Below you will find Splunk rule information: the title (<>), the description (<<DESCRIPTION>>), and the SPL (Search Processing Language) query (<<SPL_QUERY>>). @@ -22,7 +26,7 @@ Your goal is to translate the SPL query into an equivalent Elastic Security Quer ## Guidelines: - Analyze the SPL query and identify the key components. - Translate the SPL query into an equivalent ES|QL query using ECS (Elastic Common Schema) field names. -- Always start the generated ES|QL query by filtering FROM using these index patterns in the translated query: ${indexPatterns}. +- Always start the generated ES|QL query by filtering FROM using these index patterns in the translated query: {indexPatterns}. - If, in the SPL query, you find a lookup list or macro call, mention it in the summary and add a placeholder in the query with the format [macro:<macro_name>(argumentCount)] or [lookup:<lookup_name>] including the [] keys, - Examples: - \`get_duration(firstDate,secondDate)\` -> [macro:get_duration(2)] @@ -35,15 +39,14 @@ Your goal is to translate the SPL query into an equivalent Elastic Security Quer Find the Splunk rule information below: <<TITLE>> -${state.original_rule.title} +{title} <> <> -${state.original_rule.description} +{description} <> <> -${state.inline_query} +{inline_query} <> -`; -}; +`); diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/translate_rule/translate_rule.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/translate_rule/translate_rule.ts index a39e7c10146c0..6ba5edee11b22 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/translate_rule/translate_rule.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/translate_rule/translate_rule.ts @@ -9,10 +9,11 @@ import type { Logger } from '@kbn/core/server'; import type { InferenceClient } from '@kbn/inference-plugin/server'; import { SiemMigrationRuleTranslationResult } from '../../../../../../../../../../common/siem_migrations/constants'; import type { ChatModel } from '../../../../../util/actions_client_chat'; +import { getEsqlKnowledgeBase } from '../../../../../util/esql_knowledge_base_caller'; import type { RuleResourceRetriever } from '../../../../../util/rule_resource_retriever'; import type { GraphNode } from '../../types'; -import { getEsqlKnowledgeBase } from './esql_knowledge_base_caller'; -import { getEsqlTranslationPrompt } from './prompts'; +import { SIEM_RULE_MIGRATION_CIM_ECS_MAP } from './cim_ecs_map'; +import { ESQL_TRANSLATION_PROMPT } from './prompts'; interface GetTranslateRuleNodeParams { model: ChatModel; @@ -29,12 +30,20 @@ export const getTranslateRuleNode = ({ }: GetTranslateRuleNodeParams): GraphNode => { const esqlKnowledgeBaseCaller = getEsqlKnowledgeBase({ inferenceClient, connectorId, logger }); return async (state) => { - const indexPatterns = state.integrations.flatMap((integration) => - integration.data_streams.map((dataStream) => dataStream.index_pattern) - ); + const indexPatterns = state.integrations + .flatMap((integration) => + integration.data_streams.map((dataStream) => dataStream.index_pattern) + ) + .join(','); const integrationIds = state.integrations.map((integration) => integration.id); - const prompt = getEsqlTranslationPrompt(state, indexPatterns.join(',')); + const prompt = await ESQL_TRANSLATION_PROMPT.format({ + title: state.original_rule.title, + description: state.original_rule.description, + field_mapping: SIEM_RULE_MIGRATION_CIM_ECS_MAP, + inline_query: state.inline_query, + indexPatterns, + }); const response = await esqlKnowledgeBaseCaller(prompt); const esqlQuery = response.match(/```esql\n([\s\S]*?)\n```/)?.[1] ?? ''; diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/validation/esql_query.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/validation/esql_query.ts new file mode 100644 index 0000000000000..a8c1d6acff408 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/validation/esql_query.ts @@ -0,0 +1,62 @@ +/* + * 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 { ESQLAstQueryExpression, ESQLCommandOption, EditorError } from '@kbn/esql-ast'; +import { parse } from '@kbn/esql-ast'; +import { isColumnItem, isOptionItem } from '@kbn/esql-validation-autocomplete'; +import { isAggregatingQuery } from '@kbn/securitysolution-utils'; + +interface ParseEsqlQueryResult { + errors: EditorError[]; + isEsqlQueryAggregating: boolean; + hasMetadataOperator: boolean; +} + +function computeHasMetadataOperator(astExpression: ESQLAstQueryExpression): boolean { + // Check whether the `from` command has `metadata` operator + const metadataOption = getMetadataOption(astExpression); + if (!metadataOption) { + return false; + } + + // Check whether the `metadata` operator has `_id` argument + const idColumnItem = metadataOption.args.find( + (fromArg) => isColumnItem(fromArg) && fromArg.name === '_id' + ); + if (!idColumnItem) { + return false; + } + + return true; +} + +function getMetadataOption(astExpression: ESQLAstQueryExpression): ESQLCommandOption | undefined { + const fromCommand = astExpression.commands.find((x) => x.name === 'from'); + + if (!fromCommand?.args) { + return undefined; + } + + // Check whether the `from` command has `metadata` operator + for (const fromArg of fromCommand.args) { + if (isOptionItem(fromArg) && fromArg.name === 'metadata') { + return fromArg; + } + } + + return undefined; +} + +export const parseEsqlQuery = (query: string): ParseEsqlQueryResult => { + const { root, errors } = parse(query); + const isEsqlQueryAggregating = isAggregatingQuery(root); + return { + errors, + isEsqlQueryAggregating, + hasMetadataOperator: computeHasMetadataOperator(root), + }; +}; diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/validation/index.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/validation/index.ts new file mode 100644 index 0000000000000..a8c0f55191e42 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/validation/index.ts @@ -0,0 +1,7 @@ +/* + * 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 { getValidationNode } from './validation'; diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/validation/validation.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/validation/validation.ts new file mode 100644 index 0000000000000..272aedfe4793d --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/validation/validation.ts @@ -0,0 +1,43 @@ +/* + * 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 { isEmpty } from 'lodash/fp'; +import type { GraphNode } from '../../types'; +import { parseEsqlQuery } from './esql_query'; + +interface GetValidationNodeParams { + logger: Logger; +} + +/** + * This node runs all validation steps, and will redirect to the END of the graph if no errors are found. + * Any new validation steps should be added here. + */ +export const getValidationNode = ({ logger }: GetValidationNodeParams): GraphNode => { + return async (state) => { + const query = state.elastic_rule.query; + + // We want to prevent infinite loops, so we increment the iterations counter for each validation run. + const currentIteration = ++state.validation_errors.iterations; + let esqlErrors: string = ''; + if (!isEmpty(query)) { + const { errors, isEsqlQueryAggregating, hasMetadataOperator } = parseEsqlQuery(query); + if (!isEmpty(errors)) { + esqlErrors = JSON.stringify(errors); + } else if (!isEsqlQueryAggregating && !hasMetadataOperator) { + esqlErrors = + 'Queries that don’t use the STATS...BY function (non-aggregating queries) must include the "metadata _id, _version, _index" operator after the source command. For example: FROM logs* metadata _id, _version, _index.'; + } + } + if (esqlErrors) { + logger.debug(`ESQL query validation failed: ${esqlErrors}`); + } + + return { validation_errors: { iterations: currentIteration, esql_errors: esqlErrors } }; + }; +}; diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/state.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/state.ts index 8c8e9780aedf8..391d7a54f9ea8 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/state.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/state.ts @@ -14,6 +14,7 @@ import type { RuleMigration, } from '../../../../../../../../common/siem_migrations/model/rule_migration.gen'; import type { Integration } from '../../../../types'; +import type { TranslateRuleValidationErrors } from './types'; export const translateRuleState = Annotation.Root({ messages: Annotation({ @@ -33,6 +34,10 @@ export const translateRuleState = Annotation.Root({ reducer: (state, action) => ({ ...state, ...action }), default: () => ({} as ElasticRule), }), + validation_errors: Annotation({ + reducer: (current, value) => value ?? current, + default: () => ({ iterations: 0 } as TranslateRuleValidationErrors), + }), translation_result: Annotation({ reducer: (current, value) => value ?? current, default: () => SiemMigrationRuleTranslationResult.UNTRANSLATABLE, diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/types.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/types.ts index 42bf8e14c5924..44a5750812be0 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/types.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/types.ts @@ -23,3 +23,8 @@ export interface TranslateRuleGraphParams { integrationRetriever: IntegrationRetriever; logger: Logger; } + +export interface TranslateRuleValidationErrors { + iterations: number; + esql_errors?: string; +} diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/translate_rule/esql_knowledge_base_caller.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/util/esql_knowledge_base_caller.ts similarity index 100% rename from x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/translate_rule/esql_knowledge_base_caller.ts rename to x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/util/esql_knowledge_base_caller.ts