refactor search handler and service into multiple files
This commit is contained in:
parent
b98502284a
commit
1b8ba03c8b
11 changed files with 840 additions and 309 deletions
|
@ -3,7 +3,6 @@
|
||||||
*
|
*
|
||||||
* Main application file for Fylgja Slack bot
|
* Main application file for Fylgja Slack bot
|
||||||
* Initializes the Slack Bolt app with custom ExpressReceiver Registers command handlers
|
* Initializes the Slack Bolt app with custom ExpressReceiver Registers command handlers
|
||||||
* Now supports the universal /fylgja command
|
|
||||||
*/
|
*/
|
||||||
const { App, ExpressReceiver } = require('@slack/bolt');
|
const { App, ExpressReceiver } = require('@slack/bolt');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
|
@ -17,7 +16,7 @@ const FILE_NAME = getFileName(__filename);
|
||||||
const fylgjaCommandHandler = require('./handlers/fylgja_command_handler');
|
const fylgjaCommandHandler = require('./handlers/fylgja_command_handler');
|
||||||
|
|
||||||
const sigmaDetailsHandler = require('./handlers/sigma/sigma_details_handler');
|
const sigmaDetailsHandler = require('./handlers/sigma/sigma_details_handler');
|
||||||
const sigmaSearchHandler = require('./handlers/sigma/sigma_search_handler');
|
const sigmaSearchHandler = require('./handlers/sigma/sigma_search_entry_handler');
|
||||||
const sigmaCreateHandler = require('./handlers/sigma/sigma_create_handler');
|
const sigmaCreateHandler = require('./handlers/sigma/sigma_create_handler');
|
||||||
// Import the action registry
|
// Import the action registry
|
||||||
const sigmaActionRegistry = require('./handlers/sigma/actions/sigma_action_registry');
|
const sigmaActionRegistry = require('./handlers/sigma/actions/sigma_action_registry');
|
||||||
|
|
|
@ -10,7 +10,7 @@ const { generateGradientLogo } = require('./utils/cli_logo');
|
||||||
const outputManager = require('./cli_output_manager');
|
const outputManager = require('./cli_output_manager');
|
||||||
|
|
||||||
// Import command handlers
|
// Import command handlers
|
||||||
const sigmaSearchHandler = require('../handlers/sigma/sigma_search_handler');
|
const sigmaSearchHandler = require('../handlers/sigma/sigma_search_entry_handler');
|
||||||
const sigmaDetailsHandler = require('../handlers/sigma/sigma_details_handler');
|
const sigmaDetailsHandler = require('../handlers/sigma/sigma_details_handler');
|
||||||
const sigmaStatsHandler = require('../handlers/sigma/sigma_stats_handler');
|
const sigmaStatsHandler = require('../handlers/sigma/sigma_stats_handler');
|
||||||
const sigmaCreateHandler = require('../handlers/sigma/sigma_create_handler');
|
const sigmaCreateHandler = require('../handlers/sigma/sigma_create_handler');
|
||||||
|
@ -258,7 +258,7 @@ async function processCommand(input) {
|
||||||
console.log(`Executing: module=sigma, action=complexSearch, params=[${complexQuery}]`);
|
console.log(`Executing: module=sigma, action=complexSearch, params=[${complexQuery}]`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await sigmaSearchHandler.handleComplexSearch(command, respond);
|
await sigmaSearchHandler.handleCommand(command, respond);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
outputManager.displayError(error.message);
|
outputManager.displayError(error.message);
|
||||||
logger.error(`${FILE_NAME}: Command execution error: ${error.message}`);
|
logger.error(`${FILE_NAME}: Command execution error: ${error.message}`);
|
||||||
|
@ -309,11 +309,8 @@ async function processCommand(input) {
|
||||||
case 'sigma':
|
case 'sigma':
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case 'search':
|
case 'search':
|
||||||
await sigmaSearchHandler.handleCommand(command, respond);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'complexSearch':
|
case 'complexSearch':
|
||||||
await sigmaSearchHandler.handleComplexSearch(command, respond);
|
await sigmaSearchHandler.handleCommand(command, respond);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'details':
|
case 'details':
|
||||||
|
|
|
@ -3,12 +3,16 @@
|
||||||
*
|
*
|
||||||
* Main handler for the /fylgja slash command
|
* Main handler for the /fylgja slash command
|
||||||
* Parses natural language commands and routes to appropriate handlers
|
* Parses natural language commands and routes to appropriate handlers
|
||||||
*/
|
*
|
||||||
|
* This file is a mess. I've been working mostly on the CLI and other Slash Commands in Slack
|
||||||
|
* so development on this command will be put on hold for a bit
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
const logger = require('../utils/logger');
|
const logger = require('../utils/logger');
|
||||||
const { parseCommand } = require('../lang/command_parser');
|
const { parseCommand } = require('../lang/command_parser');
|
||||||
const { handleError } = require('../utils/error_handler');
|
const { handleError } = require('../utils/error_handler');
|
||||||
const { handleCommand: handleSigmaSearch, handleComplexSearch } = require('./sigma/sigma_search_handler');
|
const { handleCommand: handleSigmaSearch, handleComplexSearch } = require('./sigma/sigma_search_entry_handler');
|
||||||
const { handleCommand: handleSigmaDetails } = require('./sigma/sigma_details_handler');
|
const { handleCommand: handleSigmaDetails } = require('./sigma/sigma_details_handler');
|
||||||
const { handleCommand: handleSigmaStats } = require('./sigma/sigma_stats_handler');
|
const { handleCommand: handleSigmaStats } = require('./sigma/sigma_stats_handler');
|
||||||
const { handleCommand: handleSigmaCreate } = require('./sigma/sigma_create_handler');
|
const { handleCommand: handleSigmaCreate } = require('./sigma/sigma_create_handler');
|
||||||
|
|
135
src/handlers/sigma/sigma_basic_search_handler.js
Normal file
135
src/handlers/sigma/sigma_basic_search_handler.js
Normal file
|
@ -0,0 +1,135 @@
|
||||||
|
/**
|
||||||
|
* sigma_basic_search_handler.js
|
||||||
|
*
|
||||||
|
* Handles basic keyword search commands for Sigma rules
|
||||||
|
*/
|
||||||
|
const { searchAndConvertRules } = require('../../services/sigma/sigma_basic_search_service');
|
||||||
|
const logger = require('../../utils/logger');
|
||||||
|
const { handleError } = require('../../utils/error_handler');
|
||||||
|
const { getSearchResultBlocks } = require('../../blocks/sigma/sigma_search_results_block');
|
||||||
|
const { getFileName } = require('../../utils/file_utils');
|
||||||
|
const { extractPaginationParams } = require('../../services/sigma/utils/search_pagination_utils');
|
||||||
|
|
||||||
|
const FILE_NAME = getFileName(__filename);
|
||||||
|
const MAX_RESULTS_THRESHOLD = 99;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the basic sigma-search command
|
||||||
|
* @param {Object} command - Slack command object
|
||||||
|
* @param {Function} respond - Function to send response
|
||||||
|
*/
|
||||||
|
const handleBasicSearch = async (command, respond) => {
|
||||||
|
try {
|
||||||
|
logger.debug(`${FILE_NAME}: Processing basic sigma-search command: ${JSON.stringify(command.text)}`);
|
||||||
|
|
||||||
|
if (!command || !command.text) {
|
||||||
|
logger.warn(`${FILE_NAME}: Empty command received for sigma-search`);
|
||||||
|
await respond('Invalid command. Usage: /sigma-search [keyword]');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract keyword and pagination parameters
|
||||||
|
const { text: keyword, page, pageSize } = extractPaginationParams(command.text);
|
||||||
|
|
||||||
|
if (!keyword) {
|
||||||
|
logger.warn(`${FILE_NAME}: Missing keyword in sigma-search command`);
|
||||||
|
await respond('Invalid command: missing keyword. Usage: /sigma-search [keyword]');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`${FILE_NAME}: Searching for rules with keyword: ${keyword} (page ${page}, size ${pageSize})`);
|
||||||
|
|
||||||
|
await respond({
|
||||||
|
text: 'Searching for rules... This may take a moment.',
|
||||||
|
response_type: 'ephemeral'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Perform the search
|
||||||
|
const searchResult = await searchAndConvertRules(keyword, page, pageSize);
|
||||||
|
|
||||||
|
if (!searchResult.success) {
|
||||||
|
logger.error(`${FILE_NAME}: Search failed: ${searchResult.message}`);
|
||||||
|
await respond({
|
||||||
|
text: `Search failed: ${searchResult.message}`,
|
||||||
|
response_type: 'ephemeral'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get total count for validation
|
||||||
|
const totalCount = searchResult.pagination?.totalResults || 0;
|
||||||
|
|
||||||
|
// Check if search returned too many results
|
||||||
|
if (totalCount > MAX_RESULTS_THRESHOLD) {
|
||||||
|
logger.warn(`${FILE_NAME}: Search for "${keyword}" returned too many results (${totalCount})`);
|
||||||
|
searchResult.tooManyResults = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!searchResult.results || searchResult.results.length === 0) {
|
||||||
|
if (totalCount > 0) {
|
||||||
|
logger.warn(`${FILE_NAME}: No rules found on page ${page} for "${keyword}", but ${totalCount} total matches exist`);
|
||||||
|
await respond({
|
||||||
|
text: `No rules found on page ${page} for "${keyword}". Try a different page or refine your search.`,
|
||||||
|
response_type: 'ephemeral'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
logger.warn(`${FILE_NAME}: No rules found matching "${keyword}"`);
|
||||||
|
await respond({
|
||||||
|
text: `No rules found matching "${keyword}"`,
|
||||||
|
response_type: 'ephemeral'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isCliRequest = command.channel_id === 'cli' || command.channel_name === 'cli';
|
||||||
|
|
||||||
|
if (isCliRequest) {
|
||||||
|
// For CLI, return raw data
|
||||||
|
await respond({
|
||||||
|
responseData: searchResult.results,
|
||||||
|
response_type: 'cli'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// For Slack, generate Block Kit blocks
|
||||||
|
try {
|
||||||
|
let blocks = getSearchResultBlocks(keyword, searchResult.results, searchResult.pagination);
|
||||||
|
|
||||||
|
// Add warning for too many results
|
||||||
|
if (searchResult.tooManyResults) {
|
||||||
|
blocks.splice(1, 0, {
|
||||||
|
"type": "section",
|
||||||
|
"text": {
|
||||||
|
"type": "mrkdwn",
|
||||||
|
"text": `:warning: Your search for "${keyword}" returned ${totalCount} results, which is a lot. Displaying the first page. Consider using a more specific keyword for narrower results.`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine visibility
|
||||||
|
const isEphemeral = totalCount > 20;
|
||||||
|
|
||||||
|
// Send response
|
||||||
|
await respond({
|
||||||
|
blocks: blocks,
|
||||||
|
response_type: isEphemeral ? 'ephemeral' : 'in_channel'
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.debug(`${FILE_NAME}: Response sent successfully`);
|
||||||
|
} catch (blockError) {
|
||||||
|
await handleError(blockError, `${FILE_NAME}: Block generation`, respond, {
|
||||||
|
responseType: 'in_channel',
|
||||||
|
customMessage: `Found ${searchResult.results.length} of ${totalCount} rules matching "${keyword}" (page ${page} of ${searchResult.pagination?.totalPages || 1}). Use /sigma-details [id] to view details.`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
await handleError(error, `${FILE_NAME}: Search command handler`, respond, {
|
||||||
|
responseType: 'ephemeral'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
handleBasicSearch
|
||||||
|
};
|
133
src/handlers/sigma/sigma_complex_search_handler.js
Normal file
133
src/handlers/sigma/sigma_complex_search_handler.js
Normal file
|
@ -0,0 +1,133 @@
|
||||||
|
/**
|
||||||
|
* sigma_complex_search_handler.js
|
||||||
|
*
|
||||||
|
* Handles complex query search commands for Sigma rules
|
||||||
|
*/
|
||||||
|
const { searchAndConvertRulesComplex } = require('../../services/sigma/sigma_complex_search_service');
|
||||||
|
const logger = require('../../utils/logger');
|
||||||
|
const { handleError } = require('../../utils/error_handler');
|
||||||
|
const { getSearchResultBlocks } = require('../../blocks/sigma/sigma_search_results_block');
|
||||||
|
const { getFileName } = require('../../utils/file_utils');
|
||||||
|
const { extractPaginationParams } = require('../../services/sigma/utils/search_pagination_utils');
|
||||||
|
|
||||||
|
const FILE_NAME = getFileName(__filename);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the complex sigma-search command
|
||||||
|
* @param {Object} command - Slack command object
|
||||||
|
* @param {Function} respond - Function to send response
|
||||||
|
*/
|
||||||
|
const handleComplexSearch = async (command, respond) => {
|
||||||
|
try {
|
||||||
|
logger.debug(`${FILE_NAME}: Processing complex search command: ${JSON.stringify(command.text)}`);
|
||||||
|
|
||||||
|
if (!command || !command.text) {
|
||||||
|
logger.warn(`${FILE_NAME}: Empty command received for complex search`);
|
||||||
|
await respond('Invalid command. Usage: /sigma-search where [conditions]');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract query string and pagination parameters
|
||||||
|
const { text: queryString, page, pageSize } = extractPaginationParams(command.text);
|
||||||
|
|
||||||
|
if (!queryString) {
|
||||||
|
logger.warn(`${FILE_NAME}: Missing query in complex search command`);
|
||||||
|
await respond('Invalid command: missing query. Usage: /sigma-search where [conditions]');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`${FILE_NAME}: Performing complex search with query: ${queryString} (page ${page}, size ${pageSize})`);
|
||||||
|
|
||||||
|
await respond({
|
||||||
|
text: 'Processing complex search query... This may take a moment.',
|
||||||
|
response_type: 'ephemeral'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Perform the complex search
|
||||||
|
const searchResult = await searchAndConvertRulesComplex(queryString, page, pageSize);
|
||||||
|
|
||||||
|
if (!searchResult.success) {
|
||||||
|
logger.error(`${FILE_NAME}: Complex search failed: ${searchResult.message}`);
|
||||||
|
await respond({
|
||||||
|
text: `Search failed: ${searchResult.message}`,
|
||||||
|
response_type: 'ephemeral'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we have results
|
||||||
|
if (!searchResult.results || searchResult.results.length === 0) {
|
||||||
|
logger.warn(`${FILE_NAME}: No rules found matching complex query criteria`);
|
||||||
|
await respond({
|
||||||
|
text: `No rules found matching the specified criteria.`,
|
||||||
|
response_type: 'ephemeral'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this is a CLI request
|
||||||
|
const isCliRequest = command.channel_id === 'cli' || command.channel_name === 'cli';
|
||||||
|
|
||||||
|
if (isCliRequest) {
|
||||||
|
// For CLI, return just the raw data
|
||||||
|
await respond({
|
||||||
|
responseData: searchResult.results,
|
||||||
|
response_type: 'cli'
|
||||||
|
});
|
||||||
|
logger.info(`${FILE_NAME}: CLI response sent with ${searchResult.results.length} results`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For Slack, generate blocks for results
|
||||||
|
try {
|
||||||
|
// Generate standard search result blocks
|
||||||
|
let blocks = getSearchResultBlocks(
|
||||||
|
`Complex Query: ${queryString}`,
|
||||||
|
searchResult.results,
|
||||||
|
searchResult.pagination
|
||||||
|
);
|
||||||
|
|
||||||
|
// Customize header for complex search
|
||||||
|
if (blocks && blocks.length > 0) {
|
||||||
|
blocks[0] = {
|
||||||
|
type: "header",
|
||||||
|
text: {
|
||||||
|
type: "plain_text",
|
||||||
|
text: `Sigma Rule Search Results - Query`,
|
||||||
|
emoji: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add query description
|
||||||
|
blocks.splice(1, 0, {
|
||||||
|
type: "section",
|
||||||
|
text: {
|
||||||
|
type: "mrkdwn",
|
||||||
|
text: `*Query:* \`${queryString}\``
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send response
|
||||||
|
await respond({
|
||||||
|
blocks: blocks,
|
||||||
|
response_type: 'ephemeral'
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info(`${FILE_NAME}: Complex search Slack response sent successfully with ${searchResult.results.length} results`);
|
||||||
|
} catch (blockError) {
|
||||||
|
await handleError(blockError, `${FILE_NAME}: Complex search block generation`, respond, {
|
||||||
|
responseType: 'ephemeral',
|
||||||
|
customMessage: `Error generating results view: ${blockError.message}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
await handleError(error, `${FILE_NAME}: Complex search handler`, respond, {
|
||||||
|
responseType: 'ephemeral'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
handleComplexSearch
|
||||||
|
};
|
73
src/handlers/sigma/sigma_search_entry_handler.js
Normal file
73
src/handlers/sigma/sigma_search_entry_handler.js
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
/**
|
||||||
|
* sigma_search_entry_handler.js
|
||||||
|
*
|
||||||
|
* Entry point for all sigma search commands
|
||||||
|
* Determines whether to use basic or complex search
|
||||||
|
*/
|
||||||
|
const { handleBasicSearch } = require('./sigma_basic_search_handler');
|
||||||
|
const { handleComplexSearch } = require('./sigma_complex_search_handler');
|
||||||
|
const logger = require('../../utils/logger');
|
||||||
|
const { getFileName } = require('../../utils/file_utils');
|
||||||
|
|
||||||
|
const FILE_NAME = getFileName(__filename);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detects if a command is a complex search query
|
||||||
|
* @param {Object} command - Slack command object
|
||||||
|
* @returns {boolean} True if complex search
|
||||||
|
*/
|
||||||
|
function isComplexSearch(command) {
|
||||||
|
if (!command || !command.text) return false;
|
||||||
|
|
||||||
|
const text = command.text.trim().toLowerCase();
|
||||||
|
|
||||||
|
// Check for complex query indicators
|
||||||
|
return text.startsWith('where ') ||
|
||||||
|
text.includes(' and ') ||
|
||||||
|
text.includes(' or ') ||
|
||||||
|
/title:/.test(text) ||
|
||||||
|
/logsource\./.test(text) ||
|
||||||
|
/tags:/.test(text) ||
|
||||||
|
/contains\(/.test(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main entry point for sigma search commands
|
||||||
|
* Routes to appropriate handler based on query type
|
||||||
|
* @param {Object} command - Slack command object
|
||||||
|
* @param {Function} respond - Function to send response
|
||||||
|
*/
|
||||||
|
const handleCommand = async (command, respond) => {
|
||||||
|
try {
|
||||||
|
logger.debug(`${FILE_NAME}: Processing sigma search command: ${command?.text || 'empty'}`);
|
||||||
|
|
||||||
|
if (!command) {
|
||||||
|
logger.error(`${FILE_NAME}: No command object provided`);
|
||||||
|
await respond({
|
||||||
|
text: 'Error processing command: Invalid command format',
|
||||||
|
response_type: 'ephemeral'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine search type and route to appropriate handler
|
||||||
|
if (isComplexSearch(command)) {
|
||||||
|
logger.info(`${FILE_NAME}: Detected complex search query, routing to complex handler`);
|
||||||
|
await handleComplexSearch(command, respond);
|
||||||
|
} else {
|
||||||
|
logger.info(`${FILE_NAME}: Detected basic keyword search, routing to basic handler`);
|
||||||
|
await handleBasicSearch(command, respond);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`${FILE_NAME}: Error in search entry handler: ${error.message}`);
|
||||||
|
await respond({
|
||||||
|
text: `Error processing search command: ${error.message}`,
|
||||||
|
response_type: 'ephemeral'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
handleCommand,
|
||||||
|
isComplexSearch
|
||||||
|
};
|
|
@ -1,299 +0,0 @@
|
||||||
/**
|
|
||||||
* sigma_search_handler.js
|
|
||||||
*
|
|
||||||
* Handles Sigma rule search requests from Slack commands
|
|
||||||
*/
|
|
||||||
|
|
||||||
const { searchSigmaRules, searchSigmaRulesComplex, searchAndConvertRules } = require('../../services/sigma/sigma_search_service');
|
|
||||||
const logger = require('../../utils/logger');
|
|
||||||
const { handleError } = require('../../utils/error_handler');
|
|
||||||
const { getSearchResultBlocks } = require('../../blocks/sigma/sigma_search_results_block');
|
|
||||||
const { getFileName } = require('../../utils/file_utils');
|
|
||||||
const FILE_NAME = getFileName(__filename);
|
|
||||||
|
|
||||||
const MAX_RESULTS_PER_PAGE = 10;
|
|
||||||
const MAX_RESULTS_THRESHOLD = 99;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle the sigma-search command for Sigma rules
|
|
||||||
* Searches for rules based on keywords and displays results with pagination
|
|
||||||
*
|
|
||||||
* @param {Object} command - The Slack command object
|
|
||||||
* @param {Function} respond - Function to send response back to Slack
|
|
||||||
*/
|
|
||||||
const handleCommand = async (command, respond) => {
|
|
||||||
try {
|
|
||||||
logger.debug(`${FILE_NAME}: Processing sigma-search command: ${JSON.stringify(command.text)}`);
|
|
||||||
|
|
||||||
if (!command || !command.text) {
|
|
||||||
logger.warn(`${FILE_NAME}: Empty command received for sigma-search`);
|
|
||||||
await respond('Invalid command. Usage: /sigma-search [keyword]');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract search keyword and check for pagination parameters
|
|
||||||
let keyword = command.text.trim();
|
|
||||||
let page = 1;
|
|
||||||
let pageSize = MAX_RESULTS_PER_PAGE;
|
|
||||||
|
|
||||||
// Check for pagination format: keyword page=X
|
|
||||||
const pagingMatch = keyword.match(/(.+)\s+page=(\d+)$/i);
|
|
||||||
if (pagingMatch) {
|
|
||||||
keyword = pagingMatch[1].trim();
|
|
||||||
page = parseInt(pagingMatch[2], 10) || 1;
|
|
||||||
logger.debug(`${FILE_NAME}: Detected pagination request: "${keyword}" page ${page}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for page size format: keyword limit=X
|
|
||||||
const limitMatch = keyword.match(/(.+)\s+limit=(\d+)$/i);
|
|
||||||
if (limitMatch) {
|
|
||||||
keyword = limitMatch[1].trim();
|
|
||||||
pageSize = parseInt(limitMatch[2], 10) || MAX_RESULTS_PER_PAGE;
|
|
||||||
// Ensure the page size is within reasonable limits
|
|
||||||
pageSize = Math.min(Math.max(pageSize, 1), 100);
|
|
||||||
logger.debug(`${FILE_NAME}: Detected page size request: "${keyword}" limit ${pageSize}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!keyword) {
|
|
||||||
logger.warn(`${FILE_NAME}: Missing keyword in sigma-search command`);
|
|
||||||
await respond('Invalid command: missing keyword. Usage: /sigma-search [keyword]');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info(`${FILE_NAME}: Searching for rules with keyword: ${keyword} (page ${page}, size ${pageSize})`);
|
|
||||||
logger.debug(`${FILE_NAME}: Search keyword length: ${keyword.length}`);
|
|
||||||
|
|
||||||
await respond({
|
|
||||||
text: 'Searching for rules... This may take a moment.',
|
|
||||||
response_type: 'ephemeral'
|
|
||||||
});
|
|
||||||
|
|
||||||
// Search for rules using the service function with pagination
|
|
||||||
const searchResult = await searchAndConvertRules(keyword, page, pageSize);
|
|
||||||
logger.debug(`${FILE_NAME}: Search result status: ${searchResult.success}`);
|
|
||||||
logger.debug(`${FILE_NAME}: Found ${searchResult.results?.length || 0} results out of ${searchResult.pagination?.totalResults || 0} total matches`);
|
|
||||||
logger.debug(`${FILE_NAME}: About to generate blocks for search results`);
|
|
||||||
|
|
||||||
if (!searchResult.success) {
|
|
||||||
logger.error(`${FILE_NAME}: Search failed: ${searchResult.message}`);
|
|
||||||
await respond({
|
|
||||||
text: `Search failed: ${searchResult.message}`,
|
|
||||||
response_type: 'ephemeral'
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get total count for validation
|
|
||||||
const totalCount = searchResult.pagination?.totalResults || 0;
|
|
||||||
|
|
||||||
// Check if search returned too many results
|
|
||||||
if (totalCount > MAX_RESULTS_THRESHOLD) {
|
|
||||||
logger.warn(`${FILE_NAME}: Search for "${keyword}" returned too many results (${totalCount}), displaying first page with warning`);
|
|
||||||
// Continue processing but add a notification
|
|
||||||
searchResult.tooManyResults = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!searchResult.results || searchResult.results.length === 0) {
|
|
||||||
if (totalCount > 0) {
|
|
||||||
logger.warn(`${FILE_NAME}: No rules found on page ${page} for "${keyword}", but ${totalCount} total matches exist`);
|
|
||||||
await respond({
|
|
||||||
text: `No rules found on page ${page} for "${keyword}". Try a different page or refine your search.`,
|
|
||||||
response_type: 'ephemeral'
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
logger.warn(`${FILE_NAME}: No rules found matching "${keyword}"`);
|
|
||||||
await respond({
|
|
||||||
text: `No rules found matching "${keyword}"`,
|
|
||||||
response_type: 'ephemeral'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isCliRequest = command.channel_id === 'cli' || command.channel_name === 'cli';
|
|
||||||
|
|
||||||
if (isCliRequest) {
|
|
||||||
// For CLI, just return the raw data
|
|
||||||
await respond({
|
|
||||||
responseData: searchResult.results,
|
|
||||||
response_type: 'cli'
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// For Slack, generate and return Block Kit blocks
|
|
||||||
let blocks;
|
|
||||||
try {
|
|
||||||
logger.debug(`${FILE_NAME}: Calling getSearchResultBlocks with ${searchResult.results.length} results`);
|
|
||||||
// If we have too many results, add a warning block at the beginning
|
|
||||||
if (searchResult.tooManyResults) {
|
|
||||||
blocks = getSearchResultBlocks(keyword, searchResult.results, searchResult.pagination);
|
|
||||||
// Insert warning at the beginning of blocks (after the header)
|
|
||||||
blocks.splice(1, 0, {
|
|
||||||
"type": "section",
|
|
||||||
"text": {
|
|
||||||
"type": "mrkdwn",
|
|
||||||
"text": `:warning: Your search for "${keyword}" returned ${totalCount} results, which is a lot. Displaying the first page. Consider using a more specific keyword for narrower results.`
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
blocks = getSearchResultBlocks(keyword, searchResult.results, searchResult.pagination);
|
|
||||||
}
|
|
||||||
logger.debug(`${FILE_NAME}: Successfully generated ${blocks?.length || 0} blocks`);
|
|
||||||
|
|
||||||
// Determine if this should be visible to everyone or just the user
|
|
||||||
const isEphemeral = totalCount > 20;
|
|
||||||
|
|
||||||
// Add debug log before sending response
|
|
||||||
logger.debug(`${FILE_NAME}: About to send response with ${blocks?.length || 0} blocks`);
|
|
||||||
|
|
||||||
// Respond with the search results
|
|
||||||
// Respond with the search results
|
|
||||||
await respond({
|
|
||||||
blocks: blocks,
|
|
||||||
response_type: isEphemeral ? 'ephemeral' : 'in_channel'
|
|
||||||
});
|
|
||||||
} catch (blockError) {
|
|
||||||
// Use error handler for block generation errors
|
|
||||||
await handleError(blockError, `${FILE_NAME}: Block generation`, respond, {
|
|
||||||
responseType: 'in_channel',
|
|
||||||
customMessage: `Found ${searchResult.results.length} of ${totalCount} rules matching "${keyword}" (page ${page} of ${searchResult.pagination?.totalPages || 1}). Use /sigma-details [id] to view details.`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add debug log after sending response
|
|
||||||
logger.debug(`${FILE_NAME}: Response sent successfully`);
|
|
||||||
} catch (error) {
|
|
||||||
// Use error handler for unexpected errors
|
|
||||||
await handleError(error, `${FILE_NAME}: Search command handler`, respond, {
|
|
||||||
responseType: 'ephemeral'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle the complex search command for Sigma rules
|
|
||||||
* Processes advanced search queries with multiple conditions
|
|
||||||
*
|
|
||||||
* @param {Object} command - The Slack command object
|
|
||||||
* @param {Function} respond - Function to send response back to Slack
|
|
||||||
*/
|
|
||||||
const handleComplexSearch = async (command, respond) => {
|
|
||||||
try {
|
|
||||||
logger.debug(`${FILE_NAME}: Processing complex search command: ${JSON.stringify(command.text)}`);
|
|
||||||
|
|
||||||
if (!command || !command.text) {
|
|
||||||
logger.warn(`${FILE_NAME}: Empty command received for complex search`);
|
|
||||||
await respond('Invalid command. Usage: /sigma-search where [conditions]');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract query string
|
|
||||||
let queryString = command.text.trim();
|
|
||||||
let page = 1;
|
|
||||||
let pageSize = MAX_RESULTS_PER_PAGE;
|
|
||||||
|
|
||||||
// Check for pagination format: query page=X
|
|
||||||
const pagingMatch = queryString.match(/(.+)\s+page=(\d+)$/i);
|
|
||||||
if (pagingMatch) {
|
|
||||||
queryString = pagingMatch[1].trim();
|
|
||||||
page = parseInt(pagingMatch[2], 10) || 1;
|
|
||||||
logger.debug(`${FILE_NAME}: Detected pagination request in complex search: page ${page}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for page size format: query limit=X
|
|
||||||
const limitMatch = queryString.match(/(.+)\s+limit=(\d+)$/i);
|
|
||||||
if (limitMatch) {
|
|
||||||
queryString = limitMatch[1].trim();
|
|
||||||
pageSize = parseInt(limitMatch[2], 10) || MAX_RESULTS_PER_PAGE;
|
|
||||||
// Ensure the page size is within reasonable limits
|
|
||||||
pageSize = Math.min(Math.max(pageSize, 1), 100);
|
|
||||||
logger.debug(`${FILE_NAME}: Detected page size request in complex search: limit ${pageSize}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info(`${FILE_NAME}: Performing complex search with query: ${queryString}`);
|
|
||||||
|
|
||||||
await respond({
|
|
||||||
text: 'Processing complex search query... This may take a moment.',
|
|
||||||
response_type: 'ephemeral'
|
|
||||||
});
|
|
||||||
|
|
||||||
// Perform the complex search
|
|
||||||
const searchResult = await searchSigmaRulesComplex(queryString, page, pageSize);
|
|
||||||
|
|
||||||
if (!searchResult.success) {
|
|
||||||
logger.error(`${FILE_NAME}: Complex search failed: ${searchResult.message}`);
|
|
||||||
await respond({
|
|
||||||
text: `Search failed: ${searchResult.message}`,
|
|
||||||
response_type: 'ephemeral'
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if we have results
|
|
||||||
if (!searchResult.results || searchResult.results.length === 0) {
|
|
||||||
logger.warn(`${FILE_NAME}: No rules found matching complex query criteria`);
|
|
||||||
await respond({
|
|
||||||
text: `No rules found matching the specified criteria.`,
|
|
||||||
response_type: 'ephemeral'
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate blocks with pagination support
|
|
||||||
let blocks;
|
|
||||||
try {
|
|
||||||
// Use the standard search result blocks but with a modified header
|
|
||||||
blocks = getSearchResultBlocks(
|
|
||||||
`Complex Query: ${queryString}`,
|
|
||||||
searchResult.results,
|
|
||||||
searchResult.pagination
|
|
||||||
);
|
|
||||||
|
|
||||||
// Replace the header to indicate it's a complex search
|
|
||||||
// TODO: should be moved to dedicated block file
|
|
||||||
if (blocks && blocks.length > 0) {
|
|
||||||
blocks[0] = {
|
|
||||||
type: "header",
|
|
||||||
text: {
|
|
||||||
type: "plain_text",
|
|
||||||
text: `Sigma Rule Search Results - Query`,
|
|
||||||
emoji: true
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add a description of the search criteria
|
|
||||||
blocks.splice(1, 0, {
|
|
||||||
type: "section",
|
|
||||||
text: {
|
|
||||||
type: "mrkdwn",
|
|
||||||
text: `*Query:* \`${queryString}\``
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (blockError) {
|
|
||||||
await handleError(blockError, `${FILE_NAME}: Complex search block generation`, respond, {
|
|
||||||
responseType: 'ephemeral',
|
|
||||||
customMessage: `Error generating results view: ${blockError.message}`
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Respond with the search results
|
|
||||||
await respond({
|
|
||||||
blocks: blocks,
|
|
||||||
responseData: searchResult.results,
|
|
||||||
response_type: 'ephemeral' // Complex searches are usually more specific to the user
|
|
||||||
});
|
|
||||||
|
|
||||||
logger.info(`${FILE_NAME}: Complex search response sent successfully with ${searchResult.results.length} results`);
|
|
||||||
} catch (error) {
|
|
||||||
await handleError(error, `${FILE_NAME}: Complex search handler`, respond, {
|
|
||||||
responseType: 'ephemeral'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
handleCommand,
|
|
||||||
handleComplexSearch
|
|
||||||
};
|
|
150
src/services/sigma/sigma_basic_search_service.js
Normal file
150
src/services/sigma/sigma_basic_search_service.js
Normal file
|
@ -0,0 +1,150 @@
|
||||||
|
/**
|
||||||
|
* sigma_basic_search_service.js
|
||||||
|
*
|
||||||
|
* Service for basic keyword searches of Sigma rules
|
||||||
|
*/
|
||||||
|
const { searchRules } = require('../../sigma_db/queries');
|
||||||
|
const { convertSigmaRule } = require('./sigma_converter_service');
|
||||||
|
const logger = require('../../utils/logger');
|
||||||
|
const { getFileName } = require('../../utils/file_utils');
|
||||||
|
const { validateKeyword, validatePagination } = require('./utils/search_validation_utils');
|
||||||
|
const { createPaginationInfo, createEmptyResponse } = require('./utils/search_pagination_utils');
|
||||||
|
|
||||||
|
const FILE_NAME = getFileName(__filename);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Searches for Sigma rules by keyword
|
||||||
|
* @param {string} keyword - Keyword to search for
|
||||||
|
* @param {number} page - Page number (1-based)
|
||||||
|
* @param {number} pageSize - Results per page
|
||||||
|
* @returns {Promise<Object>} Search results with pagination
|
||||||
|
*/
|
||||||
|
async function searchSigmaRules(keyword, page = 1, pageSize = 10) {
|
||||||
|
// Validate keyword
|
||||||
|
const keywordValidation = validateKeyword(keyword);
|
||||||
|
if (!keywordValidation.isValid) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: keywordValidation.message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate pagination
|
||||||
|
const { page: validatedPage, pageSize: validatedPageSize, offset } =
|
||||||
|
validatePagination(page, pageSize);
|
||||||
|
|
||||||
|
logger.info(`${FILE_NAME}: Searching for Sigma rules with keyword: "${keywordValidation.trimmedKeyword}" (page ${validatedPage}, size ${validatedPageSize})`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Perform the database search
|
||||||
|
const searchResult = await searchRules(keywordValidation.trimmedKeyword, validatedPageSize, offset);
|
||||||
|
|
||||||
|
// Extract results and total count
|
||||||
|
let allResults = [];
|
||||||
|
let totalCount = 0;
|
||||||
|
|
||||||
|
if (searchResult) {
|
||||||
|
if (Array.isArray(searchResult)) {
|
||||||
|
allResults = searchResult;
|
||||||
|
totalCount = searchResult.length;
|
||||||
|
} else if (typeof searchResult === 'object') {
|
||||||
|
if (Array.isArray(searchResult.results)) {
|
||||||
|
allResults = searchResult.results;
|
||||||
|
totalCount = searchResult.totalCount || 0;
|
||||||
|
} else if (searchResult.totalCount !== undefined) {
|
||||||
|
totalCount = searchResult.totalCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(`${FILE_NAME}: Found ${allResults.length} results of ${totalCount} total`);
|
||||||
|
|
||||||
|
// Handle no results case
|
||||||
|
if (allResults.length === 0 && totalCount === 0) {
|
||||||
|
return createEmptyResponse(
|
||||||
|
`No rules found matching "${keywordValidation.trimmedKeyword}"`,
|
||||||
|
createPaginationInfo(validatedPage, validatedPageSize, 0)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create pagination info
|
||||||
|
const pagination = createPaginationInfo(validatedPage, validatedPageSize, totalCount);
|
||||||
|
|
||||||
|
// Check for valid page number
|
||||||
|
if (offset >= totalCount && totalCount > 0) {
|
||||||
|
return createEmptyResponse(
|
||||||
|
`No results on page ${validatedPage}. Try a previous page.`,
|
||||||
|
pagination
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return successful results
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
results: allResults,
|
||||||
|
count: allResults.length,
|
||||||
|
pagination
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`${FILE_NAME}: Error searching for rules: ${error.message}`);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: `Error searching for rules: ${error.message}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Searches and converts Sigma rules (with full rule details)
|
||||||
|
* @param {string} keyword - Keyword to search for
|
||||||
|
* @param {number} page - Page number (1-based)
|
||||||
|
* @param {number} pageSize - Results per page
|
||||||
|
* @returns {Promise<Object>} Search results with converted rules
|
||||||
|
*/
|
||||||
|
async function searchAndConvertRules(keyword, page = 1, pageSize = 10) {
|
||||||
|
try {
|
||||||
|
// Perform the basic search
|
||||||
|
const searchResult = await searchSigmaRules(keyword, page, pageSize);
|
||||||
|
|
||||||
|
if (!searchResult.success || !searchResult.results || searchResult.results.length === 0) {
|
||||||
|
return searchResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(`${FILE_NAME}: Converting ${searchResult.results.length} search results to full rule objects`);
|
||||||
|
|
||||||
|
// Convert each rule to full rule object
|
||||||
|
const convertedResults = [];
|
||||||
|
for (const result of searchResult.results) {
|
||||||
|
try {
|
||||||
|
const conversionResult = await convertSigmaRule(result.id);
|
||||||
|
if (conversionResult.success && conversionResult.rule) {
|
||||||
|
convertedResults.push(conversionResult.rule);
|
||||||
|
} else {
|
||||||
|
logger.warn(`${FILE_NAME}: Failed to convert rule ${result.id}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`${FILE_NAME}: Error converting rule ${result.id}: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return results with original pagination info
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
results: convertedResults,
|
||||||
|
count: convertedResults.length,
|
||||||
|
originalCount: searchResult.results.length,
|
||||||
|
pagination: searchResult.pagination
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`${FILE_NAME}: Error in searchAndConvertRules: ${error.message}`);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: `Error searching and converting rules: ${error.message}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
searchSigmaRules,
|
||||||
|
searchAndConvertRules
|
||||||
|
};
|
149
src/services/sigma/sigma_complex_search_service.js
Normal file
149
src/services/sigma/sigma_complex_search_service.js
Normal file
|
@ -0,0 +1,149 @@
|
||||||
|
/**
|
||||||
|
* sigma_complex_search_service.js
|
||||||
|
*
|
||||||
|
* Service for complex query searches of Sigma rules
|
||||||
|
*/
|
||||||
|
const { searchRulesComplex } = require('../../sigma_db/queries');
|
||||||
|
const { parseComplexQuery } = require('../../lang/query_parser');
|
||||||
|
const { convertSigmaRule } = require('./sigma_converter_service');
|
||||||
|
const logger = require('../../utils/logger');
|
||||||
|
const { getFileName } = require('../../utils/file_utils');
|
||||||
|
const { validateQueryString, validatePagination } = require('./utils/search_validation_utils');
|
||||||
|
const { createPaginationInfo, createEmptyResponse } = require('./utils/search_pagination_utils');
|
||||||
|
|
||||||
|
const FILE_NAME = getFileName(__filename);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs a complex search for Sigma rules
|
||||||
|
* @param {string} queryString - Complex query string
|
||||||
|
* @param {number} page - Page number (1-based)
|
||||||
|
* @param {number} pageSize - Results per page
|
||||||
|
* @returns {Promise<Object>} Search results with pagination
|
||||||
|
*/
|
||||||
|
async function searchSigmaRulesComplex(queryString, page = 1, pageSize = 10) {
|
||||||
|
// Validate query string
|
||||||
|
const queryValidation = validateQueryString(queryString);
|
||||||
|
if (!queryValidation.isValid) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: queryValidation.message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate pagination
|
||||||
|
const { page: validatedPage, pageSize: validatedPageSize, offset } =
|
||||||
|
validatePagination(page, pageSize);
|
||||||
|
|
||||||
|
logger.info(`${FILE_NAME}: Performing complex search with query: "${queryValidation.trimmedQuery}" (page ${validatedPage}, size ${validatedPageSize})`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Parse the complex query
|
||||||
|
const parsedQuery = parseComplexQuery(queryValidation.trimmedQuery);
|
||||||
|
|
||||||
|
if (!parsedQuery.valid) {
|
||||||
|
logger.warn(`${FILE_NAME}: Invalid complex query: ${parsedQuery.error}`);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: `Invalid query: ${parsedQuery.error}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute complex search
|
||||||
|
const searchResult = await searchRulesComplex(parsedQuery, validatedPageSize, offset);
|
||||||
|
|
||||||
|
// Extract results and total count
|
||||||
|
let allResults = [];
|
||||||
|
let totalCount = 0;
|
||||||
|
|
||||||
|
if (searchResult) {
|
||||||
|
if (Array.isArray(searchResult.results)) {
|
||||||
|
allResults = searchResult.results;
|
||||||
|
totalCount = searchResult.totalCount || 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(`${FILE_NAME}: Found ${allResults.length} results of ${totalCount} total`);
|
||||||
|
|
||||||
|
// Handle no results case
|
||||||
|
if (allResults.length === 0) {
|
||||||
|
return createEmptyResponse(
|
||||||
|
'No rules found matching the complex query criteria',
|
||||||
|
createPaginationInfo(validatedPage, validatedPageSize, totalCount)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create pagination info
|
||||||
|
const pagination = createPaginationInfo(validatedPage, validatedPageSize, totalCount);
|
||||||
|
|
||||||
|
// Return successful results
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
results: allResults,
|
||||||
|
count: allResults.length,
|
||||||
|
query: parsedQuery,
|
||||||
|
pagination
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`${FILE_NAME}: Error in complex search: ${error.message}`);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: `Error performing complex search: ${error.message}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs complex search and converts results to full rule objects
|
||||||
|
* @param {string} queryString - Complex query string
|
||||||
|
* @param {number} page - Page number (1-based)
|
||||||
|
* @param {number} pageSize - Results per page
|
||||||
|
* @returns {Promise<Object>} Search results with converted rules
|
||||||
|
*/
|
||||||
|
async function searchAndConvertRulesComplex(queryString, page = 1, pageSize = 10) {
|
||||||
|
try {
|
||||||
|
// Perform complex search
|
||||||
|
const searchResult = await searchSigmaRulesComplex(queryString, page, pageSize);
|
||||||
|
|
||||||
|
if (!searchResult.success || !searchResult.results || searchResult.results.length === 0) {
|
||||||
|
return searchResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(`${FILE_NAME}: Converting ${searchResult.results.length} complex search results to full rule objects`);
|
||||||
|
|
||||||
|
// Convert each result to full rule object
|
||||||
|
const convertedResults = [];
|
||||||
|
for (const result of searchResult.results) {
|
||||||
|
try {
|
||||||
|
const conversionResult = await convertSigmaRule(result.id);
|
||||||
|
if (conversionResult.success && conversionResult.rule) {
|
||||||
|
convertedResults.push(conversionResult.rule);
|
||||||
|
} else {
|
||||||
|
logger.warn(`${FILE_NAME}: Failed to convert rule ${result.id}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`${FILE_NAME}: Error converting rule ${result.id}: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return results with original pagination info
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
results: convertedResults,
|
||||||
|
count: convertedResults.length,
|
||||||
|
originalCount: searchResult.results.length,
|
||||||
|
query: searchResult.query,
|
||||||
|
pagination: searchResult.pagination
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`${FILE_NAME}: Error in searchAndConvertRulesComplex: ${error.message}`);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: `Error searching and converting rules: ${error.message}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
searchSigmaRulesComplex,
|
||||||
|
searchAndConvertRulesComplex
|
||||||
|
};
|
85
src/services/sigma/utils/search_pagination_utils.js
Normal file
85
src/services/sigma/utils/search_pagination_utils.js
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
/**
|
||||||
|
* search_pagination_utils.js
|
||||||
|
*
|
||||||
|
* Utility functions for handling search pagination
|
||||||
|
*/
|
||||||
|
const logger = require('../../../utils/logger');
|
||||||
|
const { getFileName } = require('../../../utils/file_utils');
|
||||||
|
const FILE_NAME = getFileName(__filename);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a standard pagination info object
|
||||||
|
* @param {number} page - Current page number (1-based)
|
||||||
|
* @param {number} pageSize - Size of each page
|
||||||
|
* @param {number} totalCount - Total number of results
|
||||||
|
* @returns {Object} Standardized pagination object
|
||||||
|
*/
|
||||||
|
function createPaginationInfo(page, pageSize, totalCount) {
|
||||||
|
const totalPages = Math.ceil(totalCount / pageSize);
|
||||||
|
const offset = (page - 1) * pageSize;
|
||||||
|
const hasMore = (offset + pageSize) < totalCount;
|
||||||
|
|
||||||
|
return {
|
||||||
|
currentPage: page,
|
||||||
|
pageSize: pageSize,
|
||||||
|
totalPages: totalPages,
|
||||||
|
totalResults: totalCount,
|
||||||
|
hasMore: hasMore
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts pagination parameters from command text
|
||||||
|
* @param {string} text - The command text to parse
|
||||||
|
* @returns {Object} Extracted parameters and cleaned text
|
||||||
|
*/
|
||||||
|
function extractPaginationParams(text) {
|
||||||
|
if (!text) return { text: '', page: 1, pageSize: 10 };
|
||||||
|
|
||||||
|
let cleanedText = text.trim();
|
||||||
|
let page = 1;
|
||||||
|
let pageSize = 10;
|
||||||
|
|
||||||
|
// Extract page parameter
|
||||||
|
const pagingMatch = cleanedText.match(/(.+)\s+page=(\d+)$/i);
|
||||||
|
if (pagingMatch) {
|
||||||
|
cleanedText = pagingMatch[1].trim();
|
||||||
|
page = parseInt(pagingMatch[2], 10) || 1;
|
||||||
|
logger.debug(`${FILE_NAME}: Extracted page ${page} from command text`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract limit parameter
|
||||||
|
const limitMatch = cleanedText.match(/(.+)\s+limit=(\d+)$/i);
|
||||||
|
if (limitMatch) {
|
||||||
|
cleanedText = limitMatch[1].trim();
|
||||||
|
pageSize = parseInt(limitMatch[2], 10) || 10;
|
||||||
|
logger.debug(`${FILE_NAME}: Extracted limit ${pageSize} from command text`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
text: cleanedText,
|
||||||
|
page,
|
||||||
|
pageSize
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a standard response object for empty result sets
|
||||||
|
* @param {string} message - Message to include in the response
|
||||||
|
* @param {Object} pagination - Pagination info
|
||||||
|
* @returns {Object} Standardized empty response
|
||||||
|
*/
|
||||||
|
function createEmptyResponse(message, pagination) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
results: [],
|
||||||
|
message,
|
||||||
|
pagination
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
createPaginationInfo,
|
||||||
|
extractPaginationParams,
|
||||||
|
createEmptyResponse
|
||||||
|
};
|
105
src/services/sigma/utils/search_validation_utils.js
Normal file
105
src/services/sigma/utils/search_validation_utils.js
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
/**
|
||||||
|
* search_validation_utils.js
|
||||||
|
*
|
||||||
|
* Utility functions for validating search parameters
|
||||||
|
*/
|
||||||
|
const logger = require('../../../utils/logger');
|
||||||
|
const { getFileName } = require('../../../utils/file_utils');
|
||||||
|
const FILE_NAME = getFileName(__filename);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates a search keyword
|
||||||
|
* @param {string} keyword - The keyword to validate
|
||||||
|
* @returns {Object} Validation result object
|
||||||
|
*/
|
||||||
|
function validateKeyword(keyword) {
|
||||||
|
if (!keyword || typeof keyword !== 'string') {
|
||||||
|
logger.warn(`${FILE_NAME}: Cannot search rules: Missing or invalid keyword`);
|
||||||
|
return {
|
||||||
|
isValid: false,
|
||||||
|
message: 'Missing or invalid search keyword'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const trimmedKeyword = keyword.trim();
|
||||||
|
if (trimmedKeyword.length === 0) {
|
||||||
|
logger.warn(`${FILE_NAME}: Cannot search rules: Empty keyword after trimming`);
|
||||||
|
return {
|
||||||
|
isValid: false,
|
||||||
|
message: 'Search keyword cannot be empty'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isValid: true,
|
||||||
|
trimmedKeyword
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates query string for complex search
|
||||||
|
* @param {string} queryString - The query string to validate
|
||||||
|
* @returns {Object} Validation result object
|
||||||
|
*/
|
||||||
|
function validateQueryString(queryString) {
|
||||||
|
if (!queryString || typeof queryString !== 'string') {
|
||||||
|
logger.warn(`${FILE_NAME}: Cannot perform complex search: Missing or invalid query string`);
|
||||||
|
return {
|
||||||
|
isValid: false,
|
||||||
|
message: 'Missing or invalid complex query'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const trimmedQuery = queryString.trim();
|
||||||
|
if (trimmedQuery.length === 0) {
|
||||||
|
logger.warn(`${FILE_NAME}: Cannot perform complex search: Empty query after trimming`);
|
||||||
|
return {
|
||||||
|
isValid: false,
|
||||||
|
message: 'Complex query cannot be empty'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isValid: true,
|
||||||
|
trimmedQuery
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates pagination parameters
|
||||||
|
* @param {number} page - Page number (1-based)
|
||||||
|
* @param {number} pageSize - Number of results per page
|
||||||
|
* @param {number} maxPageSize - Maximum allowed page size
|
||||||
|
* @returns {Object} Validated pagination parameters
|
||||||
|
*/
|
||||||
|
function validatePagination(page = 1, pageSize = 10, maxPageSize = 100) {
|
||||||
|
let validatedPage = page;
|
||||||
|
let validatedPageSize = pageSize;
|
||||||
|
|
||||||
|
if (typeof validatedPage !== 'number' || validatedPage < 1) {
|
||||||
|
logger.warn(`${FILE_NAME}: Invalid page number: ${page}, defaulting to 1`);
|
||||||
|
validatedPage = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof validatedPageSize !== 'number' || validatedPageSize < 1) {
|
||||||
|
logger.warn(`${FILE_NAME}: Invalid page size: ${pageSize}, defaulting to 10`);
|
||||||
|
validatedPageSize = 10;
|
||||||
|
} else if (validatedPageSize > maxPageSize) {
|
||||||
|
logger.warn(`${FILE_NAME}: Page size ${pageSize} exceeds max ${maxPageSize}, limiting to ${maxPageSize}`);
|
||||||
|
validatedPageSize = maxPageSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
const offset = (validatedPage - 1) * validatedPageSize;
|
||||||
|
|
||||||
|
return {
|
||||||
|
page: validatedPage,
|
||||||
|
pageSize: validatedPageSize,
|
||||||
|
offset
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
validateKeyword,
|
||||||
|
validateQueryString,
|
||||||
|
validatePagination
|
||||||
|
};
|
Loading…
Add table
Add a link
Reference in a new issue