Merge pull request 'add support for multiple spaces' (#1) from managing-multiple-spaces into main
Reviewed-on: https://codeberg.org/charlottecroce/fylgja/pulls/1
This commit is contained in:
commit
9ceda08145
10 changed files with 498 additions and 92 deletions
|
@ -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'
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
});
|
});
|
|
@ -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);
|
||||||
|
|
||||||
/**
|
/**
|
|
@ -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);
|
||||||
|
|
||||||
/**
|
/**
|
127
src/blocks/sigma/sigma_space_selection_block.js
Normal file
127
src/blocks/sigma/sigma_space_selection_block.js
Normal 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
|
||||||
|
};
|
|
@ -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);
|
||||||
|
|
||||||
/**
|
/**
|
|
@ -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);
|
||||||
|
|
||||||
/**
|
/**
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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
|
||||||
};
|
};
|
Loading…
Add table
Add a link
Reference in a new issue