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>
206 lines
5.5 KiB
JavaScript
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; |