357 lines
No EOL
12 KiB
JavaScript
357 lines
No EOL
12 KiB
JavaScript
/**
|
|
* sigma_siem_actions.js
|
|
*
|
|
* Handlers for sending Sigma rules to SIEM and space-related operations
|
|
*/
|
|
const logger = require('../../../utils/logger');
|
|
const { handleError } = require('../../../utils/error_handler');
|
|
const { getSigmaRuleDetails } = require('../../../services/sigma/sigma_details_service');
|
|
const { convertRuleToBackend } = require('../../../services/sigma/sigma_backend_converter');
|
|
const { sendRuleToSiem } = require('../../../services/elastic/elastic_send_rule_to_siem_service');
|
|
const { getAllSpaces } = require('../../../services/elastic/elastic_api_service');
|
|
const { getSpaceSelectionBlocks } = require('../../../blocks/sigma/sigma_space_selection_block');
|
|
|
|
const { SIGMA_CLI_CONFIG } = require('../../../config/appConfig');
|
|
|
|
const FILE_NAME = 'sigma_siem_actions.js';
|
|
|
|
/**
|
|
* Parse JSON rule payload and add required fields
|
|
*
|
|
* @param {string} ruleOutput - The JSON rule as string
|
|
* @param {string} ruleId - The rule ID
|
|
* @param {Object} conversionResult - Result from convertRuleToBackend
|
|
* @param {Object} selectedSpace - Optional space configuration
|
|
* @returns {Object} Prepared rule payload
|
|
* @throws {Error} If JSON parsing fails
|
|
*/
|
|
const prepareRulePayload = (ruleOutput, ruleId, conversionResult, selectedSpace = null) => {
|
|
const rulePayload = JSON.parse(ruleOutput);
|
|
|
|
// Add required fields if not present
|
|
rulePayload.rule_id = rulePayload.rule_id || ruleId;
|
|
rulePayload.from = rulePayload.from || "now-360s";
|
|
rulePayload.to = rulePayload.to || "now";
|
|
rulePayload.interval = rulePayload.interval || "5m";
|
|
|
|
// Set index pattern from space configuration if available
|
|
if (selectedSpace && selectedSpace.indexPattern) {
|
|
rulePayload.index = Array.isArray(selectedSpace.indexPattern)
|
|
? selectedSpace.indexPattern
|
|
: [selectedSpace.indexPattern];
|
|
logger.debug(`${FILE_NAME}: Setting index pattern from space config: ${JSON.stringify(rulePayload.index)}`);
|
|
}
|
|
|
|
// Make sure required fields are present
|
|
if (!rulePayload.name) {
|
|
rulePayload.name = conversionResult.rule?.title || `Sigma Rule ${ruleId}`;
|
|
}
|
|
|
|
if (!rulePayload.description) {
|
|
rulePayload.description = conversionResult.rule?.description ||
|
|
`Converted from Sigma rule: ${ruleId}`;
|
|
}
|
|
|
|
if (!rulePayload.risk_score) {
|
|
// Map Sigma level to risk score
|
|
const levelMap = {
|
|
'critical': 90,
|
|
'high': 73,
|
|
'medium': 50,
|
|
'low': 25,
|
|
'informational': 10
|
|
};
|
|
|
|
rulePayload.risk_score = levelMap[conversionResult.rule?.level] || 50;
|
|
}
|
|
|
|
if (!rulePayload.severity) {
|
|
rulePayload.severity = conversionResult.rule?.level || 'medium';
|
|
}
|
|
|
|
if (!rulePayload.enabled) {
|
|
rulePayload.enabled = true;
|
|
}
|
|
|
|
return rulePayload;
|
|
};
|
|
|
|
/**
|
|
* Register SIEM and space-related action handlers
|
|
*
|
|
* @param {Object} app - The Slack app instance
|
|
*/
|
|
const registerSiemActions = (app) => {
|
|
logger.info(`${FILE_NAME}: Registering SIEM-related action handlers`);
|
|
|
|
// Handle "Send to SIEM" button clicks
|
|
app.action('send_sigma_rule_to_siem', async ({ body, ack, respond }) => {
|
|
try {
|
|
await ack();
|
|
logger.debug(`${FILE_NAME}: send_sigma_rule_to_siem action received: ${JSON.stringify(body.actions)}`);
|
|
|
|
if (!body || !body.actions || !body.actions[0] || !body.actions[0].value) {
|
|
logger.error(`${FILE_NAME}: Invalid action payload: missing rule ID`);
|
|
await respond({
|
|
text: 'Error: Could not determine which rule to send',
|
|
replace_original: false,
|
|
response_type: 'ephemeral'
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Extract rule ID from action value
|
|
// Value format is "send_sigma_rule_to_siem_[ruleID]"
|
|
const actionValue = body.actions[0].value;
|
|
const ruleId = actionValue.replace('send_sigma_rule_to_siem_', '');
|
|
|
|
if (!ruleId) {
|
|
logger.error(`${FILE_NAME}: Missing rule ID in action value: ${actionValue}`);
|
|
await respond({
|
|
text: 'Error: Missing rule ID in button data',
|
|
replace_original: false,
|
|
response_type: 'ephemeral'
|
|
});
|
|
return;
|
|
}
|
|
|
|
logger.info(`${FILE_NAME}: Sending rule ${ruleId} to SIEM`);
|
|
|
|
// Inform user that processing is happening
|
|
await respond({
|
|
text: `Sending rule ${ruleId} to Elasticsearch SIEM...`,
|
|
replace_original: false,
|
|
response_type: 'ephemeral'
|
|
});
|
|
|
|
// Get the converted rule in Elasticsearch format using config from YAML
|
|
const config = {
|
|
backend: SIGMA_CLI_CONFIG.backend,
|
|
target: SIGMA_CLI_CONFIG.target,
|
|
format: SIGMA_CLI_CONFIG.format
|
|
};
|
|
|
|
logger.info(`${FILE_NAME}: Converting rule ${ruleId} for SIEM export`);
|
|
const conversionResult = await convertRuleToBackend(ruleId, config);
|
|
|
|
if (!conversionResult.success) {
|
|
logger.error(`${FILE_NAME}: Rule conversion failed: ${conversionResult.message}`);
|
|
await respond({
|
|
text: `Error: Failed to convert rule for SIEM: ${conversionResult.message}`,
|
|
replace_original: false,
|
|
response_type: 'ephemeral'
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Parse the converted rule JSON
|
|
let rulePayload;
|
|
try {
|
|
rulePayload = prepareRulePayload(conversionResult.output, ruleId, conversionResult);
|
|
} catch (parseError) {
|
|
logger.error(`${FILE_NAME}: Failed to parse converted rule JSON: ${parseError.message}`);
|
|
await respond({
|
|
text: `Error: The converted rule is not valid JSON: ${parseError.message}`,
|
|
replace_original: false,
|
|
response_type: 'ephemeral'
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Send the rule to Elasticsearch using api service
|
|
try {
|
|
const result = await sendRuleToSiem(rulePayload);
|
|
|
|
if (result.success) {
|
|
logger.info(`${FILE_NAME}: Successfully sent rule ${ruleId} to SIEM`);
|
|
await respond({
|
|
text: `✅ Success! Rule "${rulePayload.name}" has been added to your Elasticsearch SIEM.`,
|
|
replace_original: false,
|
|
response_type: 'in_channel'
|
|
});
|
|
} else {
|
|
logger.error(`${FILE_NAME}: Error sending rule to SIEM: ${result.message}`);
|
|
await respond({
|
|
text: `Error: Failed to add rule to SIEM: ${result.message}`,
|
|
replace_original: false,
|
|
response_type: 'ephemeral'
|
|
});
|
|
}
|
|
} catch (error) {
|
|
await handleError(error, `${FILE_NAME}: send_sigma_rule_to_siem action`, respond, {
|
|
replaceOriginal: false
|
|
});
|
|
}
|
|
} catch (error) {
|
|
await handleError(error, `${FILE_NAME}: send_sigma_rule_to_siem action`, respond, {
|
|
replaceOriginal: false
|
|
});
|
|
}
|
|
});
|
|
|
|
// Handle space selection button click
|
|
app.action('select_space_for_rule', async ({ body, ack, respond }) => {
|
|
try {
|
|
await ack();
|
|
logger.debug(`${FILE_NAME}: select_space_for_rule action received: ${JSON.stringify(body.actions)}`);
|
|
|
|
if (!body || !body.actions || !body.actions[0] || !body.actions[0].value) {
|
|
logger.error(`${FILE_NAME}: Invalid action payload: missing rule ID`);
|
|
await respond({
|
|
text: 'Error: Could not determine which rule to select space for',
|
|
replace_original: false,
|
|
response_type: 'ephemeral'
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Extract rule ID from value
|
|
const actionValue = body.actions[0].value;
|
|
const ruleId = actionValue.replace('select_space_for_rule_', '');
|
|
|
|
// Get rule information to display in the space selection message
|
|
const sigmaRuleDetailsResult = await getSigmaRuleDetails(ruleId);
|
|
const ruleInfo = sigmaRuleDetailsResult.success ? sigmaRuleDetailsResult.explanation : { title: ruleId };
|
|
|
|
// Generate blocks for space selection
|
|
const blocks = getSpaceSelectionBlocks(ruleId, ruleInfo);
|
|
|
|
// Show space selection options
|
|
await respond({
|
|
blocks: blocks,
|
|
replace_original: false,
|
|
response_type: 'ephemeral'
|
|
});
|
|
|
|
} catch (error) {
|
|
await handleError(error, `${FILE_NAME}: select_space_for_rule action`, respond, {
|
|
replaceOriginal: false
|
|
});
|
|
}
|
|
});
|
|
|
|
// Handle space selection cancel button
|
|
app.action('cancel_space_selection', async ({ body, ack, respond }) => {
|
|
try {
|
|
await ack();
|
|
await respond({
|
|
text: 'Space selection cancelled.',
|
|
replace_original: false,
|
|
response_type: 'ephemeral'
|
|
});
|
|
} catch (error) {
|
|
await handleError(error, `${FILE_NAME}: cancel_space_selection action`, respond, {
|
|
replaceOriginal: false
|
|
});
|
|
}
|
|
});
|
|
|
|
// Dynamic handler for all space selection buttons
|
|
// This uses a pattern matcher to match any action ID that starts with "send_rule_to_space_"
|
|
app.action(/^send_rule_to_space_(.*)$/, async ({ body, action, ack, respond }) => {
|
|
try {
|
|
await ack();
|
|
logger.debug(`${FILE_NAME}: Space selection action received: ${JSON.stringify(action)}`);
|
|
|
|
// Extract rule ID and space ID from the action value
|
|
const actionValue = action.value;
|
|
const parts = actionValue.split('_');
|
|
const spaceId = parts.pop(); // Last part is the space ID
|
|
const ruleId = actionValue.match(/send_rule_to_space_(.+)_/)[1]; // Extract full UUID
|
|
|
|
logger.info(`${FILE_NAME}: Selected space ${spaceId} for rule ${ruleId}`);
|
|
|
|
|
|
// Get space info
|
|
const spaces = getAllSpaces();
|
|
const selectedSpace = spaces.find(s => s.id === spaceId);
|
|
|
|
if (!selectedSpace) {
|
|
logger.error(`${FILE_NAME}: Space not found: ${spaceId}`);
|
|
await respond({
|
|
text: `Error: Space "${spaceId}" not found in configuration`,
|
|
replace_original: false,
|
|
response_type: 'ephemeral'
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Inform user that processing is happening
|
|
await respond({
|
|
text: `Sending rule ${ruleId} to ${selectedSpace.emoji || ''} ${selectedSpace.name} space...`,
|
|
replace_original: false,
|
|
response_type: 'ephemeral'
|
|
});
|
|
|
|
// Get the converted rule in Elasticsearch format
|
|
const config = {
|
|
backend: SIGMA_CLI_CONFIG.backend,
|
|
target: SIGMA_CLI_CONFIG.target,
|
|
format: SIGMA_CLI_CONFIG.format
|
|
};
|
|
|
|
logger.info(`${FILE_NAME}: Converting rule ${ruleId} for SIEM export to space ${spaceId}`);
|
|
const conversionResult = await convertRuleToBackend(ruleId, config);
|
|
|
|
if (!conversionResult.success) {
|
|
logger.error(`${FILE_NAME}: Rule conversion failed: ${conversionResult.message}`);
|
|
await respond({
|
|
text: `Error: Failed to convert rule for SIEM: ${conversionResult.message}`,
|
|
replace_original: false,
|
|
response_type: 'ephemeral'
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Parse the converted rule JSON
|
|
let rulePayload;
|
|
try {
|
|
rulePayload = prepareRulePayload(conversionResult.output, ruleId, conversionResult, selectedSpace);
|
|
} catch (parseError) {
|
|
logger.error(`${FILE_NAME}: Failed to parse converted rule JSON: ${parseError.message}`);
|
|
await respond({
|
|
text: `Error: The converted rule is not valid JSON: ${parseError.message}`,
|
|
replace_original: false,
|
|
response_type: 'ephemeral'
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Send the rule to the selected Elasticsearch space
|
|
try {
|
|
const result = await sendRuleToSiem(rulePayload, spaceId);
|
|
|
|
if (result.success) {
|
|
logger.info(`${FILE_NAME}: Successfully sent rule ${ruleId} to space ${spaceId}`);
|
|
await respond({
|
|
text: `✅ Success! Rule "${rulePayload.name}" has been added to the ${selectedSpace.emoji || ''} ${selectedSpace.name} space in Elasticsearch.`,
|
|
replace_original: false,
|
|
response_type: 'in_channel'
|
|
});
|
|
} else {
|
|
logger.error(`${FILE_NAME}: Error sending rule to SIEM: ${result.message}`);
|
|
await respond({
|
|
text: `Error: Failed to add rule to the ${selectedSpace.name} space: ${result.message}`,
|
|
replace_original: false,
|
|
response_type: 'ephemeral'
|
|
});
|
|
}
|
|
} catch (error) {
|
|
await handleError(error, `${FILE_NAME}: send_rule_to_space action`, respond, {
|
|
replaceOriginal: false
|
|
});
|
|
}
|
|
} catch (error) {
|
|
await handleError(error, `${FILE_NAME}: send_rule_to_space action`, respond, {
|
|
replaceOriginal: false
|
|
});
|
|
}
|
|
});
|
|
|
|
logger.info(`${FILE_NAME}: All SIEM action handlers registered successfully`);
|
|
};
|
|
|
|
module.exports = {
|
|
registerSiemActions,
|
|
prepareRulePayload
|
|
}; |