Merge branch 'main' of codeberg.org:charlottecroce/fylgja

merge conflict
This commit is contained in:
Charlotte Croce 2025-04-16 18:13:51 -04:00
commit 9be68df982
10 changed files with 498 additions and 92 deletions

View file

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

View file

@ -4,9 +4,9 @@
* Creates Slack Block Kit blocks for displaying Sigma rule explanations
*
*/
const logger = require('../utils/logger');
const logger = require('../../utils/logger');
const { getFileName } = require('../utils/file_utils');
const { getFileName } = require('../../utils/file_utils');
const FILE_NAME = getFileName(__filename);
/**

View file

@ -4,11 +4,10 @@
* Generates Slack Block Kit blocks for displaying Sigma rule search results
* Includes pagination controls for navigating large result sets
*
* @author Fylgja Development Team
*/
const logger = require('../utils/logger');
const logger = require('../../utils/logger');
const { getFileName } = require('../utils/file_utils');
const { getFileName } = require('../../utils/file_utils');
const FILE_NAME = getFileName(__filename);
/**

View file

@ -0,0 +1,127 @@
/**
* space_selection_block.js
*
* Provides block templates for displaying Elasticsearch space selection in Slack
*/
const logger = require('../../utils/logger');
const { getAllSpaces } = require('../../services/elastic/elastic_api_service');
const { getFileName } = require('../../utils/file_utils');
const FILE_NAME = getFileName(__filename);
/**
* Generate blocks for space selection for a specific rule
*
* @param {string} ruleId - The ID of the rule to send to a space
* @param {Object} ruleInfo - Optional rule title and other information
* @returns {Array} Array of blocks for Slack message
*/
function getSpaceSelectionBlocks(ruleId, ruleInfo = {}) {
logger.debug(`${FILE_NAME}: Generating space selection blocks for rule: ${ruleId}`);
// Get all configured spaces
const spaces = getAllSpaces();
if (!spaces || spaces.length === 0) {
logger.warn(`${FILE_NAME}: No spaces configured for selection blocks`);
return [{
type: 'section',
text: {
type: 'mrkdwn',
text: 'No Elasticsearch spaces are configured. Please update your configuration.'
}
}];
}
// Create header block
const blocks = [
{
type: 'header',
text: {
type: 'plain_text',
text: `Select a Space for Rule: ${ruleInfo.title || ruleId}`,
emoji: true
}
},
{
type: 'section',
text: {
type: 'mrkdwn',
text: `Please select which Elasticsearch space you want to send this rule to:`
}
},
{
type: 'divider'
}
];
// Create buttons for each space
// If we have many spaces, we need to create multiple action blocks
// Slack only allows 5 buttons per actions block
const BUTTONS_PER_ACTION = 5;
let actionBlocks = [];
let currentActionBlock = {
type: 'actions',
elements: []
};
// Add buttons for each space
spaces.forEach((space, index) => {
// Create the button for this space
const button = {
type: 'button',
text: {
type: 'plain_text',
text: `${space.emoji || ''} ${space.name}`,
emoji: true
},
value: `send_rule_to_space_${ruleId}_${space.id}`,
action_id: `send_rule_to_space_${space.id}`
};
// Add button to current action block
currentActionBlock.elements.push(button);
// If we've reached the button limit or this is the last space,
// add the current action block to our collection
if (currentActionBlock.elements.length === BUTTONS_PER_ACTION || index === spaces.length - 1) {
actionBlocks.push(currentActionBlock);
// Start a new action block if we have more spaces
if (index < spaces.length - 1) {
currentActionBlock = {
type: 'actions',
elements: []
};
}
}
});
// Add all the action blocks to our blocks array
blocks.push(...actionBlocks);
// Add a cancel button
blocks.push({
type: 'actions',
elements: [
{
type: 'button',
text: {
type: 'plain_text',
text: 'Cancel',
emoji: true
},
style: 'danger',
value: `cancel_space_selection`,
action_id: 'cancel_space_selection'
}
]
});
logger.debug(`${FILE_NAME}: Generated ${blocks.length} blocks for space selection`);
return blocks;
}
module.exports = {
getSpaceSelectionBlocks
};

View file

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

View file

