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
JavaScript
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