diff --git a/src/fylgja-cli.js b/src/fylgja-cli.js index e38bda7..5ef1a0a 100644 --- a/src/fylgja-cli.js +++ b/src/fylgja-cli.js @@ -1,12 +1,614 @@ -#!/usr/bin/env node /** * fylgja-cli.js * - * Command-line executable entry point for Fylgja CLI + * Interactive CLI interface */ -// Import the CLI starter function -const { startCLI } = require('./fylgja-cli/cli'); +const readline = require('readline'); +// Import chalk with compatibility for both ESM and CommonJS +let chalk; +try { + // First try CommonJS import (chalk v4.x) + chalk = require('chalk'); +} catch (e) { + // If that fails, provide a fallback implementation + chalk = { + blue: (text) => text, + green: (text) => text, + red: (text) => text, + yellow: (text) => text, + cyan: (text) => text, + white: (text) => text, + dim: (text) => text, + hex: () => (text) => text + }; +} -// Run the CLI application -startCLI(); \ No newline at end of file +const { parseCommand } = require('./lang/command_parser'); +const logger = require('./utils/logger'); +const sigmaSearchHandler = require('./handlers/sigma/sigma_search_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'); + +// Import CLI formatters +const { + formatSigmaStats, + formatSigmaSearchResults, + formatSigmaDetails +} = require('./utils/cli_formatters'); + +// Set logger to CLI mode (prevents console output) +logger.setCliMode(true); + +// 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'); +} + +const FILE_NAME = 'fylgja-cli.js'; + +// ASCII art logo for the CLI +const ASCII_LOGO = ` +░▒▓████████▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░ ░▒▓██████▓▒░ ░▒▓█▓▒░░▒▓██████▓▒░ +░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░ +░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░ +░▒▓██████▓▒░ ░▒▓██████▓▒░░▒▓█▓▒░ ░▒▓█▓▒▒▓███▓▒░ ░▒▓█▓▒░▒▓████████▓▒░ +░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░ +░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░ +░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓████████▓▒░▒▓██████▓▒░ ░▒▓██████▓▒░░▒▓█▓▒░░▒▓█▓▒░ +`; + +// Command history array +let commandHistory = []; +let historyIndex = -1; + +// Create the readline interface +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + completer: completer, + prompt: 'fylgja> ' +}); + +/** + * 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', + 'sigma stats', + 'stats sigma', + 'search sigma rules where title contains', + 'search rules where tags include', + 'search rules where logsource.category ==', + 'search rules where modified after', + 'help', + 'exit', + 'quit', + 'clear' + ]; + + const hits = commands.filter((c) => c.startsWith(line)); + return [hits.length ? hits : commands, line]; +} + +/** + * Normalize and wrap text for table display + * @param {string} text Text to normalize and wrap + * @param {number} maxWidth Maximum width per line + * @returns {string[]} Array of wrapped lines + */ +function normalizeAndWrap(text, maxWidth) { + if (!text) return ['']; + + // Convert to string and normalize newlines + text = String(text || ''); + + // Replace all literal newlines with spaces + text = text.replace(/\n/g, ' '); + + // Now apply word wrapping + if (text.length <= maxWidth) return [text]; + + const words = text.split(' '); + const lines = []; + let currentLine = ''; + + for (const word of words) { + // Skip empty words (could happen if there were multiple spaces) + if (!word) continue; + + // If adding this word would exceed max width + if ((currentLine.length + word.length + (currentLine ? 1 : 0)) > maxWidth) { + // Push current line if not empty + if (currentLine) { + lines.push(currentLine); + currentLine = ''; + } + + // If the word itself is longer than maxWidth, we need to split it + if (word.length > maxWidth) { + let remaining = word; + while (remaining.length > 0) { + const chunk = remaining.substring(0, maxWidth); + lines.push(chunk); + remaining = remaining.substring(maxWidth); + } + } else { + currentLine = word; + } + } else { + // Add word to current line + currentLine = currentLine ? `${currentLine} ${word}` : word; + } + } + + // Add the last line if not empty + if (currentLine) { + lines.push(currentLine); + } + + return lines; +} + +/** + * Format CLI output similar to MySQL + * @param {Object} data The data to format + * @param {string} type The type of data (results, details, stats) + */ +function formatOutput(data, type) { + if (!data) { + console.log('No data returned from the server.'); + return; + } + + switch (type) { + case 'search_results': + // Search results table format remains the same + console.log('\n+-------+----------------------+------------------+-------------+'); + console.log('| ID | Title | Author | Level |'); + console.log('+-------+----------------------+------------------+-------------+'); + + if (data.results && data.results.length > 0) { + data.results.forEach(rule => { + const id = (rule.id || '').padEnd(5).substring(0, 5); + const title = (rule.title || '').padEnd(20).substring(0, 20); + const author = (rule.author || 'Unknown').padEnd(16).substring(0, 16); + const level = (rule.level || 'medium').padEnd(11).substring(0, 11); + + console.log(`| ${id} | ${title} | ${author} | ${level} |`); + }); + } else { + console.log('| No results found |'); + } + + console.log('+-------+----------------------+------------------+-------------+'); + console.log(`${data.totalCount || 0} rows in set`); + break; + + case 'details': + // Set a fixed width for the entire table + const sigmaDetailsKeyWidth = 22; + const sigmaDetailsValueWidth = 50; + + // Create the table borders + const detailsHeaderLine = '╔' + '═'.repeat(sigmaDetailsKeyWidth) + '╦' + '═'.repeat(sigmaDetailsValueWidth) + '╗'; + const sigmaDetailsDividerLine = '╠' + '═'.repeat(sigmaDetailsKeyWidth) + '╬' + '═'.repeat(sigmaDetailsValueWidth) + '╣'; + const sigmaDetailsRowSeparator = '╟' + '─'.repeat(sigmaDetailsKeyWidth) + '╫' + '─'.repeat(sigmaDetailsValueWidth) + '╢'; + const sigmaDetailsFooterLine = '╚' + '═'.repeat(sigmaDetailsKeyWidth) + '╩' + '═'.repeat(sigmaDetailsValueWidth) + '╝'; + + console.log('\n' + detailsHeaderLine); + console.log(`║ ${'Field'.padEnd(sigmaDetailsKeyWidth - 2)} ║ ${'Value'.padEnd(sigmaDetailsValueWidth - 2)} ║`); + console.log(sigmaDetailsDividerLine); + + // Track whether we need to add a row separator + let isFirstRow = true; + + for (const [key, value] of Object.entries(data)) { + if (typeof value !== 'object' || value === null) { + // Add separator between rows (but not before the first row) + if (!isFirstRow) { + console.log(sigmaDetailsRowSeparator); + } + isFirstRow = false; + + const formattedKey = key.padEnd(sigmaDetailsKeyWidth - 2); + + // Handle wrapping + const lines = normalizeAndWrap(value, sigmaDetailsValueWidth - 2); + + // Print first line with the key + console.log(`║ ${formattedKey} ║ ${lines[0].padEnd(sigmaDetailsValueWidth - 2)} ║`); + + // Print additional lines if there are any + for (let i = 1; i < lines.length; i++) { + console.log(`║ ${' '.repeat(sigmaDetailsKeyWidth - 2)} ║ ${lines[i].padEnd(sigmaDetailsValueWidth - 2)} ║`); + } + } + } + + console.log(sigmaDetailsFooterLine); + break; + case 'stats': + // Set column widths + const sigmaStatsMetricWidth = 25; + const sigmaStatsValueWidth = 26; + + // Create the table borders + const sigmaStatsHeaderLine = '╔' + '═'.repeat(sigmaStatsMetricWidth) + '╦' + '═'.repeat(sigmaStatsValueWidth) + '╗'; + const sigmaStatsDividerLine = '╠' + '═'.repeat(sigmaStatsMetricWidth) + '╬' + '═'.repeat(sigmaStatsValueWidth) + '╣'; + const sigmaStatsRowSeparator = '╟' + '─'.repeat(sigmaStatsMetricWidth) + '╫' + '─'.repeat(sigmaStatsValueWidth) + '╢'; + const sigmaStatsFooterLine = '╚' + '═'.repeat(sigmaStatsMetricWidth) + '╩' + '═'.repeat(sigmaStatsValueWidth) + '╝'; + + console.log('\n' + sigmaStatsHeaderLine); + console.log(`║ ${'Metric'.padEnd(sigmaStatsMetricWidth - 2)} ║ ${'Value'.padEnd(sigmaStatsValueWidth - 2)} ║`); + console.log(sigmaStatsDividerLine); + + // Track whether we need to add a row separator + let statsIsFirstRow = true; + + for (const [key, value] of Object.entries(data)) { + // Add separator between rows (but not before the first row) + if (!statsIsFirstRow) { + console.log(sigmaStatsRowSeparator); + } + statsIsFirstRow = false; + + const formattedKey = key.padEnd(sigmaStatsMetricWidth - 2); + const formattedValue = String(value || '').padEnd(sigmaStatsValueWidth - 2); + + console.log(`║ ${formattedKey} ║ ${formattedValue} ║`); + } + + console.log(sigmaStatsFooterLine); + break; + + default: + console.log(JSON.stringify(data, null, 2)); + } +} + +/** + * Parse out any basic search keywords from a complexSearch query + * This helps with the search commands that don't quite match the expected format + * @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; +} + +/** + * 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; + } + + // Special CLI commands + if (input.trim().toLowerCase() === 'exit' || input.trim().toLowerCase() === 'quit') { + console.log('Goodbye!'); + rl.close(); + process.exit(0); + } + + if (input.trim().toLowerCase() === 'clear') { + console.clear(); + rl.prompt(); + return; + } + + // Special case for simple search + if (input.trim().match(/^search\s+sigma\s+(.+)$/i)) { + const keyword = input.trim().match(/^search\s+sigma\s+(.+)$/i)[1]; + + // Add to command history + commandHistory.push(input); + historyIndex = commandHistory.length; + + // 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) { + console.error(`Error: ${error.message}`); + logger.error(`${FILE_NAME}: Command execution error: ${error.message}`); + logger.debug(`${FILE_NAME}: Error stack: ${error.stack}`); + rl.prompt(); + } + + return; + } + + // Add to command history + commandHistory.push(input); + historyIndex = commandHistory.length; + + // Parse command using existing parser + const parsedCommand = await parseCommand(input); + + if (!parsedCommand.success) { + console.log(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 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 custom respond function for CLI + const respond = createRespondFunction(action, module, params); + + try { + switch (module) { + case 'sigma': + switch (action) { + case 'search': + await sigmaSearchHandler.handleCommand(command, respond); + break; + + case 'complexSearch': + await sigmaSearchHandler.handleComplexSearch(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: + console.log(`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; + + case 'help': + displayHelp(); + rl.prompt(); + break; + + default: + console.log(`Unknown module: ${module}`); + rl.prompt(); + } + } catch (error) { + console.error(`Error: ${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) { + console.error(`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(); + } +} + +/** + * Create a custom respond function for handling results + * @param {string} action The action being performed + * @param {string} module The module being used + * @param {Array} params The parameters for the action + * @returns {Function} A respond function for handling results + */ +function createRespondFunction(action, module, params) { + return async (response) => { + if (typeof response === 'string') { + console.log(response); + rl.prompt(); + return; + } + + // First check for the responseData property (directly from service) + if (response.responseData) { + // Format the data using the appropriate formatter + if (module === 'sigma') { + let formattedData; + + if (action === 'search' || action === 'complexSearch') { + formattedData = formatSigmaSearchResults(response.responseData); + formatOutput(formattedData, 'search_results'); + } else if (action === 'details') { + formattedData = formatSigmaDetails(response.responseData); + formatOutput(formattedData, 'details'); + } else if (action === 'stats') { + formattedData = formatSigmaStats(response.responseData); + formatOutput(formattedData, '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 { + console.log('Command completed successfully.'); + } + + rl.prompt(); + }; +} + +/** + * Display help text + */ +function displayHelp() { + const helpText = ` +Fylgja CLI Help + +Basic Sigma Commands: +- search sigma - Search for Sigma rules by keyword +- details sigma - Get details about a specific Sigma rule +- stats sigma - Get statistics about Sigma rules database + +Advanced Sigma Search Commands: +- search sigma where title contains "ransomware" - Search by title +- search sigma where tags include privilege_escalation - Search by tags +- search sigma where logsource.category == "process_creation" - Search by log source +- search sigma where modified after 2024-01-01 - Search by modification date + + +- exit or quit - Exit the CLI +- clear - Clear the terminal screen +- help - Display this help text + `; + + console.log(helpText); +} + +/** + * Start the CLI application + */ +function startCLI() { + console.log(ASCII_LOGO); + console.log(`Fylgja CLI v${version} - Interactive SIEM Management Tool`); + console.log(`Type 'help' for usage information or 'exit' to quit\n`); + + // 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', () => { + console.log('Goodbye!'); + process.exit(0); + }); +} + +// Check if running directly +if (require.main === module) { + startCLI(); +} else { + // Export functions for integration with main app + module.exports = { + startCLI + }; +} \ No newline at end of file diff --git a/src/fylgja-cli/cli.js b/src/fylgja-cli/cli.js deleted file mode 100644 index d70ab35..0000000 --- a/src/fylgja-cli/cli.js +++ /dev/null @@ -1,438 +0,0 @@ -/** - * cli.js - * - * Interactive CLI interface - */ -const readline = require('readline'); -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_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'); - -// 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> ' -}); - -/** - * 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]; -} - -/** - * 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 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 - * @returns {Function} A respond function for handler callbacks - */ -function createRespondFunction(action, module, params) { - // 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) { - if (module === 'sigma') { - if (action === 'search' || action === 'complexSearch') { - // Convert array response to expected format if needed - let dataToFormat = response.responseData; - - // Wrap responseData array in proper structure - 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(); - }; -} - -/** - * 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; - } - - // 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(); - } - - 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.handleComplexSearch(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(); - } - - return; - } - - // Parse command using existing parser - 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 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 custom respond function for CLI - const respond = createRespondFunction(action, module, params); - - try { - switch (module) { - case 'sigma': - switch (action) { - case 'search': - await sigmaSearchHandler.handleCommand(command, respond); - break; - - case 'complexSearch': - await sigmaSearchHandler.handleComplexSearch(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(); - } - } 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(generateGradientLogo()); - 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 - }; -} - diff --git a/src/fylgja-cli/cli_output_manager.js b/src/fylgja-cli/cli_output_manager.js deleted file mode 100644 index 3727f2f..0000000 --- a/src/fylgja-cli/cli_output_manager.js +++ /dev/null @@ -1,123 +0,0 @@ -/** - * cli_output_manager.js - * - * Centralized manager for CLI output formatting and display - */ -const colors = require('./utils/colors'); -const formatters = require('./formatters'); - -/** - * CLI Output Manager - * Handles all CLI output formatting and display - */ -class CliOutputManager { - /** - * Create a new CLI Output Manager - */ - constructor() { - this.colors = colors; - } - - /** - * Display formatted output based on data type - * - * @param {any} data - Data to format and display - * @param {string} type - Type of data (search_results, details, stats) - */ - display(data, type) { - if (!data) { - console.log('No data returned from the server.'); - return; - } - - console.log(); // Empty line for spacing - - switch (type) { - case 'search_results': - const formattedResults = formatters.formatSigmaSearchResults(data); - console.log(formatters.renderSigmaSearchResults(formattedResults)); - break; - - case 'details': - const formattedDetails = formatters.formatSigmaDetails(data); - console.log(formatters.renderSigmaDetails(formattedDetails)); - break; - - case 'stats': - const formattedStats = formatters.formatSigmaStats(data); - console.log(formatters.renderSigmaStats(formattedStats)); - break; - - default: - // For unknown types, just display JSON - console.log(JSON.stringify(data, null, 2)); - } - } - - /** - * Display a progress message - * - * @param {string} message - Progress message to display - */ - displayProgress(message) { - console.log(this.colors.dim(message)); - } - - /** - * Display an error message - * - * @param {string} message - Error message to display - */ - displayError(message) { - console.error(this.colors.error(`Error: ${message}`)); - } - - /** - * Display a success message - * - * @param {string} message - Success message to display - */ - displaySuccess(message) { - console.log(this.colors.success(message)); - } - - /** - * Display a warning message - * - * @param {string} message - Warning message to display - */ - displayWarning(message) { - console.log(this.colors.warning(`Warning: ${message}`)); - } - - /** - * Display help text - */ - displayHelp() { - const helpText = ` -Fylgja CLI Help - -Basic Sigma Commands: -- search sigma - Search for Sigma rules by keyword -- details sigma - Get details about a specific Sigma rule -- stats sigma - Get statistics about Sigma rules database - -Advanced Sigma Search Commands: -- search sigma where title contains "ransomware" - Search by title -- search sigma where tags include privilege_escalation - Search by tags -- search sigma where logsource.category == "process_creation" - Search by log source -- search sigma where modified after 2024-01-01 - Search by modification date -- search sigma rules where title contains "ransomware" - Alternative syntax -- search sigma rules where tags include privilege_escalation - Alternative syntax - -CLI Commands: -- exit or quit - Exit the CLI -- clear - Clear the terminal screen -- help - Display this help text -`; - - console.log(helpText); - } -} - -module.exports = new CliOutputManager(); \ No newline at end of file diff --git a/src/fylgja-cli/formatters/index.js b/src/fylgja-cli/formatters/index.js deleted file mode 100644 index 9b1b9cd..0000000 --- a/src/fylgja-cli/formatters/index.js +++ /dev/null @@ -1,36 +0,0 @@ -/** - * formatters/index.js - * - * Exports all formatters for easy importing - */ - -// Text and table formatters -const textFormatter = require('./text_formatter'); -const tableFormatter = require('./table_formatter'); - -// Domain-specific formatters -const sigmaFormatter = require('./sigma_formatter'); - -// Re-export all formatters -module.exports = { - // Text utilities - wrapText: textFormatter.wrapText, - formatDate: textFormatter.formatDate, - formatNumber: textFormatter.formatNumber, - - // Table utilities - formatTable: tableFormatter.formatTable, - formatKeyValueTable: tableFormatter.formatKeyValueTable, - formatTableHeader: tableFormatter.formatTableHeader, - formatTableRow: tableFormatter.formatTableRow, - - // Sigma formatters - formatSigmaDetails: sigmaFormatter.formatSigmaDetails, - formatSigmaStats: sigmaFormatter.formatSigmaStats, - formatSigmaSearchResults: sigmaFormatter.formatSigmaSearchResults, - - // Rendering functions for CLI output - renderSigmaDetails: sigmaFormatter.renderSigmaDetails, - renderSigmaStats: sigmaFormatter.renderSigmaStats, - renderSigmaSearchResults: sigmaFormatter.renderSigmaSearchResults -}; \ No newline at end of file diff --git a/src/fylgja-cli/formatters/sigma_formatter.js b/src/fylgja-cli/formatters/sigma_formatter.js deleted file mode 100644 index e552b7a..0000000 --- a/src/fylgja-cli/formatters/sigma_formatter.js +++ /dev/null @@ -1,196 +0,0 @@ -/** - * sigma_formatter.js - * - * Specialized formatters for Sigma rule data - */ -const { wrapText, formatDate, formatNumber } = require('./text_formatter'); -const { formatKeyValueTable, formatTable } = require('./table_formatter'); -const colors = require('../utils/colors'); - -/** - * Format Sigma rule details for CLI display - * - * @param {Object} ruleDetails - The rule details object - * @returns {Object} Formatted rule details - */ -function formatSigmaDetails(ruleDetails) { - if (!ruleDetails) { - return null; - } - - // Create a flattened object for display in CLI table format - return { - 'ID': ruleDetails.id || 'Unknown', - 'Title': ruleDetails.title || 'Untitled Rule', - 'Description': ruleDetails.description || 'No description provided', - 'Author': ruleDetails.author || 'Unknown author', - 'Severity': ruleDetails.severity || 'Unknown', - 'Status': ruleDetails.status || 'Unknown', - 'Created': formatDate(ruleDetails.date), - 'Modified': formatDate(ruleDetails.modified), - 'Detection': ruleDetails.detectionExplanation || 'No detection specified', - 'False Positives': Array.isArray(ruleDetails.falsePositives) ? - ruleDetails.falsePositives.join(', ') : 'None specified', - 'Tags': Array.isArray(ruleDetails.tags) ? - ruleDetails.tags.join(', ') : 'None', - 'References': Array.isArray(ruleDetails.references) ? - ruleDetails.references.join(', ') : 'None' - }; -} - -/** - * Format Sigma statistics for CLI display - * - * @param {Object} stats - The statistics object - * @returns {Object} Formatted stats - */ -function formatSigmaStats(stats) { - if (!stats) { - return { error: 'No statistics data available' }; - } - - // Create a simplified object suitable for table display - const formattedStats = { - 'Last Update': formatDate(stats.lastUpdate), - 'Total Rules': formatNumber(stats.totalRules), - 'Database Health': `${stats.databaseHealth.contentPercentage}% Complete`, - - // OS breakdown - 'Windows Rules': formatNumber(stats.operatingSystems.windows), - 'Linux Rules': formatNumber(stats.operatingSystems.linux), - 'macOS Rules': formatNumber(stats.operatingSystems.macos), - 'Other OS Rules': formatNumber(stats.operatingSystems.other), - }; - - // Add severity levels - if (stats.severityLevels && Array.isArray(stats.severityLevels)) { - stats.severityLevels.forEach(level => { - const levelName = level.level - ? level.level.charAt(0).toUpperCase() + level.level.slice(1) - : 'Unknown'; - - formattedStats[`${levelName} Severity`] = formatNumber(level.count); - }); - } - - // Add top MITRE tactics if available - if (stats.mitreTactics && Array.isArray(stats.mitreTactics)) { - stats.mitreTactics.slice(0, 5).forEach(tactic => { // Only top 5 - const formattedTactic = tactic.tactic - .replace(/-/g, ' ') - .split(' ') - .map(word => word.charAt(0).toUpperCase() + word.slice(1)) - .join(' '); - - formattedStats[`MITRE: ${formattedTactic}`] = formatNumber(tactic.count); - }); - } - - return formattedStats; -} - -/** - * Format Sigma search results for CLI display - * - * @param {Object} searchResults - The search results object - * @returns {Object} Formatted search results - */ -function formatSigmaSearchResults(searchResults) { - if (!searchResults || !searchResults.results) { - return { error: 'No search results available' }; - } - - // Map raw results to formatted results - const formattedResults = searchResults.results.map(rule => { - // Get logsource.product field - let osProduct = 'N/A'; - - if (rule.logsource && rule.logsource.product) { - osProduct = rule.logsource.product; - } - - return { - id: rule.id || '', - title: rule.title || '', - author: rule.author || 'Unknown', - level: rule.level || 'medium', - osProduct: osProduct, - tags: rule.tags || [] - }; - }); - - return { - results: formattedResults, - totalCount: searchResults.totalCount || 0 - }; -} - -/** - * Render formatted Sigma details as a string - * - * @param {Object} formattedDetails - Formatted details object - * @returns {string} CLI-ready output - */ -function renderSigmaDetails(formattedDetails) { - if (!formattedDetails) { - return 'No details available'; - } - - return formatKeyValueTable(formattedDetails, 24, 50).join('\n'); -} - -/** - * Render formatted Sigma statistics as a string - * - * @param {Object} formattedStats - Formatted statistics object - * @returns {string} CLI-ready output - */ -function renderSigmaStats(formattedStats) { - if (!formattedStats) { - return 'No statistics available'; - } - - return formatKeyValueTable(formattedStats, 25, 30, colors.statsKey).join('\n'); -} - -/** - * Render formatted Sigma search results as a string - * - * @param {Object} formattedResults - Formatted search results object - * @returns {string} CLI-ready output - */ -function renderSigmaSearchResults(formattedResults) { - if (!formattedResults || formattedResults.error) { - return formattedResults.error || 'No search results available'; - } - - const headers = ['#', 'Title', 'OS/Product', 'ID']; - const widths = [5, 32, 15, 13]; - - // Create rows from results - const rows = formattedResults.results.map((rule, index) => { - return [ - (index + 1).toString(), - rule.title.substring(0, 31), // Trim to fit column width - rule.osProduct.substring(0, 14), - rule.id.substring(0, 12) - ]; - }); - - // Generate table - const tableLines = formatTable(headers, rows, widths); - - // Add count footer - const countLine = colors.count(`${formattedResults.totalCount} rows in set`); - - return [...tableLines, '', countLine].join('\n'); -} - -module.exports = { - formatSigmaDetails, - formatSigmaStats, - formatSigmaSearchResults, - renderSigmaDetails, - renderSigmaStats, - renderSigmaSearchResults -}; \ No newline at end of file diff --git a/src/fylgja-cli/formatters/table_formatter.js b/src/fylgja-cli/formatters/table_formatter.js deleted file mode 100644 index 1e05046..0000000 --- a/src/fylgja-cli/formatters/table_formatter.js +++ /dev/null @@ -1,97 +0,0 @@ -/** - * table_formatter.js - * - * Table formatting utilities for CLI output - */ -const colors = require('../utils/colors'); -const { wrapText } = require('./text_formatter'); - -/** - * Creates a formatted table row with fixed column widths - * - * @param {string[]} columns - Array of column values - * @param {number[]} widths - Array of column widths - * @param {Function[]} formatters - Array of formatting functions for each column (optional) - * @returns {string} Formatted table row - */ -function formatTableRow(columns, widths, formatters = []) { - return columns.map((value, index) => { - const formatter = formatters[index] || (text => text); - return formatter(String(value || '').padEnd(widths[index])); - }).join(''); -} - -/** - * Formats a header row for a table - * - * @param {string[]} headers - Array of header texts - * @param {number[]} widths - Array of column widths - * @returns {string} Formatted header row - */ -function formatTableHeader(headers, widths) { - return formatTableRow(headers, widths, headers.map(() => colors.header)); -} - -/** - * Formats data as a key-value table - * - * @param {Object} data - Object with key-value pairs to display - * @param {number} keyWidth - Width of the key column - * @param {number} valueWidth - Width of the value column - * @param {Function} keyFormatter - Formatting function for keys - * @returns {string[]} Array of formatted lines - */ -function formatKeyValueTable(data, keyWidth = 25, valueWidth = 50, keyFormatter = colors.key) { - const lines = []; - - for (const [key, value] of Object.entries(data)) { - if (typeof value !== 'object' || value === null) { - const formattedKey = keyFormatter(key.padEnd(keyWidth)); - - // Handle wrapping for multiline values - const wrappedLines = wrapText(value, valueWidth); - - // First line includes the key - lines.push(`${formattedKey}${wrappedLines[0] || ''}`); - - // Additional lines are indented to align with the value column - for (let i = 1; i < wrappedLines.length; i++) { - lines.push(`${' '.repeat(keyWidth)}${wrappedLines[i]}`); - } - } - } - - return lines; -} - -/** - * Creates a full table with headers and rows - * - * @param {string[]} headers - Table headers - * @param {Array>} rows - Table data rows - * @param {number[]} widths - Column widths - * @returns {string[]} Array of formatted table lines - */ -function formatTable(headers, rows, widths) { - const lines = []; - - // Add header - lines.push(formatTableHeader(headers, widths)); - - // Add separator line (optional) - // lines.push(widths.map(w => '-'.repeat(w)).join('')); - - // Add data rows - rows.forEach(row => { - lines.push(formatTableRow(row, widths)); - }); - - return lines; -} - -module.exports = { - formatTableRow, - formatTableHeader, - formatKeyValueTable, - formatTable -}; \ No newline at end of file diff --git a/src/fylgja-cli/formatters/text_formatter.js b/src/fylgja-cli/formatters/text_formatter.js deleted file mode 100644 index 910c44b..0000000 --- a/src/fylgja-cli/formatters/text_formatter.js +++ /dev/null @@ -1,97 +0,0 @@ -/** - * text_formatter.js - * - * Common text formatting utilities for CLI output - */ - -/** - * Wraps text at specified length, handling different data types - * - * @param {any} text - Text to wrap (will be converted to string) - * @param {number} maxWidth - Maximum line width - * @returns {string[]} Array of wrapped lines - */ -function wrapText(text, maxWidth = 80) { - // Handle non-string or empty inputs - if (text === undefined || text === null) return ['']; - - // Convert to string and normalize newlines - const normalizedText = String(text).replace(/\n/g, ' '); - - // If text fits in one line, return it as a single-element array - if (normalizedText.length <= maxWidth) return [normalizedText]; - - const words = normalizedText.split(' '); - const lines = []; - let currentLine = ''; - - for (const word of words) { - // Skip empty words (could happen if there were multiple spaces) - if (!word) continue; - - // If adding this word would exceed max width - if ((currentLine.length + word.length + (currentLine ? 1 : 0)) > maxWidth) { - // Push current line if not empty - if (currentLine) { - lines.push(currentLine); - currentLine = ''; - } - - // If the word itself is longer than maxWidth, we need to split it - if (word.length > maxWidth) { - let remaining = word; - while (remaining.length > 0) { - const chunk = remaining.substring(0, maxWidth); - lines.push(chunk); - remaining = remaining.substring(maxWidth); - } - } else { - currentLine = word; - } - } else { - // Add word to current line - currentLine = currentLine ? `${currentLine} ${word}` : word; - } - } - - // Add the last line if not empty - if (currentLine) { - lines.push(currentLine); - } - - return lines; - } - - /** - * Formats a date string for display - * - * @param {string} dateString - Date string to format - * @returns {string} Formatted date string - */ - function formatDate(dateString) { - if (!dateString) return 'Unknown'; - - try { - const date = new Date(dateString); - return date.toLocaleString(); - } catch (error) { - return dateString; - } - } - - /** - * Formats a number with locale-specific thousands separators - * - * @param {number} num - Number to format - * @returns {string} Formatted number string - */ - function formatNumber(num) { - if (typeof num !== 'number') return String(num || ''); - return num.toLocaleString(); - } - - module.exports = { - wrapText, - formatDate, - formatNumber - }; \ No newline at end of file diff --git a/src/fylgja-cli/utils/cli_logo.js b/src/fylgja-cli/utils/cli_logo.js deleted file mode 100644 index c547d40..0000000 --- a/src/fylgja-cli/utils/cli_logo.js +++ /dev/null @@ -1,105 +0,0 @@ -/** - * cli_logo.js - * - * Gradient-colored ASCII logo for CLI - */ - -// Define the ASCII logo -const logoLines = [ - '░▒▓████████▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░ ░▒▓██████▓▒░ ░▒▓█▓▒░░▒▓██████▓▒░ ', - '░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░ ', - '░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░ ', - '░▒▓██████▓▒░ ░▒▓██████▓▒░░▒▓█▓▒░ ░▒▓█▓▒▒▓███▓▒░ ░▒▓█▓▒░▒▓████████▓▒░ ', - '░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░ ', - '░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░ ', - '░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓████████▓▒░▒▓██████▓▒░ ░▒▓██████▓▒░░▒▓█▓▒░░▒▓█▓▒░ ' - ]; - - // Colors (hex codes without # prefix) - const colors = { - teal: '7DE2D1', - darkTeal: '339989', - offWhite: 'FFFAFB', - lavender: '8F95D3' - }; - - // Convert hex to RGB - function hexToRgb(hex) { - const r = parseInt(hex.substring(0, 2), 16); - const g = parseInt(hex.substring(2, 4), 16); - const b = parseInt(hex.substring(4, 6), 16); - return { r, g, b }; - } - - // Linear interpolation between two colors - function interpolateColor(color1, color2, factor) { - return { - r: Math.round(color1.r + factor * (color2.r - color1.r)), - g: Math.round(color1.g + factor * (color2.g - color1.g)), - b: Math.round(color1.b + factor * (color2.b - color1.b)) - }; - } - - // Get ANSI true color escape code - function getTrueColorCode(r, g, b) { - return `\x1b[38;2;${r};${g};${b}m`; - } - - // Get ANSI 256-color escape code (fallback for terminals without true color support) - function get256ColorCode(r, g, b) { - // Convert RGB to approximate 256-color code - const ansi256 = 16 + - 36 * Math.round(r * 5 / 255) + - 6 * Math.round(g * 5 / 255) + - Math.round(b * 5 / 255); - - return `\x1b[38;5;${ansi256}m`; - } - - /** - * Generate a 2D gradient (both horizontal and vertical gradients combined) - * This creates the most dramatic effect but is more processing-intensive - * @returns {string} The gradient-colored logo - */ - function generateGradientLogo() { - // Convert hex colors to RGB (corners of the 2D gradient) - const topLeft = hexToRgb(colors.teal); - const topRight = hexToRgb(colors.darkTeal); - const bottomLeft = hexToRgb(colors.offWhite); - const bottomRight = hexToRgb(colors.lavender); - - // Initialize gradient logo - let gradientLogo = '\n'; - - // Process each line - for (let y = 0; y < logoLines.length; y++) { - const line = logoLines[y]; - const verticalPosition = y / (logoLines.length - 1); - - // Interpolate top and bottom colors - const leftColor = interpolateColor(topLeft, bottomLeft, verticalPosition); - const rightColor = interpolateColor(topRight, bottomRight, verticalPosition); - - // Process each character in the line - for (let x = 0; x < line.length; x++) { - const char = line[x]; - const horizontalPosition = x / (line.length - 1); - - // Interpolate between left and right colors - const color = interpolateColor(leftColor, rightColor, horizontalPosition); - - // Apply the color and add the character - gradientLogo += getTrueColorCode(color.r, color.g, color.b) + char; - } - - // Reset color and add newline - gradientLogo += '\x1b[0m\n'; - } - - return gradientLogo; - } - - // Export the gradient logo functions - module.exports = { - generateGradientLogo, - }; \ No newline at end of file diff --git a/src/fylgja-cli/utils/colors.js b/src/fylgja-cli/utils/colors.js deleted file mode 100644 index 3810479..0000000 --- a/src/fylgja-cli/utils/colors.js +++ /dev/null @@ -1,63 +0,0 @@ -/** - * colors.js - * - * Centralized color definitions and formatting functions for CLI output - * Using direct ANSI color codes instead of chalk - */ - -// Define ANSI color codes -const ansiCodes = { - // Text formatting - reset: '\x1b[0m', - bold: '\x1b[1m', - dim: '\x1b[2m', - underscore: '\x1b[4m', - blink: '\x1b[5m', - reverse: '\x1b[7m', - hidden: '\x1b[8m', - - // Foreground colors - black: '\x1b[30m', - red: '\x1b[31m', - green: '\x1b[32m', - yellow: '\x1b[33m', - blue: '\x1b[34m', - magenta: '\x1b[35m', - cyan: '\x1b[36m', - white: '\x1b[37m', - - // Background colors - bgBlack: '\x1b[40m', - bgRed: '\x1b[41m', - bgGreen: '\x1b[42m', - bgYellow: '\x1b[43m', - bgBlue: '\x1b[44m', - bgMagenta: '\x1b[45m', - bgCyan: '\x1b[46m', - bgWhite: '\x1b[47m' - }; - - // Create color formatting functions - const colors = { - // Basic formatting functions - blue: (text) => `${ansiCodes.blue}${text}${ansiCodes.reset}`, - green: (text) => `${ansiCodes.green}${text}${ansiCodes.reset}`, - red: (text) => `${ansiCodes.red}${text}${ansiCodes.reset}`, - yellow: (text) => `${ansiCodes.yellow}${text}${ansiCodes.reset}`, - cyan: (text) => `${ansiCodes.cyan}${text}${ansiCodes.reset}`, - white: (text) => `${ansiCodes.white}${text}${ansiCodes.reset}`, - dim: (text) => `${ansiCodes.dim}${text}${ansiCodes.reset}`, - bold: (text) => `${ansiCodes.bold}${text}${ansiCodes.reset}`, - - // Composite styles for specific UI elements - header: (text) => `${ansiCodes.cyan}${ansiCodes.bold}${text}${ansiCodes.reset}`, - key: (text) => `${ansiCodes.blue}${text}${ansiCodes.reset}`, - statsKey: (text) => `${ansiCodes.yellow}${text}${ansiCodes.reset}`, - value: (text) => `${ansiCodes.white}${text}${ansiCodes.reset}`, - count: (text) => `${ansiCodes.green}${text}${ansiCodes.reset}`, - error: (text) => `${ansiCodes.red}${text}${ansiCodes.reset}`, - warning: (text) => `${ansiCodes.yellow}${text}${ansiCodes.reset}`, - success: (text) => `${ansiCodes.green}${ansiCodes.bold}${text}${ansiCodes.reset}` - }; - - module.exports = colors; \ No newline at end of file diff --git a/src/handlers/sigma/sigma_search_handler.js b/src/handlers/sigma/sigma_search_handler.js index 7613239..2932b8d 100644 --- a/src/handlers/sigma/sigma_search_handler.js +++ b/src/handlers/sigma/sigma_search_handler.js @@ -147,7 +147,6 @@ const handleCommand = async (command, respond) => { // Respond with the search results await respond({ blocks: blocks, - responseData: searchResult.results, response_type: isEphemeral ? 'ephemeral' : 'in_channel' }); @@ -272,7 +271,6 @@ const handleComplexSearch = async (command, respond) => { // Respond with the search results await respond({ blocks: blocks, - responseData: searchResult.results, response_type: 'ephemeral' // Complex searches are usually more specific to the user }); diff --git a/src/lang/command_patterns.js b/src/lang/command_patterns.js index 7180deb..b95c582 100644 --- a/src/lang/command_patterns.js +++ b/src/lang/command_patterns.js @@ -17,7 +17,7 @@ const commandPatterns = [ // Sigma details patterns { - name: 'details-sigma', + name: 'sigma-details', regex: /^details\s+sigma\s+(.+)$/i, action: 'details', module: 'sigma', @@ -25,28 +25,13 @@ const commandPatterns = [ }, // Sigma search patterns { - name: 'search-sigma-complex-1', - regex: /^search\s+sigma\s+rules?\s*(where|with)\s+(.+)$/i, + name: 'sigma-search', + regex: /^(search|find)\s+(sigma\s+)?(rules|detections)?\s*(where|with)\s+(.+)$/i, action: 'complexSearch', module: 'sigma', - params: [4] // complex query conditions in capturing group 4 - }, - // Alternate form without "rules" - { - name: 'search-sigma-complex-2', - regex: /^search\s+sigma\s+(where|with)\s+(.+)$/i, - action: 'complexSearch', - module: 'sigma', - params: [3] // complex query conditions in capturing group 3 - }, - // Simple keyword search pattern - { - name: 'search-sigma-simple', - regex: /^search\s+sigma\s+(.+)$/i, - action: 'search', - module: 'sigma', - params: [2] // keyword is in capturing group 2 + params: [5] // complex query conditions in capturing group 5 }, + // Sigma create patterns { name: 'sigma-create', @@ -56,8 +41,16 @@ const commandPatterns = [ params: [2] // rule ID is in capturing group 2 }, + // Sigma stats patterns { - name: 'stats-sigma', + name: 'sigma-stats-first', + regex: /^sigma\s+stats$/i, + action: 'stats', + module: 'sigma', + params: [] + }, + { + name: 'sigma-stats-second', regex: /^stats\s+sigma$/i, action: 'stats', module: 'sigma', diff --git a/src/utils/cli_formatters.js b/src/utils/cli_formatters.js index 1ef9e8e..9914284 100644 --- a/src/utils/cli_formatters.js +++ b/src/utils/cli_formatters.js @@ -6,7 +6,6 @@ */ const chalk = require('chalk'); - /** * Wraps text at specified length * @param {string} text - Text to wrap @@ -147,27 +146,16 @@ function formatSigmaSearchResults(searchResults) { // Return a structure with results and meta info return { - results: searchResults.results.map(rule => { - // Get logsource.product field - let osProduct = 'N/A'; - - // Only use logsource.product if it exists - if (rule.logsource && rule.logsource.product) { - osProduct = rule.logsource.product; - } - - return { - id: rule.id || '', - title: wrapText(rule.title || '', 60), // Use narrower width for table columns - author: rule.author || 'Unknown', - level: rule.level || 'medium', - osProduct: osProduct, - tags: rule.tags || [] // Include the original tags for potential reference - }; - }), + results: searchResults.results.map(rule => ({ + id: rule.id || '', + title: wrapText(rule.title || '', 60), // Use narrower width for table columns + author: rule.author || 'Unknown', + level: rule.level || 'medium' + })), totalCount: searchResults.totalCount || 0 }; } + module.exports = { formatSigmaStats, formatSigmaSearchResults,