refactor search handler and service into multiple files

This commit is contained in:
Charlotte Croce 2025-04-20 22:02:02 -04:00
parent b98502284a
commit 1b8ba03c8b
11 changed files with 840 additions and 309 deletions

View file

@ -3,7 +3,6 @@
*
* Main application file for Fylgja Slack bot
* Initializes the Slack Bolt app with custom ExpressReceiver Registers command handlers
* Now supports the universal /fylgja command
*/
const { App, ExpressReceiver } = require('@slack/bolt');
const fs = require('fs');
@ -17,7 +16,7 @@ const FILE_NAME = getFileName(__filename);
const fylgjaCommandHandler = require('./handlers/fylgja_command_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');
// Import the action registry
const sigmaActionRegistry = require('./handlers/sigma/actions/sigma_action_registry');

View file

@ -10,7 +10,7 @@ const { generateGradientLogo } = require('./utils/cli_logo');
const outputManager = require('./cli_output_manager');
// 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 sigmaStatsHandler = require('../handlers/sigma/sigma_stats_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}]`);
try {
await sigmaSearchHandler.handleComplexSearch(command, respond);
await sigmaSearchHandler.handleCommand(command, respond);
} catch (error) {
outputManager.displayError(error.message);
logger.error(`${FILE_NAME}: Command execution error: ${error.message}`);
@ -309,11 +309,8 @@ async function processCommand(input) {
case 'sigma':
switch (action) {
case 'search':
await sigmaSearchHandler.handleCommand(command, respond);
break;
case 'complexSearch':
await sigmaSearchHandler.handleComplexSearch(command, respond);
await sigmaSearchHandler.handleCommand(command, respond);
break;
case 'details':

View file

@ -3,12 +3,16 @@
*
* Main handler for the /fylgja slash command
* 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 { parseCommand } = require('../lang/command_parser');
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: handleSigmaStats } = require('./sigma/sigma_stats_handler');
const { handleCommand: handleSigmaCreate } = require('./sigma/sigma_create_handler');

View 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
};

View 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
};

View 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
};

View file

@ -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
};

View 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
};

View 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
};

View 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
};

View 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
};