refactor sigma actions handler into multiple files
This commit is contained in:
parent
bfabd6de2a
commit
31d6296c6e
10 changed files with 853 additions and 769 deletions
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
173
src/handlers/sigma/actions/sigma_action_core.js
Normal file
173
src/handlers/sigma/actions/sigma_action_core.js
Normal file
|
@ -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<void>}
|
||||
*/
|
||||
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<void>}
|
||||
*/
|
||||
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
|
||||
};
|
38
src/handlers/sigma/actions/sigma_action_registry.js
Normal file
38
src/handlers/sigma/actions/sigma_action_registry.js
Normal file
|
@ -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
|
||||
};
|
58
src/handlers/sigma/actions/sigma_conversion_actions.js
Normal file
58
src/handlers/sigma/actions/sigma_conversion_actions.js
Normal file
|
@ -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
|
||||
};
|
357
src/handlers/sigma/actions/sigma_siem_actions.js
Normal file
357
src/handlers/sigma/actions/sigma_siem_actions.js
Normal file
|
@ -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
|
||||
};
|
216
src/handlers/sigma/actions/sigma_view_actions.js
Normal file
216
src/handlers/sigma/actions/sigma_view_actions.js
Normal file
|
@ -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
|
||||
};
|
|
@ -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<void>}
|
||||
*/
|
||||
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<void>}
|
||||
*/
|
||||
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
|
||||
};
|
|
@ -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';
|
||||
|
|
|
@ -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
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue