refactor CLI and decouple from handlers

This commit is contained in:
Charlotte Croce 2025-04-20 22:21:00 -04:00
parent 1b8ba03c8b
commit f90a08dcde
2 changed files with 272 additions and 181 deletions

View file

@ -8,16 +8,7 @@ const { parseCommand } = require('../lang/command_parser');
const logger = require('../utils/logger'); const logger = require('../utils/logger');
const { generateGradientLogo } = require('./utils/cli_logo'); const { generateGradientLogo } = require('./utils/cli_logo');
const outputManager = require('./cli_output_manager'); const outputManager = require('./cli_output_manager');
const handlerRegistry = require('../handlers/handler_registry');
// 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');
// Set constants // Set constants
const FILE_NAME = 'cli.js'; const FILE_NAME = 'cli.js';
@ -43,36 +34,37 @@ const rl = readline.createInterface({
prompt: 'fylgja> ' 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 * Command auto-completion function
* @param {string} line Current command line input * @param {string} line Current command line input
* @returns {Array} Array with possible completions and the substring being completed * @returns {Array} Array with possible completions and the substring being completed
*/ */
function completer(line) { function completer(line) {
const commands = [ const hits = availableCommands.filter((c) => c.startsWith(line));
'search sigma', return [hits.length ? hits : availableCommands, line];
'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];
} }
/** /**
@ -98,14 +90,39 @@ function extractSearchKeywords(input) {
return 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 * Create a custom respond function for handlers
* @param {string} action - The action being performed * @param {Object} context - Command execution context
* @param {string} module - The module handling the action
* @param {Array} params - The parameters for the action
* @returns {Function} A respond function for handler callbacks * @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 // Keep track of whether we're waiting for results
let isWaitingForResults = false; let isWaitingForResults = false;
@ -132,12 +149,15 @@ function createRespondFunction(action, module, params) {
// Check for the responseData property (directly from service) // Check for the responseData property (directly from service)
if (response.responseData) { if (response.responseData) {
const { module, action } = context.meta;
// Display data based on module and action type
if (module === 'sigma') { if (module === 'sigma') {
if (action === 'search' || action === 'complexSearch') { if (['search', 'complexSearch'].includes(action)) {
// Convert array response to expected format if needed // Convert array response to expected format if needed
let dataToFormat = response.responseData; let dataToFormat = response.responseData;
// Wrap responseData array in proper structure // Wrap responseData array in proper structure if needed
if (Array.isArray(dataToFormat)) { if (Array.isArray(dataToFormat)) {
dataToFormat = { dataToFormat = {
results: 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 * Process a command from the CLI
* @param {string} input User input command * @param {string} input User input command
@ -183,93 +315,26 @@ async function processCommand(input) {
return; 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 // Add to command history
commandHistory.push(input); commandHistory.push(input);
historyIndex = commandHistory.length; historyIndex = commandHistory.length;
// Special case for simple search // Handle built-in commands
if (input.trim().match(/^search\s+sigma\s+(.+)$/i) && !input.trim().toLowerCase().includes('where') && !input.trim().toLowerCase().includes('with')) { if (handleBuiltInCommands(input)) {
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; return;
} }
// Special case for complex search // Handle simple search
const complexSearchMatch = input.trim().match(/^search\s+sigma\s+(rules\s+|detections\s+)?(where|with)\s+(.+)$/i); if (await handleSimpleSearch(input)) {
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();
}
return; 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); const parsedCommand = await parseCommand(input);
if (!parsedCommand.success) { if (!parsedCommand.success) {
@ -284,72 +349,26 @@ async function processCommand(input) {
// Only show execution info to the user, not sending to logger // Only show execution info to the user, not sending to logger
console.log(`Executing: module=${module}, action=${action}, params=[${params}]`); console.log(`Executing: module=${module}, action=${action}, params=[${params}]`);
// Create fake command object similar to Slack's // Create command context
const command = { const context = createCommandContext(
text: Array.isArray(params) && params.length > 0 ? params[0] : input, Array.isArray(params) && params.length > 0 ? params[0] : input,
user_id: 'cli_user', module,
user_name: 'cli_user', action,
command: '/fylgja', params
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 // Create custom respond function for CLI
const respond = createRespondFunction(action, module, params); const respond = createRespondFunction(context);
try { try {
switch (module) { // Get handler from registry
case 'sigma': const handler = handlerRegistry.getHandler(module, action);
switch (action) {
case 'search': if (handler) {
case 'complexSearch': await handler.handleCommand(context.command, respond);
await sigmaSearchHandler.handleCommand(command, respond); } else {
break; outputManager.displayWarning(`Unknown handler for ${module}.${action}`);
rl.prompt();
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) { } catch (error) {
outputManager.displayError(error.message); outputManager.displayError(error.message);
@ -431,5 +450,4 @@ if (require.main === module) {
module.exports = { module.exports = {
startCLI startCLI
}; };
} }

View file

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