format CLI tables
This commit is contained in:
parent
fd394fff36
commit
845440962d
2 changed files with 226 additions and 87 deletions
|
@ -36,10 +36,10 @@ const { handleCommand: handleConfig } = require('./handlers/config/config_handle
|
||||||
const { handleCommand: handleStats } = require('./handlers/stats/stats_handler');
|
const { handleCommand: handleStats } = require('./handlers/stats/stats_handler');
|
||||||
|
|
||||||
// Import CLI formatters
|
// Import CLI formatters
|
||||||
const {
|
const {
|
||||||
formatSigmaStats,
|
formatSigmaStats,
|
||||||
formatSigmaSearchResults,
|
formatSigmaSearchResults,
|
||||||
formatSigmaDetails
|
formatSigmaDetails
|
||||||
} = require('./utils/cli_formatters');
|
} = require('./utils/cli_formatters');
|
||||||
|
|
||||||
// Set logger to CLI mode (prevents console output)
|
// Set logger to CLI mode (prevents console output)
|
||||||
|
@ -86,24 +86,83 @@ const rl = readline.createInterface({
|
||||||
*/
|
*/
|
||||||
function completer(line) {
|
function completer(line) {
|
||||||
const commands = [
|
const commands = [
|
||||||
'search sigma',
|
'search sigma',
|
||||||
'details sigma',
|
'details sigma',
|
||||||
'sigma stats',
|
'sigma stats',
|
||||||
'stats sigma',
|
'stats sigma',
|
||||||
'search sigma rules where title contains',
|
'search sigma rules where title contains',
|
||||||
'search rules where tags include',
|
'search rules where tags include',
|
||||||
'search rules where logsource.category ==',
|
'search rules where logsource.category ==',
|
||||||
'search rules where modified after',
|
'search rules where modified after',
|
||||||
'help',
|
'help',
|
||||||
'exit',
|
'exit',
|
||||||
'quit',
|
'quit',
|
||||||
'clear'
|
'clear'
|
||||||
];
|
];
|
||||||
|
|
||||||
const hits = commands.filter((c) => c.startsWith(line));
|
const hits = commands.filter((c) => c.startsWith(line));
|
||||||
return [hits.length ? hits : commands, line];
|
return [hits.length ? hits : commands, line];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize and wrap text for table display
|
||||||
|
* @param {string} text Text to normalize and wrap
|
||||||
|
* @param {number} maxWidth Maximum width per line
|
||||||
|
* @returns {string[]} Array of wrapped lines
|
||||||
|
*/
|
||||||
|
function normalizeAndWrap(text, maxWidth) {
|
||||||
|
if (!text) return [''];
|
||||||
|
|
||||||
|
// Convert to string and normalize newlines
|
||||||
|
text = String(text || '');
|
||||||
|
|
||||||
|
// Replace all literal newlines with spaces
|
||||||
|
text = text.replace(/\n/g, ' ');
|
||||||
|
|
||||||
|
// Now apply word wrapping
|
||||||
|
if (text.length <= maxWidth) return [text];
|
||||||
|
|
||||||
|
const words = text.split(' ');
|
||||||
|
const lines = [];
|
||||||
|
let currentLine = '';
|
||||||
|
|
||||||
|
for (const word of words) {
|
||||||
|
// Skip empty words (could happen if there were multiple spaces)
|
||||||
|
if (!word) continue;
|
||||||
|
|
||||||
|
// If adding this word would exceed max width
|
||||||
|
if ((currentLine.length + word.length + (currentLine ? 1 : 0)) > maxWidth) {
|
||||||
|
// Push current line if not empty
|
||||||
|
if (currentLine) {
|
||||||
|
lines.push(currentLine);
|
||||||
|
currentLine = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the word itself is longer than maxWidth, we need to split it
|
||||||
|
if (word.length > maxWidth) {
|
||||||
|
let remaining = word;
|
||||||
|
while (remaining.length > 0) {
|
||||||
|
const chunk = remaining.substring(0, maxWidth);
|
||||||
|
lines.push(chunk);
|
||||||
|
remaining = remaining.substring(maxWidth);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
currentLine = word;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Add word to current line
|
||||||
|
currentLine = currentLine ? `${currentLine} ${word}` : word;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the last line if not empty
|
||||||
|
if (currentLine) {
|
||||||
|
lines.push(currentLine);
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format CLI output similar to MySQL
|
* Format CLI output similar to MySQL
|
||||||
* @param {Object} data The data to format
|
* @param {Object} data The data to format
|
||||||
|
@ -117,59 +176,105 @@ function formatOutput(data, type) {
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'search_results':
|
case 'search_results':
|
||||||
|
// Search results table format remains the same
|
||||||
console.log('\n+-------+----------------------+------------------+-------------+');
|
console.log('\n+-------+----------------------+------------------+-------------+');
|
||||||
console.log('| ID | Title | Author | Level |');
|
console.log('| ID | Title | Author | Level |');
|
||||||
console.log('+-------+----------------------+------------------+-------------+');
|
console.log('+-------+----------------------+------------------+-------------+');
|
||||||
|
|
||||||
if (data.results && data.results.length > 0) {
|
if (data.results && data.results.length > 0) {
|
||||||
data.results.forEach(rule => {
|
data.results.forEach(rule => {
|
||||||
const id = (rule.id || '').padEnd(5).substring(0, 5);
|
const id = (rule.id || '').padEnd(5).substring(0, 5);
|
||||||
const title = (rule.title || '').padEnd(20).substring(0, 20);
|
const title = (rule.title || '').padEnd(20).substring(0, 20);
|
||||||
const author = (rule.author || 'Unknown').padEnd(16).substring(0, 16);
|
const author = (rule.author || 'Unknown').padEnd(16).substring(0, 16);
|
||||||
const level = (rule.level || 'medium').padEnd(11).substring(0, 11);
|
const level = (rule.level || 'medium').padEnd(11).substring(0, 11);
|
||||||
|
|
||||||
console.log(`| ${id} | ${title} | ${author} | ${level} |`);
|
console.log(`| ${id} | ${title} | ${author} | ${level} |`);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
console.log('| No results found |');
|
console.log('| No results found |');
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('+-------+----------------------+------------------+-------------+');
|
console.log('+-------+----------------------+------------------+-------------+');
|
||||||
console.log(`${data.totalCount || 0} rows in set`);
|
console.log(`${data.totalCount || 0} rows in set`);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'details':
|
case 'details':
|
||||||
console.log('\n+----------------------+--------------------------------------------------+');
|
// Set a fixed width for the entire table
|
||||||
console.log('| Field | Value |');
|
const sigmaDetailsKeyWidth = 22;
|
||||||
console.log('+----------------------+--------------------------------------------------+');
|
const sigmaDetailsValueWidth = 50;
|
||||||
|
|
||||||
|
// Create the table borders
|
||||||
|
const detailsHeaderLine = '╔' + '═'.repeat(sigmaDetailsKeyWidth) + '╦' + '═'.repeat(sigmaDetailsValueWidth) + '╗';
|
||||||
|
const sigmaDetailsDividerLine = '╠' + '═'.repeat(sigmaDetailsKeyWidth) + '╬' + '═'.repeat(sigmaDetailsValueWidth) + '╣';
|
||||||
|
const sigmaDetailsRowSeparator = '╟' + '─'.repeat(sigmaDetailsKeyWidth) + '╫' + '─'.repeat(sigmaDetailsValueWidth) + '╢';
|
||||||
|
const sigmaDetailsFooterLine = '╚' + '═'.repeat(sigmaDetailsKeyWidth) + '╩' + '═'.repeat(sigmaDetailsValueWidth) + '╝';
|
||||||
|
|
||||||
|
console.log('\n' + detailsHeaderLine);
|
||||||
|
console.log(`║ ${'Field'.padEnd(sigmaDetailsKeyWidth - 2)} ║ ${'Value'.padEnd(sigmaDetailsValueWidth - 2)} ║`);
|
||||||
|
console.log(sigmaDetailsDividerLine);
|
||||||
|
|
||||||
|
// Track whether we need to add a row separator
|
||||||
|
let isFirstRow = true;
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(data)) {
|
for (const [key, value] of Object.entries(data)) {
|
||||||
if (typeof value !== 'object' || value === null) {
|
if (typeof value !== 'object' || value === null) {
|
||||||
const formattedKey = key.padEnd(20).substring(0, 20);
|
// Add separator between rows (but not before the first row)
|
||||||
const formattedValue = String(value || '').padEnd(48).substring(0, 48);
|
if (!isFirstRow) {
|
||||||
|
console.log(sigmaDetailsRowSeparator);
|
||||||
console.log(`| ${formattedKey} | ${formattedValue} |`);
|
}
|
||||||
|
isFirstRow = false;
|
||||||
|
|
||||||
|
const formattedKey = key.padEnd(sigmaDetailsKeyWidth - 2);
|
||||||
|
|
||||||
|
// Handle wrapping
|
||||||
|
const lines = normalizeAndWrap(value, sigmaDetailsValueWidth - 2);
|
||||||
|
|
||||||
|
// Print first line with the key
|
||||||
|
console.log(`║ ${formattedKey} ║ ${lines[0].padEnd(sigmaDetailsValueWidth - 2)} ║`);
|
||||||
|
|
||||||
|
// Print additional lines if there are any
|
||||||
|
for (let i = 1; i < lines.length; i++) {
|
||||||
|
console.log(`║ ${' '.repeat(sigmaDetailsKeyWidth - 2)} ║ ${lines[i].padEnd(sigmaDetailsValueWidth - 2)} ║`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('+----------------------+--------------------------------------------------+');
|
console.log(sigmaDetailsFooterLine);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'stats':
|
case 'stats':
|
||||||
console.log('\n+--------------------+---------------+');
|
// Set column widths
|
||||||
console.log('| Metric | Value |');
|
const sigmaStatsMetricWidth = 25;
|
||||||
console.log('+--------------------+---------------+');
|
const sigmaStatsValueWidth = 26;
|
||||||
|
|
||||||
|
// Create the table borders
|
||||||
|
const sigmaStatsHeaderLine = '╔' + '═'.repeat(sigmaStatsMetricWidth) + '╦' + '═'.repeat(sigmaStatsValueWidth) + '╗';
|
||||||
|
const sigmaStatsDividerLine = '╠' + '═'.repeat(sigmaStatsMetricWidth) + '╬' + '═'.repeat(sigmaStatsValueWidth) + '╣';
|
||||||
|
const sigmaStatsRowSeparator = '╟' + '─'.repeat(sigmaStatsMetricWidth) + '╫' + '─'.repeat(sigmaStatsValueWidth) + '╢';
|
||||||
|
const sigmaStatsFooterLine = '╚' + '═'.repeat(sigmaStatsMetricWidth) + '╩' + '═'.repeat(sigmaStatsValueWidth) + '╝';
|
||||||
|
|
||||||
|
console.log('\n' + sigmaStatsHeaderLine);
|
||||||
|
console.log(`║ ${'Metric'.padEnd(sigmaStatsMetricWidth - 2)} ║ ${'Value'.padEnd(sigmaStatsValueWidth - 2)} ║`);
|
||||||
|
console.log(sigmaStatsDividerLine);
|
||||||
|
|
||||||
|
// Track whether we need to add a row separator
|
||||||
|
let statsIsFirstRow = true;
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(data)) {
|
for (const [key, value] of Object.entries(data)) {
|
||||||
const formattedKey = key.padEnd(18).substring(0, 18);
|
// Add separator between rows (but not before the first row)
|
||||||
const formattedValue = String(value || '').padEnd(13).substring(0, 13);
|
if (!statsIsFirstRow) {
|
||||||
|
console.log(sigmaStatsRowSeparator);
|
||||||
console.log(`| ${formattedKey} | ${formattedValue} |`);
|
}
|
||||||
|
statsIsFirstRow = false;
|
||||||
|
|
||||||
|
const formattedKey = key.padEnd(sigmaStatsMetricWidth - 2);
|
||||||
|
const formattedValue = String(value || '').padEnd(sigmaStatsValueWidth - 2);
|
||||||
|
|
||||||
|
console.log(`║ ${formattedKey} ║ ${formattedValue} ║`);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('+--------------------+---------------+');
|
console.log(sigmaStatsFooterLine);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
console.log(JSON.stringify(data, null, 2));
|
console.log(JSON.stringify(data, null, 2));
|
||||||
}
|
}
|
||||||
|
@ -183,18 +288,18 @@ function formatOutput(data, type) {
|
||||||
*/
|
*/
|
||||||
function extractSearchKeywords(input) {
|
function extractSearchKeywords(input) {
|
||||||
if (!input) return '';
|
if (!input) return '';
|
||||||
|
|
||||||
// Try to extract keywords from common patterns
|
// Try to extract keywords from common patterns
|
||||||
if (input.includes('title contains')) {
|
if (input.includes('title contains')) {
|
||||||
const match = input.match(/title\s+contains\s+["']([^"']+)["']/i);
|
const match = input.match(/title\s+contains\s+["']([^"']+)["']/i);
|
||||||
if (match) return match[1];
|
if (match) return match[1];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (input.includes('tags include')) {
|
if (input.includes('tags include')) {
|
||||||
const match = input.match(/tags\s+include\s+(\S+)/i);
|
const match = input.match(/tags\s+include\s+(\S+)/i);
|
||||||
if (match) return match[1];
|
if (match) return match[1];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default - just return the input as is
|
// Default - just return the input as is
|
||||||
return input;
|
return input;
|
||||||
}
|
}
|
||||||
|
@ -210,28 +315,28 @@ async function processCommand(input) {
|
||||||
rl.prompt();
|
rl.prompt();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Special CLI commands
|
// Special CLI commands
|
||||||
if (input.trim().toLowerCase() === 'exit' || input.trim().toLowerCase() === 'quit') {
|
if (input.trim().toLowerCase() === 'exit' || input.trim().toLowerCase() === 'quit') {
|
||||||
console.log('Goodbye!');
|
console.log('Goodbye!');
|
||||||
rl.close();
|
rl.close();
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (input.trim().toLowerCase() === 'clear') {
|
if (input.trim().toLowerCase() === 'clear') {
|
||||||
console.clear();
|
console.clear();
|
||||||
rl.prompt();
|
rl.prompt();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Special case for simple search
|
// Special case for simple search
|
||||||
if (input.trim().match(/^search\s+sigma\s+(.+)$/i)) {
|
if (input.trim().match(/^search\s+sigma\s+(.+)$/i)) {
|
||||||
const keyword = input.trim().match(/^search\s+sigma\s+(.+)$/i)[1];
|
const keyword = input.trim().match(/^search\s+sigma\s+(.+)$/i)[1];
|
||||||
|
|
||||||
// Add to command history
|
// Add to command history
|
||||||
commandHistory.push(input);
|
commandHistory.push(input);
|
||||||
historyIndex = commandHistory.length;
|
historyIndex = commandHistory.length;
|
||||||
|
|
||||||
// Create fake command object
|
// Create fake command object
|
||||||
const command = {
|
const command = {
|
||||||
text: keyword,
|
text: keyword,
|
||||||
|
@ -241,12 +346,12 @@ async function processCommand(input) {
|
||||||
channel_id: 'cli',
|
channel_id: 'cli',
|
||||||
channel_name: 'cli'
|
channel_name: 'cli'
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create custom respond function
|
// Create custom respond function
|
||||||
const respond = createRespondFunction('search', 'sigma', [keyword]);
|
const respond = createRespondFunction('search', 'sigma', [keyword]);
|
||||||
|
|
||||||
console.log(`Executing: module=sigma, action=search, params=[${keyword}]`);
|
console.log(`Executing: module=sigma, action=search, params=[${keyword}]`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await sigmaSearchHandler.handleCommand(command, respond);
|
await sigmaSearchHandler.handleCommand(command, respond);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -255,29 +360,29 @@ async function processCommand(input) {
|
||||||
logger.debug(`${FILE_NAME}: Error stack: ${error.stack}`);
|
logger.debug(`${FILE_NAME}: Error stack: ${error.stack}`);
|
||||||
rl.prompt();
|
rl.prompt();
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add to command history
|
// Add to command history
|
||||||
commandHistory.push(input);
|
commandHistory.push(input);
|
||||||
historyIndex = commandHistory.length;
|
historyIndex = commandHistory.length;
|
||||||
|
|
||||||
// Parse command using existing parser
|
// Parse command using existing parser
|
||||||
const parsedCommand = await parseCommand(input);
|
const parsedCommand = await parseCommand(input);
|
||||||
|
|
||||||
if (!parsedCommand.success) {
|
if (!parsedCommand.success) {
|
||||||
console.log(parsedCommand.message || "Command not recognized. Type 'help' for usage.");
|
console.log(parsedCommand.message || "Command not recognized. Type 'help' for usage.");
|
||||||
rl.prompt();
|
rl.prompt();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract the command details
|
// Extract the command details
|
||||||
const { action, module, params } = parsedCommand.command;
|
const { action, module, params } = parsedCommand.command;
|
||||||
|
|
||||||
// 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 fake command object similar to Slack's
|
||||||
const command = {
|
const command = {
|
||||||
text: Array.isArray(params) && params.length > 0 ? params[0] : input,
|
text: Array.isArray(params) && params.length > 0 ? params[0] : input,
|
||||||
|
@ -287,17 +392,17 @@ async function processCommand(input) {
|
||||||
channel_id: 'cli',
|
channel_id: 'cli',
|
||||||
channel_name: 'cli'
|
channel_name: 'cli'
|
||||||
};
|
};
|
||||||
|
|
||||||
// Special handling for complexSearch to extract keywords
|
// Special handling for complexSearch to extract keywords
|
||||||
if (action === 'complexSearch' && module === 'sigma' && params.length > 0) {
|
if (action === 'complexSearch' && module === 'sigma' && params.length > 0) {
|
||||||
// Try to extract keywords from complex queries
|
// Try to extract keywords from complex queries
|
||||||
const searchTerms = extractSearchKeywords(params[0]);
|
const searchTerms = extractSearchKeywords(params[0]);
|
||||||
command.text = searchTerms || 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(action, module, params);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
switch (module) {
|
switch (module) {
|
||||||
case 'sigma':
|
case 'sigma':
|
||||||
|
@ -305,50 +410,50 @@ async function processCommand(input) {
|
||||||
case 'search':
|
case 'search':
|
||||||
await sigmaSearchHandler.handleCommand(command, respond);
|
await sigmaSearchHandler.handleCommand(command, respond);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'complexSearch':
|
case 'complexSearch':
|
||||||
await sigmaSearchHandler.handleComplexSearch(command, respond);
|
await sigmaSearchHandler.handleComplexSearch(command, respond);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'details':
|
case 'details':
|
||||||
await sigmaDetailsHandler.handleCommand(command, respond);
|
await sigmaDetailsHandler.handleCommand(command, respond);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'stats':
|
case 'stats':
|
||||||
await sigmaStatsHandler.handleCommand(command, respond);
|
await sigmaStatsHandler.handleCommand(command, respond);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'create':
|
case 'create':
|
||||||
await sigmaCreateHandler.handleCommand(command, respond);
|
await sigmaCreateHandler.handleCommand(command, respond);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
console.log(`Unknown Sigma action: ${action}`);
|
console.log(`Unknown Sigma action: ${action}`);
|
||||||
rl.prompt();
|
rl.prompt();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'alerts':
|
case 'alerts':
|
||||||
await handleAlerts(command, respond);
|
await handleAlerts(command, respond);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'case':
|
case 'case':
|
||||||
await handleCase(command, respond);
|
await handleCase(command, respond);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'config':
|
case 'config':
|
||||||
await handleConfig(command, respond);
|
await handleConfig(command, respond);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'stats':
|
case 'stats':
|
||||||
await handleStats(command, respond);
|
await handleStats(command, respond);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'help':
|
case 'help':
|
||||||
displayHelp();
|
displayHelp();
|
||||||
rl.prompt();
|
rl.prompt();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
console.log(`Unknown module: ${module}`);
|
console.log(`Unknown module: ${module}`);
|
||||||
rl.prompt();
|
rl.prompt();
|
||||||
|
@ -383,13 +488,13 @@ function createRespondFunction(action, module, params) {
|
||||||
rl.prompt();
|
rl.prompt();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// First check for the responseData property (directly from service)
|
// First check for the responseData property (directly from service)
|
||||||
if (response.responseData) {
|
if (response.responseData) {
|
||||||
// Format the data using the appropriate formatter
|
// Format the data using the appropriate formatter
|
||||||
if (module === 'sigma') {
|
if (module === 'sigma') {
|
||||||
let formattedData;
|
let formattedData;
|
||||||
|
|
||||||
if (action === 'search' || action === 'complexSearch') {
|
if (action === 'search' || action === 'complexSearch') {
|
||||||
formattedData = formatSigmaSearchResults(response.responseData);
|
formattedData = formatSigmaSearchResults(response.responseData);
|
||||||
formatOutput(formattedData, 'search_results');
|
formatOutput(formattedData, 'search_results');
|
||||||
|
@ -413,7 +518,7 @@ function createRespondFunction(action, module, params) {
|
||||||
} else {
|
} else {
|
||||||
console.log('Command completed successfully.');
|
console.log('Command completed successfully.');
|
||||||
}
|
}
|
||||||
|
|
||||||
rl.prompt();
|
rl.prompt();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -441,7 +546,7 @@ Advanced Sigma Search Commands:
|
||||||
- clear - Clear the terminal screen
|
- clear - Clear the terminal screen
|
||||||
- help - Display this help text
|
- help - Display this help text
|
||||||
`;
|
`;
|
||||||
|
|
||||||
console.log(helpText);
|
console.log(helpText);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -452,7 +557,7 @@ function startCLI() {
|
||||||
console.log(ASCII_LOGO);
|
console.log(ASCII_LOGO);
|
||||||
console.log(`Fylgja CLI v${version} - Interactive SIEM Management Tool`);
|
console.log(`Fylgja CLI v${version} - Interactive SIEM Management Tool`);
|
||||||
console.log(`Type 'help' for usage information or 'exit' to quit\n`);
|
console.log(`Type 'help' for usage information or 'exit' to quit\n`);
|
||||||
|
|
||||||
// Set up key bindings for history navigation
|
// Set up key bindings for history navigation
|
||||||
rl._writeToOutput = function _writeToOutput(stringToWrite) {
|
rl._writeToOutput = function _writeToOutput(stringToWrite) {
|
||||||
if (stringToWrite === '\\u001b[A' || stringToWrite === '\\u001b[B') {
|
if (stringToWrite === '\\u001b[A' || stringToWrite === '\\u001b[B') {
|
||||||
|
@ -461,7 +566,7 @@ function startCLI() {
|
||||||
}
|
}
|
||||||
rl.output.write(stringToWrite);
|
rl.output.write(stringToWrite);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Set up key listeners for history
|
// Set up key listeners for history
|
||||||
rl.input.on('keypress', (char, key) => {
|
rl.input.on('keypress', (char, key) => {
|
||||||
if (key && key.name === 'up') {
|
if (key && key.name === 'up') {
|
||||||
|
@ -485,13 +590,13 @@ function startCLI() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
rl.prompt();
|
rl.prompt();
|
||||||
|
|
||||||
rl.on('line', async (line) => {
|
rl.on('line', async (line) => {
|
||||||
await processCommand(line.trim());
|
await processCommand(line.trim());
|
||||||
});
|
});
|
||||||
|
|
||||||
rl.on('close', () => {
|
rl.on('close', () => {
|
||||||
console.log('Goodbye!');
|
console.log('Goodbye!');
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
|
|
|
@ -6,6 +6,40 @@
|
||||||
*/
|
*/
|
||||||
const chalk = require('chalk');
|
const chalk = require('chalk');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wraps text at specified length
|
||||||
|
* @param {string} text - Text to wrap
|
||||||
|
* @param {number} maxLength - Maximum line length
|
||||||
|
* @returns {string} Wrapped text
|
||||||
|
*/
|
||||||
|
function wrapText(text, maxLength = 80) {
|
||||||
|
if (!text || typeof text !== 'string') {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (text.length <= maxLength) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
const words = text.split(' ');
|
||||||
|
let wrappedText = '';
|
||||||
|
let currentLine = '';
|
||||||
|
|
||||||
|
words.forEach(word => {
|
||||||
|
// If adding this word would exceed max length, start a new line
|
||||||
|
if ((currentLine + word).length + 1 > maxLength) {
|
||||||
|
wrappedText += currentLine.trim() + '\n';
|
||||||
|
currentLine = word + ' ';
|
||||||
|
} else {
|
||||||
|
currentLine += word + ' ';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add the last line
|
||||||
|
wrappedText += currentLine.trim();
|
||||||
|
|
||||||
|
return wrappedText;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format Sigma rule details for CLI display
|
* Format Sigma rule details for CLI display
|
||||||
|
@ -20,17 +54,17 @@ function formatSigmaDetails(ruleDetails) {
|
||||||
// Create a flattened object for display in CLI table format
|
// Create a flattened object for display in CLI table format
|
||||||
const formattedDetails = {
|
const formattedDetails = {
|
||||||
'ID': ruleDetails.id || 'Unknown',
|
'ID': ruleDetails.id || 'Unknown',
|
||||||
'Title': ruleDetails.title || 'Untitled Rule',
|
'Title': wrapText(ruleDetails.title || 'Untitled Rule', 80),
|
||||||
'Description': ruleDetails.description || 'No description provided',
|
'Description': wrapText(ruleDetails.description || 'No description provided', 80),
|
||||||
'Author': ruleDetails.author || 'Unknown author',
|
'Author': ruleDetails.author || 'Unknown author',
|
||||||
'Severity': ruleDetails.severity || 'Unknown',
|
'Severity': ruleDetails.severity || 'Unknown',
|
||||||
'Detection': ruleDetails.detectionExplanation || 'No detection specified',
|
'Detection': wrapText(ruleDetails.detectionExplanation || 'No detection specified', 80),
|
||||||
'False Positives': Array.isArray(ruleDetails.falsePositives) ?
|
'False Positives': wrapText(Array.isArray(ruleDetails.falsePositives) ?
|
||||||
ruleDetails.falsePositives.join(', ') : 'None specified',
|
ruleDetails.falsePositives.join(', ') : 'None specified', 80),
|
||||||
'Tags': Array.isArray(ruleDetails.tags) ?
|
'Tags': wrapText(Array.isArray(ruleDetails.tags) ?
|
||||||
ruleDetails.tags.join(', ') : 'None',
|
ruleDetails.tags.join(', ') : 'None', 80),
|
||||||
'References': Array.isArray(ruleDetails.references) ?
|
'References': wrapText(Array.isArray(ruleDetails.references) ?
|
||||||
ruleDetails.references.join(', ') : 'None'
|
ruleDetails.references.join(', ') : 'None', 80)
|
||||||
};
|
};
|
||||||
|
|
||||||
return formattedDetails;
|
return formattedDetails;
|
||||||
|
@ -114,7 +148,7 @@ function formatSigmaSearchResults(searchResults) {
|
||||||
return {
|
return {
|
||||||
results: searchResults.results.map(rule => ({
|
results: searchResults.results.map(rule => ({
|
||||||
id: rule.id || '',
|
id: rule.id || '',
|
||||||
title: rule.title || '',
|
title: wrapText(rule.title || '', 60), // Use narrower width for table columns
|
||||||
author: rule.author || 'Unknown',
|
author: rule.author || 'Unknown',
|
||||||
level: rule.level || 'medium'
|
level: rule.level || 'medium'
|
||||||
})),
|
})),
|
||||||
|
@ -122,9 +156,9 @@ function formatSigmaSearchResults(searchResults) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
formatSigmaStats,
|
formatSigmaStats,
|
||||||
formatSigmaSearchResults,
|
formatSigmaSearchResults,
|
||||||
formatSigmaDetails
|
formatSigmaDetails,
|
||||||
|
wrapText
|
||||||
};
|
};
|
Loading…
Add table
Add a link
Reference in a new issue