refactor cli into multiple files
This commit is contained in:
parent
853b60d762
commit
34143c4241
10 changed files with 1065 additions and 768 deletions
438
src/fylgja-cli/cli.js
Normal file
438
src/fylgja-cli/cli.js
Normal file
|
@ -0,0 +1,438 @@
|
|||
/**
|
||||
* 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
|
||||
};
|
||||
}
|
||||
|
Loading…
Add table
Add a link
Reference in a new issue