diff --git a/slack.example.yml b/slack.example.yml index 79ed59b..68f6575 100644 --- a/slack.example.yml +++ b/slack.example.yml @@ -59,6 +59,10 @@ features: url: http://SERVER_DOMAIN_NAME/slack/events description: Show statistics should_escape: false + - command: /fylgja + url: http://SERVER_DOMAIN_NAME/slack/events + description: Run fylgja commands + should_escape: false oauth_config: scopes: bot: diff --git a/src/app.js b/src/app.js index c5e99be..f2cc711 100644 --- a/src/app.js +++ b/src/app.js @@ -17,7 +17,8 @@ const FILE_NAME = getFileName(__filename); const sigmaDetailsHandler = require('./handlers/sigma/sigma_details_handler'); const sigmaSearchHandler = require('./handlers/sigma/sigma_search_handler'); const sigmaCreateHandler = require('./handlers/sigma/sigma_create_handler'); -const sigmaActionHandlers = require('./handlers/sigma/sigma_action_handlers'); +// Import the action registry +const sigmaActionRegistry = require('./handlers/sigma/actions/sigma_action_registry'); //const configCommand = require('./commands/config/index.js'); //const alertsCommand = require('./commands/alerts/index.js'); //const caseCommand = require('./commands/case/index.js'); @@ -113,8 +114,8 @@ app.command('/sigma-stats', async ({ command, ack, respond }) => { } }); -// Register all button action handlers from centralized module -sigmaActionHandlers.registerActionHandlers(app); +// Register all button action handlers from the modular registry +sigmaActionRegistry.registerActionHandlers(app); /** * Listen for any message in DMs diff --git a/src/handlers/sigma/actions/sigma_action_core.js b/src/handlers/sigma/actions/sigma_action_core.js new file mode 100644 index 0000000..7a9af17 --- /dev/null +++ b/src/handlers/sigma/actions/sigma_action_core.js @@ -0,0 +1,173 @@ +/** + * sigma_action_core.js + * + * Core utility functions for Sigma-related Slack actions + */ +const logger = require('../../../utils/logger'); +const { handleError } = require('../../../utils/error_handler'); +const { explainSigmaRule } = require('../../../services/sigma/sigma_details_service'); +const { convertRuleToBackend } = require('../../../services/sigma/sigma_backend_converter'); +const { getRuleExplanationBlocks } = require('../../../blocks/sigma/sigma_details_block'); +const { getConversionResultBlocks } = require('../../../blocks/sigma/sigma_conversion_block'); + +const { SIGMA_CLI_CONFIG } = require('../../../config/appConfig'); + +const FILE_NAME = 'sigma_action_core.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 + }); + } +}; + +module.exports = { + processRuleDetails, + processRuleConversion +}; \ No newline at end of file diff --git a/src/handlers/sigma/actions/sigma_action_registry.js b/src/handlers/sigma/actions/sigma_action_registry.js new file mode 100644 index 0000000..9f78726 --- /dev/null +++ b/src/handlers/sigma/actions/sigma_action_registry.js @@ -0,0 +1,38 @@ +/** + * sigma_action_registry.js + * + * Main registry that imports and registers all Sigma action handlers + */ +const logger = require('../../../utils/logger'); +const { registerViewActions } = require('./sigma_view_actions'); +const { registerConversionActions } = require('./sigma_conversion_actions'); +const { registerSiemActions } = require('./sigma_siem_actions'); +const { processRuleDetails, processRuleConversion } = require('./sigma_action_core'); + +const FILE_NAME = 'sigma_action_registry.js'; + +/** + * Register all Sigma-related action handlers + * + * @param {Object} app - The Slack app instance + */ +const registerActionHandlers = (app) => { + logger.info(`${FILE_NAME}: Registering all sigma action handlers`); + + // Register view-related handlers (view YAML, view details, pagination) + registerViewActions(app); + + // Register conversion-related handlers + registerConversionActions(app); + + // Register SIEM-related handlers (send to SIEM, space selection) + registerSiemActions(app); + + logger.info(`${FILE_NAME}: All sigma action handlers registered successfully`); +}; + +module.exports = { + registerActionHandlers, + processRuleDetails, + processRuleConversion +}; \ No newline at end of file diff --git a/src/handlers/sigma/actions/sigma_conversion_actions.js b/src/handlers/sigma/actions/sigma_conversion_actions.js new file mode 100644 index 0000000..04ca34f --- /dev/null +++ b/src/handlers/sigma/actions/sigma_conversion_actions.js @@ -0,0 +1,58 @@ +/** + * sigma_conversion_actions.js + * + * Handlers for Sigma rule conversion actions + */ +const logger = require('../../../utils/logger'); +const { handleError } = require('../../../utils/error_handler'); +const { processRuleConversion } = require('./sigma_action_core'); + +const FILE_NAME = 'sigma_conversion_actions.js'; + +/** + * Register conversion-related action handlers + * + * @param {Object} app - The Slack app instance + */ +const registerConversionActions = (app) => { + logger.info(`${FILE_NAME}: Registering conversion-related action handlers`); + + // 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 + }); + } + }); + + logger.info(`${FILE_NAME}: All conversion action handlers registered successfully`); +}; + +module.exports = { + registerConversionActions +}; \ No newline at end of file diff --git a/src/handlers/sigma/actions/sigma_siem_actions.js b/src/handlers/sigma/actions/sigma_siem_actions.js new file mode 100644 index 0000000..d446fcd --- /dev/null +++ b/src/handlers/sigma/actions/sigma_siem_actions.js @@ -0,0 +1,357 @@ +/** + * 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 { explainSigmaRule } = 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 explainResult = await explainSigmaRule(ruleId); + const ruleInfo = explainResult.success ? explainResult.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 +}; \ No newline at end of file diff --git a/src/handlers/sigma/actions/sigma_view_actions.js b/src/handlers/sigma/actions/sigma_view_actions.js new file mode 100644 index 0000000..d896f31 --- /dev/null +++ b/src/handlers/sigma/actions/sigma_view_actions.js @@ -0,0 +1,216 @@ +/** + * sigma_view_actions.js + * + * Handlers for viewing Sigma rule data and search results + */ +const logger = require('../../../utils/logger'); +const { handleError } = require('../../../utils/error_handler'); +const { getSigmaRuleYaml } = require('../../../services/sigma/sigma_details_service'); +const { searchSigmaRules } = require('../../../services/sigma/sigma_search_service'); +const { getYamlViewBlocks } = require('../../../blocks/sigma/sigma_view_yaml_block'); +const { getSearchResultBlocks } = require('../../../blocks/sigma/sigma_search_results_block'); +const { processRuleDetails } = require('./sigma_action_core'); + +const FILE_NAME = 'sigma_view_actions.js'; + +/** + * 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 view-related action handlers + * + * @param {Object} app - The Slack app instance + */ +const registerViewActions = (app) => { + logger.info(`${FILE_NAME}: Registering view-related action handlers`); + + // 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 "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 view action handlers registered successfully`); +}; + +module.exports = { + registerViewActions, + handlePaginationAction +}; \ No newline at end of file diff --git a/src/handlers/sigma/sigma_action_handlers.js b/src/handlers/sigma/sigma_action_handlers.js deleted file mode 100644 index 81a0445..0000000 --- a/src/handlers/sigma/sigma_action_handlers.js +++ /dev/null @@ -1,760 +0,0 @@ -/** - * 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/sigma_view_yaml_block'); -const { getSearchResultBlocks } = require('../../blocks/sigma/sigma_search_results_block'); -const { getConversionResultBlocks } = require('../../blocks/sigma/sigma_conversion_block'); -const { getRuleExplanationBlocks } = require('../../blocks/sigma/sigma_details_block'); -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, 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`); - - // 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 explainResult = await explainSigmaRule(ruleId); - const ruleInfo = explainResult.success ? explainResult.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 = 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"; - - // Set index pattern from space configuration if available - if (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; - } - - } 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 - }); - } - }); -}; - - -module.exports = { - registerActionHandlers, - processRuleDetails, - processRuleConversion -}; \ No newline at end of file diff --git a/src/handlers/sigma/sigma_create_handler.js b/src/handlers/sigma/sigma_create_handler.js index 48384a1..1965f1c 100644 --- a/src/handlers/sigma/sigma_create_handler.js +++ b/src/handlers/sigma/sigma_create_handler.js @@ -2,11 +2,11 @@ * sigma_create_handler.js * * Handles Sigma rule conversion requests from Slack commands - * Action handlers moved to sigma_action_handlers.js + * Action handlers moved to sigma_action_core.js */ const logger = require('../../utils/logger'); const { handleError } = require('../../utils/error_handler'); -const { processRuleConversion } = require('./sigma_action_handlers'); +const { processRuleConversion } = require('./actions/sigma_action_core'); const { SIGMA_CLI_CONFIG } = require('../../config/appConfig'); const FILE_NAME = 'sigma_create_handler.js'; diff --git a/src/handlers/sigma/sigma_details_handler.js b/src/handlers/sigma/sigma_details_handler.js index ce5bbb7..55844b3 100644 --- a/src/handlers/sigma/sigma_details_handler.js +++ b/src/handlers/sigma/sigma_details_handler.js @@ -7,10 +7,8 @@ const logger = require('../../utils/logger'); const { handleError } = require('../../utils/error_handler'); const { explainSigmaRule } = require('../../services/sigma/sigma_details_service'); -const { processRuleDetails } = require('./sigma_action_handlers'); - +const { processRuleDetails } = require('./actions/sigma_action_core'); const FILE_NAME = 'sigma_details_handler.js'; - /** * Handle the sigma-details command for Sigma rules * @@ -50,7 +48,6 @@ const handleCommand = async (command, respond) => { }); } }; - module.exports = { handleCommand }; \ No newline at end of file