searchCLI #5
4 changed files with 260 additions and 108 deletions
|
@ -3,27 +3,6 @@
|
|||
*
|
||||
* Interactive CLI interface
|
||||
*/
|
||||
|
||||
const readline = require('readline');
|
||||
// Import chalk with compatibility for both ESM and CommonJS
|
||||
let chalk;
|
||||
try {
|
||||
// First try CommonJS import (chalk v4.x)
|
||||
chalk = require('chalk');
|
||||
} catch (e) {
|
||||
// If that fails, provide a fallback implementation
|
||||
chalk = {
|
||||
blue: (text) => text,
|
||||
green: (text) => text,
|
||||
red: (text) => text,
|
||||
yellow: (text) => text,
|
||||
cyan: (text) => text,
|
||||
white: (text) => text,
|
||||
dim: (text) => text,
|
||||
hex: () => (text) => text
|
||||
};
|
||||
}
|
||||
|
||||
const { parseCommand } = require('./lang/command_parser');
|
||||
const logger = require('./utils/logger');
|
||||
const sigmaSearchHandler = require('./handlers/sigma/sigma_search_handler');
|
||||
|
@ -34,6 +13,58 @@ const { handleCommand: handleAlerts } = require('./handlers/alerts/alerts_handle
|
|||
const { handleCommand: handleCase } = require('./handlers/case/case_handler');
|
||||
const { handleCommand: handleConfig } = require('./handlers/config/config_handler');
|
||||
const { handleCommand: handleStats } = require('./handlers/stats/stats_handler');
|
||||
const readline = require('readline');
|
||||
|
||||
|
||||
const colors = {
|
||||
// ANSI color codes
|
||||
reset: '\x1b[0m',
|
||||
bright: '\x1b[1m',
|
||||
dim: '\x1b[2m',
|
||||
underscore: '\x1b[4m',
|
||||
blink: '\x1b[5m',
|
||||
reverse: '\x1b[7m',
|
||||
hidden: '\x1b[8m',
|
||||
|
||||
// Foreground colors
|
||||
black: '\x1b[30m',
|
||||
red: '\x1b[31m',
|
||||
green: '\x1b[32m',
|
||||
yellow: '\x1b[33m',
|
||||
blue: '\x1b[34m',
|
||||
magenta: '\x1b[35m',
|
||||
cyan: '\x1b[36m',
|
||||
white: '\x1b[37m',
|
||||
|
||||
// Background colors
|
||||
bgBlack: '\x1b[40m',
|
||||
bgRed: '\x1b[41m',
|
||||
bgGreen: '\x1b[42m',
|
||||
bgYellow: '\x1b[43m',
|
||||
bgBlue: '\x1b[44m',
|
||||
bgMagenta: '\x1b[45m',
|
||||
bgCyan: '\x1b[46m',
|
||||
bgWhite: '\x1b[47m'
|
||||
};
|
||||
|
||||
// Create simple color functions
|
||||
const colorize = {
|
||||
blue: (text) => `${colors.blue}${text}${colors.reset}`,
|
||||
green: (text) => `${colors.green}${text}${colors.reset}`,
|
||||
red: (text) => `${colors.red}${text}${colors.reset}`,
|
||||
yellow: (text) => `${colors.yellow}${text}${colors.reset}`,
|
||||
cyan: (text) => `${colors.cyan}${text}${colors.reset}`,
|
||||
white: (text) => `${colors.white}${text}${colors.reset}`,
|
||||
dim: (text) => `${colors.dim}${text}${colors.reset}`,
|
||||
};
|
||||
|
||||
const tableColors = {
|
||||
header: colorize.cyan,
|
||||
key: colorize.blue,
|
||||
statsKey: colorize.yellow,
|
||||
count: colorize.green,
|
||||
dim: colorize.dim
|
||||
};
|
||||
|
||||
// Import CLI formatters
|
||||
const {
|
||||
|
@ -91,9 +122,17 @@ function completer(line) {
|
|||
'sigma stats',
|
||||
'stats sigma',
|
||||
'search sigma rules where title contains',
|
||||
'search rules where tags include',
|
||||
'search rules where logsource.category ==',
|
||||
'search rules where modified after',
|
||||
'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',
|
||||
|
@ -163,8 +202,9 @@ function normalizeAndWrap(text, maxWidth) {
|
|||
return lines;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Format CLI output similar to MySQL
|
||||
* Format CLI output
|
||||
* @param {Object} data The data to format
|
||||
* @param {string} type The type of data (results, details, stats)
|
||||
*/
|
||||
|
@ -174,105 +214,133 @@ function formatOutput(data, type) {
|
|||
return;
|
||||
}
|
||||
|
||||
console.log();
|
||||
|
||||
// word wrapping function
|
||||
function normalizeAndWrap(text, maxWidth) {
|
||||
if (text === undefined || text === null) return [''];
|
||||
|
||||
// Convert to string and normalize newlines
|
||||
const normalized = String(text).replace(/\n/g, ' ');
|
||||
|
||||
// If text fits in one line, return it
|
||||
if (normalized.length <= maxWidth) return [normalized];
|
||||
|
||||
const words = normalized.split(' ');
|
||||
const lines = [];
|
||||
let currentLine = '';
|
||||
|
||||
for (const word of words) {
|
||||
// Skip empty words
|
||||
if (!word) continue;
|
||||
|
||||
// Check 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 = '';
|
||||
}
|
||||
|
||||
// Handle words longer than maxWidth by splitting them
|
||||
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.length ? lines : [''];
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case 'search_results':
|
||||
// Search results table format remains the same
|
||||
console.log('\n+-------+----------------------+------------------+-------------+');
|
||||
console.log('| ID | Title | Author | Level |');
|
||||
console.log('+-------+----------------------+------------------+-------------+');
|
||||
// Header
|
||||
console.log(
|
||||
tableColors.header(
|
||||
'#'.padEnd(5) +
|
||||
'Title'.padEnd(32) +
|
||||
'OS/Product'.padEnd(15) +
|
||||
'ID'.padEnd(13)
|
||||
)
|
||||
);
|
||||
|
||||
if (data.results && data.results.length > 0) {
|
||||
data.results.forEach(rule => {
|
||||
const id = (rule.id || '').padEnd(5).substring(0, 5);
|
||||
const title = (rule.title || '').padEnd(20).substring(0, 20);
|
||||
const author = (rule.author || 'Unknown').padEnd(16).substring(0, 16);
|
||||
const level = (rule.level || 'medium').padEnd(11).substring(0, 11);
|
||||
data.results.forEach((rule, index) => {
|
||||
const num = (index + 1).toString().padEnd(5);
|
||||
const title = (rule.title || '').substring(0, 32).padEnd(32);
|
||||
|
||||
console.log(`| ${id} | ${title} | ${author} | ${level} |`);
|
||||
// Extract OS/Product from tags or logsource if available
|
||||
let osProduct = 'Unknown';
|
||||
if (rule.tags && Array.isArray(rule.tags)) {
|
||||
// Look for os tags like windows, linux, macos
|
||||
const osTags = rule.tags.filter(tag =>
|
||||
['windows', 'linux', 'macos', 'unix', 'azure', 'aws', 'gcp'].includes(tag.toLowerCase())
|
||||
);
|
||||
if (osTags.length > 0) {
|
||||
osProduct = osTags[0].charAt(0).toUpperCase() + osTags[0].slice(1);
|
||||
}
|
||||
} else if (rule.logsource && rule.logsource.product) {
|
||||
osProduct = rule.logsource.product;
|
||||
}
|
||||
|
||||
osProduct = osProduct.substring(0, 15).padEnd(15);
|
||||
const id = (rule.id || '').substring(0, 13).padEnd(13);
|
||||
|
||||
console.log(`${num}${title}${osProduct}${id}`);
|
||||
});
|
||||
} else {
|
||||
console.log('| No results found |');
|
||||
console.log(tableColors.dim('No results found'));
|
||||
}
|
||||
|
||||
console.log('+-------+----------------------+------------------+-------------+');
|
||||
console.log(`${data.totalCount || 0} rows in set`);
|
||||
console.log(tableColors.count(`${data.totalCount || 0} rows in set`));
|
||||
break;
|
||||
|
||||
case 'details':
|
||||
// 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;
|
||||
const detailsKeyWidth = 24;
|
||||
const detailsValueWidth = 50;
|
||||
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
if (typeof value !== 'object' || value === null) {
|
||||
// Add separator between rows (but not before the first row)
|
||||
if (!isFirstRow) {
|
||||
console.log(sigmaDetailsRowSeparator);
|
||||
}
|
||||
isFirstRow = false;
|
||||
|
||||
const formattedKey = key.padEnd(sigmaDetailsKeyWidth - 2);
|
||||
const formattedKey = tableColors.key(key.padEnd(detailsKeyWidth - 2));
|
||||
|
||||
// Handle wrapping
|
||||
const lines = normalizeAndWrap(value, sigmaDetailsValueWidth - 2);
|
||||
const lines = normalizeAndWrap(value, detailsValueWidth);
|
||||
|
||||
// Print first line with the key
|
||||
console.log(`║ ${formattedKey} ║ ${lines[0].padEnd(sigmaDetailsValueWidth - 2)} ║`);
|
||||
console.log(`${formattedKey} ${lines[0]}`);
|
||||
|
||||
// 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(`${' '.repeat(detailsKeyWidth)} ${lines[i]}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(sigmaDetailsFooterLine);
|
||||
break;
|
||||
|
||||
case 'stats':
|
||||
// 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;
|
||||
const statsMetricWidth = 25;
|
||||
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
// Add separator between rows (but not before the first row)
|
||||
if (!statsIsFirstRow) {
|
||||
console.log(sigmaStatsRowSeparator);
|
||||
}
|
||||
statsIsFirstRow = false;
|
||||
const formattedKey = tableColors.statsKey(key.padEnd(statsMetricWidth - 2));
|
||||
const formattedValue = String(value || '');
|
||||
|
||||
const formattedKey = key.padEnd(sigmaStatsMetricWidth - 2);
|
||||
const formattedValue = String(value || '').padEnd(sigmaStatsValueWidth - 2);
|
||||
|
||||
console.log(`║ ${formattedKey} ║ ${formattedValue} ║`);
|
||||
console.log(`${formattedKey} ${formattedValue}`);
|
||||
}
|
||||
|
||||
console.log(sigmaStatsFooterLine);
|
||||
break;
|
||||
|
||||
default:
|
||||
|
@ -330,7 +398,7 @@ async function processCommand(input) {
|
|||
}
|
||||
|
||||
// Special case for simple search
|
||||
if (input.trim().match(/^search\s+sigma\s+(.+)$/i)) {
|
||||
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];
|
||||
|
||||
// Add to command history
|
||||
|
@ -364,6 +432,42 @@ async function processCommand(input) {
|
|||
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];
|
||||
|
||||
// Add to command history
|
||||
commandHistory.push(input);
|
||||
historyIndex = commandHistory.length;
|
||||
|
||||
// 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) {
|
||||
console.error(`Error: ${error.message}`);
|
||||
logger.error(`${FILE_NAME}: Command execution error: ${error.message}`);
|
||||
logger.debug(`${FILE_NAME}: Error stack: ${error.stack}`);
|
||||
rl.prompt();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Add to command history
|
||||
commandHistory.push(input);
|
||||
historyIndex = commandHistory.length;
|
||||
|
@ -475,14 +579,32 @@ async function processCommand(input) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Create a custom respond function for handling results
|
||||
* Custom respond function for handling results
|
||||
* @param {string} action The action being performed
|
||||
* @param {string} module The module being used
|
||||
* @param {Array} params The parameters for the action
|
||||
* @returns {Function} A respond function for handling results
|
||||
*/
|
||||
function createRespondFunction(action, module, params) {
|
||||
// Keep track of whether we're waiting for results
|
||||
let isWaitingForResults = false;
|
||||
|
||||
return async (response) => {
|
||||
// Check if this is a progress message
|
||||
const isProgressMessage =
|
||||
typeof response === 'object' &&
|
||||
response.text &&
|
||||
!response.responseData &&
|
||||
(response.text.includes('moment') ||
|
||||
response.text.includes('searching') ||
|
||||
response.text.includes('processing'));
|
||||
|
||||
if (isProgressMessage) {
|
||||
console.log(response.text);
|
||||
isWaitingForResults = true;
|
||||
return; // Don't show prompt after progress messages
|
||||
}
|
||||
|
||||
if (typeof response === 'string') {
|
||||
console.log(response);
|
||||
rl.prompt();
|
||||
|
@ -519,6 +641,8 @@ function createRespondFunction(action, module, params) {
|
|||
console.log('Command completed successfully.');
|
||||
}
|
||||
|
||||
// Reset waiting state and show prompt after results
|
||||
isWaitingForResults = false;
|
||||
rl.prompt();
|
||||
};
|
||||
}
|
||||
|
@ -540,8 +664,10 @@ Advanced Sigma Search Commands:
|
|||
- search sigma where tags include privilege_escalation - Search by tags
|
||||
- search sigma where logsource.category == "process_creation" - Search by log source
|
||||
- search sigma where modified after 2024-01-01 - Search by modification date
|
||||
- search sigma rules where title contains "ransomware" - Alternative syntax
|
||||
- search sigma rules where tags include privilege_escalation - Alternative syntax
|
||||
|
||||
|
||||
CLI Commands:
|
||||
- exit or quit - Exit the CLI
|
||||
- clear - Clear the terminal screen
|
||||
- help - Display this help text
|
||||
|
@ -549,7 +675,6 @@ Advanced Sigma Search Commands:
|
|||
|
||||
console.log(helpText);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the CLI application
|
||||
*/
|
||||
|
|
|
@ -25,13 +25,28 @@ const commandPatterns = [
|
|||
},
|
||||
// Sigma search patterns
|
||||
{
|
||||
name: 'sigma-search',
|
||||
regex: /^(search|find)\s+(sigma\s+)?(rules|detections)?\s*(where|with)\s+(.+)$/i,
|
||||
name: 'sigma-search-complex-1',
|
||||
regex: /^(search|find)\s+sigma\s+rules?\s*(where|with)\s+(.+)$/i,
|
||||
action: 'complexSearch',
|
||||
module: 'sigma',
|
||||
params: [5] // complex query conditions in capturing group 5
|
||||
params: [4] // complex query conditions in capturing group 4
|
||||
},
|
||||
// Alternate form without "rules"
|
||||
{
|
||||
name: 'sigma-search-complex-2',
|
||||
regex: /^(search|find)\s+sigma\s+(where|with)\s+(.+)$/i,
|
||||
action: 'complexSearch',
|
||||
module: 'sigma',
|
||||
params: [3] // complex query conditions in capturing group 3
|
||||
},
|
||||
// Simple keyword search pattern
|
||||
{
|
||||
name: 'sigma-search-simple',
|
||||
regex: /^(search|find)\s+sigma\s+(.+)$/i,
|
||||
action: 'search',
|
||||
module: 'sigma',
|
||||
params: [2] // keyword is in capturing group 2
|
||||
},
|
||||
|
||||
// Sigma create patterns
|
||||
{
|
||||
name: 'sigma-create',
|
||||
|
|
0
src/logo.js
Normal file
0
src/logo.js
Normal file
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
const chalk = require('chalk');
|
||||
|
||||
|
||||
/**
|
||||
* Wraps text at specified length
|
||||
* @param {string} text - Text to wrap
|
||||
|
@ -146,16 +147,27 @@ function formatSigmaSearchResults(searchResults) {
|
|||
|
||||
// Return a structure with results and meta info
|
||||
return {
|
||||
results: searchResults.results.map(rule => ({
|
||||
id: rule.id || '',
|
||||
title: wrapText(rule.title || '', 60), // Use narrower width for table columns
|
||||
author: rule.author || 'Unknown',
|
||||
level: rule.level || 'medium'
|
||||
})),
|
||||
results: searchResults.results.map(rule => {
|
||||
// Get logsource.product field
|
||||
let osProduct = 'N/A';
|
||||
|
||||
// Only use logsource.product if it exists
|
||||
if (rule.logsource && rule.logsource.product) {
|
||||
osProduct = rule.logsource.product;
|
||||
}
|
||||
|
||||
return {
|
||||
id: rule.id || '',
|
||||
title: wrapText(rule.title || '', 60), // Use narrower width for table columns
|
||||
author: rule.author || 'Unknown',
|
||||
level: rule.level || 'medium',
|
||||
osProduct: osProduct,
|
||||
tags: rule.tags || [] // Include the original tags for potential reference
|
||||
};
|
||||
}),
|
||||
totalCount: searchResults.totalCount || 0
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
formatSigmaStats,
|
||||
formatSigmaSearchResults,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue