format CLI tables

This commit is contained in:
Charlotte Croce 2025-04-19 13:18:38 -04:00
parent fd394fff36
commit 845440962d
2 changed files with 226 additions and 87 deletions

View file

@ -104,6 +104,65 @@ function completer(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,6 +176,7 @@ 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('+-------+----------------------+------------------+-------------+');
@ -139,35 +199,80 @@ function formatOutput(data, type) {
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);
}
isFirstRow = false;
console.log(`| ${formattedKey} | ${formattedValue} |`); 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);
}
statsIsFirstRow = false;
console.log(`| ${formattedKey} | ${formattedValue} |`); 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:

View file

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