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,36 +34,37 @@ const rl = readline.createInterface({
prompt: 'fylgja> '
});
// Available commands for auto-completion
const availableCommands = [
'search sigma',
'details sigma',
'stats sigma',
'search sigma rules where title contains',
'search sigma where title contains',
'search sigma rules where tags include',
'search sigma where tags include',
'search sigma rules where logsource.category ==',
'search sigma where logsource.category ==',
'search sigma rules where modified after',
'search sigma where modified after',
'search sigma rules where author is',
'search sigma where author is',
'search sigma rules where level is',
'search sigma where level is',
'help',
'exit',
'quit',
'clear'
];
/**
* Command auto-completion function
* @param {string} line Current command line input
* @returns {Array} Array with possible completions and the substring being completed
*/
function completer(line) {
const commands = [
'search sigma',
'details sigma',
'stats sigma',
'search sigma rules where title contains',
'search sigma where title contains',
'search sigma rules where tags include',
'search sigma where tags include',
'search sigma rules where logsource.category ==',
'search sigma where logsource.category ==',
'search sigma rules where modified after',
'search sigma where modified after',
'search sigma rules where author is',
'search sigma where author is',
'search sigma rules where level is',
'search sigma where level is',
'help',
'exit',
'quit',
'clear'
];
const hits = commands.filter((c) => c.startsWith(line));
return [hits.length ? hits : commands, line];
const hits = availableCommands.filter((c) => c.startsWith(line));
return [hits.length ? hits : availableCommands, line];
}
/**
@ -98,14 +90,39 @@ function extractSearchKeywords(input) {
return input;
}
/**
* Create a standardized command context object
* @param {string} text - Command text
* @param {string} module - Module name
* @param {string} action - Action name
* @param {Array} params - Command parameters
* @returns {Object} Command context object
*/
function createCommandContext(text, module, action, params) {
return {
command: {
text,
user_id: 'cli_user',
user_name: 'cli_user',
command: '/fylgja',
channel_id: 'cli',
channel_name: 'cli'
},
meta: {
module,
action,
params,
source: 'cli'
}
};
}
/**
* Create a custom respond function for handlers
* @param {string} action - The action being performed
* @param {string} module - The module handling the action
* @param {Array} params - The parameters for the action
* @param {Object} context - Command execution context
* @returns {Function} A respond function for handler callbacks
*/
function createRespondFunction(action, module, params) {
function createRespondFunction(context) {
// Keep track of whether we're waiting for results
let isWaitingForResults = false;
@ -132,12 +149,15 @@ function createRespondFunction(action, module, params) {
// Check for the responseData property (directly from service)
if (response.responseData) {
const { module, action } = context.meta;
// Display data based on module and action type
if (module === 'sigma') {
if (action === 'search' || action === 'complexSearch') {
if (['search', 'complexSearch'].includes(action)) {
// Convert array response to expected format if needed
let dataToFormat = response.responseData;
// Wrap responseData array in proper structure
// Wrap responseData array in proper structure if needed
if (Array.isArray(dataToFormat)) {
dataToFormat = {
results: dataToFormat,
@ -171,6 +191,118 @@ function createRespondFunction(action, module, params) {
};
}
/**
* Handle built-in CLI commands
* @param {string} input User input command
* @returns {boolean} True if command was handled, false otherwise
*/
function handleBuiltInCommands(input) {
const trimmedInput = input.trim().toLowerCase();
if (trimmedInput === 'exit' || trimmedInput === 'quit') {
outputManager.displaySuccess('Goodbye!');
rl.close();
process.exit(0);
return true;
}
if (trimmedInput === 'clear') {
console.clear();
rl.prompt();
return true;
}
if (trimmedInput === 'help') {
outputManager.displayHelp();
rl.prompt();
return true;
}
return false;
}
/**
* Handle simple search commands
* @param {string} input User input command
* @returns {boolean} True if command was handled, false otherwise
*/
async function handleSimpleSearch(input) {
const match = input.trim().match(/^search\s+sigma\s+(.+)$/i);
if (match && !input.trim().toLowerCase().includes('where') && !input.trim().toLowerCase().includes('with')) {
const keyword = match[1];
const context = createCommandContext(keyword, 'sigma', 'search', [keyword]);
const respond = createRespondFunction(context);
console.log(`Executing: module=sigma, action=search, params=[${keyword}]`);
try {
// Get handler from registry
const handler = handlerRegistry.getHandler('sigma', 'search');
if (handler) {
await handler.handleCommand(context.command, respond);
} else {
outputManager.displayError('Handler not found for sigma search');
rl.prompt();
}
} catch (error) {
outputManager.displayError(error.message);
logger.error(`${FILE_NAME}: Command execution error: ${error.message}`);
logger.debug(`${FILE_NAME}: Error stack: ${error.stack}`);
rl.prompt();
}
return true;
}
return false;
}
/**
* Handle complex search commands
* @param {string} input User input command
* @returns {boolean} True if command was handled, false otherwise
*/
async function handleComplexSearch(input) {
const complexSearchMatch = input.trim().match(/^search\s+sigma\s+(rules\s+|detections\s+)?(where|with)\s+(.+)$/i);
if (complexSearchMatch) {
const complexQuery = complexSearchMatch[3];
const searchTerms = extractSearchKeywords(complexQuery);
const context = createCommandContext(
searchTerms || complexQuery,
'sigma',
'complexSearch',
[complexQuery]
);
const respond = createRespondFunction(context);
console.log(`Executing: module=sigma, action=complexSearch, params=[${complexQuery}]`);
try {
// Get handler from registry
const handler = handlerRegistry.getHandler('sigma', 'search');
if (handler) {
await handler.handleCommand(context.command, respond);
} else {
outputManager.displayError('Handler not found for sigma complex search');
rl.prompt();
}
} catch (error) {
outputManager.displayError(error.message);
logger.error(`${FILE_NAME}: Command execution error: ${error.message}`);
logger.debug(`${FILE_NAME}: Error stack: ${error.stack}`);
rl.prompt();
}
return true;
}
return false;
}
/**
* Process a command from the CLI
* @param {string} input User input command
@ -183,93 +315,26 @@ async function processCommand(input) {
return;
}
// Special CLI commands
if (input.trim().toLowerCase() === 'exit' || input.trim().toLowerCase() === 'quit') {
outputManager.displaySuccess('Goodbye!');
rl.close();
process.exit(0);
}
if (input.trim().toLowerCase() === 'clear') {
console.clear();
rl.prompt();
return;
}
if (input.trim().toLowerCase() === 'help') {
outputManager.displayHelp();
rl.prompt();
return;
}
// Add to command history
commandHistory.push(input);
historyIndex = commandHistory.length;
// Special case for simple search
if (input.trim().match(/^search\s+sigma\s+(.+)$/i) && !input.trim().toLowerCase().includes('where') && !input.trim().toLowerCase().includes('with')) {
const keyword = input.trim().match(/^search\s+sigma\s+(.+)$/i)[1];
// Create fake command object
const command = {
text: keyword,
user_id: 'cli_user',
user_name: 'cli_user',
command: '/fylgja',
channel_id: 'cli',
channel_name: 'cli'
};
// Create custom respond function
const respond = createRespondFunction('search', 'sigma', [keyword]);
console.log(`Executing: module=sigma, action=search, params=[${keyword}]`);
try {
await sigmaSearchHandler.handleCommand(command, respond);
} catch (error) {
outputManager.displayError(error.message);
logger.error(`${FILE_NAME}: Command execution error: ${error.message}`);
logger.debug(`${FILE_NAME}: Error stack: ${error.stack}`);
rl.prompt();
}
// Handle built-in commands
if (handleBuiltInCommands(input)) {
return;
}
// Special case for complex search
const complexSearchMatch = input.trim().match(/^search\s+sigma\s+(rules\s+|detections\s+)?(where|with)\s+(.+)$/i);
if (complexSearchMatch) {
const complexQuery = complexSearchMatch[3];
// Create fake command object
const command = {
text: complexQuery,
user_id: 'cli_user',
user_name: 'cli_user',
command: '/fylgja',
channel_id: 'cli',
channel_name: 'cli'
};
// Create custom respond function
const respond = createRespondFunction('complexSearch', 'sigma', [complexQuery]);
console.log(`Executing: module=sigma, action=complexSearch, params=[${complexQuery}]`);
try {
await sigmaSearchHandler.handleCommand(command, respond);
} catch (error) {
outputManager.displayError(error.message);
logger.error(`${FILE_NAME}: Command execution error: ${error.message}`);
logger.debug(`${FILE_NAME}: Error stack: ${error.stack}`);
rl.prompt();
}
// Handle simple search
if (await handleSimpleSearch(input)) {
return;
}
// Parse command using existing parser
// Handle complex search
if (await handleComplexSearch(input)) {
return;
}
// Parse command using existing parser for everything else
const parsedCommand = await parseCommand(input);
if (!parsedCommand.success) {
@ -284,72 +349,26 @@ async function processCommand(input) {
// Only show execution info to the user, not sending to logger
console.log(`Executing: module=${module}, action=${action}, params=[${params}]`);
// Create fake command object similar to Slack's
const command = {
text: Array.isArray(params) && params.length > 0 ? params[0] : input,
user_id: 'cli_user',
user_name: 'cli_user',
command: '/fylgja',
channel_id: 'cli',
channel_name: 'cli'
};
// Special handling for complexSearch to extract keywords
if (action === 'complexSearch' && module === 'sigma' && params.length > 0) {
// Try to extract keywords from complex queries
const searchTerms = extractSearchKeywords(params[0]);
command.text = searchTerms || params[0];
}
// Create command context
const context = createCommandContext(
Array.isArray(params) && params.length > 0 ? params[0] : input,
module,
action,
params
);
// Create custom respond function for CLI
const respond = createRespondFunction(action, module, params);
const respond = createRespondFunction(context);
try {
switch (module) {
case 'sigma':
switch (action) {
case 'search':
case 'complexSearch':
await sigmaSearchHandler.handleCommand(command, respond);
break;
case 'details':
await sigmaDetailsHandler.handleCommand(command, respond);
break;
case 'stats':
await sigmaStatsHandler.handleCommand(command, respond);
break;
case 'create':
await sigmaCreateHandler.handleCommand(command, respond);
break;
default:
outputManager.displayWarning(`Unknown Sigma action: ${action}`);
rl.prompt();
}
break;
case 'alerts':
await handleAlerts(command, respond);
break;
case 'case':
await handleCase(command, respond);
break;
case 'config':
await handleConfig(command, respond);
break;
case 'stats':
await handleStats(command, respond);
break;
default:
outputManager.displayWarning(`Unknown module: ${module}`);
rl.prompt();
// Get handler from registry
const handler = handlerRegistry.getHandler(module, action);
if (handler) {
await handler.handleCommand(context.command, respond);
} else {
outputManager.displayWarning(`Unknown handler for ${module}.${action}`);
rl.prompt();
}
} catch (error) {
outputManager.displayError(error.message);
@ -431,5 +450,4 @@ if (require.main === module) {
module.exports = {
startCLI
};
}
}

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