Files
LuHost/utils/security-logger.js
Nathan Schneider 3aed09b60f Initial commit: LuHost - Luanti Server Management Web Interface
A modern web interface for Luanti (Minetest) server management with ContentDB integration.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-23 17:32:37 -06:00

206 lines
5.5 KiB
JavaScript

const fs = require('fs').promises;
const path = require('path');
class SecurityLogger {
constructor() {
this.logFile = path.join(process.cwd(), 'security.log');
this.maxLogSize = 10 * 1024 * 1024; // 10MB
this.maxLogFiles = 5;
}
async log(level, event, details = {}, req = null) {
const timestamp = new Date().toISOString();
// Extract safe request information
const requestInfo = req ? {
ip: req.ip || req.connection.remoteAddress,
userAgent: req.get('User-Agent'),
method: req.method,
url: req.originalUrl || req.url,
userId: req.session?.user?.id,
username: req.session?.user?.username
} : {};
const logEntry = {
timestamp,
level,
event,
details,
request: requestInfo,
pid: process.pid
};
const logLine = JSON.stringify(logEntry) + '\n';
try {
// Check if log rotation is needed
await this.rotateLogIfNeeded();
// Append to log file
await fs.appendFile(this.logFile, logLine);
// Also log to console for development
if (process.env.NODE_ENV !== 'production') {
console.log(`[SECURITY] ${level.toUpperCase()}: ${event}`, details);
}
} catch (error) {
console.error('Failed to write security log:', error);
}
}
async rotateLogIfNeeded() {
try {
const stats = await fs.stat(this.logFile);
if (stats.size > this.maxLogSize) {
// Rotate logs
for (let i = this.maxLogFiles - 1; i > 0; i--) {
const oldFile = `${this.logFile}.${i}`;
const newFile = `${this.logFile}.${i + 1}`;
try {
await fs.rename(oldFile, newFile);
} catch (error) {
// File might not exist, continue
}
}
// Move current log to .1
await fs.rename(this.logFile, `${this.logFile}.1`);
}
} catch (error) {
// Log file might not exist yet, that's fine
}
}
// Security event logging methods
async logAuthSuccess(req, username) {
await this.log('info', 'AUTH_SUCCESS', {
username,
sessionId: req.sessionID
}, req);
}
async logAuthFailure(req, username, reason) {
await this.log('warn', 'AUTH_FAILURE', {
username,
reason,
sessionId: req.sessionID
}, req);
}
async logCommandExecution(req, command, result) {
await this.log('info', 'COMMAND_EXECUTION', {
command,
result: result ? 'success' : 'failed'
}, req);
}
async logConfigChange(req, section, changes) {
await this.log('info', 'CONFIG_CHANGE', {
section,
changes: Object.keys(changes)
}, req);
}
async logSecurityViolation(req, violationType, details) {
await this.log('error', 'SECURITY_VIOLATION', {
violationType,
details
}, req);
}
async logServerStart(req, worldName, options = {}) {
await this.log('info', 'SERVER_START', {
worldName,
options
}, req);
}
async logServerStop(req, forced = false) {
await this.log('info', 'SERVER_STOP', {
forced
}, req);
}
async logFileAccess(req, filePath, operation) {
await this.log('info', 'FILE_ACCESS', {
filePath,
operation
}, req);
}
async logSuspiciousActivity(req, activityType, details) {
await this.log('warn', 'SUSPICIOUS_ACTIVITY', {
activityType,
details
}, req);
}
async logRateLimitExceeded(req) {
await this.log('warn', 'RATE_LIMIT_EXCEEDED', {
limit: 'request_rate'
}, req);
}
async logCSRFViolation(req) {
await this.log('error', 'CSRF_VIOLATION', {
referer: req.get('Referer'),
origin: req.get('Origin')
}, req);
}
async logInputValidationFailure(req, field, value, reason) {
await this.log('warn', 'INPUT_VALIDATION_FAILURE', {
field,
valueLength: value ? value.length : 0,
reason
}, req);
}
// Read security logs (for admin interface)
async getRecentLogs(limit = 100) {
try {
const content = await fs.readFile(this.logFile, 'utf-8');
const lines = content.trim().split('\n').filter(line => line);
return lines.slice(-limit).map(line => {
try {
return JSON.parse(line);
} catch {
return { error: 'Failed to parse log line', line };
}
}).reverse(); // Most recent first
} catch (error) {
return [];
}
}
// Get security metrics
async getSecurityMetrics(hours = 24) {
const logs = await this.getRecentLogs(10000); // Large sample
const since = new Date(Date.now() - hours * 60 * 60 * 1000);
const recentLogs = logs.filter(log =>
log.timestamp && new Date(log.timestamp) > since
);
const metrics = {
totalEvents: recentLogs.length,
authFailures: recentLogs.filter(log => log.event === 'AUTH_FAILURE').length,
securityViolations: recentLogs.filter(log => log.event === 'SECURITY_VIOLATION').length,
suspiciousActivity: recentLogs.filter(log => log.event === 'SUSPICIOUS_ACTIVITY').length,
rateLimitExceeded: recentLogs.filter(log => log.event === 'RATE_LIMIT_EXCEEDED').length,
csrfViolations: recentLogs.filter(log => log.event === 'CSRF_VIOLATION').length,
commandExecutions: recentLogs.filter(log => log.event === 'COMMAND_EXECUTION').length,
configChanges: recentLogs.filter(log => log.event === 'CONFIG_CHANGE').length
};
return metrics;
}
}
// Singleton instance
const securityLogger = new SecurityLogger();
module.exports = securityLogger;