From 1b8ba03c8b7f2c26959b27580e4850f89de17c49 Mon Sep 17 00:00:00 2001 From: Charlotte Croce Date: Sun, 20 Apr 2025 22:02:02 -0400 Subject: [PATCH] refactor search handler and service into multiple files --- src/app.js | 3 +- src/fylgja-cli/cli.js | 9 +- src/handlers/fylgja_command_handler.js | 8 +- .../sigma/sigma_basic_search_handler.js | 135 ++++++++ .../sigma/sigma_complex_search_handler.js | 133 ++++++++ .../sigma/sigma_search_entry_handler.js | 73 +++++ src/handlers/sigma/sigma_search_handler.js | 299 ------------------ .../sigma/sigma_basic_search_service.js | 150 +++++++++ .../sigma/sigma_complex_search_service.js | 149 +++++++++ .../sigma/utils/search_pagination_utils.js | 85 +++++ .../sigma/utils/search_validation_utils.js | 105 ++++++ 11 files changed, 840 insertions(+), 309 deletions(-) create mode 100644 src/handlers/sigma/sigma_basic_search_handler.js create mode 100644 src/handlers/sigma/sigma_complex_search_handler.js create mode 100644 src/handlers/sigma/sigma_search_entry_handler.js delete mode 100644 src/handlers/sigma/sigma_search_handler.js create mode 100644 src/services/sigma/sigma_basic_search_service.js create mode 100644 src/services/sigma/sigma_complex_search_service.js create mode 100644 src/services/sigma/utils/search_pagination_utils.js create mode 100644 src/services/sigma/utils/search_validation_utils.js diff --git a/src/app.js b/src/app.js index 1a17bb3..d8fd2d9 100644 --- a/src/app.js +++ b/src/app.js @@ -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'); diff --git a/src/fylgja-cli/cli.js b/src/fylgja-cli/cli.js index d70ab35..28b3ecf 100644 --- a/src/fylgja-cli/cli.js +++ b/src/fylgja-cli/cli.js @@ -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': diff --git a/src/handlers/fylgja_command_handler.js b/src/handlers/fylgja_command_handler.js index f9b2b37..efa9a54 100644 --- a/src/handlers/fylgja_command_handler.js +++ b/src/handlers/fylgja_command_handler.js @@ -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'); diff --git a/src/handlers/sigma/sigma_basic_search_handler.js b/src/handlers/sigma/sigma_basic_search_handler.js new file mode 100644 index 0000000..b1587eb --- /dev/null +++ b/src/handlers/sigma/sigma_basic_search_handler.js @@ -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 +}; \ No newline at end of file diff --git a/src/handlers/sigma/sigma_complex_search_handler.js b/src/handlers/sigma/sigma_complex_search_handler.js new file mode 100644 index 0000000..6357b77 --- /dev/null +++ b/src/handlers/sigma/sigma_complex_search_handler.js @@ -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 +}; \ No newline at end of file diff --git a/src/handlers/sigma/sigma_search_entry_handler.js b/src/handlers/sigma/sigma_search_entry_handler.js new file mode 100644 index 0000000..fe7fdd1 --- /dev/null +++ b/src/handlers/sigma/sigma_search_entry_handler.js @@ -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 +}; \ No newline at end of file diff --git a/src/handlers/sigma/sigma_search_handler.js b/src/handlers/sigma/sigma_search_handler.js deleted file mode 100644 index fe4d645..0000000 --- a/src/handlers/sigma/sigma_search_handler.js +++ /dev/null @@ -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 -}; \ No newline at end of file diff --git a/src/services/sigma/sigma_basic_search_service.js b/src/services/sigma/sigma_basic_search_service.js new file mode 100644 index 0000000..bfb1fbe --- /dev/null +++ b/src/services/sigma/sigma_basic_search_service.js @@ -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} 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} 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 +}; \ No newline at end of file diff --git a/src/services/sigma/sigma_complex_search_service.js b/src/services/sigma/sigma_complex_search_service.js new file mode 100644 index 0000000..d9931de --- /dev/null +++ b/src/services/sigma/sigma_complex_search_service.js @@ -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} 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} 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 +}; \ No newline at end of file diff --git a/src/services/sigma/utils/search_pagination_utils.js b/src/services/sigma/utils/search_pagination_utils.js new file mode 100644 index 0000000..16351e0 --- /dev/null +++ b/src/services/sigma/utils/search_pagination_utils.js @@ -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 +}; \ No newline at end of file diff --git a/src/services/sigma/utils/search_validation_utils.js b/src/services/sigma/utils/search_validation_utils.js new file mode 100644 index 0000000..a653db9 --- /dev/null +++ b/src/services/sigma/utils/search_validation_utils.js @@ -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 +}; \ No newline at end of file