searchCLI #5

Merged
lotte merged 4 commits from searchCLI into main 2025-04-21 00:39:02 +00:00
12 changed files with 1203 additions and 629 deletions

View file

@ -1,614 +1,12 @@
#!/usr/bin/env node
/**
* fylgja-cli.js
*
* Interactive CLI interface
* Command-line executable entry point for Fylgja CLI
*/
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
};
}
// Import the CLI starter function
const { startCLI } = require('./fylgja-cli/cli');
const { parseCommand } = require('./lang/command_parser');
const logger = require('./utils/logger');
const sigmaSearchHandler = require('./handlers/sigma/sigma_search_handler');
const sigmaDetailsHandler = require('./handlers/sigma/sigma_details_handler');
const sigmaStatsHandler = require('./handlers/sigma/sigma_stats_handler');
const sigmaCreateHandler = require('./handlers/sigma/sigma_create_handler');
const { handleCommand: handleAlerts } = require('./handlers/alerts/alerts_handler');
const { handleCommand: handleCase } = require('./handlers/case/case_handler');
const { handleCommand: handleConfig } = require('./handlers/config/config_handler');
const { handleCommand: handleStats } = require('./handlers/stats/stats_handler');
// Import CLI formatters
const {
formatSigmaStats,
formatSigmaSearchResults,
formatSigmaDetails
} = require('./utils/cli_formatters');
// Set logger to CLI mode (prevents console output)
logger.setCliMode(true);
// 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');
}
const FILE_NAME = 'fylgja-cli.js';
// ASCII art logo for the CLI
const ASCII_LOGO = `
`;
// Command history array
let commandHistory = [];
let historyIndex = -1;
// Create the readline interface
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
completer: completer,
prompt: 'fylgja> '
});
/**
* 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 commands = [
'search sigma',
'details sigma',
'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',
'help',
'exit',
'quit',
'clear'
];
const hits = commands.filter((c) => c.startsWith(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
* @param {string} type The type of data (results, details, stats)
*/
function formatOutput(data, type) {
if (!data) {
console.log('No data returned from the server.');
return;
}
switch (type) {
case 'search_results':
// Search results table format remains the same
console.log('\n+-------+----------------------+------------------+-------------+');
console.log('| ID | Title | Author | Level |');
console.log('+-------+----------------------+------------------+-------------+');
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);
console.log(`| ${id} | ${title} | ${author} | ${level} |`);
});
} else {
console.log('| No results found |');
}
console.log('+-------+----------------------+------------------+-------------+');
console.log(`${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;
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);
// 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(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;
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 = key.padEnd(sigmaStatsMetricWidth - 2);
const formattedValue = String(value || '').padEnd(sigmaStatsValueWidth - 2);
console.log(`${formattedKey}${formattedValue}`);
}
console.log(sigmaStatsFooterLine);
break;
default:
console.log(JSON.stringify(data, null, 2));
}
}
/**
* Parse out any basic search keywords from a complexSearch query
* This helps with the search commands that don't quite match the expected format
* @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;
}
/**
* 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;
}
// Special CLI commands
if (input.trim().toLowerCase() === 'exit' || input.trim().toLowerCase() === 'quit') {
console.log('Goodbye!');
rl.close();
process.exit(0);
}
if (input.trim().toLowerCase() === 'clear') {
console.clear();
rl.prompt();
return;
}
// Special case for simple search
if (input.trim().match(/^search\s+sigma\s+(.+)$/i)) {
const keyword = input.trim().match(/^search\s+sigma\s+(.+)$/i)[1];
// Add to command history
commandHistory.push(input);
historyIndex = commandHistory.length;
// Create fake command object
const command = {
text: keyword,
user_id: 'cli_user',
user_name: 'cli_user',
command: '/fylgja',
channel_id: 'cli',
channel_name: 'cli'
};
// Create custom respond function
const respond = createRespondFunction('search', 'sigma', [keyword]);
console.log(`Executing: module=sigma, action=search, params=[${keyword}]`);
try {
await sigmaSearchHandler.handleCommand(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;
// Parse command using existing parser
const parsedCommand = await parseCommand(input);
if (!parsedCommand.success) {
console.log(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 fake command object similar to Slack's
const command = {
text: Array.isArray(params) && params.length > 0 ? params[0] : input,
user_id: 'cli_user',
user_name: 'cli_user',
command: '/fylgja',
channel_id: 'cli',
channel_name: 'cli'
};
// Special handling for complexSearch to extract keywords
if (action === 'complexSearch' && module === 'sigma' && params.length > 0) {
// Try to extract keywords from complex queries
const searchTerms = extractSearchKeywords(params[0]);
command.text = searchTerms || params[0];
}
// Create custom respond function for CLI
const respond = createRespondFunction(action, module, params);
try {
switch (module) {
case 'sigma':
switch (action) {
case 'search':
await sigmaSearchHandler.handleCommand(command, respond);
break;
case 'complexSearch':
await sigmaSearchHandler.handleComplexSearch(command, respond);
break;
case 'details':
await sigmaDetailsHandler.handleCommand(command, respond);
break;
case 'stats':
await sigmaStatsHandler.handleCommand(command, respond);
break;
case 'create':
await sigmaCreateHandler.handleCommand(command, respond);
break;
default:
console.log(`Unknown Sigma action: ${action}`);
rl.prompt();
}
break;
case 'alerts':
await handleAlerts(command, respond);
break;
case 'case':
await handleCase(command, respond);
break;
case 'config':
await handleConfig(command, respond);
break;
case 'stats':
await handleStats(command, respond);
break;
case 'help':
displayHelp();
rl.prompt();
break;
default:
console.log(`Unknown module: ${module}`);
rl.prompt();
}
} catch (error) {
console.error(`Error: ${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) {
console.error(`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();
}
}
/**
* Create a 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) {
return async (response) => {
if (typeof response === 'string') {
console.log(response);
rl.prompt();
return;
}
// First check for the responseData property (directly from service)
if (response.responseData) {
// Format the data using the appropriate formatter
if (module === 'sigma') {
let formattedData;
if (action === 'search' || action === 'complexSearch') {
formattedData = formatSigmaSearchResults(response.responseData);
formatOutput(formattedData, 'search_results');
} else if (action === 'details') {
formattedData = formatSigmaDetails(response.responseData);
formatOutput(formattedData, 'details');
} else if (action === 'stats') {
formattedData = formatSigmaStats(response.responseData);
formatOutput(formattedData, '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 {
console.log('Command completed successfully.');
}
rl.prompt();
};
}
/**
* Display help text
*/
function displayHelp() {
const helpText = `
Fylgja CLI Help
Basic Sigma Commands:
- search sigma <keyword> - Search for Sigma rules by keyword
- details sigma <rule_id> - Get details about a specific Sigma rule
- stats sigma - Get statistics about Sigma rules database
Advanced Sigma Search Commands:
- search sigma where title contains "ransomware" - Search by title
- 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
- exit or quit - Exit the CLI
- clear - Clear the terminal screen
- help - Display this help text
`;
console.log(helpText);
}
/**
* Start the CLI application
*/
function startCLI() {
console.log(ASCII_LOGO);
console.log(`Fylgja CLI v${version} - Interactive SIEM Management Tool`);
console.log(`Type 'help' for usage information or 'exit' to quit\n`);
// 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', () => {
console.log('Goodbye!');
process.exit(0);
});
}
// Check if running directly
if (require.main === module) {
startCLI();
} else {
// Export functions for integration with main app
module.exports = {
startCLI
};
}
// Run the CLI application
startCLI();

438
src/fylgja-cli/cli.js Normal file
View file

@ -0,0 +1,438 @@
/**
* cli.js
*
* Interactive CLI interface
*/
const readline = require('readline');
const { parseCommand } = require('../lang/command_parser');
const logger = require('../utils/logger');
const { generateGradientLogo } = require('./utils/cli_logo');
const outputManager = require('./cli_output_manager');
// Import command handlers
const sigmaSearchHandler = require('../handlers/sigma/sigma_search_handler');
const sigmaDetailsHandler = require('../handlers/sigma/sigma_details_handler');
const sigmaStatsHandler = require('../handlers/sigma/sigma_stats_handler');
const sigmaCreateHandler = require('../handlers/sigma/sigma_create_handler');
const { handleCommand: handleAlerts } = require('../handlers/alerts/alerts_handler');
const { handleCommand: handleCase } = require('../handlers/case/case_handler');
const { handleCommand: handleConfig } = require('../handlers/config/config_handler');
const { handleCommand: handleStats } = require('../handlers/stats/stats_handler');
// 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> '
});
/**
* 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 commands = [
'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'
];
const hits = commands.filter((c) => c.startsWith(line));
return [hits.length ? hits : commands, 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 custom respond function for handlers
* @param {string} action - The action being performed
* @param {string} module - The module handling the action
* @param {Array} params - The parameters for the action
* @returns {Function} A respond function for handler callbacks
*/
function createRespondFunction(action, module, params) {
// 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) {
if (module === 'sigma') {
if (action === 'search' || action === 'complexSearch') {
// Convert array response to expected format if needed
let dataToFormat = response.responseData;
// Wrap responseData array in proper structure
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();
};
}
/**
* 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;
}
// Special CLI commands
if (input.trim().toLowerCase() === 'exit' || input.trim().toLowerCase() === 'quit') {
outputManager.displaySuccess('Goodbye!');
rl.close();
process.exit(0);
}
if (input.trim().toLowerCase() === 'clear') {
console.clear();
rl.prompt();
return;
}
if (input.trim().toLowerCase() === 'help') {
outputManager.displayHelp();
rl.prompt();
return;
}
// Add to command history
commandHistory.push(input);
historyIndex = commandHistory.length;
// Special case for simple search
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];
// Create fake command object
const command = {
text: keyword,
user_id: 'cli_user',
user_name: 'cli_user',
command: '/fylgja',
channel_id: 'cli',
channel_name: 'cli'
};
// Create custom respond function
const respond = createRespondFunction('search', 'sigma', [keyword]);
console.log(`Executing: module=sigma, action=search, params=[${keyword}]`);
try {
await sigmaSearchHandler.handleCommand(command, respond);
} 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;
}
// 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];
// 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) {
outputManager.displayError(error.message);
logger.error(`${FILE_NAME}: Command execution error: ${error.message}`);
logger.debug(`${FILE_NAME}: Error stack: ${error.stack}`);
rl.prompt();
}
return;
}
// Parse command using existing parser
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 fake command object similar to Slack's
const command = {
text: Array.isArray(params) && params.length > 0 ? params[0] : input,
user_id: 'cli_user',
user_name: 'cli_user',
command: '/fylgja',
channel_id: 'cli',
channel_name: 'cli'
};
// Special handling for complexSearch to extract keywords
if (action === 'complexSearch' && module === 'sigma' && params.length > 0) {
// Try to extract keywords from complex queries
const searchTerms = extractSearchKeywords(params[0]);
command.text = searchTerms || params[0];
}
// Create custom respond function for CLI
const respond = createRespondFunction(action, module, params);
try {
switch (module) {
case 'sigma':
switch (action) {
case 'search':
await sigmaSearchHandler.handleCommand(command, respond);
break;
case 'complexSearch':
await sigmaSearchHandler.handleComplexSearch(command, respond);
break;
case 'details':
await sigmaDetailsHandler.handleCommand(command, respond);
break;
case 'stats':
await sigmaStatsHandler.handleCommand(command, respond);
break;
case 'create':
await sigmaCreateHandler.handleCommand(command, respond);
break;
default:
outputManager.displayWarning(`Unknown Sigma action: ${action}`);
rl.prompt();
}
break;
case 'alerts':
await handleAlerts(command, respond);
break;
case 'case':
await handleCase(command, respond);
break;
case 'config':
await handleConfig(command, respond);
break;
case 'stats':
await handleStats(command, respond);
break;
default:
outputManager.displayWarning(`Unknown module: ${module}`);
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(generateGradientLogo());
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
};
}

View file

@ -0,0 +1,123 @@
/**
* cli_output_manager.js
*
* Centralized manager for CLI output formatting and display
*/
const colors = require('./utils/colors');
const formatters = require('./formatters');
/**
* CLI Output Manager
* Handles all CLI output formatting and display
*/
class CliOutputManager {
/**
* Create a new CLI Output Manager
*/
constructor() {
this.colors = colors;
}
/**
* Display formatted output based on data type
*
* @param {any} data - Data to format and display
* @param {string} type - Type of data (search_results, details, stats)
*/
display(data, type) {
if (!data) {
console.log('No data returned from the server.');
return;
}
console.log(); // Empty line for spacing
switch (type) {
case 'search_results':
const formattedResults = formatters.formatSigmaSearchResults(data);
console.log(formatters.renderSigmaSearchResults(formattedResults));
break;
case 'details':
const formattedDetails = formatters.formatSigmaDetails(data);
console.log(formatters.renderSigmaDetails(formattedDetails));
break;
case 'stats':
const formattedStats = formatters.formatSigmaStats(data);
console.log(formatters.renderSigmaStats(formattedStats));
break;
default:
// For unknown types, just display JSON
console.log(JSON.stringify(data, null, 2));
}
}
/**
* Display a progress message
*
* @param {string} message - Progress message to display
*/
displayProgress(message) {
console.log(this.colors.dim(message));
}
/**
* Display an error message
*
* @param {string} message - Error message to display
*/
displayError(message) {
console.error(this.colors.error(`Error: ${message}`));
}
/**
* Display a success message
*
* @param {string} message - Success message to display
*/
displaySuccess(message) {
console.log(this.colors.success(message));
}
/**
* Display a warning message
*
* @param {string} message - Warning message to display
*/
displayWarning(message) {
console.log(this.colors.warning(`Warning: ${message}`));
}
/**
* Display help text
*/
displayHelp() {
const helpText = `
Fylgja CLI Help
Basic Sigma Commands:
- search sigma <keyword> - Search for Sigma rules by keyword
- details sigma <rule_id> - Get details about a specific Sigma rule
- stats sigma - Get statistics about Sigma rules database
Advanced Sigma Search Commands:
- search sigma where title contains "ransomware" - Search by title
- 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
`;
console.log(helpText);
}
}
module.exports = new CliOutputManager();

View file

@ -0,0 +1,36 @@
/**
* formatters/index.js
*
* Exports all formatters for easy importing
*/
// Text and table formatters
const textFormatter = require('./text_formatter');
const tableFormatter = require('./table_formatter');
// Domain-specific formatters
const sigmaFormatter = require('./sigma_formatter');
// Re-export all formatters
module.exports = {
// Text utilities
wrapText: textFormatter.wrapText,
formatDate: textFormatter.formatDate,
formatNumber: textFormatter.formatNumber,
// Table utilities
formatTable: tableFormatter.formatTable,
formatKeyValueTable: tableFormatter.formatKeyValueTable,
formatTableHeader: tableFormatter.formatTableHeader,
formatTableRow: tableFormatter.formatTableRow,
// Sigma formatters
formatSigmaDetails: sigmaFormatter.formatSigmaDetails,
formatSigmaStats: sigmaFormatter.formatSigmaStats,
formatSigmaSearchResults: sigmaFormatter.formatSigmaSearchResults,
// Rendering functions for CLI output
renderSigmaDetails: sigmaFormatter.renderSigmaDetails,
renderSigmaStats: sigmaFormatter.renderSigmaStats,
renderSigmaSearchResults: sigmaFormatter.renderSigmaSearchResults
};

View file

@ -0,0 +1,196 @@
/**
* sigma_formatter.js
*
* Specialized formatters for Sigma rule data
*/
const { wrapText, formatDate, formatNumber } = require('./text_formatter');
const { formatKeyValueTable, formatTable } = require('./table_formatter');
const colors = require('../utils/colors');
/**
* Format Sigma rule details for CLI display
*
* @param {Object} ruleDetails - The rule details object
* @returns {Object} Formatted rule details
*/
function formatSigmaDetails(ruleDetails) {
if (!ruleDetails) {
return null;
}
// Create a flattened object for display in CLI table format
return {
'ID': ruleDetails.id || 'Unknown',
'Title': ruleDetails.title || 'Untitled Rule',
'Description': ruleDetails.description || 'No description provided',
'Author': ruleDetails.author || 'Unknown author',
'Severity': ruleDetails.severity || 'Unknown',
'Status': ruleDetails.status || 'Unknown',
'Created': formatDate(ruleDetails.date),
'Modified': formatDate(ruleDetails.modified),
'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'
};
}
/**
* Format Sigma statistics for CLI display
*
* @param {Object} stats - The statistics object
* @returns {Object} Formatted stats
*/
function formatSigmaStats(stats) {
if (!stats) {
return { error: 'No statistics data available' };
}
// Create a simplified object suitable for table display
const formattedStats = {
'Last Update': formatDate(stats.lastUpdate),
'Total Rules': formatNumber(stats.totalRules),
'Database Health': `${stats.databaseHealth.contentPercentage}% Complete`,
// OS breakdown
'Windows Rules': formatNumber(stats.operatingSystems.windows),
'Linux Rules': formatNumber(stats.operatingSystems.linux),
'macOS Rules': formatNumber(stats.operatingSystems.macos),
'Other OS Rules': formatNumber(stats.operatingSystems.other),
};
// Add severity levels
if (stats.severityLevels && Array.isArray(stats.severityLevels)) {
stats.severityLevels.forEach(level => {
const levelName = level.level
? level.level.charAt(0).toUpperCase() + level.level.slice(1)
: 'Unknown';
formattedStats[`${levelName} Severity`] = formatNumber(level.count);
});
}
// Add top MITRE tactics if available
if (stats.mitreTactics && Array.isArray(stats.mitreTactics)) {
stats.mitreTactics.slice(0, 5).forEach(tactic => { // Only top 5
const formattedTactic = tactic.tactic
.replace(/-/g, ' ')
.split(' ')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
formattedStats[`MITRE: ${formattedTactic}`] = formatNumber(tactic.count);
});
}
return formattedStats;
}
/**
* Format Sigma search results for CLI display
*
* @param {Object} searchResults - The search results object
* @returns {Object} Formatted search results
*/
function formatSigmaSearchResults(searchResults) {
if (!searchResults || !searchResults.results) {
return { error: 'No search results available' };
}
// Map raw results to formatted results
const formattedResults = searchResults.results.map(rule => {
// Get logsource.product field
let osProduct = 'N/A';
if (rule.logsource && rule.logsource.product) {
osProduct = rule.logsource.product;
}
return {
id: rule.id || '',
title: rule.title || '',
author: rule.author || 'Unknown',
level: rule.level || 'medium',
osProduct: osProduct,
tags: rule.tags || []
};
});
return {
results: formattedResults,
totalCount: searchResults.totalCount || 0
};
}
/**
* Render formatted Sigma details as a string
*
* @param {Object} formattedDetails - Formatted details object
* @returns {string} CLI-ready output
*/
function renderSigmaDetails(formattedDetails) {
if (!formattedDetails) {
return 'No details available';
}
return formatKeyValueTable(formattedDetails, 24, 50).join('\n');
}
/**
* Render formatted Sigma statistics as a string
*
* @param {Object} formattedStats - Formatted statistics object
* @returns {string} CLI-ready output
*/
function renderSigmaStats(formattedStats) {
if (!formattedStats) {
return 'No statistics available';
}
return formatKeyValueTable(formattedStats, 25, 30, colors.statsKey).join('\n');
}
/**
* Render formatted Sigma search results as a string
*
* @param {Object} formattedResults - Formatted search results object
* @returns {string} CLI-ready output
*/
function renderSigmaSearchResults(formattedResults) {
if (!formattedResults || formattedResults.error) {
return formattedResults.error || 'No search results available';
}
const headers = ['#', 'Title', 'OS/Product', 'ID'];
const widths = [5, 32, 15, 13];
// Create rows from results
const rows = formattedResults.results.map((rule, index) => {
return [
(index + 1).toString(),
rule.title.substring(0, 31), // Trim to fit column width
rule.osProduct.substring(0, 14),
rule.id.substring(0, 12)
];
});
// Generate table
const tableLines = formatTable(headers, rows, widths);
// Add count footer
const countLine = colors.count(`${formattedResults.totalCount} rows in set`);
return [...tableLines, '', countLine].join('\n');
}
module.exports = {
formatSigmaDetails,
formatSigmaStats,
formatSigmaSearchResults,
renderSigmaDetails,
renderSigmaStats,
renderSigmaSearchResults
};

View file

@ -0,0 +1,97 @@
/**
* table_formatter.js
*
* Table formatting utilities for CLI output
*/
const colors = require('../utils/colors');
const { wrapText } = require('./text_formatter');
/**
* Creates a formatted table row with fixed column widths
*
* @param {string[]} columns - Array of column values
* @param {number[]} widths - Array of column widths
* @param {Function[]} formatters - Array of formatting functions for each column (optional)
* @returns {string} Formatted table row
*/
function formatTableRow(columns, widths, formatters = []) {
return columns.map((value, index) => {
const formatter = formatters[index] || (text => text);
return formatter(String(value || '').padEnd(widths[index]));
}).join('');
}
/**
* Formats a header row for a table
*
* @param {string[]} headers - Array of header texts
* @param {number[]} widths - Array of column widths
* @returns {string} Formatted header row
*/
function formatTableHeader(headers, widths) {
return formatTableRow(headers, widths, headers.map(() => colors.header));
}
/**
* Formats data as a key-value table
*
* @param {Object} data - Object with key-value pairs to display
* @param {number} keyWidth - Width of the key column
* @param {number} valueWidth - Width of the value column
* @param {Function} keyFormatter - Formatting function for keys
* @returns {string[]} Array of formatted lines
*/
function formatKeyValueTable(data, keyWidth = 25, valueWidth = 50, keyFormatter = colors.key) {
const lines = [];
for (const [key, value] of Object.entries(data)) {
if (typeof value !== 'object' || value === null) {
const formattedKey = keyFormatter(key.padEnd(keyWidth));
// Handle wrapping for multiline values
const wrappedLines = wrapText(value, valueWidth);
// First line includes the key
lines.push(`${formattedKey}${wrappedLines[0] || ''}`);
// Additional lines are indented to align with the value column
for (let i = 1; i < wrappedLines.length; i++) {
lines.push(`${' '.repeat(keyWidth)}${wrappedLines[i]}`);
}
}
}
return lines;
}
/**
* Creates a full table with headers and rows
*
* @param {string[]} headers - Table headers
* @param {Array<Array<string>>} rows - Table data rows
* @param {number[]} widths - Column widths
* @returns {string[]} Array of formatted table lines
*/
function formatTable(headers, rows, widths) {
const lines = [];
// Add header
lines.push(formatTableHeader(headers, widths));
// Add separator line (optional)
// lines.push(widths.map(w => '-'.repeat(w)).join(''));
// Add data rows
rows.forEach(row => {
lines.push(formatTableRow(row, widths));
});
return lines;
}
module.exports = {
formatTableRow,
formatTableHeader,
formatKeyValueTable,
formatTable
};

View file

@ -0,0 +1,97 @@
/**
* text_formatter.js
*
* Common text formatting utilities for CLI output
*/
/**
* Wraps text at specified length, handling different data types
*
* @param {any} text - Text to wrap (will be converted to string)
* @param {number} maxWidth - Maximum line width
* @returns {string[]} Array of wrapped lines
*/
function wrapText(text, maxWidth = 80) {
// Handle non-string or empty inputs
if (text === undefined || text === null) return [''];
// Convert to string and normalize newlines
const normalizedText = String(text).replace(/\n/g, ' ');
// If text fits in one line, return it as a single-element array
if (normalizedText.length <= maxWidth) return [normalizedText];
const words = normalizedText.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;
}
/**
* Formats a date string for display
*
* @param {string} dateString - Date string to format
* @returns {string} Formatted date string
*/
function formatDate(dateString) {
if (!dateString) return 'Unknown';
try {
const date = new Date(dateString);
return date.toLocaleString();
} catch (error) {
return dateString;
}
}
/**
* Formats a number with locale-specific thousands separators
*
* @param {number} num - Number to format
* @returns {string} Formatted number string
*/
function formatNumber(num) {
if (typeof num !== 'number') return String(num || '');
return num.toLocaleString();
}
module.exports = {
wrapText,
formatDate,
formatNumber
};

View file

@ -0,0 +1,105 @@
/**
* cli_logo.js
*
* Gradient-colored ASCII logo for CLI
*/
// Define the ASCII logo
const logoLines = [
'░▒▓████████▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░ ░▒▓██████▓▒░ ░▒▓█▓▒░░▒▓██████▓▒░ ',
'░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░ ',
'░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░ ',
'░▒▓██████▓▒░ ░▒▓██████▓▒░░▒▓█▓▒░ ░▒▓█▓▒▒▓███▓▒░ ░▒▓█▓▒░▒▓████████▓▒░ ',
'░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░ ',
'░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░ ',
'░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓████████▓▒░▒▓██████▓▒░ ░▒▓██████▓▒░░▒▓█▓▒░░▒▓█▓▒░ '
];
// Colors (hex codes without # prefix)
const colors = {
teal: '7DE2D1',
darkTeal: '339989',
offWhite: 'FFFAFB',
lavender: '8F95D3'
};
// Convert hex to RGB
function hexToRgb(hex) {
const r = parseInt(hex.substring(0, 2), 16);
const g = parseInt(hex.substring(2, 4), 16);
const b = parseInt(hex.substring(4, 6), 16);
return { r, g, b };
}
// Linear interpolation between two colors
function interpolateColor(color1, color2, factor) {
return {
r: Math.round(color1.r + factor * (color2.r - color1.r)),
g: Math.round(color1.g + factor * (color2.g - color1.g)),
b: Math.round(color1.b + factor * (color2.b - color1.b))
};
}
// Get ANSI true color escape code
function getTrueColorCode(r, g, b) {
return `\x1b[38;2;${r};${g};${b}m`;
}
// Get ANSI 256-color escape code (fallback for terminals without true color support)
function get256ColorCode(r, g, b) {
// Convert RGB to approximate 256-color code
const ansi256 = 16 +
36 * Math.round(r * 5 / 255) +
6 * Math.round(g * 5 / 255) +
Math.round(b * 5 / 255);
return `\x1b[38;5;${ansi256}m`;
}
/**
* Generate a 2D gradient (both horizontal and vertical gradients combined)
* This creates the most dramatic effect but is more processing-intensive
* @returns {string} The gradient-colored logo
*/
function generateGradientLogo() {
// Convert hex colors to RGB (corners of the 2D gradient)
const topLeft = hexToRgb(colors.teal);
const topRight = hexToRgb(colors.darkTeal);
const bottomLeft = hexToRgb(colors.offWhite);
const bottomRight = hexToRgb(colors.lavender);
// Initialize gradient logo
let gradientLogo = '\n';
// Process each line
for (let y = 0; y < logoLines.length; y++) {
const line = logoLines[y];
const verticalPosition = y / (logoLines.length - 1);
// Interpolate top and bottom colors
const leftColor = interpolateColor(topLeft, bottomLeft, verticalPosition);
const rightColor = interpolateColor(topRight, bottomRight, verticalPosition);
// Process each character in the line
for (let x = 0; x < line.length; x++) {
const char = line[x];
const horizontalPosition = x / (line.length - 1);
// Interpolate between left and right colors
const color = interpolateColor(leftColor, rightColor, horizontalPosition);
// Apply the color and add the character
gradientLogo += getTrueColorCode(color.r, color.g, color.b) + char;
}
// Reset color and add newline
gradientLogo += '\x1b[0m\n';
}
return gradientLogo;
}
// Export the gradient logo functions
module.exports = {
generateGradientLogo,
};

View file

@ -0,0 +1,63 @@
/**
* colors.js
*
* Centralized color definitions and formatting functions for CLI output
* Using direct ANSI color codes instead of chalk
*/
// Define ANSI color codes
const ansiCodes = {
// Text formatting
reset: '\x1b[0m',
bold: '\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 color formatting functions
const colors = {
// Basic formatting functions
blue: (text) => `${ansiCodes.blue}${text}${ansiCodes.reset}`,
green: (text) => `${ansiCodes.green}${text}${ansiCodes.reset}`,
red: (text) => `${ansiCodes.red}${text}${ansiCodes.reset}`,
yellow: (text) => `${ansiCodes.yellow}${text}${ansiCodes.reset}`,
cyan: (text) => `${ansiCodes.cyan}${text}${ansiCodes.reset}`,
white: (text) => `${ansiCodes.white}${text}${ansiCodes.reset}`,
dim: (text) => `${ansiCodes.dim}${text}${ansiCodes.reset}`,
bold: (text) => `${ansiCodes.bold}${text}${ansiCodes.reset}`,
// Composite styles for specific UI elements
header: (text) => `${ansiCodes.cyan}${ansiCodes.bold}${text}${ansiCodes.reset}`,
key: (text) => `${ansiCodes.blue}${text}${ansiCodes.reset}`,
statsKey: (text) => `${ansiCodes.yellow}${text}${ansiCodes.reset}`,
value: (text) => `${ansiCodes.white}${text}${ansiCodes.reset}`,
count: (text) => `${ansiCodes.green}${text}${ansiCodes.reset}`,
error: (text) => `${ansiCodes.red}${text}${ansiCodes.reset}`,
warning: (text) => `${ansiCodes.yellow}${text}${ansiCodes.reset}`,
success: (text) => `${ansiCodes.green}${ansiCodes.bold}${text}${ansiCodes.reset}`
};
module.exports = colors;

View file

@ -147,6 +147,7 @@ const handleCommand = async (command, respond) => {
// Respond with the search results
await respond({
blocks: blocks,
responseData: searchResult.results,
response_type: isEphemeral ? 'ephemeral' : 'in_channel'
});
@ -271,6 +272,7 @@ const handleComplexSearch = async (command, respond) => {
// Respond with the search results
await respond({
blocks: blocks,
responseData: searchResult.results,
response_type: 'ephemeral' // Complex searches are usually more specific to the user
});

View file

@ -17,7 +17,7 @@
const commandPatterns = [
// Sigma details patterns
{
name: 'sigma-details',
name: 'details-sigma',
regex: /^details\s+sigma\s+(.+)$/i,
action: 'details',
module: 'sigma',
@ -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: 'search-sigma-complex-1',
regex: /^search\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: 'search-sigma-complex-2',
regex: /^search\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: 'search-sigma-simple',
regex: /^search\s+sigma\s+(.+)$/i,
action: 'search',
module: 'sigma',
params: [2] // keyword is in capturing group 2
},
// Sigma create patterns
{
name: 'sigma-create',
@ -41,16 +56,8 @@ const commandPatterns = [
params: [2] // rule ID is in capturing group 2
},
// Sigma stats patterns
{
name: 'sigma-stats-first',
regex: /^sigma\s+stats$/i,
action: 'stats',
module: 'sigma',
params: []
},
{
name: 'sigma-stats-second',
name: 'stats-sigma',
regex: /^stats\s+sigma$/i,
action: 'stats',
module: 'sigma',

View 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,