/** * sigma_siem_actions.js * * Handlers for sending Sigma rules to SIEM and space-related operations */ const logger = require('../../../utils/logger'); const { handleError } = require('../../../utils/error_handler'); const { getSigmaRuleDetails } = require('../../../services/sigma/sigma_details_service'); const { convertRuleToBackend } = require('../../../services/sigma/sigma_backend_converter'); const { sendRuleToSiem } = require('../../../services/elastic/elastic_send_rule_to_siem_service'); const { getAllSpaces } = require('../../../services/elastic/elastic_api_service'); const { getSpaceSelectionBlocks } = require('../../../blocks/sigma/sigma_space_selection_block'); const { SIGMA_CLI_CONFIG } = require('../../../config/appConfig'); const FILE_NAME = 'sigma_siem_actions.js'; /** * Parse JSON rule payload and add required fields * * @param {string} ruleOutput - The JSON rule as string * @param {string} ruleId - The rule ID * @param {Object} conversionResult - Result from convertRuleToBackend * @param {Object} selectedSpace - Optional space configuration * @returns {Object} Prepared rule payload * @throws {Error} If JSON parsing fails */ const prepareRulePayload = (ruleOutput, ruleId, conversionResult, selectedSpace = null) => { const rulePayload = JSON.parse(ruleOutput); // 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"; // Set index pattern from space configuration if available if (selectedSpace && selectedSpace.indexPattern) { rulePayload.index = Array.isArray(selectedSpace.indexPattern) ? selectedSpace.indexPattern : [selectedSpace.indexPattern]; logger.debug(`${FILE_NAME}: Setting index pattern from space config: ${JSON.stringify(rulePayload.index)}`); } // 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; } return rulePayload; }; /** * Register SIEM and space-related action handlers * * @param {Object} app - The Slack app instance */ const registerSiemActions = (app) => { logger.info(`${FILE_NAME}: Registering SIEM-related 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 = prepareRulePayload(conversionResult.output, ruleId, conversionResult); } 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 space selection button click app.action('select_space_for_rule', async ({ body, ack, respond }) => { try { await ack(); logger.debug(`${FILE_NAME}: select_space_for_rule 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 select space for', replace_original: false, response_type: 'ephemeral' }); return; } // Extract rule ID from value const actionValue = body.actions[0].value; const ruleId = actionValue.replace('select_space_for_rule_', ''); // Get rule information to display in the space selection message const sigmaRuleDetailsResult = await getSigmaRuleDetails(ruleId); const ruleInfo = sigmaRuleDetailsResult.success ? sigmaRuleDetailsResult.explanation : { title: ruleId }; // Generate blocks for space selection const blocks = getSpaceSelectionBlocks(ruleId, ruleInfo); // Show space selection options await respond({ blocks: blocks, replace_original: false, response_type: 'ephemeral' }); } catch (error) { await handleError(error, `${FILE_NAME}: select_space_for_rule action`, respond, { replaceOriginal: false }); } }); // Handle space selection cancel button app.action('cancel_space_selection', async ({ body, ack, respond }) => { try { await ack(); await respond({ text: 'Space selection cancelled.', replace_original: false, response_type: 'ephemeral' }); } catch (error) { await handleError(error, `${FILE_NAME}: cancel_space_selection action`, respond, { replaceOriginal: false }); } }); // Dynamic handler for all space selection buttons // This uses a pattern matcher to match any action ID that starts with "send_rule_to_space_" app.action(/^send_rule_to_space_(.*)$/, async ({ body, action, ack, respond }) => { try { await ack(); logger.debug(`${FILE_NAME}: Space selection action received: ${JSON.stringify(action)}`); // Extract rule ID and space ID from the action value const actionValue = action.value; const parts = actionValue.split('_'); const spaceId = parts.pop(); // Last part is the space ID const ruleId = actionValue.match(/send_rule_to_space_(.+)_/)[1]; // Extract full UUID logger.info(`${FILE_NAME}: Selected space ${spaceId} for rule ${ruleId}`); // Get space info const spaces = getAllSpaces(); const selectedSpace = spaces.find(s => s.id === spaceId); if (!selectedSpace) { logger.error(`${FILE_NAME}: Space not found: ${spaceId}`); await respond({ text: `Error: Space "${spaceId}" not found in configuration`, replace_original: false, response_type: 'ephemeral' }); return; } // Inform user that processing is happening await respond({ text: `Sending rule ${ruleId} to ${selectedSpace.emoji || ''} ${selectedSpace.name} space...`, replace_original: false, response_type: 'ephemeral' }); // Get the converted rule in Elasticsearch format 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 to space ${spaceId}`); 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 = prepareRulePayload(conversionResult.output, ruleId, conversionResult, selectedSpace); } 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 the selected Elasticsearch space try { const result = await sendRuleToSiem(rulePayload, spaceId); if (result.success) { logger.info(`${FILE_NAME}: Successfully sent rule ${ruleId} to space ${spaceId}`); await respond({ text: `✅ Success! Rule "${rulePayload.name}" has been added to the ${selectedSpace.emoji || ''} ${selectedSpace.name} space in Elasticsearch.`, 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 the ${selectedSpace.name} space: ${result.message}`, replace_original: false, response_type: 'ephemeral' }); } } catch (error) { await handleError(error, `${FILE_NAME}: send_rule_to_space action`, respond, { replaceOriginal: false }); } } catch (error) { await handleError(error, `${FILE_NAME}: send_rule_to_space action`, respond, { replaceOriginal: false }); } }); logger.info(`${FILE_NAME}: All SIEM action handlers registered successfully`); }; module.exports = { registerSiemActions, prepareRulePayload };