UNPKG

mcp-cve-intelligence-server-lite

Version:

Lite Model Context Protocol server for comprehensive CVE intelligence gathering with multi-source exploit discovery, designed for security professionals and cybersecurity researchers

315 lines 11 kB
import winston from 'winston'; import path from 'path'; import fs from 'fs'; import { getAppConfiguration } from '../config/index.js'; // Get configuration from centralized system const getLogConfig = () => { const config = getAppConfiguration(); return { MAX_LOG_FILES: config.logging.maxFiles, MAX_FILE_SIZE: config.logging.maxFileSizeMB * 1024 * 1024, // Convert MB to bytes LOG_LEVEL: config.logging.level, LOG_DIR: config.logging.dir, enableConsole: config.logging.enableConsole, enableFile: config.logging.enableFile, }; }; // Configuration constants - dynamically loaded from centralized config const LOG_CONFIG = getLogConfig(); // Define the log file types that this logger creates const LOG_FILE_TYPES = { MAIN: 'cve-server', ERROR: 'error', EXCEPTIONS: 'exceptions', REJECTIONS: 'rejections', }; // Ensure logs directory exists const logsDir = path.join(process.cwd(), LOG_CONFIG.LOG_DIR); if (!fs.existsSync(logsDir)) { fs.mkdirSync(logsDir, { recursive: true }); } // Generate timestamp for log file names const getTimestampForFilename = () => { const now = new Date(); return `${now.toISOString().replace(/[:.]/g, '-').split('T')[0]}_${now.toTimeString().split(' ')[0].replace(/:/g, '-')}`; }; // Generate log filenames with timestamps const timestamp = getTimestampForFilename(); const getLogFilename = (baseName) => { const ext = path.extname(baseName); const nameWithoutExt = path.basename(baseName, ext); return `${nameWithoutExt}_${timestamp}${ext}`; }; // Custom format for structured logging const customFormat = winston.format.combine(winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss', }), winston.format.errors({ stack: true }), winston.format.json(), winston.format.printf(({ timestamp, level, message, service, ...meta }) => { const logEntry = { timestamp, level, message, service: service || 'cve-intelligence-server', ...meta, }; return JSON.stringify(logEntry); })); // Create transports array based on configuration const transports = []; // Add console transport only if enabled in configuration if (LOG_CONFIG.enableConsole) { transports.push(new winston.transports.Console({ format: winston.format.combine(winston.format.uncolorize(), winston.format.printf(({ timestamp, level, message, service, ...meta }) => { const metaStr = Object.keys(meta).length > 0 ? ` ${JSON.stringify(meta)}` : ''; return `${timestamp} [${service || 'cve-intelligence-server'}] ${level}: ${message}${metaStr}`; })), })); } // Add file transport only if enabled in configuration if (LOG_CONFIG.enableFile) { transports.push(new winston.transports.File({ filename: path.join(logsDir, getLogFilename(`${LOG_FILE_TYPES.MAIN}.log`)), })); } // Create exception and rejection handlers based on configuration const exceptionHandlers = []; const rejectionHandlers = []; if (LOG_CONFIG.enableFile) { const fileTransport = new winston.transports.File({ filename: path.join(logsDir, getLogFilename(`${LOG_FILE_TYPES.MAIN}.log`)), }); exceptionHandlers.push(fileTransport); rejectionHandlers.push(fileTransport); } // Create logger instance const logger = winston.createLogger({ level: LOG_CONFIG.LOG_LEVEL, format: customFormat, defaultMeta: { service: 'cve-intelligence-server' }, transports, // Handle uncaught exceptions and rejections in the same main log file exceptionHandlers, rejectionHandlers, }); /** * Utility function to clean up old log files * Keeps only the most recent N log files for each type */ export const cleanupOldLogFiles = (keepCount = LOG_CONFIG.MAX_LOG_FILES) => { try { if (!fs.existsSync(logsDir)) { return; } const files = fs.readdirSync(logsDir); // Define log file pattern for the single main log file const logPattern = { pattern: /^cve-server_.*\.log/, type: 'main' }; const logFiles = files .filter(file => logPattern.pattern.test(file)) .map(file => { const filePath = path.join(logsDir, file); return { name: file, path: filePath, stats: fs.statSync(filePath), }; }) .sort((a, b) => b.stats.mtime.getTime() - a.stats.mtime.getTime()); if (logFiles.length > keepCount) { // Keep only the most recent files, delete older ones const filesToDelete = logFiles.slice(keepCount); logger.debug(`Cleaning up ${filesToDelete.length} old ${logPattern.type} log files (keeping ${keepCount} most recent)`); filesToDelete.forEach(file => { try { fs.unlinkSync(file.path); logger.debug(`Cleaned up old log file: ${file.name}`); } catch (error) { logger.warn(`Failed to delete old log file ${file.name}:`, error); } }); } } catch (error) { logger.error('Error during log cleanup:', error); } }; /** * Check if log file exceeds maximum size and trigger rotation if needed */ export const checkLogFileSize = (filePath) => { try { if (fs.existsSync(filePath)) { const stats = fs.statSync(filePath); return stats.size > LOG_CONFIG.MAX_FILE_SIZE; } return false; } catch (error) { logger.warn('Failed to check log file size:', error); return false; } }; /** * Rotate log file by renaming it with timestamp suffix */ export const rotateLogFile = (filePath) => { try { if (fs.existsSync(filePath)) { const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const rotatedPath = `${filePath}.${timestamp}`; fs.renameSync(filePath, rotatedPath); logger.info(`Log file rotated: ${filePath} -> ${rotatedPath}`); // Clean up old files after rotation cleanupOldLogFiles(); } } catch (error) { logger.error('Failed to rotate log file:', error); } }; // Enhanced logging methods with context export const createContextLogger = (context) => { return { debug: (message, meta) => { logger.debug(message, { context, ...meta }); }, info: (message, meta) => { logger.info(message, { context, ...meta }); }, warn: (message, meta) => { logger.warn(message, { context, ...meta }); }, error: (message, error, meta) => { logger.error(message, { context, error: error ? { message: error.message, stack: error.stack, name: error.name, } : undefined, ...meta, }); }, // Specialized logging methods for CVE operations cveSearch: (filters, resultCount, duration) => { logger.info('CVE search completed', { context, operation: 'search', filters, resultCount, durationMs: duration, }); }, cveRetrieve: (cveId, source, fromCache, duration) => { logger.info('CVE details retrieved', { context, operation: 'retrieve', cveId, source, fromCache, durationMs: duration, }); }, sourceSync: (sourceName, recordCount, duration) => { logger.info('Source synchronization completed', { context, operation: 'source_sync', sourceName, recordCount, durationMs: duration, }); }, httpRequest: (method, url, statusCode, duration, meta) => { logger.info('HTTP request completed', { context, operation: 'http_request', method, url, statusCode, durationMs: duration, ...meta, }); }, exploitFound: (cveId, exploitCount, sources) => { logger.info('Exploits found', { context, operation: 'exploit_search', cveId, exploitCount, sources, }); }, }; }; // Performance monitoring export const createPerformanceTimer = (operation) => { const start = Date.now(); return { end: (additionalMeta) => { const duration = Date.now() - start; logger.debug(`Performance: ${operation}`, { operation, durationMs: duration, ...additionalMeta, }); return duration; }, }; }; // Log startup information about single timestamped log file logger.debug('Logger initialized with single timestamped log file', { logDirectory: logsDir, timestamp, logFile: getLogFilename(`${LOG_FILE_TYPES.MAIN}.log`), }); // Initialize log file rotation check on startup const initializeLogRotation = () => { const logFile = path.join(logsDir, getLogFilename(`${LOG_FILE_TYPES.MAIN}.log`)); if (checkLogFileSize(logFile)) { rotateLogFile(logFile); } // Clean up old log files on startup cleanupOldLogFiles(); }; // Initialize on startup initializeLogRotation(); /** * Get current active log file paths */ export const getActiveLogFiles = () => { const logFile = path.join(logsDir, getLogFilename(`${LOG_FILE_TYPES.MAIN}.log`)); return fs.existsSync(logFile) ? [logFile] : []; }; /** * Get log file statistics for monitoring */ export const getLogFileStats = () => { try { if (!fs.existsSync(logsDir)) { return []; } const files = fs.readdirSync(logsDir); const logPattern = { pattern: /^cve-server_.*\.log/, type: 'main' }; const stats = []; files.forEach(file => { if (file.endsWith('.log')) { const filePath = path.join(logsDir, file); const fileStats = fs.statSync(filePath); // Determine file type - only main log files are used now const type = logPattern.pattern.test(file) ? logPattern.type : 'unknown'; stats.push({ name: file, size: fileStats.size, modified: fileStats.mtime, type, }); } }); return stats.sort((a, b) => b.modified.getTime() - a.modified.getTime()); } catch (error) { logger.error('Error getting log file stats:', error); return []; } }; export default logger; //# sourceMappingURL=logger.js.map