create CLI and sigma stats function in CLI

This commit is contained in:
Charlotte Croce 2025-04-18 17:15:41 -04:00
parent 85bb8958b8
commit 519c87fb04
9 changed files with 849 additions and 46 deletions

2
fylgja-cli Executable file
View file

@ -0,0 +1,2 @@
#!/bin/bash
node "$(dirname "$0")/src/fylgja-cli.js" "$@"

59
fylgja-cli.md Normal file
View file

@ -0,0 +1,59 @@
# Fylgja CLI Interface
The Fylgja CLI provides an interactive command-line interface for managing SIEM rules, similar to MySQL's CLI.
## Usage
Start the CLI interface:
```bash
npm run cli
```
Or use the direct launcher:
```bash
./fylgja-cli
```
## Features
- **Interactive Prompt**: MySQL-style prompt with command history
- **Tab Completion**: Press Tab to auto-complete commands
- **Command History**: Use Up/Down arrows to navigate previous commands
- **Formatted Output**: Table-based output formats for different commands
- **Color Coding**: Visual indicators for severity levels and result types
## Available Commands
### Basic Commands
```
search <keyword> Search for Sigma rules by keyword
details <rule_id> Get details about a specific Sigma rule
stats Get statistics about Sigma rules database
help Display help information
exit/quit Exit the CLI
clear Clear the terminal screen
```
### Advanced Search Commands
```
search sigma rules where title contains "ransomware"
find rules where tags include privilege_escalation
search rules where logsource.category == "process_creation"
find rules where modified after 2024-01-01
```
## Examples
```
fylgja> search rules where level is "high"
fylgja> details 5f35f6c7-80a7-4ca0-a41f-31e8ac557233
fylgja> stats
```
## Integration with Slack Bot
The CLI interface uses the same command parsing and execution logic as the Slack bot, ensuring consistency across interfaces.

65
package-lock.json generated
View file

@ -10,10 +10,13 @@
"license": "ISC",
"dependencies": {
"@slack/bolt": "^4.2.1",
"axios": "^1.6.7",
"chalk": "^5.4.1",
"dotenv": "^16.4.7",
"express": "^5.1.0",
"glob": "^8.1.0",
"js-yaml": "^4.1.0",
"readline": "^1.3.0",
"sqlite3": "^5.1.7"
},
"devDependencies": {
@ -659,35 +662,17 @@
}
},
"node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"version": "5.4.1",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz",
"integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==",
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=10"
"node": "^12.17.0 || ^14.13 || >=16.0.0"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/chalk/node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/chownr": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
@ -797,6 +782,36 @@
"url": "https://github.com/open-cli-tools/concurrently?sponsor=1"
}
},
"node_modules/concurrently/node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/concurrently/node_modules/chalk/node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/console-control-strings": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
@ -2441,6 +2456,12 @@
"node": ">= 6"
}
},
"node_modules/readline": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/readline/-/readline-1.3.0.tgz",
"integrity": "sha512-k2d6ACCkiNYz222Fs/iNze30rRJ1iIicW7JuX/7/cozvih6YCkFZH+J6mAFDVgv0dRBaAyr4jDqC95R2y4IADg==",
"license": "BSD"
},
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",

View file

