format CLI tables
This commit is contained in:
parent
fd394fff36
commit
845440962d
2 changed files with 226 additions and 87 deletions
|
@ -104,6 +104,65 @@ function completer(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
|
||||
* @param {Object} data The data to format
|
||||
|
@ -117,6 +176,7 @@ function formatOutput(data, type) {
|
|||
|
||||
switch (type) {
|
||||
case 'search_results':
|
||||
// Search results table format remains the same
|
||||
console.log('\n+-------+----------------------+------------------+-------------+');
|
||||
console.log('| ID | Title | Author | Level |');
|
||||
console.log('+-------+----------------------+------------------+-------------+');
|
||||
|
@ -139,35 +199,80 @@ function formatOutput(data, type) {
|
|||
break;
|
||||
|
||||
case 'details':
|
||||
console.log('\n+----------------------+--------------------------------------------------+');
|
||||
console.log('| Field | Value |');
|
||||
console.log('+----------------------+--------------------------------------------------+');
|
||||
// Set a fixed width for the entire table
|
||||
const sigmaDetailsKeyWidth = 22;
|
||||
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)) {
|
||||
if (typeof value !== 'object' || value === null) {
|
||||
const formattedKey = key.padEnd(20).substring(0, 20);
|
||||
const formattedValue = String(value || '').padEnd(48).substring(0, 48);
|
||||
// Add separator between rows (but not before the first row)
|
||||
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;
|
||||
|
||||
case 'stats':
|
||||
console.log('\n+--------------------+---------------+');
|
||||
console.log('| Metric | Value |');
|
||||
console.log('+--------------------+---------------+');
|
||||
// Set column widths
|
||||
const sigmaStatsMetricWidth = 25;
|
||||
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)) {
|
||||
const formattedKey = key.padEnd(18).substring(0, 18);
|
||||
const formattedValue = String(value || '').padEnd(13).substring(0, 13);
|
||||
// Add separator between rows (but not before the first row)
|
||||
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;
|
||||
|
||||
default:
|
||||
|
|
|
@ -6,6 +6,40 @@
|
|||
*/
|
||||
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
|
||||
|
@ -20,17 +54,17 @@ function formatSigmaDetails(ruleDetails) {
|
|||
// Create a flattened object for display in CLI table format
|
||||
const formattedDetails = {
|
||||
'ID': ruleDetails.id || 'Unknown',
|
||||
'Title': ruleDetails.title || 'Untitled Rule',
|
||||
'Description': ruleDetails.description || 'No description provided',
|
||||
'Title': wrapText(ruleDetails.title || 'Untitled Rule', 80),
|
||||
'Description': wrapText(ruleDetails.description || 'No description provided', 80),
|
||||
'Author': ruleDetails.author || 'Unknown author',
|
||||
'Severity': ruleDetails.severity || 'Unknown',
|
||||
'Detection': ruleDetails.detectionExplanation || 'No detection specified',
|
||||
'False Positives': Array.isArray(ruleDetails.falsePositives) ?
|
||||
ruleDetails.falsePositives.join(', ') : 'None specified',
|
||||
'Tags': Array.isArray(ruleDetails.tags) ?
|
||||
ruleDetails.tags.join(', ') : 'None',
|
||||
'References': Array.isArray(ruleDetails.references) ?
|
||||
ruleDetails.references.join(', ') : 'None'
|
||||
'Detection': wrapText(ruleDetails.detectionExplanation || 'No detection specified', 80),
|
||||
'False Positives': wrapText(Array.isArray(ruleDetails.falsePositives) ?
|
||||
ruleDetails.falsePositives.join(', ') : 'None specified', 80),
|
||||
'Tags': wrapText(Array.isArray(ruleDetails.tags) ?
|
||||
ruleDetails.tags.join(', ') : 'None', 80),
|
||||
'References': wrapText(Array.isArray(ruleDetails.references) ?
|
||||
ruleDetails.references.join(', ') : 'None', 80)
|
||||
};
|
||||
|
||||
return formattedDetails;
|
||||
|
@ -114,7 +148,7 @@ function formatSigmaSearchResults(searchResults) {
|
|||
return {
|
||||
results: searchResults.results.map(rule => ({
|
||||
id: rule.id || '',
|
||||
title: rule.title || '',
|
||||
title: wrapText(rule.title || '', 60), // Use narrower width for table columns
|
||||
author: rule.author || 'Unknown',
|
||||
level: rule.level || 'medium'
|
||||
})),
|
||||
|
@ -122,9 +156,9 @@ function formatSigmaSearchResults(searchResults) {
|
|||
};
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
formatSigmaStats,
|
||||
formatSigmaSearchResults,
|
||||
formatSigmaDetails
|
||||
formatSigmaDetails,
|
||||
wrapText
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue