/** * cli.js * * Interactive CLI interface */ const readline = require('readline'); const { parseCommand } = require('../lang/command_parser'); const logger = require('../utils/logger'); const { generateGradientLogo, generateNormalLogo } = require('./utils/cli_logo'); const outputManager = require('./cli_output_manager'); const handlerRegistry = require('../handlers/handler_registry'); // Set constants const FILE_NAME = 'cli.js'; // Try to get version, but provide fallback if package.json can't be found let version = '1.0.0'; try { const packageJson = require('../package.json'); version = packageJson.version; } catch (e) { console.log('Could not load package.json, using default version'); } // Command history management let commandHistory = []; let historyIndex = -1; // Create the readline interface const rl = readline.createInterface({ input: process.stdin, output: process.stdout, completer: completer, 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 hits = availableCommands.filter((c) => c.startsWith(line)); return [hits.length ? hits : availableCommands, line]; } /** * Parse out any basic search keywords from a complexSearch query * @param {string} input The complex search query * @returns {string} Extracted keywords */ function extractSearchKeywords(input) { if (!input) return ''; // Try to extract keywords from common patterns if (input.includes('title contains')) { const match = input.match(/title\s+contains\s+["']([^"']+)["']/i); if (match) return match[1]; } if (input.includes('tags include')) { const match = input.match(/tags\s+include\s+(\S+)/i); if (match) return match[1]; } // Default - just return the input as is 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 {Object} context - Command execution context * @returns {Function} A respond function for handler callbacks */ function createRespondFunction(context) { // Keep track of whether we're waiting for results let isWaitingForResults = false; return async (response) => { const isProgressMessage = typeof response === 'object' && response.text && !response.responseData && (response.text.includes('moment') || response.text.includes('searching') || response.text.includes('processing')); if (isProgressMessage) { outputManager.displayProgress(response.text); isWaitingForResults = true; return; // Don't show prompt after progress messages } if (typeof response === 'string') { console.log(response); rl.prompt(); return; } // 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 (['search', 'complexSearch'].includes(action)) { // Convert array response to expected format if needed let dataToFormat = response.responseData; // Wrap responseData array in proper structure if needed if (Array.isArray(dataToFormat)) { dataToFormat = { results: dataToFormat, totalCount: dataToFormat.length }; } outputManager.display(dataToFormat, 'search_results'); } else if (action === 'details') { outputManager.display(response.responseData, 'details'); } else if (action === 'stats') { outputManager.display(response.responseData, 'stats'); } else { console.log(JSON.stringify(response.responseData, null, 2)); } } else { // For other modules, just display the JSON console.log(JSON.stringify(response.responseData, null, 2)); } } // Fallback for text-only responses else if (response.text) { console.log(response.text); } else { outputManager.displaySuccess('Command completed successfully.'); } // Reset waiting state and show prompt after results isWaitingForResults = false; rl.prompt(); }; } /** * 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 */ async function processCommand(input) { try { // Skip empty commands if (!input.trim()) { rl.prompt(); return; } // Add to command history commandHistory.push(input); historyIndex = commandHistory.length; // Handle built-in commands if (handleBuiltInCommands(input)) { return; } // Handle simple search if (await handleSimpleSearch(input)) { return; } // Handle complex search if (await handleComplexSearch(input)) { return; } // Parse command using existing parser for everything else const parsedCommand = await parseCommand(input); if (!parsedCommand.success) { outputManager.displayWarning(parsedCommand.message || "Command not recognized. Type 'help' for usage."); rl.prompt(); return; } // Extract the command details const { action, module, params } = parsedCommand.command; // Only show execution info to the user, not sending to logger console.log(`Executing: module=${module}, action=${action}, params=[${params}]`); // 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(context); try { // 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); // Log to file but not console logger.error(`${FILE_NAME}: Command execution error: ${error.message}`); logger.debug(`${FILE_NAME}: Error stack: ${error.stack}`); rl.prompt(); } } catch (error) { outputManager.displayError(`Fatal error: ${error.message}`); // Log to file but not console logger.error(`${FILE_NAME}: Fatal error: ${error.message}`); logger.debug(`${FILE_NAME}: Error stack: ${error.stack}`); rl.prompt(); } } /** * Start the CLI */ function startCLI() { console.log(generateNormalLogo()); console.log(`Fylgja CLI v${version} - Interactive SIEM Management Tool`); console.log(`Type 'help' for usage information or 'exit' to quit\n`); // Set logger to CLI mode (prevents console output) logger.setCliMode(true); // Set up key bindings for history navigation rl._writeToOutput = function _writeToOutput(stringToWrite) { if (stringToWrite === '\\u001b[A' || stringToWrite === '\\u001b[B') { // Don't output control characters for up/down arrows return; } rl.output.write(stringToWrite); }; // Set up key listeners for history rl.input.on('keypress', (char, key) => { if (key && key.name === 'up') { if (historyIndex > 0) { historyIndex--; rl.line = commandHistory[historyIndex]; rl.cursor = rl.line.length; rl._refreshLine(); } } else if (key && key.name === 'down') { if (historyIndex < commandHistory.length - 1) { historyIndex++; rl.line = commandHistory[historyIndex]; rl.cursor = rl.line.length; rl._refreshLine(); } else if (historyIndex === commandHistory.length - 1) { historyIndex = commandHistory.length; rl.line = ''; rl.cursor = 0; rl._refreshLine(); } } }); rl.prompt(); rl.on('line', async (line) => { await processCommand(line.trim()); }); rl.on('close', () => { outputManager.displaySuccess('Goodbye!'); process.exit(0); }); } // Check if running directly if (require.main === module) { startCLI(); } else { // Export functions for integration with main app module.exports = { startCLI }; }