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>
This commit is contained in:
206
utils/security-logger.js
Normal file
206
utils/security-logger.js
Normal file
@@ -0,0 +1,206 @@
|
||||
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;
|
Reference in New Issue
Block a user