@ -7,7 +7,8 @@
"ngrok": "ngrok http 3000 --log=stdout --url=tolerant-bull-ideal.ngrok-free.app",
"dev": "concurrently \"npm run start\" \"npm run ngrok\"",
"update-db": "node src/sigma_db/sigma_db_initialize.js",
"test": "echo \"Error: no test specified\" && exit 1"
"test": "echo \"Error: no test specified\" && exit 1",
"cli": "node src/fylgja-cli.js"
},
"keywords": [],
"author": "",
@ -16,10 +17,12 @@
"dependencies": {
"@slack/bolt": "^4.2.1",
"axios": "^1.6.7",
"chalk": "^5.4.1",
"dotenv": "^16.4.7",
"express": "^5.1.0",
"glob": "^8.1.0",
"js-yaml": "^4.1.0",
"readline": "^1.3.0",
"sqlite3": "^5.1.7"
},
"devDependencies": {

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

@ -0,0 +1,509 @@
/**
* fylgja-cli.js
*
* 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');
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];
}
/**
* 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':
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':
console.log('\n+----------------------+--------------------------------------------------+');
console.log('| Field | Value |');
console.log('+----------------------+--------------------------------------------------+');
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);
console.log(`| ${formattedKey} | ${formattedValue} |`);
}
}
console.log('+----------------------+--------------------------------------------------+');
break;
case 'stats':
console.log('\n+--------------------+---------------+');
console.log('| Metric | Value |');
console.log('+--------------------+---------------+');
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);
console.log(`| ${formattedKey} | ${formattedValue} |`);
}
console.log('+--------------------+---------------+');
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
- sigma stats - Get statistics about Sigma rules database
Advanced Sigma Search Commands:
- search sigma rules where title contains "ransomware" - Search by title
- search sigma rules where tags include privilege_escalation - Search by tags
- search sigma rules where logsource.category == "process_creation" - Search by log source
- search sigma rules 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
};
}

View file

@ -39,7 +39,7 @@ const handleCommand = async (command, respond) => {
return;
}
// Generate blocks for displaying statistics
// For Slack responses, generate Block Kit blocks
let blocks;
try {
blocks = getStatsBlocks(statsResult.stats);
@ -51,9 +51,10 @@ const handleCommand = async (command, respond) => {
return;
}
// Return the response
// Return the response with both blocks for Slack and responseData for CLI
await respond({
blocks: blocks,
responseData: statsResult.stats, // Include raw data for CLI
response_type: 'in_channel'
});
} catch (error) {

View file

@ -31,11 +31,25 @@ async function getSigmaStats() {
};
}
// Format the data in a consistent structure for both CLI and Slack
const formattedStats = {
lastUpdate: statsResult.stats.lastUpdate,
totalRules: statsResult.stats.totalRules,
databaseHealth: statsResult.stats.databaseHealth,
operatingSystems: statsResult.stats.operatingSystems,
severityLevels: statsResult.stats.severityLevels,
mitreTactics: statsResult.stats.mitreTactics,
topAuthors: statsResult.stats.topAuthors,
// Add any other statistics needed
};
logger.info(`${FILE_NAME}: Successfully collected database statistics`);
return {
success: true,
stats: statsResult.stats
stats: formattedStats,
// Include raw response data for direct use by CLI
responseData: formattedStats
};
} catch (error) {
logger.error(`${FILE_NAME}: Error processing statistics: ${error.message}`);

171
src/utils/cli_formatters.js Normal file
View file

@ -0,0 +1,171 @@
/**
* cli_formatters.js
*
* Dedicated formatters for CLI output of various data types
* Converts raw data into formatted CLI-friendly displays
*/
const chalk = require('chalk');
/**
* Format Sigma statistics for CLI display
*
* @param {Object} stats - The statistics object
* @returns {Object} Formatted stats ready for CLI display
*/
function formatSigmaStats(stats) {
if (!stats) {
return { error: 'No statistics data available' };
}
// Format date
const formatDate = (dateString) => {
if (!dateString) return 'Unknown';
try {
const date = new Date(dateString);
return date.toLocaleString();
} catch (error) {
return dateString;
}
};
// Create a simplified object suitable for table display
const formattedStats = {
'Last Update': formatDate(stats.lastUpdate),
'Total Rules': stats.totalRules.toLocaleString(),
'Database Health': `${stats.databaseHealth.contentPercentage}% Complete`,
// OS breakdown
'Windows Rules': stats.operatingSystems.windows.toLocaleString(),
'Linux Rules': stats.operatingSystems.linux.toLocaleString(),
'macOS Rules': stats.operatingSystems.macos.toLocaleString(),
'Other OS Rules': stats.operatingSystems.other.toLocaleString(),
// Add severity levels
...(stats.severityLevels || []).reduce((acc, level) => {
const levelName = level.level
? level.level.charAt(0).toUpperCase() + level.level.slice(1)
: 'Unknown';
acc[`${levelName} Severity`] = level.count.toLocaleString();
return acc;
}, {})
};
// Add top MITRE tactics if available
if (stats.mitreTactics && stats.mitreTactics.length > 0) {
stats.mitreTactics.forEach((tactic, index) => {
if (index < 5) { // Only include top 5 for brevity
const formattedTactic = tactic.tactic
.replace(/-/g, ' ')
.split(' ')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
formattedStats[`MITRE: ${formattedTactic}`] = tactic.count.toLocaleString();
}
});
}
return formattedStats;
}
/**
* Format Sigma search results for CLI display
*
* @param {Object} searchResults - The search results object
* @returns {Object} Formatted results ready for CLI display
*/
function formatSigmaSearchResults(searchResults) {
if (!searchResults || !searchResults.results) {
return { error: 'No search results available' };
}
// Return a structure with results and meta info
return {
results: searchResults.results.map(rule => ({
id: rule.id || '',
title: rule.title || '',
author: rule.author || 'Unknown',
level: rule.level || 'medium'
})),
totalCount: searchResults.totalCount || 0
};
}
/**
* Format Sigma rule details for CLI display
*
* @param {Object} ruleDetails - The rule details object
* @returns {Object} Formatted details ready for CLI display
*/
function formatSigmaDetails(ruleDetails) {
if (!ruleDetails) {
return { error: 'No rule details available' };
}
// Filter and format the rule details for CLI display
const formattedDetails = {};
// Include only the most important fields for display
const fieldsToInclude = [
'id', 'title', 'description', 'status', 'author',
'level', 'falsepositives', 'references',
'created', 'modified'
];
// Add detection information if available
if (ruleDetails.detection && ruleDetails.detection.condition) {
fieldsToInclude.push('detection_condition');
formattedDetails['detection_condition'] = ruleDetails.detection.condition;
}
// Add logsource information if available
if (ruleDetails.logsource) {
if (ruleDetails.logsource.product) {
fieldsToInclude.push('logsource_product');
formattedDetails['logsource_product'] = ruleDetails.logsource.product;
}
if (ruleDetails.logsource.category) {
fieldsToInclude.push('logsource_category');
formattedDetails['logsource_category'] = ruleDetails.logsource.category;
}
if (ruleDetails.logsource.service) {
fieldsToInclude.push('logsource_service');
formattedDetails['logsource_service'] = ruleDetails.logsource.service;
}
}
// Format date fields
const dateFields = ['created', 'modified'];
for (const [key, value] of Object.entries(ruleDetails)) {
if (fieldsToInclude.includes(key)) {
// Format dates
if (dateFields.includes(key) && value) {
try {
formattedDetails[key] = new Date(value).toLocaleString();
} catch (e) {
formattedDetails[key] = value;
}
}
// Format arrays
else if (Array.isArray(value)) {
formattedDetails[key] = value.join(', ');
}
// Default handling
else {
formattedDetails[key] = value;
}
}
}
return formattedDetails;
}
module.exports = {
formatSigmaStats,
formatSigmaSearchResults,
formatSigmaDetails
};

View file

@ -1,6 +1,7 @@
/**
* logger.js
* Handles logging functionality
*
* Handles logging functionality with CLI mode support
*/
const fs = require('fs');
const path = require('path');
@ -26,11 +27,28 @@ if (!fs.existsSync(LOGS_DIR)) {
// Use log file from config if available, otherwise use default
const LOG_FILE = LOGGING_CONFIG?.file
? path.resolve(path.join(__dirname, '..', '..'), LOGGING_CONFIG.file)
? path.resolve(path.join(__dirname, '..', '..'), LOGGING_CONFIG.file)
: path.join(LOGS_DIR, 'fylgja.log');
// Flag to determine if we're running in CLI mode
let isCliMode = false;
// Create logger object
const logger = {
/**
* Set the CLI mode flag
* @param {boolean} mode - True to enable CLI mode (no console output)
*/
setCliMode: (mode) => {
isCliMode = !!mode;
},
/**
* Check if running in CLI mode
* @returns {boolean} CLI mode status
*/
isCliMode: () => isCliMode,
/**
* Internal method to write log entry to file and console if level meets threshold
* @param {string} level - Log level (DEBUG, INFO, WARN, ERROR)
@ -48,23 +66,28 @@ const logger = {
try {
fs.appendFileSync(LOG_FILE, logEntry);
} catch (err) {
console.error(`Failed to write to log file: ${err.message}`);
// If in CLI mode, don't output to console
if (!isCliMode) {
console.error(`Failed to write to log file: ${err.message}`);
}
}
// Also log to console with appropriate method
switch (level) {
case 'ERROR':
console.error(logEntry.trim());
break;
case 'WARN':
console.warn(logEntry.trim());
break;
case 'DEBUG':
console.debug(logEntry.trim());
break;
case 'INFO':
default:
console.info(logEntry.trim());
// Only log to console if not in CLI mode
if (!isCliMode) {
switch (level) {
case 'ERROR':
console.error(logEntry.trim());
break;
case 'WARN':
console.warn(logEntry.trim());
break;
case 'DEBUG':
console.debug(logEntry.trim());
break;
case 'INFO':
default:
console.info(logEntry.trim());
}
}
}
},