From f90a08dcde4cbbb87a72cb3d07cdb1c599bbc967 Mon Sep 17 00:00:00 2001 From: Charlotte Croce Date: Sun, 20 Apr 2025 22:21:00 -0400 Subject: [PATCH] refactor CLI and decouple from handlers --- src/fylgja-cli/cli.js | 380 ++++++++++++++++--------------- src/handlers/handler_registry.js | 73 ++++++ 2 files changed, 272 insertions(+), 181 deletions(-) create mode 100644 src/handlers/handler_registry.js diff --git a/src/fylgja-cli/cli.js b/src/fylgja-cli/cli.js index 28b3ecf..8cf3598 100644 --- a/src/fylgja-cli/cli.js +++ b/src/fylgja-cli/cli.js @@ -8,16 +8,7 @@ const { parseCommand } = require('../lang/command_parser'); const logger = require('../utils/logger'); const { generateGradientLogo } = require('./utils/cli_logo'); const outputManager = require('./cli_output_manager'); - -// Import command handlers -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'); -const { handleCommand: handleAlerts } = require('../handlers/alerts/alerts_handler'); -const { handleCommand: handleCase } = require('../handlers/case/case_handler'); -const { handleCommand: handleConfig } = require('../handlers/config/config_handler'); -const { handleCommand: handleStats } = require('../handlers/stats/stats_handler'); +const handlerRegistry = require('../handlers/handler_registry'); // Set constants const FILE_NAME = 'cli.js'; @@ -43,36 +34,37 @@ const rl = readline.createInterface({ prompt: 'fylgja> ' }); +// Available commands for auto-completion +const availableCommands = [ + 'search sigma', + 'details sigma', + 'stats sigma', + 'search sigma rules where title contains', + 'search sigma where title contains', + 'search sigma rules where tags include', + 'search sigma where tags include', + 'search sigma rules where logsource.category ==', + 'search sigma where logsource.category ==', + 'search sigma rules where modified after', + 'search sigma where modified after', + 'search sigma rules where author is', + 'search sigma where author is', + 'search sigma rules where level is', + 'search sigma where level is', + 'help', + 'exit', + 'quit', + 'clear' +]; + /** * Command auto-completion function * @param {string} line Current command line input * @returns {Array} Array with possible completions and the substring being completed */ function completer(line) { - const commands = [ - 'search sigma', - 'details sigma', - 'stats sigma', - 'search sigma rules where title contains', - 'search sigma where title contains', - 'search sigma rules where tags include', - 'search sigma where tags include', - 'search sigma rules where logsource.category ==', - 'search sigma where logsource.category ==', - 'search sigma rules where modified after', - 'search sigma where modified after', - 'search sigma rules where author is', - 'search sigma where author is', - 'search sigma rules where level is', - 'search sigma where level is', - 'help', - 'exit', - 'quit', - 'clear' - ]; - - const hits = commands.filter((c) => c.startsWith(line)); - return [hits.length ? hits : commands, line]; + const hits = availableCommands.filter((c) => c.startsWith(line)); + return [hits.length ? hits : availableCommands, line]; } /** @@ -98,14 +90,39 @@ function extractSearchKeywords(input) { return input; } +/** + * Create a standardized command context object + * @param {string} text - Command text + * @param {string} module - Module name + * @param {string} action - Action name + * @param {Array} params - Command parameters + * @returns {Object} Command context object + */ +function createCommandContext(text, module, action, params) { + return { + command: { + text, + user_id: 'cli_user', + user_name: 'cli_user', + command: '/fylgja', + channel_id: 'cli', + channel_name: 'cli' + }, + meta: { + module, + action, + params, + source: 'cli' + } + }; +} + /** * Create a custom respond function for handlers - * @param {string} action - The action being performed - * @param {string} module - The module handling the action - * @param {Array} params - The parameters for the action + * @param {Object} context - Command execution context * @returns {Function} A respond function for handler callbacks */ -function createRespondFunction(action, module, params) { +function createRespondFunction(context) { // Keep track of whether we're waiting for results let isWaitingForResults = false; @@ -132,12 +149,15 @@ function createRespondFunction(action, module, params) { // Check for the responseData property (directly from service) if (response.responseData) { + const { module, action } = context.meta; + + // Display data based on module and action type if (module === 'sigma') { - if (action === 'search' || action === 'complexSearch') { + if (['search', 'complexSearch'].includes(action)) { // Convert array response to expected format if needed let dataToFormat = response.responseData; - // Wrap responseData array in proper structure + // Wrap responseData array in proper structure if needed if (Array.isArray(dataToFormat)) { dataToFormat = { results: dataToFormat, @@ -171,6 +191,118 @@ function createRespondFunction(action, module, params) { }; } +/** + * Handle built-in CLI commands + * @param {string} input User input command + * @returns {boolean} True if command was handled, false otherwise + */ +function handleBuiltInCommands(input) { + const trimmedInput = input.trim().toLowerCase(); + + if (trimmedInput === 'exit' || trimmedInput === 'quit') { + outputManager.displaySuccess('Goodbye!'); + rl.close(); + process.exit(0); + return true; + } + + if (trimmedInput === 'clear') { + console.clear(); + rl.prompt(); + return true; + } + + if (trimmedInput === 'help') { + outputManager.displayHelp(); + rl.prompt(); + return true; + } + + return false; +} + +/** + * Handle simple search commands + * @param {string} input User input command + * @returns {boolean} True if command was handled, false otherwise + */ +async function handleSimpleSearch(input) { + const match = input.trim().match(/^search\s+sigma\s+(.+)$/i); + + if (match && !input.trim().toLowerCase().includes('where') && !input.trim().toLowerCase().includes('with')) { + const keyword = match[1]; + const context = createCommandContext(keyword, 'sigma', 'search', [keyword]); + const respond = createRespondFunction(context); + + console.log(`Executing: module=sigma, action=search, params=[${keyword}]`); + + try { + // Get handler from registry + const handler = handlerRegistry.getHandler('sigma', 'search'); + if (handler) { + await handler.handleCommand(context.command, respond); + } else { + outputManager.displayError('Handler not found for sigma search'); + rl.prompt(); + } + } catch (error) { + outputManager.displayError(error.message); + logger.error(`${FILE_NAME}: Command execution error: ${error.message}`); + logger.debug(`${FILE_NAME}: Error stack: ${error.stack}`); + rl.prompt(); + } + + return true; + } + + return false; +} + +/** + * Handle complex search commands + * @param {string} input User input command + * @returns {boolean} True if command was handled, false otherwise + */ +async function handleComplexSearch(input) { + const complexSearchMatch = input.trim().match(/^search\s+sigma\s+(rules\s+|detections\s+)?(where|with)\s+(.+)$/i); + + if (complexSearchMatch) { + const complexQuery = complexSearchMatch[3]; + const searchTerms = extractSearchKeywords(complexQuery); + + const context = createCommandContext( + searchTerms || complexQuery, + 'sigma', + 'complexSearch', + [complexQuery] + ); + + const respond = createRespondFunction(context); + + console.log(`Executing: module=sigma, action=complexSearch, params=[${complexQuery}]`); + + try { + // Get handler from registry + const handler = handlerRegistry.getHandler('sigma', 'search'); + if (handler) { + await handler.handleCommand(context.command, respond); + } else { + outputManager.displayError('Handler not found for sigma complex search'); + rl.prompt(); + } + } catch (error) { + outputManager.displayError(error.message); + logger.error(`${FILE_NAME}: Command execution error: ${error.message}`); + logger.debug(`${FILE_NAME}: Error stack: ${error.stack}`); + rl.prompt(); + } + + return true; + } + + return false; +} + /** * Process a command from the CLI * @param {string} input User input command @@ -183,93 +315,26 @@ async function processCommand(input) { return; } - // Special CLI commands - if (input.trim().toLowerCase() === 'exit' || input.trim().toLowerCase() === 'quit') { - outputManager.displaySuccess('Goodbye!'); - rl.close(); - process.exit(0); - } - - if (input.trim().toLowerCase() === 'clear') { - console.clear(); - rl.prompt(); - return; - } - - if (input.trim().toLowerCase() === 'help') { - outputManager.displayHelp(); - rl.prompt(); - return; - } - // Add to command history commandHistory.push(input); historyIndex = commandHistory.length; - // Special case for simple search - if (input.trim().match(/^search\s+sigma\s+(.+)$/i) && !input.trim().toLowerCase().includes('where') && !input.trim().toLowerCase().includes('with')) { - const keyword = input.trim().match(/^search\s+sigma\s+(.+)$/i)[1]; - - // Create fake command object - const command = { - text: keyword, - user_id: 'cli_user', - user_name: 'cli_user', - command: '/fylgja', - channel_id: 'cli', - channel_name: 'cli' - }; - - // Create custom respond function - const respond = createRespondFunction('search', 'sigma', [keyword]); - - console.log(`Executing: module=sigma, action=search, params=[${keyword}]`); - - try { - await sigmaSearchHandler.handleCommand(command, respond); - } catch (error) { - outputManager.displayError(error.message); - logger.error(`${FILE_NAME}: Command execution error: ${error.message}`); - logger.debug(`${FILE_NAME}: Error stack: ${error.stack}`); - rl.prompt(); - } - + // Handle built-in commands + if (handleBuiltInCommands(input)) { return; } - // Special case for complex search - const complexSearchMatch = input.trim().match(/^search\s+sigma\s+(rules\s+|detections\s+)?(where|with)\s+(.+)$/i); - if (complexSearchMatch) { - const complexQuery = complexSearchMatch[3]; - - // Create fake command object - const command = { - text: complexQuery, - user_id: 'cli_user', - user_name: 'cli_user', - command: '/fylgja', - channel_id: 'cli', - channel_name: 'cli' - }; - - // Create custom respond function - const respond = createRespondFunction('complexSearch', 'sigma', [complexQuery]); - - console.log(`Executing: module=sigma, action=complexSearch, params=[${complexQuery}]`); - - try { - await sigmaSearchHandler.handleCommand(command, respond); - } catch (error) { - outputManager.displayError(error.message); - logger.error(`${FILE_NAME}: Command execution error: ${error.message}`); - logger.debug(`${FILE_NAME}: Error stack: ${error.stack}`); - rl.prompt(); - } - + // Handle simple search + if (await handleSimpleSearch(input)) { return; } - // Parse command using existing parser + // Handle complex search + if (await handleComplexSearch(input)) { + return; + } + + // Parse command using existing parser for everything else const parsedCommand = await parseCommand(input); if (!parsedCommand.success) { @@ -284,72 +349,26 @@ async function processCommand(input) { // Only show execution info to the user, not sending to logger console.log(`Executing: module=${module}, action=${action}, params=[${params}]`); - // Create fake command object similar to Slack's - const command = { - text: Array.isArray(params) && params.length > 0 ? params[0] : input, - user_id: 'cli_user', - user_name: 'cli_user', - command: '/fylgja', - channel_id: 'cli', - channel_name: 'cli' - }; - - // Special handling for complexSearch to extract keywords - if (action === 'complexSearch' && module === 'sigma' && params.length > 0) { - // Try to extract keywords from complex queries - const searchTerms = extractSearchKeywords(params[0]); - command.text = searchTerms || params[0]; - } + // Create command context + const context = createCommandContext( + Array.isArray(params) && params.length > 0 ? params[0] : input, + module, + action, + params + ); // Create custom respond function for CLI - const respond = createRespondFunction(action, module, params); + const respond = createRespondFunction(context); try { - switch (module) { - case 'sigma': - switch (action) { - case 'search': - case 'complexSearch': - await sigmaSearchHandler.handleCommand(command, respond); - break; - - case 'details': - await sigmaDetailsHandler.handleCommand(command, respond); - break; - - case 'stats': - await sigmaStatsHandler.handleCommand(command, respond); - break; - - case 'create': - await sigmaCreateHandler.handleCommand(command, respond); - break; - - default: - outputManager.displayWarning(`Unknown Sigma action: ${action}`); - rl.prompt(); - } - break; - - case 'alerts': - await handleAlerts(command, respond); - break; - - case 'case': - await handleCase(command, respond); - break; - - case 'config': - await handleConfig(command, respond); - break; - - case 'stats': - await handleStats(command, respond); - break; - - default: - outputManager.displayWarning(`Unknown module: ${module}`); - rl.prompt(); + // Get handler from registry + const handler = handlerRegistry.getHandler(module, action); + + if (handler) { + await handler.handleCommand(context.command, respond); + } else { + outputManager.displayWarning(`Unknown handler for ${module}.${action}`); + rl.prompt(); } } catch (error) { outputManager.displayError(error.message); @@ -431,5 +450,4 @@ if (require.main === module) { module.exports = { startCLI }; -} - +} \ No newline at end of file diff --git a/src/handlers/handler_registry.js b/src/handlers/handler_registry.js new file mode 100644 index 0000000..7feda95 --- /dev/null +++ b/src/handlers/handler_registry.js @@ -0,0 +1,73 @@ +/** + * handler_registry.js + * + * Centralized registry for command handlers + * Decouples the CLI from specific handler implementations + */ +const logger = require('../utils/logger'); +const FILE_NAME = 'handler_registry.js'; + +// Create a registry to store handlers +const handlers = {}; + +/** + * Registers a handler for a specific module and action + * @param {string} module - The module name (e.g., 'sigma') + * @param {string} action - The action name (e.g., 'search') + * @param {Object} handler - The handler object with a handleCommand method + */ +function registerHandler(module, action, handler) { + const key = `${module}:${action}`; + + if (!handler || typeof handler.handleCommand !== 'function') { + logger.error(`${FILE_NAME}: Invalid handler for ${key}. Must have handleCommand method.`); + return; + } + + handlers[key] = handler; + logger.debug(`${FILE_NAME}: Registered handler for ${key}`); +} + +/** + * Gets a handler for a specific module and action + * @param {string} module - The module name (e.g., 'sigma') + * @param {string} action - The action name (e.g., 'search') + * @returns {Object|null} The handler object or null if not found + */ +function getHandler(module, action) { + const key = `${module}:${action}`; + return handlers[key] || null; +} + +/** + * Initializes the handler registry with all available handlers + */ +function initializeRegistry() { + try { + // Import all handlers + 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'); + + // Register Sigma handlers + registerHandler('sigma', 'search', sigmaSearchHandler); + registerHandler('sigma', 'complexSearch', sigmaSearchHandler); + registerHandler('sigma', 'details', sigmaDetailsHandler); + registerHandler('sigma', 'stats', sigmaStatsHandler); + registerHandler('sigma', 'create', sigmaCreateHandler); + + logger.info(`${FILE_NAME}: Handler registry initialized successfully`); + } catch (error) { + logger.error(`${FILE_NAME}: Error initializing handler registry: ${error.message}`); + logger.debug(`${FILE_NAME}: Error stack: ${error.stack}`); + } +} + +// Initialize the registry when the module is loaded +initializeRegistry(); + +module.exports = { + registerHandler, + getHandler +}; \ No newline at end of file