453 lines
No EOL
13 KiB
JavaScript
453 lines
No EOL
13 KiB
JavaScript
/**
|
|
* cli.js
|
|
*
|
|
* Interactive CLI interface
|
|
*/
|
|
const readline = require('readline');
|
|
const { parseCommand } = require('../lang/command_parser');
|
|
const logger = require('../utils/logger');
|
|
const { generateGradientLogo, generateNormalLogo } = require('./utils/cli_logo');
|
|
const outputManager = require('./cli_output_manager');
|
|
const handlerRegistry = require('../handlers/handler_registry');
|
|
|
|
// 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> '
|
|
});
|
|
|
|
// 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 hits = availableCommands.filter((c) => c.startsWith(line));
|
|
return [hits.length ? hits : availableCommands, 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 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 {Object} context - Command execution context
|
|
* @returns {Function} A respond function for handler callbacks
|
|
*/
|
|
function createRespondFunction(context) {
|
|
// 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) {
|
|
const { module, action } = context.meta;
|
|
|
|
// Display data based on module and action type
|
|
if (module === 'sigma') {
|
|
if (['search', 'complexSearch'].includes(action)) {
|
|
// Convert array response to expected format if needed
|
|
let dataToFormat = response.responseData;
|
|
|
|
// Wrap responseData array in proper structure if needed
|
|
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();
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
async function processCommand(input) {
|
|
try {
|
|
// Skip empty commands
|
|
if (!input.trim()) {
|
|
rl.prompt();
|
|
return;
|
|
}
|
|
|
|
// Add to command history
|
|
commandHistory.push(input);
|
|
historyIndex = commandHistory.length;
|
|
|
|
// Handle built-in commands
|
|
if (handleBuiltInCommands(input)) {
|
|
return;
|
|
}
|
|
|
|
// Handle simple search
|
|
if (await handleSimpleSearch(input)) {
|
|
return;
|
|
}
|
|
|
|
// Handle complex search
|
|
if (await handleComplexSearch(input)) {
|
|
return;
|
|
}
|
|
|
|
// Parse command using existing parser for everything else
|
|
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 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(context);
|
|
|
|
try {
|
|
// 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);
|
|
// 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(generateNormalLogo());
|
|
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
|
|
};
|
|
} |