add support for multiple spaces

This commit is contained in:
Charlotte Croce 2025-04-16 18:01:35 -04:00
parent 7988853b57
commit 351c4e4ef4
10 changed files with 498 additions and 92 deletions

View file

@ -3,9 +3,9 @@
* *
* Provides block templates for displaying Sigma rule conversion results in Slack * 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); const FILE_NAME = getFileName(__filename);
/** /**
@ -93,11 +93,11 @@ function getConversionResultBlocks(conversionResult) {
type: 'button', type: 'button',
text: { text: {
type: 'plain_text', type: 'plain_text',
text: 'send_sigma_rule_to_siem', text: '🚀 Send to Elasticsearch',
emoji: true emoji: true
}, },
value: `send_sigma_rule_to_siem_${rule.id}`, value: `select_space_for_rule_${rule.id}`,
action_id: 'send_sigma_rule_to_siem' action_id: 'select_space_for_rule'
}, },
] ]
}); });

View file

@ -4,9 +4,9 @@
* Creates Slack Block Kit blocks for displaying Sigma rule explanations * 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); const FILE_NAME = getFileName(__filename);
/** /**

View file

@ -4,11 +4,10 @@
* Generates Slack Block Kit blocks for displaying Sigma rule search results * Generates Slack Block Kit blocks for displaying Sigma rule search results
* Includes pagination controls for navigating large result sets * 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); const FILE_NAME = getFileName(__filename);
/** /**

View file

@ -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
};

View file

@ -3,9 +3,9 @@
* *
* Creates Slack Block Kit blocks for displaying Sigma rule database statistics * 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); const FILE_NAME = getFileName(__filename);
/** /**

View file

@ -4,9 +4,9 @@
* Creates Slack Block Kit blocks for displaying Sigma rule YAML content * 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); const FILE_NAME = getFileName(__filename);
/** /**

View file

@ -1,3 +1,8 @@
/*
* appConfig.js
*
* retrives configuration data from fylgja.yml file
*/
const path = require('path'); const path = require('path');
const fs = require('fs'); const fs = require('fs');
const yaml = require('js-yaml'); const yaml = require('js-yaml');
@ -60,10 +65,19 @@ module.exports = {
// Elasticsearch configuration from YAML // Elasticsearch configuration from YAML
ELASTICSEARCH_CONFIG: { ELASTICSEARCH_CONFIG: {
apiEndpoint: yamlConfig?.elastic?.['api-endpoint'] || protocol: yamlConfig?.elasticsearch?.protocol || "http",
"http://localhost:5601/api/detection_engine/rules", hosts: yamlConfig?.elasticsearch?.hosts || ["localhost:9200"],
credentials: yamlConfig?.elastic?.['elastic-authentication-credentials'] || username: yamlConfig?.elasticsearch?.username || process.env.ELASTIC_USERNAME || "elastic",
"elastic:changeme" 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 // Logging configuration from YAML

View file

@ -8,11 +8,13 @@ const { handleError } = require('../../utils/error_handler');
const { explainSigmaRule, getSigmaRuleYaml } = require('../../services/sigma/sigma_details_service'); const { explainSigmaRule, getSigmaRuleYaml } = require('../../services/sigma/sigma_details_service');
const { convertRuleToBackend } = require('../../services/sigma/sigma_backend_converter'); const { convertRuleToBackend } = require('../../services/sigma/sigma_backend_converter');
const { searchSigmaRules } = require('../../services/sigma/sigma_search_service'); const { searchSigmaRules } = require('../../services/sigma/sigma_search_service');
const { getYamlViewBlocks } = require('../../blocks/sigma_view_yaml_block'); const { getYamlViewBlocks } = require('../../blocks/sigma/sigma_view_yaml_block');
const { getSearchResultBlocks } = require('../../blocks/sigma_search_results_block'); const { getSearchResultBlocks } = require('../../blocks/sigma/sigma_search_results_block');
const { getConversionResultBlocks } = require('../../blocks/sigma_conversion_block'); const { getConversionResultBlocks } = require('../../blocks/sigma/sigma_conversion_block');
const { getRuleExplanationBlocks } = require('../../blocks/sigma_details_block'); const { getRuleExplanationBlocks } = require('../../blocks/sigma/sigma_details_block');
const { sendRuleToSiem } = require('../../services/elastic/elastic_api_service'); 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'); const { SIGMA_CLI_CONFIG, ELASTICSEARCH_CONFIG } = require('../../config/appConfig');
@ -38,13 +40,13 @@ const processRuleDetails = async (ruleId, respond, replaceOriginal = false, resp
}); });
return; return;
} }
logger.info(`${FILE_NAME}: Processing details for sigma rule: ${ruleId}`); logger.info(`${FILE_NAME}: Processing details for sigma rule: ${ruleId}`);
// Get Sigma rule details // Get Sigma rule details
logger.info(`${FILE_NAME}: Calling explainSigmaRule with ID: '${ruleId}'`); logger.info(`${FILE_NAME}: Calling explainSigmaRule with ID: '${ruleId}'`);
const result = await explainSigmaRule(ruleId); const result = await explainSigmaRule(ruleId);
if (!result.success) { if (!result.success) {
logger.error(`${FILE_NAME}: Rule details retrieval failed: ${result.message}`); logger.error(`${FILE_NAME}: Rule details retrieval failed: ${result.message}`);
await respond({ await respond({
@ -54,7 +56,7 @@ const processRuleDetails = async (ruleId, respond, replaceOriginal = false, resp
}); });
return; return;
} }
if (!result.explanation) { if (!result.explanation) {
logger.error(`${FILE_NAME}: Rule details succeeded but no explanation object was returned`); logger.error(`${FILE_NAME}: Rule details succeeded but no explanation object was returned`);
await respond({ await respond({
@ -64,9 +66,9 @@ const processRuleDetails = async (ruleId, respond, replaceOriginal = false, resp
}); });
return; return;
} }
logger.info(`${FILE_NAME}: Rule ${ruleId} details retrieved successfully`); logger.info(`${FILE_NAME}: Rule ${ruleId} details retrieved successfully`);
// Generate blocks // Generate blocks
let blocks; let blocks;
try { try {
@ -79,7 +81,7 @@ const processRuleDetails = async (ruleId, respond, replaceOriginal = false, resp
}); });
return; return;
} }
// Respond with the details // Respond with the details
await respond({ await respond({
blocks: blocks, blocks: blocks,
@ -115,25 +117,25 @@ const processRuleConversion = async (ruleId, config, respond, replaceOriginal =
}); });
return; return;
} }
logger.info(`${FILE_NAME}: Processing conversion for sigma rule: ${ruleId}`); logger.info(`${FILE_NAME}: Processing conversion for sigma rule: ${ruleId}`);
// Set default configuration from YAML config if not provided // Set default configuration from YAML config if not provided
const conversionConfig = config || { const conversionConfig = config || {
backend: SIGMA_CLI_CONFIG.backend, backend: SIGMA_CLI_CONFIG.backend,
target: SIGMA_CLI_CONFIG.target, target: SIGMA_CLI_CONFIG.target,
format: SIGMA_CLI_CONFIG.format format: SIGMA_CLI_CONFIG.format
}; };
await respond({ await respond({
text: `Converting rule ${ruleId} using ${conversionConfig.backend}/${conversionConfig.target} to ${conversionConfig.format}...`, text: `Converting rule ${ruleId} using ${conversionConfig.backend}/${conversionConfig.target} to ${conversionConfig.format}...`,
replace_original: replaceOriginal, replace_original: replaceOriginal,
response_type: 'ephemeral' response_type: 'ephemeral'
}); });
// Get the rule and convert it // Get the rule and convert it
const conversionResult = await convertRuleToBackend(ruleId, conversionConfig); const conversionResult = await convertRuleToBackend(ruleId, conversionConfig);
if (!conversionResult.success) { if (!conversionResult.success) {
logger.error(`${FILE_NAME}: Rule conversion failed: ${conversionResult.message}`); logger.error(`${FILE_NAME}: Rule conversion failed: ${conversionResult.message}`);
await respond({ await respond({
@ -143,7 +145,7 @@ const processRuleConversion = async (ruleId, config, respond, replaceOriginal =
}); });
return; return;
} }
// Generate blocks for displaying the result // Generate blocks for displaying the result
let blocks; let blocks;
try { try {
@ -156,7 +158,7 @@ const processRuleConversion = async (ruleId, config, respond, replaceOriginal =
}); });
return; return;
} }
// Respond with the conversion result // Respond with the conversion result
await respond({ await respond({
blocks: blocks, blocks: blocks,
@ -266,13 +268,13 @@ const handlePaginationAction = async (body, ack, respond) => {
*/ */
const registerActionHandlers = (app) => { const registerActionHandlers = (app) => {
logger.info(`${FILE_NAME}: Registering consolidated sigma action handlers`); logger.info(`${FILE_NAME}: Registering consolidated sigma action handlers`);
// Handle "Send to SIEM" button clicks // Handle "Send to SIEM" button clicks
app.action('send_sigma_rule_to_siem', async ({ body, ack, respond }) => { app.action('send_sigma_rule_to_siem', async ({ body, ack, respond }) => {
try { try {
await ack(); await ack();
logger.debug(`${FILE_NAME}: send_sigma_rule_to_siem action received: ${JSON.stringify(body.actions)}`); 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) { if (!body || !body.actions || !body.actions[0] || !body.actions[0].value) {
logger.error(`${FILE_NAME}: Invalid action payload: missing rule ID`); logger.error(`${FILE_NAME}: Invalid action payload: missing rule ID`);
await respond({ await respond({
@ -282,12 +284,12 @@ const registerActionHandlers = (app) => {
}); });
return; return;
} }
// Extract rule ID from action value // Extract rule ID from action value
// Value format is "send_sigma_rule_to_siem_[ruleID]" // Value format is "send_sigma_rule_to_siem_[ruleID]"
const actionValue = body.actions[0].value; const actionValue = body.actions[0].value;
const ruleId = actionValue.replace('send_sigma_rule_to_siem_', ''); const ruleId = actionValue.replace('send_sigma_rule_to_siem_', '');
if (!ruleId) { if (!ruleId) {
logger.error(`${FILE_NAME}: Missing rule ID in action value: ${actionValue}`); logger.error(`${FILE_NAME}: Missing rule ID in action value: ${actionValue}`);
await respond({ await respond({
@ -297,26 +299,26 @@ const registerActionHandlers = (app) => {
}); });
return; return;
} }
logger.info(`${FILE_NAME}: Sending rule ${ruleId} to SIEM`); logger.info(`${FILE_NAME}: Sending rule ${ruleId} to SIEM`);
// Inform user that processing is happening // Inform user that processing is happening
await respond({ await respond({
text: `Sending rule ${ruleId} to Elasticsearch SIEM...`, text: `Sending rule ${ruleId} to Elasticsearch SIEM...`,
replace_original: false, replace_original: false,
response_type: 'ephemeral' response_type: 'ephemeral'
}); });
// Get the converted rule in Elasticsearch format using config from YAML // Get the converted rule in Elasticsearch format using config from YAML
const config = { const config = {
backend: SIGMA_CLI_CONFIG.backend, backend: SIGMA_CLI_CONFIG.backend,
target: SIGMA_CLI_CONFIG.target, target: SIGMA_CLI_CONFIG.target,
format: SIGMA_CLI_CONFIG.format format: SIGMA_CLI_CONFIG.format
}; };
logger.info(`${FILE_NAME}: Converting rule ${ruleId} for SIEM export`); logger.info(`${FILE_NAME}: Converting rule ${ruleId} for SIEM export`);
const conversionResult = await convertRuleToBackend(ruleId, config); const conversionResult = await convertRuleToBackend(ruleId, config);
if (!conversionResult.success) { if (!conversionResult.success) {
logger.error(`${FILE_NAME}: Rule conversion failed: ${conversionResult.message}`); logger.error(`${FILE_NAME}: Rule conversion failed: ${conversionResult.message}`);
await respond({ await respond({
@ -326,28 +328,28 @@ const registerActionHandlers = (app) => {
}); });
return; return;
} }
// Parse the converted rule JSON // Parse the converted rule JSON
let rulePayload; let rulePayload;
try { try {
rulePayload = JSON.parse(conversionResult.output); rulePayload = JSON.parse(conversionResult.output);
// Add required fields if not present // Add required fields if not present
rulePayload.rule_id = rulePayload.rule_id || ruleId; rulePayload.rule_id = rulePayload.rule_id || ruleId;
rulePayload.from = rulePayload.from || "now-360s"; rulePayload.from = rulePayload.from || "now-360s";
rulePayload.to = rulePayload.to || "now"; rulePayload.to = rulePayload.to || "now";
rulePayload.interval = rulePayload.interval || "5m"; rulePayload.interval = rulePayload.interval || "5m";
// Make sure required fields are present // Make sure required fields are present
if (!rulePayload.name) { if (!rulePayload.name) {
rulePayload.name = conversionResult.rule?.title || `Sigma Rule ${ruleId}`; rulePayload.name = conversionResult.rule?.title || `Sigma Rule ${ruleId}`;
} }
if (!rulePayload.description) { if (!rulePayload.description) {
rulePayload.description = conversionResult.rule?.description || rulePayload.description = conversionResult.rule?.description ||
`Converted from Sigma rule: ${ruleId}`; `Converted from Sigma rule: ${ruleId}`;
} }
if (!rulePayload.risk_score) { if (!rulePayload.risk_score) {
// Map Sigma level to risk score // Map Sigma level to risk score
const levelMap = { const levelMap = {
@ -357,18 +359,18 @@ const registerActionHandlers = (app) => {
'low': 25, 'low': 25,
'informational': 10 'informational': 10
}; };
rulePayload.risk_score = levelMap[conversionResult.rule?.level] || 50; rulePayload.risk_score = levelMap[conversionResult.rule?.level] || 50;
} }
if (!rulePayload.severity) { if (!rulePayload.severity) {
rulePayload.severity = conversionResult.rule?.level || 'medium'; rulePayload.severity = conversionResult.rule?.level || 'medium';
} }
if (!rulePayload.enabled) { if (!rulePayload.enabled) {
rulePayload.enabled = true; rulePayload.enabled = true;
} }
} catch (parseError) { } catch (parseError) {
logger.error(`${FILE_NAME}: Failed to parse converted rule JSON: ${parseError.message}`); logger.error(`${FILE_NAME}: Failed to parse converted rule JSON: ${parseError.message}`);
await respond({ await respond({
@ -378,11 +380,11 @@ const registerActionHandlers = (app) => {
}); });
return; return;
} }
// Send the rule to Elasticsearch using api service // Send the rule to Elasticsearch using api service
try { try {
const result = await sendRuleToSiem(rulePayload); const result = await sendRuleToSiem(rulePayload);
if (result.success) { if (result.success) {
logger.info(`${FILE_NAME}: Successfully sent rule ${ruleId} to SIEM`); logger.info(`${FILE_NAME}: Successfully sent rule ${ruleId} to SIEM`);
await respond({ await respond({
@ -409,14 +411,14 @@ const registerActionHandlers = (app) => {
}); });
} }
}); });
// Handle View YAML button clicks // Handle View YAML button clicks
app.action('view_yaml', async ({ body, ack, respond }) => { app.action('view_yaml', async ({ body, ack, respond }) => {
logger.info(`${FILE_NAME}: VIEW_YAML ACTION TRIGGERED`); logger.info(`${FILE_NAME}: VIEW_YAML ACTION TRIGGERED`);
try { try {
await ack(); await ack();
logger.debug(`${FILE_NAME}: View YAML action received: ${JSON.stringify(body.actions)}`); logger.debug(`${FILE_NAME}: View YAML action received: ${JSON.stringify(body.actions)}`);
if (!body || !body.actions || !body.actions[0] || !body.actions[0].value) { if (!body || !body.actions || !body.actions[0] || !body.actions[0].value) {
logger.error(`${FILE_NAME}: Invalid action payload: missing rule ID`); logger.error(`${FILE_NAME}: Invalid action payload: missing rule ID`);
await respond({ await respond({
@ -425,20 +427,20 @@ const registerActionHandlers = (app) => {
}); });
return; return;
} }
// Extract rule ID from button value // Extract rule ID from button value
// Handle both formats: direct ID from search results or view_yaml_{ruleId} from details view // Handle both formats: direct ID from search results or view_yaml_{ruleId} from details view
let ruleId = body.actions[0].value; let ruleId = body.actions[0].value;
if (ruleId.startsWith('view_yaml_')) { if (ruleId.startsWith('view_yaml_')) {
ruleId = ruleId.replace('view_yaml_', ''); ruleId = ruleId.replace('view_yaml_', '');
} }
logger.info(`${FILE_NAME}: View YAML button clicked for rule: ${ruleId}`); logger.info(`${FILE_NAME}: View YAML button clicked for rule: ${ruleId}`);
// Get Sigma rule YAML // Get Sigma rule YAML
const result = await getSigmaRuleYaml(ruleId); const result = await getSigmaRuleYaml(ruleId);
logger.debug(`${FILE_NAME}: YAML retrieval result: ${JSON.stringify(result, null, 2)}`); logger.debug(`${FILE_NAME}: YAML retrieval result: ${JSON.stringify(result, null, 2)}`);
if (!result.success) { if (!result.success) {
logger.error(`${FILE_NAME}: Rule YAML retrieval failed: ${result.message}`); logger.error(`${FILE_NAME}: Rule YAML retrieval failed: ${result.message}`);
await respond({ await respond({
@ -447,12 +449,12 @@ const registerActionHandlers = (app) => {
}); });
return; return;
} }
logger.info(`${FILE_NAME}: Rule ${ruleId} YAML retrieved successfully via button click`); logger.info(`${FILE_NAME}: Rule ${ruleId} YAML retrieved successfully via button click`);
// Use the module to generate blocks // Use the module to generate blocks
const blocks = getYamlViewBlocks(ruleId, result.yaml || ''); const blocks = getYamlViewBlocks(ruleId, result.yaml || '');
// Respond with the YAML content // Respond with the YAML content
await respond({ await respond({
blocks: blocks, blocks: blocks,
@ -464,7 +466,7 @@ const registerActionHandlers = (app) => {
}); });
} }
}); });
// Handle convert_rule_to_siem button clicks // Handle convert_rule_to_siem button clicks
app.action('convert_rule_to_siem', async ({ body, ack, respond }) => { app.action('convert_rule_to_siem', async ({ body, ack, respond }) => {
try { try {
@ -479,17 +481,17 @@ const registerActionHandlers = (app) => {
}); });
return; return;
} }
// Extract rule ID from button value // Extract rule ID from button value
const ruleId = body.actions[0].value.replace('convert_rule_to_siem_', ''); const ruleId = body.actions[0].value.replace('convert_rule_to_siem_', '');
logger.info(`${FILE_NAME}: convert_rule_to_siem button clicked for rule: ${ruleId}`); logger.info(`${FILE_NAME}: convert_rule_to_siem button clicked for rule: ${ruleId}`);
const config = { const config = {
backend: 'lucene', backend: 'lucene',
target: 'ecs_windows', target: 'ecs_windows',
format: 'siem_rule_ndjson' format: 'siem_rule_ndjson'
}; };
await processRuleConversion(ruleId, config, respond, false, 'in_channel'); await processRuleConversion(ruleId, config, respond, false, 'in_channel');
} catch (error) { } catch (error) {
await handleError(error, `${FILE_NAME}: convert_rule_to_siem action`, respond, { 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 // Handle "View Rule Details" button clicks from search results
app.action('view_rule_details', async ({ body, ack, respond }) => { app.action('view_rule_details', async ({ body, ack, respond }) => {
logger.info(`${FILE_NAME}: VIEW_RULE_DETAILS ACTION TRIGGERED`); 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 }) => { app.action('search_next_page', async ({ body, ack, respond }) => {
await handlePaginationAction(body, ack, respond); await handlePaginationAction(body, ack, respond);
}); });
logger.info(`${FILE_NAME}: All sigma action handlers registered successfully`); 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 = { module.exports = {
registerActionHandlers, registerActionHandlers,
processRuleDetails, processRuleDetails,

View file

@ -6,7 +6,7 @@
const { searchSigmaRules } = require('../../services/sigma/sigma_search_service'); const { searchSigmaRules } = require('../../services/sigma/sigma_search_service');
const logger = require('../../utils/logger'); const logger = require('../../utils/logger');
const { handleError } = require('../../utils/error_handler'); 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 { getFileName } = require('../../utils/file_utils');
const FILE_NAME = getFileName(__filename); const FILE_NAME = getFileName(__filename);

View file

@ -12,32 +12,76 @@ const FILE_NAME = 'elastic_api_service.js';
/** /**
* Get Elasticsearch configuration with credentials * Get Elasticsearch configuration with credentials
* *
* @param {string} spaceId - Optional space ID to get configuration for
* @returns {Object} Configuration object with URL and credentials * @returns {Object} Configuration object with URL and credentials
*/ */
const getElasticConfig = () => { const getElasticConfig = (spaceId = null) => {
return { // Default config
url: ELASTICSEARCH_CONFIG.apiEndpoint.split('/api/')[0] || process.env.ELASTIC_URL, const config = {
username: ELASTICSEARCH_CONFIG.credentials.split(':')[0] || process.env.ELASTIC_USERNAME, protocol: ELASTICSEARCH_CONFIG.protocol || "http",
password: ELASTICSEARCH_CONFIG.credentials.split(':')[1] || process.env.ELASTIC_PASSWORD, 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 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 {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>} - Object containing success status and response/error information * @returns {Promise<Object>} - Object containing success status and response/error information
*/ */
const sendRuleToSiem = async (rulePayload) => { const sendRuleToSiem = async (rulePayload, spaceId = 'default') => {
logger.info(`${FILE_NAME}: Sending rule to Elasticsearch SIEM`); logger.info(`${FILE_NAME}: Sending rule to Elasticsearch SIEM in space: ${spaceId}`);
try { try {
const elasticConfig = getElasticConfig(); const elasticConfig = getElasticConfig(spaceId);
const apiUrl = elasticConfig.apiEndpoint; 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}`); 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 // Send the request to Elasticsearch
const response = await axios({ const response = await axios({
method: 'post', method: 'post',
@ -55,18 +99,19 @@ const sendRuleToSiem = async (rulePayload) => {
// Process the response // Process the response
if (response.status >= 200 && response.status < 300) { 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 { return {
success: true, success: true,
status: response.status, status: response.status,
data: response.data data: response.data,
space: elasticConfig.space
}; };
} else { } else {
logger.error(`${FILE_NAME}: Error sending rule to SIEM. Status: ${response.status}, Response: ${JSON.stringify(response.data)}`); logger.error(`${FILE_NAME}: Error sending rule to SIEM. Status: ${response.status}, Response: ${JSON.stringify(response.data)}`);
return { return {
success: false, success: false,
status: response.status, 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 data: response.data
}; };
} }
@ -92,6 +137,7 @@ const sendRuleToSiem = async (rulePayload) => {
* @param {Object} options - Request options * @param {Object} options - Request options
* @param {string} options.method - HTTP method (get, post, put, delete) * @param {string} options.method - HTTP method (get, post, put, delete)
* @param {string} options.endpoint - API endpoint (appended to base URL) * @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.data - Request payload
* @param {Object} options.params - URL parameters * @param {Object} options.params - URL parameters
* @param {Object} options.headers - Additional headers * @param {Object} options.headers - Additional headers
@ -99,14 +145,24 @@ const sendRuleToSiem = async (rulePayload) => {
*/ */
const makeElasticRequest = async (options) => { const makeElasticRequest = async (options) => {
try { try {
const elasticConfig = getElasticConfig(); const elasticConfig = getElasticConfig(options.spaceId);
const baseUrl = elasticConfig.url; const baseUrl = elasticConfig.protocol + '://' + elasticConfig.hosts[0];
// Build the full URL - use provided endpoint or default API endpoint // 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}` : `${baseUrl}${options.endpoint.startsWith('/') ? '' : '/'}${options.endpoint}` :
elasticConfig.apiEndpoint; 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}`); logger.debug(`${FILE_NAME}: Making ${options.method} request to: ${url}`);
// Set up default headers // Set up default headers
@ -133,7 +189,8 @@ const makeElasticRequest = async (options) => {
return { return {
success: response.status >= 200 && response.status < 300, success: response.status >= 200 && response.status < 300,
status: response.status, status: response.status,
data: response.data data: response.data,
space: elasticConfig.space
}; };
} catch (error) { } catch (error) {
logger.error(`${FILE_NAME}: Error in Elasticsearch API request: ${error.message}`); logger.error(`${FILE_NAME}: Error in Elasticsearch API request: ${error.message}`);
@ -150,5 +207,6 @@ const makeElasticRequest = async (options) => {
module.exports = { module.exports = {
sendRuleToSiem, sendRuleToSiem,
makeElasticRequest, makeElasticRequest,
getElasticConfig getElasticConfig,
getAllSpaces
}; };