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 { 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,13 +34,8 @@ const rl = readline.createInterface({
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 = [
// Available commands for auto-completion
const availableCommands = [
'search sigma',
'details sigma',
'stats sigma',
@ -69,10 +55,16 @@ function completer(line) {
'exit',
'quit',
'clear'
];
];
const hits = commands.filter((c) => c.startsWith(line));
return [hits.length ? hits : commands, line];
/**
* 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];
}
/**
@ -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,71 +349,25 @@ 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;
// Get handler from registry
const handler = handlerRegistry.getHandler(module, action);
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}`);
if (handler) {
await handler.handleCommand(context.command, respond);
} else {
outputManager.displayWarning(`Unknown handler for ${module}.${action}`);
rl.prompt();
}
} catch (error) {
@ -432,4 +451,3 @@ if (require.main === module) {
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
};