diff --git a/src/blocks/sigma_conversion_block.js b/src/blocks/sigma/sigma_conversion_block.js similarity index 92% rename from src/blocks/sigma_conversion_block.js rename to src/blocks/sigma/sigma_conversion_block.js index 58a76c0..8190e6f 100644 --- a/src/blocks/sigma_conversion_block.js +++ b/src/blocks/sigma/sigma_conversion_block.js @@ -3,9 +3,9 @@ * * Provides block templates for displaying Sigma rule conversion results in Slack */ -const logger = require('../utils/logger'); +const logger = require('../../utils/logger'); -const { getFileName } = require('../utils/file_utils'); +const { getFileName } = require('../../utils/file_utils'); const FILE_NAME = getFileName(__filename); /** @@ -93,11 +93,11 @@ function getConversionResultBlocks(conversionResult) { type: 'button', text: { type: 'plain_text', - text: 'send_sigma_rule_to_siem', + text: '🚀 Send to Elasticsearch', emoji: true }, - value: `send_sigma_rule_to_siem_${rule.id}`, - action_id: 'send_sigma_rule_to_siem' + value: `select_space_for_rule_${rule.id}`, + action_id: 'select_space_for_rule' }, ] }); diff --git a/src/blocks/sigma_details_block.js b/src/blocks/sigma/sigma_details_block.js similarity index 98% rename from src/blocks/sigma_details_block.js rename to src/blocks/sigma/sigma_details_block.js index a528b04..d79a410 100644 --- a/src/blocks/sigma_details_block.js +++ b/src/blocks/sigma/sigma_details_block.js @@ -4,9 +4,9 @@ * Creates Slack Block Kit blocks for displaying Sigma rule explanations * */ -const logger = require('../utils/logger'); +const logger = require('../../utils/logger'); -const { getFileName } = require('../utils/file_utils'); +const { getFileName } = require('../../utils/file_utils'); const FILE_NAME = getFileName(__filename); /** diff --git a/src/blocks/sigma_search_results_block.js b/src/blocks/sigma/sigma_search_results_block.js similarity index 97% rename from src/blocks/sigma_search_results_block.js rename to src/blocks/sigma/sigma_search_results_block.js index 4fa3ef7..e118c17 100644 --- a/src/blocks/sigma_search_results_block.js +++ b/src/blocks/sigma/sigma_search_results_block.js @@ -4,11 +4,10 @@ * Generates Slack Block Kit blocks for displaying Sigma rule search results * Includes pagination controls for navigating large result sets * - * @author Fylgja Development Team */ -const logger = require('../utils/logger'); +const logger = require('../../utils/logger'); -const { getFileName } = require('../utils/file_utils'); +const { getFileName } = require('../../utils/file_utils'); const FILE_NAME = getFileName(__filename); /** diff --git a/src/blocks/sigma/sigma_space_selection_block.js b/src/blocks/sigma/sigma_space_selection_block.js new file mode 100644 index 0000000..262b20c --- /dev/null +++ b/src/blocks/sigma/sigma_space_selection_block.js @@ -0,0 +1,127 @@ +/** + * space_selection_block.js + * + * Provides block templates for displaying Elasticsearch space selection in Slack + */ +const logger = require('../../utils/logger'); +const { getAllSpaces } = require('../../services/elastic/elastic_api_service'); + +const { getFileName } = require('../../utils/file_utils'); +const FILE_NAME = getFileName(__filename); + +/** + * Generate blocks for space selection for a specific rule + * + * @param {string} ruleId - The ID of the rule to send to a space + * @param {Object} ruleInfo - Optional rule title and other information + * @returns {Array} Array of blocks for Slack message + */ +function getSpaceSelectionBlocks(ruleId, ruleInfo = {}) { + logger.debug(`${FILE_NAME}: Generating space selection blocks for rule: ${ruleId}`); + + // Get all configured spaces + const spaces = getAllSpaces(); + + if (!spaces || spaces.length === 0) { + logger.warn(`${FILE_NAME}: No spaces configured for selection blocks`); + return [{ + type: 'section', + text: { + type: 'mrkdwn', + text: 'No Elasticsearch spaces are configured. Please update your configuration.' + } + }]; + } + + // Create header block + const blocks = [ + { + type: 'header', + text: { + type: 'plain_text', + text: `Select a Space for Rule: ${ruleInfo.title || ruleId}`, + emoji: true + } + }, + { + type: 'section', + text: { + type: 'mrkdwn', + text: `Please select which Elasticsearch space you want to send this rule to:` + } + }, + { + type: 'divider' + } + ]; + + // Create buttons for each space + // If we have many spaces, we need to create multiple action blocks + // Slack only allows 5 buttons per actions block + const BUTTONS_PER_ACTION = 5; + let actionBlocks = []; + let currentActionBlock = { + type: 'actions', + elements: [] + }; + + // Add buttons for each space + spaces.forEach((space, index) => { + // Create the button for this space + const button = { + type: 'button', + text: { + type: 'plain_text', + text: `${space.emoji || ''} ${space.name}`, + emoji: true + }, + value: `send_rule_to_space_${ruleId}_${space.id}`, + action_id: `send_rule_to_space_${space.id}` + }; + + // Add button to current action block + currentActionBlock.elements.push(button); + + // If we've reached the button limit or this is the last space, + // add the current action block to our collection + if (currentActionBlock.elements.length === BUTTONS_PER_ACTION || index === spaces.length - 1) { + actionBlocks.push(currentActionBlock); + + // Start a new action block if we have more spaces + if (index < spaces.length - 1) { + currentActionBlock = { + type: 'actions', + elements: [] + }; + } + } + }); + + // Add all the action blocks to our blocks array + blocks.push(...actionBlocks); + + // Add a cancel button + blocks.push({ + type: 'actions', + elements: [ + { + type: 'button', + text: { + type: 'plain_text', + text: 'Cancel', + emoji: true + }, + style: 'danger', + value: `cancel_space_selection`, + action_id: 'cancel_space_selection' + } + ] + }); + + logger.debug(`${FILE_NAME}: Generated ${blocks.length} blocks for space selection`); + return blocks; +} + +module.exports = { + getSpaceSelectionBlocks +}; \ No newline at end of file diff --git a/src/blocks/sigma_stats_block.js b/src/blocks/sigma/sigma_stats_block.js similarity index 98% rename from src/blocks/sigma_stats_block.js rename to src/blocks/sigma/sigma_stats_block.js index 2c3c086..7ecd721 100644 --- a/src/blocks/sigma_stats_block.js +++ b/src/blocks/sigma/sigma_stats_block.js @@ -3,9 +3,9 @@ * * Creates Slack Block Kit blocks for displaying Sigma rule database statistics */ -const logger = require('../utils/logger'); +const logger = require('../../utils/logger'); -const { getFileName } = require('../utils/file_utils'); +const { getFileName } = require('../../utils/file_utils'); const FILE_NAME = getFileName(__filename); /** diff --git a/src/blocks/sigma_view_yaml_block.js b/src/blocks/sigma/sigma_view_yaml_block.js similarity index 94% rename from src/blocks/sigma_view_yaml_block.js rename to src/blocks/sigma/sigma_view_yaml_block.js index 79351cb..7568bcd 100644 --- a/src/blocks/sigma_view_yaml_block.js +++ b/src/blocks/sigma/sigma_view_yaml_block.js @@ -4,9 +4,9 @@ * Creates Slack Block Kit blocks for displaying Sigma rule YAML content * */ -const logger = require('../utils/logger'); +const logger = require('../../utils/logger'); -const { getFileName } = require('../utils/file_utils'); +const { getFileName } = require('../../utils/file_utils'); const FILE_NAME = getFileName(__filename); /** diff --git a/src/config/appConfig.js b/src/config/appConfig.js index 1ece4b9..31ca394 100644 --- a/src/config/appConfig.js +++ b/src/config/appConfig.js @@ -1,3 +1,8 @@ +/* + * appConfig.js + * + * retrives configuration data from fylgja.yml file + */ const path = require('path'); const fs = require('fs'); const yaml = require('js-yaml'); @@ -60,10 +65,19 @@ module.exports = { // Elasticsearch configuration from YAML ELASTICSEARCH_CONFIG: { - apiEndpoint: yamlConfig?.elastic?.['api-endpoint'] || - "http://localhost:5601/api/detection_engine/rules", - credentials: yamlConfig?.elastic?.['elastic-authentication-credentials'] || - "elastic:changeme" + protocol: yamlConfig?.elasticsearch?.protocol || "http", + hosts: yamlConfig?.elasticsearch?.hosts || ["localhost:9200"], + username: yamlConfig?.elasticsearch?.username || process.env.ELASTIC_USERNAME || "elastic", + password: yamlConfig?.elasticsearch?.password || process.env.ELASTIC_PASSWORD || "changeme", + apiEndpoint: yamlConfig?.elasticsearch?.api_endpoint || "http://localhost:5601/api/detection_engine/rules", + spaces: yamlConfig?.elasticsearch?.spaces || [ + { + name: "Default", + id: "default", + indexPattern: "logs-*", + emoji: "🔍" + } + ] }, // Logging configuration from YAML diff --git a/src/handlers/sigma/sigma_action_handlers.js b/src/handlers/sigma/sigma_action_handlers.js index 6bd1a13..49edcd5 100644 --- a/src/handlers/sigma/sigma_action_handlers.js +++ b/src/handlers/sigma/sigma_action_handlers.js @@ -8,11 +8,13 @@ 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 { 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_api_service'); +const { getSpaceSelectionBlocks } = require('../../blocks/sigma/sigma_space_selection_block'); +const { getAllSpaces } = require('../../services/elastic/elastic_api_service'); const { SIGMA_CLI_CONFIG, ELASTICSEARCH_CONFIG } = require('../../config/appConfig'); @@ -38,13 +40,13 @@ const processRuleDetails = async (ruleId, respond, replaceOriginal = false, resp }); 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({ @@ -54,7 +56,7 @@ const processRuleDetails = async (ruleId, respond, replaceOriginal = false, resp }); return; } - + if (!result.explanation) { logger.error(`${FILE_NAME}: Rule details succeeded but no explanation object was returned`); await respond({ @@ -64,9 +66,9 @@ const processRuleDetails = async (ruleId, respond, replaceOriginal = false, resp }); return; } - + logger.info(`${FILE_NAME}: Rule ${ruleId} details retrieved successfully`); - + // Generate blocks let blocks; try { @@ -79,7 +81,7 @@ const processRuleDetails = async (ruleId, respond, replaceOriginal = false, resp }); return; } - + // Respond with the details await respond({ blocks: blocks, @@ -115,25 +117,25 @@ const processRuleConversion = async (ruleId, config, respond, replaceOriginal = }); 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({ @@ -143,7 +145,7 @@ const processRuleConversion = async (ruleId, config, respond, replaceOriginal = }); return; } - + // Generate blocks for displaying the result let blocks; try { @@ -156,7 +158,7 @@ const processRuleConversion = async (ruleId, config, respond, replaceOriginal = }); return; } - + // Respond with the conversion result await respond({ blocks: blocks, @@ -266,13 +268,13 @@ const handlePaginationAction = async (body, ack, respond) => { */ 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({ @@ -282,12 +284,12 @@ const registerActionHandlers = (app) => { }); 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({ @@ -297,26 +299,26 @@ const registerActionHandlers = (app) => { }); 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({ @@ -326,28 +328,28 @@ const registerActionHandlers = (app) => { }); 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 || + rulePayload.description = conversionResult.rule?.description || `Converted from Sigma rule: ${ruleId}`; } - + if (!rulePayload.risk_score) { // Map Sigma level to risk score const levelMap = { @@ -357,18 +359,18 @@ const registerActionHandlers = (app) => { '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({ @@ -378,11 +380,11 @@ const registerActionHandlers = (app) => { }); 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({ @@ -409,14 +411,14 @@ const registerActionHandlers = (app) => { }); } }); - + // 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({ @@ -425,20 +427,20 @@ const registerActionHandlers = (app) => { }); 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({ @@ -447,12 +449,12 @@ const registerActionHandlers = (app) => { }); 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, @@ -464,7 +466,7 @@ const registerActionHandlers = (app) => { }); } }); - + // Handle convert_rule_to_siem button clicks app.action('convert_rule_to_siem', async ({ body, ack, respond }) => { try { @@ -479,17 +481,17 @@ const registerActionHandlers = (app) => { }); 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, { @@ -497,8 +499,8 @@ const registerActionHandlers = (app) => { }); } }); - - + + // 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`); @@ -541,10 +543,216 @@ const registerActionHandlers = (app) => { 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, diff --git a/src/handlers/sigma/sigma_search_handler.js b/src/handlers/sigma/sigma_search_handler.js index 574a8bf..e161b7d 100644 --- a/src/handlers/sigma/sigma_search_handler.js +++ b/src/handlers/sigma/sigma_search_handler.js @@ -6,7 +6,7 @@ const { searchSigmaRules } = require('../../services/sigma/sigma_search_service'); const logger = require('../../utils/logger'); const { handleError } = require('../../utils/error_handler'); -const { getSearchResultBlocks } = require('../../blocks/sigma_search_results_block'); +const { getSearchResultBlocks } = require('../../blocks/sigma/sigma_search_results_block'); const { getFileName } = require('../../utils/file_utils'); const FILE_NAME = getFileName(__filename); diff --git a/src/services/elastic/elastic_api_service.js b/src/services/elastic/elastic_api_service.js index 939c859..965c36b 100644 --- a/src/services/elastic/elastic_api_service.js +++ b/src/services/elastic/elastic_api_service.js @@ -12,32 +12,76 @@ const FILE_NAME = 'elastic_api_service.js'; /** * Get Elasticsearch configuration with credentials * + * @param {string} spaceId - Optional space ID to get configuration for * @returns {Object} Configuration object with URL and credentials */ -const getElasticConfig = () => { - return { - url: ELASTICSEARCH_CONFIG.apiEndpoint.split('/api/')[0] || process.env.ELASTIC_URL, - username: ELASTICSEARCH_CONFIG.credentials.split(':')[0] || process.env.ELASTIC_USERNAME, - password: ELASTICSEARCH_CONFIG.credentials.split(':')[1] || process.env.ELASTIC_PASSWORD, +const getElasticConfig = (spaceId = null) => { + // Default config + const config = { + protocol: ELASTICSEARCH_CONFIG.protocol || "http", + hosts: ELASTICSEARCH_CONFIG.hosts || ["localhost:9200"], + username: ELASTICSEARCH_CONFIG.username || process.env.ELASTIC_USERNAME, + password: ELASTICSEARCH_CONFIG.password || process.env.ELASTIC_PASSWORD, apiEndpoint: ELASTICSEARCH_CONFIG.apiEndpoint }; + + // If space ID provided, find the specific space + if (spaceId) { + const space = ELASTICSEARCH_CONFIG.spaces.find(s => s.id === spaceId); + if (space) { + // Apply space-specific configuration overrides if they exist + return { + ...config, + space: space + }; + } + } + + return config; }; /** - * Send a rule to Elasticsearch SIEM + * Get all configured Elasticsearch spaces + * + * @returns {Array} List of all configured spaces + */ +const getAllSpaces = () => { + return ELASTICSEARCH_CONFIG.spaces || []; +}; + +/** + * Send a rule to Elasticsearch SIEM in a specific space * * @param {Object} rulePayload - The rule payload to send to Elasticsearch + * @param {string} spaceId - The ID of the space to send the rule to * @returns {Promise} - Object containing success status and response/error information */ -const sendRuleToSiem = async (rulePayload) => { - logger.info(`${FILE_NAME}: Sending rule to Elasticsearch SIEM`); +const sendRuleToSiem = async (rulePayload, spaceId = 'default') => { + logger.info(`${FILE_NAME}: Sending rule to Elasticsearch SIEM in space: ${spaceId}`); try { - const elasticConfig = getElasticConfig(); - const apiUrl = elasticConfig.apiEndpoint; + const elasticConfig = getElasticConfig(spaceId); + const baseApiUrl = elasticConfig.apiEndpoint; + + // Construct space-specific URL if needed + let apiUrl = baseApiUrl; + if (spaceId && spaceId !== 'default') { + // Insert space ID into URL: http://localhost:5601/api/detection_engine/rules + // becomes http://localhost:5601/s/space-id/api/detection_engine/rules + const urlParts = baseApiUrl.split('/api/'); + apiUrl = `${urlParts[0]}/s/${spaceId}/api/${urlParts[1]}`; + } logger.debug(`${FILE_NAME}: Using Elasticsearch API URL: ${apiUrl}`); + // Add index pattern to rule if provided by space config + if (elasticConfig.space && elasticConfig.space.indexPattern && !rulePayload.index) { + rulePayload.index = Array.isArray(elasticConfig.space.indexPattern) + ? elasticConfig.space.indexPattern + : [elasticConfig.space.indexPattern]; + logger.debug(`${FILE_NAME}: Adding index pattern to rule: ${JSON.stringify(rulePayload.index)}`); + } + // Send the request to Elasticsearch const response = await axios({ method: 'post', @@ -55,18 +99,19 @@ const sendRuleToSiem = async (rulePayload) => { // Process the response if (response.status >= 200 && response.status < 300) { - logger.info(`${FILE_NAME}: Successfully sent rule to SIEM`); + logger.info(`${FILE_NAME}: Successfully sent rule to SIEM in space: ${spaceId}`); return { success: true, status: response.status, - data: response.data + data: response.data, + space: elasticConfig.space }; } else { logger.error(`${FILE_NAME}: Error sending rule to SIEM. Status: ${response.status}, Response: ${JSON.stringify(response.data)}`); return { success: false, status: response.status, - message: `Failed to add rule to SIEM. Status: ${response.status}`, + message: `Failed to add rule to SIEM in space ${spaceId}. Status: ${response.status}`, data: response.data }; } @@ -92,6 +137,7 @@ const sendRuleToSiem = async (rulePayload) => { * @param {Object} options - Request options * @param {string} options.method - HTTP method (get, post, put, delete) * @param {string} options.endpoint - API endpoint (appended to base URL) + * @param {string} options.spaceId - Optional space ID * @param {Object} options.data - Request payload * @param {Object} options.params - URL parameters * @param {Object} options.headers - Additional headers @@ -99,14 +145,24 @@ const sendRuleToSiem = async (rulePayload) => { */ const makeElasticRequest = async (options) => { try { - const elasticConfig = getElasticConfig(); - const baseUrl = elasticConfig.url; + const elasticConfig = getElasticConfig(options.spaceId); + const baseUrl = elasticConfig.protocol + '://' + elasticConfig.hosts[0]; // Build the full URL - use provided endpoint or default API endpoint - const url = options.endpoint ? + let url = options.endpoint ? `${baseUrl}${options.endpoint.startsWith('/') ? '' : '/'}${options.endpoint}` : elasticConfig.apiEndpoint; + // Handle space in URL if needed + if (options.spaceId && options.spaceId !== 'default') { + // Insert space ID into URL: http://localhost:5601/api/detection_engine/rules + // becomes http://localhost:5601/s/space-id/api/detection_engine/rules + if (url.includes('/api/')) { + const urlParts = url.split('/api/'); + url = `${urlParts[0]}/s/${options.spaceId}/api/${urlParts[1]}`; + } + } + logger.debug(`${FILE_NAME}: Making ${options.method} request to: ${url}`); // Set up default headers @@ -133,7 +189,8 @@ const makeElasticRequest = async (options) => { return { success: response.status >= 200 && response.status < 300, status: response.status, - data: response.data + data: response.data, + space: elasticConfig.space }; } catch (error) { logger.error(`${FILE_NAME}: Error in Elasticsearch API request: ${error.message}`); @@ -150,5 +207,6 @@ const makeElasticRequest = async (options) => { module.exports = { sendRuleToSiem, makeElasticRequest, - getElasticConfig + getElasticConfig, + getAllSpaces }; \ No newline at end of file