/** * sigma_action_handlers.js * * Centralized action handlers for Sigma-related Slack interactions */ const logger = require('../../utils/logger'); const { handleError } = require('../../utils/error_handler'); const { explainSigmaRule, getSigmaRuleYaml } = require('../../services/sigma/sigma_details_service'); const { convertRuleToBackend } = require('../../services/sigma/sigma_backend_converter'); const { searchSigmaRules } = require('../../services/sigma/sigma_search_service'); const { getYamlViewBlocks } = require('../../blocks/sigma_view_yaml_block'); const { getSearchResultBlocks } = require('../../blocks/sigma_search_results_block'); const { getConversionResultBlocks } = require('../../blocks/sigma_conversion_block'); const { getRuleExplanationBlocks } = require('../../blocks/sigma_details_block'); const { sendRuleToSiem } = require('../../services/elastic/elastic_api_service'); const { SIGMA_CLI_CONFIG, ELASTICSEARCH_CONFIG } = require('../../config/appConfig'); const FILE_NAME = 'sigma_action_handlers.js'; /** * Process and display details for a Sigma rule * * @param {string} ruleId - The ID of the rule to get details for * @param {Function} respond - Function to send response back to Slack * @param {boolean} replaceOriginal - Whether to replace the original message * @param {string} responseType - Response type (ephemeral or in_channel) * @returns {Promise} */ const processRuleDetails = async (ruleId, respond, replaceOriginal = false, responseType = 'in_channel') => { try { if (!ruleId) { logger.warn(`${FILE_NAME}: Missing rule ID in processRuleDetails`); await respond({ text: 'Error: Missing rule ID for details', replace_original: replaceOriginal, response_type: responseType }); return; } logger.info(`${FILE_NAME}: Processing details for sigma rule: ${ruleId}`); // Get Sigma rule details logger.info(`${FILE_NAME}: Calling explainSigmaRule with ID: '${ruleId}'`); const result = await explainSigmaRule(ruleId); if (!result.success) { logger.error(`${FILE_NAME}: Rule details retrieval failed: ${result.message}`); await respond({ text: `Error: ${result.message}`, replace_original: replaceOriginal, response_type: responseType }); return; } if (!result.explanation) { logger.error(`${FILE_NAME}: Rule details succeeded but no explanation object was returned`); await respond({ text: 'Error: Generated details were empty', replace_original: replaceOriginal, response_type: responseType }); return; } logger.info(`${FILE_NAME}: Rule ${ruleId} details retrieved successfully`); // Generate blocks let blocks; try { blocks = getRuleExplanationBlocks(result.explanation); } catch (blockError) { await handleError(blockError, `${FILE_NAME}: Block generation`, respond, { replaceOriginal: replaceOriginal, responseType: responseType, customMessage: `Rule ${result.explanation.id}: ${result.explanation.title}\n${result.explanation.description}` }); return; } // Respond with the details await respond({ blocks: blocks, replace_original: replaceOriginal, response_type: responseType }); } catch (error) { await handleError(error, `${FILE_NAME}: Process rule details`, respond, { replaceOriginal: replaceOriginal, responseType: responseType }); } }; /** * Process and convert a Sigma rule to the target backend format * * @param {string} ruleId - The ID of the rule to convert * @param {Object} config - Configuration for the conversion (backend, target, format) * @param {Function} respond - Function to send response back to Slack * @param {boolean} replaceOriginal - Whether to replace the original message * @param {string} responseType - Response type (ephemeral or in_channel) * @returns {Promise} */ const processRuleConversion = async (ruleId, config, respond, replaceOriginal = false, responseType = 'in_channel') => { try { if (!ruleId) { logger.warn(`${FILE_NAME}: Missing rule ID in processRuleConversion`); await respond({ text: 'Error: Missing rule ID for conversion', replace_original: replaceOriginal, response_type: responseType }); return; } logger.info(`${FILE_NAME}: Processing conversion for sigma rule: ${ruleId}`); // Set default configuration from YAML config if not provided const conversionConfig = config || { backend: SIGMA_CLI_CONFIG.backend, target: SIGMA_CLI_CONFIG.target, format: SIGMA_CLI_CONFIG.format }; await respond({ text: `Converting rule ${ruleId} using ${conversionConfig.backend}/${conversionConfig.target} to ${conversionConfig.format}...`, replace_original: replaceOriginal, response_type: 'ephemeral' }); // Get the rule and convert it const conversionResult = await convertRuleToBackend(ruleId, conversionConfig); if (!conversionResult.success) { logger.error(`${FILE_NAME}: Rule conversion failed: ${conversionResult.message}`); await respond({ text: `Error: ${conversionResult.message}`, replace_original: replaceOriginal, response_type: responseType }); return; } // Generate blocks for displaying the result let blocks; try { blocks = getConversionResultBlocks(conversionResult); } catch (blockError) { await handleError(blockError, `${FILE_NAME}: Block generation`, respond, { replaceOriginal: replaceOriginal, responseType: responseType, customMessage: `Rule ${ruleId} converted successfully. Use the following output with your SIEM:\n\`\`\`\n${conversionResult.output}\n\`\`\`` }); return; } // Respond with the conversion result await respond({ blocks: blocks, replace_original: replaceOriginal, response_type: responseType }); } catch (error) { await handleError(error, `${FILE_NAME}: Process rule conversion`, respond, { replaceOriginal: replaceOriginal, responseType: responseType }); } }; /** * Handle pagination actions (Previous, Next) * * @param {Object} body - The action payload body * @param {Function} ack - Function to acknowledge the action * @param {Function} respond - Function to send response */ const handlePaginationAction = async (body, ack, respond) => { try { await ack(); logger.debug(`${FILE_NAME}: Pagination action received: ${JSON.stringify(body.actions)}`); if (!body || !body.actions || !body.actions[0] || !body.actions[0].value) { logger.error(`${FILE_NAME}: Invalid pagination action payload: missing parameters`); await respond({ text: 'Error: Could not process pagination request', replace_original: false }); return; } // Parse the action value which contains our pagination parameters const action = body.actions[0]; let valueData; try { valueData = JSON.parse(action.value); } catch (parseError) { await handleError(parseError, `${FILE_NAME}: Pagination value parsing`, respond, { replaceOriginal: false, customMessage: 'Error: Invalid pagination parameters' }); return; } const { keyword, page, pageSize } = valueData; if (!keyword) { logger.warn(`${FILE_NAME}: Missing keyword in pagination action`); await respond({ text: 'Error: Missing search keyword in pagination request', replace_original: false }); return; } logger.info(`${FILE_NAME}: Processing pagination request for "${keyword}" (page ${page}, size ${pageSize})`); // Perform the search with the new pagination parameters const searchResult = await searchSigmaRules(keyword, page, pageSize); if (!searchResult.success) { logger.error(`${FILE_NAME}: Search failed during pagination: ${searchResult.message}`); await respond({ text: `Error: ${searchResult.message}`, replace_original: false }); return; } // Generate the updated blocks for the search results let blocks; try { blocks = getSearchResultBlocks( keyword, searchResult.results, searchResult.pagination ); } catch (blockError) { await handleError(blockError, `${FILE_NAME}: Pagination block generation`, respond, { replaceOriginal: false, customMessage: `Error generating results view: ${blockError.message}` }); return; } // Return the response that will update the original message await respond({ blocks: blocks, replace_original: true }); } catch (error) { await handleError(error, `${FILE_NAME}: Pagination action handler`, respond, { replaceOriginal: false }); } }; /** * Register all Sigma-related action handlers * * @param {Object} app - The Slack app instance */ const registerActionHandlers = (app) => { logger.info(`${FILE_NAME}: Registering consolidated sigma action handlers`); // Handle "Send to SIEM" button clicks app.action('send_sigma_rule_to_siem', async ({ body, ack, respond }) => { try { await ack(); logger.debug(`${FILE_NAME}: send_sigma_rule_to_siem action received: ${JSON.stringify(body.actions)}`); if (!body || !body.actions || !body.actions[0] || !body.actions[0].value) { logger.error(`${FILE_NAME}: Invalid action payload: missing rule ID`); await respond({ text: 'Error: Could not determine which rule to send', replace_original: false, response_type: 'ephemeral' }); return; } // Extract rule ID from action value // Value format is "send_sigma_rule_to_siem_[ruleID]" const actionValue = body.actions[0].value; const ruleId = actionValue.replace('send_sigma_rule_to_siem_', ''); if (!ruleId) { logger.error(`${FILE_NAME}: Missing rule ID in action value: ${actionValue}`); await respond({ text: 'Error: Missing rule ID in button data', replace_original: false, response_type: 'ephemeral' }); return; } logger.info(`${FILE_NAME}: Sending rule ${ruleId} to SIEM`); // Inform user that processing is happening await respond({ text: `Sending rule ${ruleId} to Elasticsearch SIEM...`, replace_original: false, response_type: 'ephemeral' }); // Get the converted rule in Elasticsearch format using config from YAML const config = { backend: SIGMA_CLI_CONFIG.backend, target: SIGMA_CLI_CONFIG.target, format: SIGMA_CLI_CONFIG.format }; logger.info(`${FILE_NAME}: Converting rule ${ruleId} for SIEM export`); const conversionResult = await convertRuleToBackend(ruleId, config); if (!conversionResult.success) { logger.error(`${FILE_NAME}: Rule conversion failed: ${conversionResult.message}`); await respond({ text: `Error: Failed to convert rule for SIEM: ${conversionResult.message}`, replace_original: false, response_type: 'ephemeral' }); return; } // Parse the converted rule JSON let rulePayload; try { rulePayload = JSON.parse(conversionResult.output); // Add required fields if not present rulePayload.rule_id = rulePayload.rule_id || ruleId; rulePayload.from = rulePayload.from || "now-360s"; rulePayload.to = rulePayload.to || "now"; rulePayload.interval = rulePayload.interval || "5m"; // Make sure required fields are present if (!rulePayload.name) { rulePayload.name = conversionResult.rule?.title || `Sigma Rule ${ruleId}`; } if (!rulePayload.description) { rulePayload.description = conversionResult.rule?.description || `Converted from Sigma rule: ${ruleId}`; } if (!rulePayload.risk_score) { // Map Sigma level to risk score const levelMap = { 'critical': 90, 'high': 73, 'medium': 50, 'low': 25, 'informational': 10 }; rulePayload.risk_score = levelMap[conversionResult.rule?.level] || 50; } if (!rulePayload.severity) { rulePayload.severity = conversionResult.rule?.level || 'medium'; } if (!rulePayload.enabled) { rulePayload.enabled = true; } } catch (parseError) { logger.error(`${FILE_NAME}: Failed to parse converted rule JSON: ${parseError.message}`); await respond({ text: `Error: The converted rule is not valid JSON: ${parseError.message}`, replace_original: false, response_type: 'ephemeral' }); return; } // Send the rule to Elasticsearch using api service try { const result = await sendRuleToSiem(rulePayload); if (result.success) { logger.info(`${FILE_NAME}: Successfully sent rule ${ruleId} to SIEM`); await respond({ text: `✅ Success! Rule "${rulePayload.name}" has been added to your Elasticsearch SIEM.`, replace_original: false, response_type: 'in_channel' }); } else { logger.error(`${FILE_NAME}: Error sending rule to SIEM: ${result.message}`); await respond({ text: `Error: Failed to add rule to SIEM: ${result.message}`, replace_original: false, response_type: 'ephemeral' }); } } catch (error) { await handleError(error, `${FILE_NAME}: send_sigma_rule_to_siem action`, respond, { replaceOriginal: false }); } } catch (error) { await handleError(error, `${FILE_NAME}: send_sigma_rule_to_siem action`, respond, { replaceOriginal: false }); } }); // Handle View YAML button clicks app.action('view_yaml', async ({ body, ack, respond }) => { logger.info(`${FILE_NAME}: VIEW_YAML ACTION TRIGGERED`); try { await ack(); logger.debug(`${FILE_NAME}: View YAML action received: ${JSON.stringify(body.actions)}`); if (!body || !body.actions || !body.actions[0] || !body.actions[0].value) { logger.error(`${FILE_NAME}: Invalid action payload: missing rule ID`); await respond({ text: 'Error: Could not determine which rule to get YAML for', replace_original: false }); return; } // Extract rule ID from button value // Handle both formats: direct ID from search results or view_yaml_{ruleId} from details view let ruleId = body.actions[0].value; if (ruleId.startsWith('view_yaml_')) { ruleId = ruleId.replace('view_yaml_', ''); } logger.info(`${FILE_NAME}: View YAML button clicked for rule: ${ruleId}`); // Get Sigma rule YAML const result = await getSigmaRuleYaml(ruleId); logger.debug(`${FILE_NAME}: YAML retrieval result: ${JSON.stringify(result, null, 2)}`); if (!result.success) { logger.error(`${FILE_NAME}: Rule YAML retrieval failed: ${result.message}`); await respond({ text: `Error: ${result.message}`, replace_original: false }); return; } logger.info(`${FILE_NAME}: Rule ${ruleId} YAML retrieved successfully via button click`); // Use the module to generate blocks const blocks = getYamlViewBlocks(ruleId, result.yaml || ''); // Respond with the YAML content await respond({ blocks: blocks, replace_original: false }); } catch (error) { await handleError(error, `${FILE_NAME}: View YAML action`, respond, { replaceOriginal: false }); } }); // Handle convert_rule_to_siem button clicks app.action('convert_rule_to_siem', async ({ body, ack, respond }) => { try { await ack(); logger.debug(`${FILE_NAME}: convert_rule_to_siem action received: ${JSON.stringify(body.actions)}`); if (!body || !body.actions || !body.actions[0] || !body.actions[0].value) { logger.error(`${FILE_NAME}: Invalid action payload: missing rule ID`); await respond({ text: 'Error: Could not determine which rule to convert', replace_original: false }); return; } // Extract rule ID from button value const ruleId = body.actions[0].value.replace('convert_rule_to_siem_', ''); logger.info(`${FILE_NAME}: convert_rule_to_siem button clicked for rule: ${ruleId}`); const config = { backend: 'lucene', target: 'ecs_windows', format: 'siem_rule_ndjson' }; await processRuleConversion(ruleId, config, respond, false, 'in_channel'); } catch (error) { await handleError(error, `${FILE_NAME}: convert_rule_to_siem action`, respond, { replaceOriginal: false }); } }); // Handle "View Rule Details" button clicks from search results app.action('view_rule_details', async ({ body, ack, respond }) => { logger.info(`${FILE_NAME}: VIEW_RULE_DETAILS ACTION TRIGGERED`); try { await ack(); logger.debug(`${FILE_NAME}: View Rule Details action received: ${JSON.stringify(body.actions)}`); if (!body || !body.actions || !body.actions[0] || !body.actions[0].value) { logger.error(`${FILE_NAME}: Invalid action payload: missing rule ID`); await respond({ text: 'Error: Could not determine which rule to explain', replace_original: false }); return; } const ruleId = body.actions[0].value; logger.info(`${FILE_NAME}: Rule details button clicked for rule ID: ${ruleId}`); // Inform user we're processing await respond({ text: `Processing details for rule ${ruleId}...`, replace_original: false, response_type: 'ephemeral' }); await processRuleDetails(ruleId, respond, false, 'in_channel'); } catch (error) { await handleError(error, `${FILE_NAME}: View rule details action`, respond, { replaceOriginal: false }); } }); // Handle pagination button clicks app.action('search_prev_page', async ({ body, ack, respond }) => { await handlePaginationAction(body, ack, respond); }); app.action('search_next_page', async ({ body, ack, respond }) => { await handlePaginationAction(body, ack, respond); }); logger.info(`${FILE_NAME}: All sigma action handlers registered successfully`); }; module.exports = { registerActionHandlers, processRuleDetails, processRuleConversion };