@ -4,9 +4,9 @@
* Creates Slack Block Kit blocks for displaying Sigma rule YAML content
*
*/
const logger = require('../utils/logger');
const logger = require('../../utils/logger');
const { getFileName } = require('../utils/file_utils');
const { getFileName } = require('../../utils/file_utils');
const FILE_NAME = getFileName(__filename);
/**

View file

@ -1,3 +1,8 @@
/*
* appConfig.js
*
* retrives configuration data from fylgja.yml file
*/
const path = require('path');
const fs = require('fs');
const yaml = require('js-yaml');
@ -60,10 +65,19 @@ module.exports = {
// Elasticsearch configuration from YAML
ELASTICSEARCH_CONFIG: {
apiEndpoint: yamlConfig?.elastic?.['api-endpoint'] ||
"http://localhost:5601/api/detection_engine/rules",
credentials: yamlConfig?.elastic?.['elastic-authentication-credentials'] ||
"elastic:changeme"
protocol: yamlConfig?.elasticsearch?.protocol || "http",
hosts: yamlConfig?.elasticsearch?.hosts || ["localhost:9200"],
username: yamlConfig?.elasticsearch?.username || process.env.ELASTIC_USERNAME || "elastic",
password: yamlConfig?.elasticsearch?.password || process.env.ELASTIC_PASSWORD || "changeme",
apiEndpoint: yamlConfig?.elasticsearch?.api_endpoint || "http://localhost:5601/api/detection_engine/rules",
spaces: yamlConfig?.elasticsearch?.spaces || [
{
name: "Default",
id: "default",
indexPattern: "logs-*",
emoji: "🔍"
}
]
},
// Logging configuration from YAML

View file

@ -8,11 +8,13 @@ const { handleError } = require('../../utils/error_handler');
const { explainSigmaRule, getSigmaRuleYaml } = require('../../services/sigma/sigma_details_service');
const { convertRuleToBackend } = require('../../services/sigma/sigma_backend_converter');
const { searchSigmaRules } = require('../../services/sigma/sigma_search_service');
const { getYamlViewBlocks } = require('../../blocks/sigma_view_yaml_block');
const { getSearchResultBlocks } = require('../../blocks/sigma_search_results_block');
const { getConversionResultBlocks } = require('../../blocks/sigma_conversion_block');
const { getRuleExplanationBlocks } = require('../../blocks/sigma_details_block');
const { getYamlViewBlocks } = require('../../blocks/sigma/sigma_view_yaml_block');
const { getSearchResultBlocks } = require('../../blocks/sigma/sigma_search_results_block');
const { getConversionResultBlocks } = require('../../blocks/sigma/sigma_conversion_block');
const { getRuleExplanationBlocks } = require('../../blocks/sigma/sigma_details_block');
const { sendRuleToSiem } = require('../../services/elastic/elastic_api_service');
const { getSpaceSelectionBlocks } = require('../../blocks/sigma/sigma_space_selection_block');
const { getAllSpaces } = require('../../services/elastic/elastic_api_service');
const { SIGMA_CLI_CONFIG, ELASTICSEARCH_CONFIG } = require('../../config/appConfig');
@ -38,13 +40,13 @@ const processRuleDetails = async (ruleId, respond, replaceOriginal = false, resp
});
return;
}
logger.info(`${FILE_NAME}: Processing details for sigma rule: ${ruleId}`);
// Get Sigma rule details
logger.info(`${FILE_NAME}: Calling explainSigmaRule with ID: '${ruleId}'`);
const result = await explainSigmaRule(ruleId);
if (!result.success) {
logger.error(`${FILE_NAME}: Rule details retrieval failed: ${result.message}`);
await respond({
@ -54,7 +56,7 @@ const processRuleDetails = async (ruleId, respond, replaceOriginal = false, resp
});
return;
}
if (!result.explanation) {
logger.error(`${FILE_NAME}: Rule details succeeded but no explanation object was returned`);
await respond({
@ -64,9 +66,9 @@ const processRuleDetails = async (ruleId, respond, replaceOriginal = false, resp
});
return;
}
logger.info(`${FILE_NAME}: Rule ${ruleId} details retrieved successfully`);
// Generate blocks
let blocks;
try {
@ -79,7 +81,7 @@ const processRuleDetails = async (ruleId, respond, replaceOriginal = false, resp
});
return;
}
// Respond with the details
await respond({
blocks: blocks,
@ -115,25 +117,25 @@ const processRuleConversion = async (ruleId, config, respond, replaceOriginal =
});
return;
}
logger.info(`${FILE_NAME}: Processing conversion for sigma rule: ${ruleId}`);
// Set default configuration from YAML config if not provided
const conversionConfig = config || {
backend: SIGMA_CLI_CONFIG.backend,
target: SIGMA_CLI_CONFIG.target,
format: SIGMA_CLI_CONFIG.format
};
await respond({
text: `Converting rule ${ruleId} using ${conversionConfig.backend}/${conversionConfig.target} to ${conversionConfig.format}...`,
replace_original: replaceOriginal,
response_type: 'ephemeral'
});
// Get the rule and convert it
const conversionResult = await convertRuleToBackend(ruleId, conversionConfig);
if (!conversionResult.success) {
logger.error(`${FILE_NAME}: Rule conversion failed: ${conversionResult.message}`);
await respond({
@ -143,7 +145,7 @@ const processRuleConversion = async (ruleId, config, respond, replaceOriginal =
});
return;
}
// Generate blocks for displaying the result
let blocks;
try {
@ -156,7 +158,7 @@ const processRuleConversion = async (ruleId, config, respond, replaceOriginal =
});
return;
}
// Respond with the conversion result
await respond({
blocks: blocks,
@ -266,13 +268,13 @@ const handlePaginationAction = async (body, ack, respond) => {
*/
const registerActionHandlers = (app) => {
logger.info(`${FILE_NAME}: Registering consolidated sigma action handlers`);
// Handle "Send to SIEM" button clicks
app.action('send_sigma_rule_to_siem', async ({ body, ack, respond }) => {
try {
await ack();
logger.debug(`${FILE_NAME}: send_sigma_rule_to_siem action received: ${JSON.stringify(body.actions)}`);
if (!body || !body.actions || !body.actions[0] || !body.actions[0].value) {
logger.error(`${FILE_NAME}: Invalid action payload: missing rule ID`);
await respond({
@ -282,12 +284,12 @@ const registerActionHandlers = (app) => {
});
return;
}
// Extract rule ID from action value
// Value format is "send_sigma_rule_to_siem_[ruleID]"
const actionValue = body.actions[0].value;
const ruleId = actionValue.replace('send_sigma_rule_to_siem_', '');
if (!ruleId) {
logger.error(`${FILE_NAME}: Missing rule ID in action value: ${actionValue}`);
await respond({
@ -297,26 +299,26 @@ const registerActionHandlers = (app) => {
});
return;
}
logger.info(`${FILE_NAME}: Sending rule ${ruleId} to SIEM`);
// Inform user that processing is happening
await respond({
text: `Sending rule ${ruleId} to Elasticsearch SIEM...`,
replace_original: false,
response_type: 'ephemeral'
});
// Get the converted rule in Elasticsearch format using config from YAML
const config = {
backend: SIGMA_CLI_CONFIG.backend,
target: SIGMA_CLI_CONFIG.target,
format: SIGMA_CLI_CONFIG.format
};
logger.info(`${FILE_NAME}: Converting rule ${ruleId} for SIEM export`);
const conversionResult = await convertRuleToBackend(ruleId, config);
if (!conversionResult.success) {
logger.error(`${FILE_NAME}: Rule conversion failed: ${conversionResult.message}`);
await respond({
@ -326,28 +328,28 @@ const registerActionHandlers = (app) => {
});
return;
}
// Parse the converted rule JSON
let rulePayload;
try {
rulePayload = JSON.parse(conversionResult.output);
// Add required fields if not present
rulePayload.rule_id = rulePayload.rule_id || ruleId;
rulePayload.from = rulePayload.from || "now-360s";
rulePayload.to = rulePayload.to || "now";
rulePayload.interval = rulePayload.interval || "5m";
// Make sure required fields are present
if (!rulePayload.name) {
rulePayload.name = conversionResult.rule?.title || `Sigma Rule ${ruleId}`;
}
if (!rulePayload.description) {
rulePayload.description = conversionResult.rule?.description ||
rulePayload.description = conversionResult.rule?.description ||
`Converted from Sigma rule: ${ruleId}`;
}
if (!rulePayload.risk_score) {
// Map Sigma level to risk score
const levelMap = {
@ -357,18 +359,18 @@ const registerActionHandlers = (app) => {
'low': 25,
'informational': 10
};
rulePayload.risk_score = levelMap[conversionResult.rule?.level] || 50;
}
if (!rulePayload.severity) {
rulePayload.severity = conversionResult.rule?.level || 'medium';
}
if (!rulePayload.enabled) {
rulePayload.enabled = true;
}
} catch (parseError) {
logger.error(`${FILE_NAME}: Failed to parse converted rule JSON: ${parseError.message}`);
await respond({
@ -378,11 +380,11 @@ const registerActionHandlers = (app) => {
});
return;
}
// Send the rule to Elasticsearch using api service
try {
const result = await sendRuleToSiem(rulePayload);
if (result.success) {
logger.info(`${FILE_NAME}: Successfully sent rule ${ruleId} to SIEM`);
await respond({
@ -409,14 +411,14 @@ const registerActionHandlers = (app) => {
});
}
});
// Handle View YAML button clicks
app.action('view_yaml', async ({ body, ack, respond }) => {
logger.info(`${FILE_NAME}: VIEW_YAML ACTION TRIGGERED`);
try {
await ack();
logger.debug(`${FILE_NAME}: View YAML action received: ${JSON.stringify(body.actions)}`);
if (!body || !body.actions || !body.actions[0] || !body.actions[0].value) {
logger.error(`${FILE_NAME}: Invalid action payload: missing rule ID`);
await respond({
@ -425,20 +427,20 @@ const registerActionHandlers = (app) => {
});
return;
}
// Extract rule ID from button value
// Handle both formats: direct ID from search results or view_yaml_{ruleId} from details view
let ruleId = body.actions[0].value;
if (ruleId.startsWith('view_yaml_')) {
ruleId = ruleId.replace('view_yaml_', '');
}
logger.info(`${FILE_NAME}: View YAML button clicked for rule: ${ruleId}`);
// Get Sigma rule YAML
const result = await getSigmaRuleYaml(ruleId);
logger.debug(`${FILE_NAME}: YAML retrieval result: ${JSON.stringify(result, null, 2)}`);
if (!result.success) {
logger.error(`${FILE_NAME}: Rule YAML retrieval failed: ${result.message}`);
await respond({
@ -447,12 +449,12 @@ const registerActionHandlers = (app) => {
});
return;
}
logger.info(`${FILE_NAME}: Rule ${ruleId} YAML retrieved successfully via button click`);
// Use the module to generate blocks
const blocks = getYamlViewBlocks(ruleId, result.yaml || '');
// Respond with the YAML content
await respond({
blocks: blocks,
@ -464,7 +466,7 @@ const registerActionHandlers = (app) => {
});
}
});
// Handle convert_rule_to_siem button clicks
app.action('convert_rule_to_siem', async ({ body, ack, respond }) => {
try {
@ -479,17 +481,17 @@ const registerActionHandlers = (app) => {
});
return;
}
// Extract rule ID from button value
const ruleId = body.actions[0].value.replace('convert_rule_to_siem_', '');
logger.info(`${FILE_NAME}: convert_rule_to_siem button clicked for rule: ${ruleId}`);
const config = {
backend: 'lucene',
target: 'ecs_windows',
format: 'siem_rule_ndjson'
};
await processRuleConversion(ruleId, config, respond, false, 'in_channel');
} catch (error) {
await handleError(error, `${FILE_NAME}: convert_rule_to_siem action`, respond, {
@ -497,8 +499,8 @@ const registerActionHandlers = (app) => {
});
}
});
// Handle "View Rule Details" button clicks from search results
app.action('view_rule_details', async ({ body, ack, respond }) => {
logger.info(`${FILE_NAME}: VIEW_RULE_DETAILS ACTION TRIGGERED`);
@ -541,10 +543,216 @@ const registerActionHandlers = (app) => {
app.action('search_next_page', async ({ body, ack, respond }) => {
await handlePaginationAction(body, ack, respond);
});
logger.info(`${FILE_NAME}: All sigma action handlers registered successfully`);
// Handle space selection button click
app.action('select_space_for_rule', async ({ body, ack, respond }) => {
try {
await ack();
logger.debug(`${FILE_NAME}: select_space_for_rule action received: ${JSON.stringify(body.actions)}`);
if (!body || !body.actions || !body.actions[0] || !body.actions[0].value) {
logger.error(`${FILE_NAME}: Invalid action payload: missing rule ID`);
await respond({
text: 'Error: Could not determine which rule to select space for',
replace_original: false,
response_type: 'ephemeral'
});
return;
}
// Extract rule ID from value
const actionValue = body.actions[0].value;
const ruleId = actionValue.replace('select_space_for_rule_', '');
// Get rule information to display in the space selection message
const explainResult = await explainSigmaRule(ruleId);
const ruleInfo = explainResult.success ? explainResult.explanation : { title: ruleId };
// Generate blocks for space selection
const blocks = getSpaceSelectionBlocks(ruleId, ruleInfo);
// Show space selection options
await respond({
blocks: blocks,
replace_original: false,
response_type: 'ephemeral'
});
} catch (error) {
await handleError(error, `${FILE_NAME}: select_space_for_rule action`, respond, {
replaceOriginal: false
});
}
});
// Handle space selection cancel button
app.action('cancel_space_selection', async ({ body, ack, respond }) => {
try {
await ack();
await respond({
text: 'Space selection cancelled.',
replace_original: false,
response_type: 'ephemeral'
});
} catch (error) {
await handleError(error, `${FILE_NAME}: cancel_space_selection action`, respond, {
replaceOriginal: false
});
}
});
// Dynamic handler for all space selection buttons
// This uses a pattern matcher to match any action ID that starts with "send_rule_to_space_"
app.action(/^send_rule_to_space_(.*)$/, async ({ body, action, ack, respond }) => {
try {
await ack();
logger.debug(`${FILE_NAME}: Space selection action received: ${JSON.stringify(action)}`);
// Extract rule ID and space ID from the action value
const actionValue = action.value;
const parts = actionValue.split('_');
const spaceId = parts.pop(); // Last part is the space ID
const ruleId = actionValue.match(/send_rule_to_space_(.+)_/)[1]; // Extract full UUID
logger.info(`${FILE_NAME}: Selected space ${spaceId} for rule ${ruleId}`);
// Get space info
const spaces = getAllSpaces();
const selectedSpace = spaces.find(s => s.id === spaceId);
if (!selectedSpace) {
logger.error(`${FILE_NAME}: Space not found: ${spaceId}`);
await respond({
text: `Error: Space "${spaceId}" not found in configuration`,
replace_original: false,
response_type: 'ephemeral'
});
return;
}
// Inform user that processing is happening
await respond({
text: `Sending rule ${ruleId} to ${selectedSpace.emoji || ''} ${selectedSpace.name} space...`,
replace_original: false,
response_type: 'ephemeral'
});
// Get the converted rule in Elasticsearch format
const config = {
backend: SIGMA_CLI_CONFIG.backend,
target: SIGMA_CLI_CONFIG.target,
format: SIGMA_CLI_CONFIG.format
};
logger.info(`${FILE_NAME}: Converting rule ${ruleId} for SIEM export to space ${spaceId}`);
const conversionResult = await convertRuleToBackend(ruleId, config);
if (!conversionResult.success) {
logger.error(`${FILE_NAME}: Rule conversion failed: ${conversionResult.message}`);
await respond({
text: `Error: Failed to convert rule for SIEM: ${conversionResult.message}`,
replace_original: false,
response_type: 'ephemeral'
});
return;
}
// Parse the converted rule JSON
let rulePayload;
try {
rulePayload = JSON.parse(conversionResult.output);
// Add required fields if not present
rulePayload.rule_id = rulePayload.rule_id || ruleId;
rulePayload.from = rulePayload.from || "now-360s";
rulePayload.to = rulePayload.to || "now";
rulePayload.interval = rulePayload.interval || "5m";
// Set index pattern from space configuration if available
if (selectedSpace.indexPattern) {
rulePayload.index = Array.isArray(selectedSpace.indexPattern)
? selectedSpace.indexPattern
: [selectedSpace.indexPattern];
logger.debug(`${FILE_NAME}: Setting index pattern from space config: ${JSON.stringify(rulePayload.index)}`);
}
// Make sure required fields are present
if (!rulePayload.name) {
rulePayload.name = conversionResult.rule?.title || `Sigma Rule ${ruleId}`;
}
if (!rulePayload.description) {
rulePayload.description = conversionResult.rule?.description ||
`Converted from Sigma rule: ${ruleId}`;
}
if (!rulePayload.risk_score) {
// Map Sigma level to risk score
const levelMap = {
'critical': 90,
'high': 73,
'medium': 50,
'low': 25,
'informational': 10
};
rulePayload.risk_score = levelMap[conversionResult.rule?.level] || 50;
}
if (!rulePayload.severity) {
rulePayload.severity = conversionResult.rule?.level || 'medium';
}
if (!rulePayload.enabled) {
rulePayload.enabled = true;
}
} catch (parseError) {
logger.error(`${FILE_NAME}: Failed to parse converted rule JSON: ${parseError.message}`);
await respond({
text: `Error: The converted rule is not valid JSON: ${parseError.message}`,
replace_original: false,
response_type: 'ephemeral'
});
return;
}
// Send the rule to the selected Elasticsearch space
try {
const result = await sendRuleToSiem(rulePayload, spaceId);
if (result.success) {
logger.info(`${FILE_NAME}: Successfully sent rule ${ruleId} to space ${spaceId}`);
await respond({
text: `✅ Success! Rule "${rulePayload.name}" has been added to the ${selectedSpace.emoji || ''} ${selectedSpace.name} space in Elasticsearch.`,
replace_original: false,
response_type: 'in_channel'
});
} else {
logger.error(`${FILE_NAME}: Error sending rule to SIEM: ${result.message}`);
await respond({
text: `Error: Failed to add rule to the ${selectedSpace.name} space: ${result.message}`,
replace_original: false,
response_type: 'ephemeral'
});
}
} catch (error) {
await handleError(error, `${FILE_NAME}: send_rule_to_space action`, respond, {
replaceOriginal: false
});
}
} catch (error) {
await handleError(error, `${FILE_NAME}: send_rule_to_space action`, respond, {
replaceOriginal: false
});
}
});
};
module.exports = {
registerActionHandlers,
processRuleDetails,

View file

@ -6,7 +6,7 @@
const { searchSigmaRules } = require('../../services/sigma/sigma_search_service');
const logger = require('../../utils/logger');
const { handleError } = require('../../utils/error_handler');
const { getSearchResultBlocks } = require('../../blocks/sigma_search_results_block');
const { getSearchResultBlocks } = require('../../blocks/sigma/sigma_search_results_block');
const { getFileName } = require('../../utils/file_utils');
const FILE_NAME = getFileName(__filename);

View file

@ -12,32 +12,76 @@ const FILE_NAME = 'elastic_api_service.js';
/**
* Get Elasticsearch configuration with credentials
*
* @param {string} spaceId - Optional space ID to get configuration for
* @returns {Object} Configuration object with URL and credentials
*/
const getElasticConfig = () => {
return {
url: ELASTICSEARCH_CONFIG.apiEndpoint.split('/api/')[0] || process.env.ELASTIC_URL,
username: ELASTICSEARCH_CONFIG.credentials.split(':')[0] || process.env.ELASTIC_USERNAME,
password: ELASTICSEARCH_CONFIG.credentials.split(':')[1] || process.env.ELASTIC_PASSWORD,
const getElasticConfig = (spaceId = null) => {
// Default config
const config = {
protocol: ELASTICSEARCH_CONFIG.protocol || "http",
hosts: ELASTICSEARCH_CONFIG.hosts || ["localhost:9200"],
username: ELASTICSEARCH_CONFIG.username || process.env.ELASTIC_USERNAME,
password: ELASTICSEARCH_CONFIG.password || process.env.ELASTIC_PASSWORD,
apiEndpoint: ELASTICSEARCH_CONFIG.apiEndpoint
};
// If space ID provided, find the specific space
if (spaceId) {
const space = ELASTICSEARCH_CONFIG.spaces.find(s => s.id === spaceId);
if (space) {
// Apply space-specific configuration overrides if they exist
return {
...config,
space: space
};
}
}
return config;
};
/**
* Send a rule to Elasticsearch SIEM
* Get all configured Elasticsearch spaces
*
* @returns {Array} List of all configured spaces
*/
const getAllSpaces = () => {
return ELASTICSEARCH_CONFIG.spaces || [];
};
/**
* Send a rule to Elasticsearch SIEM in a specific space
*
* @param {Object} rulePayload - The rule payload to send to Elasticsearch
* @param {string} spaceId - The ID of the space to send the rule to
* @returns {Promise<Object>} - Object containing success status and response/error information
*/
const sendRuleToSiem = async (rulePayload) => {
logger.info(`${FILE_NAME}: Sending rule to Elasticsearch SIEM`);
const sendRuleToSiem = async (rulePayload, spaceId = 'default') => {
logger.info(`${FILE_NAME}: Sending rule to Elasticsearch SIEM in space: ${spaceId}`);
try {
const elasticConfig = getElasticConfig();
const apiUrl = elasticConfig.apiEndpoint;
const elasticConfig = getElasticConfig(spaceId);
const baseApiUrl = elasticConfig.apiEndpoint;
// Construct space-specific URL if needed
let apiUrl = baseApiUrl;
if (spaceId && spaceId !== 'default') {
// Insert space ID into URL: http://localhost:5601/api/detection_engine/rules
// becomes http://localhost:5601/s/space-id/api/detection_engine/rules
const urlParts = baseApiUrl.split('/api/');
apiUrl = `${urlParts[0]}/s/${spaceId}/api/${urlParts[1]}`;
}
logger.debug(`${FILE_NAME}: Using Elasticsearch API URL: ${apiUrl}`);
// Add index pattern to rule if provided by space config
if (elasticConfig.space && elasticConfig.space.indexPattern && !rulePayload.index) {
rulePayload.index = Array.isArray(elasticConfig.space.indexPattern)
? elasticConfig.space.indexPattern
: [elasticConfig.space.indexPattern];
logger.debug(`${FILE_NAME}: Adding index pattern to rule: ${JSON.stringify(rulePayload.index)}`);
}
// Send the request to Elasticsearch
const response = await axios({
method: 'post',
@ -55,18 +99,19 @@ const sendRuleToSiem = async (rulePayload) => {
// Process the response
if (response.status >= 200 && response.status < 300) {
logger.info(`${FILE_NAME}: Successfully sent rule to SIEM`);
logger.info(`${FILE_NAME}: Successfully sent rule to SIEM in space: ${spaceId}`);
return {
success: true,
status: response.status,
data: response.data
data: response.data,
space: elasticConfig.space
};
} else {
logger.error(`${FILE_NAME}: Error sending rule to SIEM. Status: ${response.status}, Response: ${JSON.stringify(response.data)}`);
return {
success: false,
status: response.status,
message: `Failed to add rule to SIEM. Status: ${response.status}`,
message: `Failed to add rule to SIEM in space ${spaceId}. Status: ${response.status}`,
data: response.data
};
}
@ -92,6 +137,7 @@ const sendRuleToSiem = async (rulePayload) => {
* @param {Object} options - Request options
* @param {string} options.method - HTTP method (get, post, put, delete)
* @param {string} options.endpoint - API endpoint (appended to base URL)
* @param {string} options.spaceId - Optional space ID
* @param {Object} options.data - Request payload
* @param {Object} options.params - URL parameters
* @param {Object} options.headers - Additional headers
@ -99,14 +145,24 @@ const sendRuleToSiem = async (rulePayload) => {
*/
const makeElasticRequest = async (options) => {
try {
const elasticConfig = getElasticConfig();
const baseUrl = elasticConfig.url;
const elasticConfig = getElasticConfig(options.spaceId);
const baseUrl = elasticConfig.protocol + '://' + elasticConfig.hosts[0];
// Build the full URL - use provided endpoint or default API endpoint
const url = options.endpoint ?
let url = options.endpoint ?
`${baseUrl}${options.endpoint.startsWith('/') ? '' : '/'}${options.endpoint}` :
elasticConfig.apiEndpoint;
// Handle space in URL if needed
if (options.spaceId && options.spaceId !== 'default') {
// Insert space ID into URL: http://localhost:5601/api/detection_engine/rules
// becomes http://localhost:5601/s/space-id/api/detection_engine/rules
if (url.includes('/api/')) {
const urlParts = url.split('/api/');
url = `${urlParts[0]}/s/${options.spaceId}/api/${urlParts[1]}`;
}
}
logger.debug(`${FILE_NAME}: Making ${options.method} request to: ${url}`);
// Set up default headers
@ -133,7 +189,8 @@ const makeElasticRequest = async (options) => {
return {
success: response.status >= 200 && response.status < 300,
status: response.status,
data: response.data
data: response.data,
space: elasticConfig.space
};
} catch (error) {
logger.error(`${FILE_NAME}: Error in Elasticsearch API request: ${error.message}`);
@ -150,5 +207,6 @@ const makeElasticRequest = async (options) => {
module.exports = {
sendRuleToSiem,
makeElasticRequest,
getElasticConfig
getElasticConfig,
getAllSpaces
};