Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 | /**
* Middleware Audit — Log automatique des actions utilisateur.
*
* S'attache aux routes mutantes (POST, PATCH, PUT, DELETE) et
* enregistre l'action dans la table audit_logs.
*
* Securite :
* - Ne log jamais les mots de passe, cles privees ou tokens
* - IP extraite depuis X-Forwarded-For (derriere Traefik)
* - Fire-and-forget (ne bloque pas la reponse)
*
* @module middleware/audit
*/
const { pool: db } = require('../db');
// Mots-cles a filtrer des details (securite OWASP)
const SENSITIVE_KEYS = ['password', 'private_key', 'encrypted_private_key', 'passphrase',
'passphrase_encrypted', 'encrypted_key', 'token', 'secret', 'key_data'];
/**
* Filtre les champs sensibles d'un objet (recursif 1 niveau).
* @param {Object} obj
* @returns {Object}
*/
function sanitizeDetails(obj) {
if (!obj || typeof obj !== 'object') return obj;
const sanitized = {};
for (const [key, value] of Object.entries(obj)) {
if (SENSITIVE_KEYS.some((sk) => key.toLowerCase().includes(sk))) {
sanitized[key] = '[REDACTED]';
} else if (typeof value === 'string' && value.length > 500) {
sanitized[key] = value.substring(0, 500) + '...[tronque]';
} else {
sanitized[key] = value;
}
}
return sanitized;
}
/**
* Determine la categorie d'audit depuis le chemin de la route.
* @param {string} path
* @returns {string}
*/
// Table de correspondance route → categorie d'audit (ordre = priorite)
const ROUTE_CATEGORIES = [
{ prefix: '/auth', category: 'auth' },
{ keyword: '/docker', category: 'docker' },
{ keyword: '/logs', category: 'file' },
{ keyword: '/files', category: 'file' },
{ keyword: '/quick-actions', category: 'server' },
{ keyword: '/backups', category: 'backup' },
{ keyword: '/cron', category: 'cron' },
{ keyword: '/alerts', category: 'alert' },
{ keyword: '/webhooks', category: 'webhook' },
{ keyword: '/annotations', category: 'annotation' },
{ keyword: '/groups', category: 'general' },
{ keyword: '/digest', category: 'settings' },
{ keyword: '/report', category: 'settings' },
{ keyword: '/ssl', category: 'ssl' },
{ keyword: '/uptime', category: 'uptime' },
{ keyword: '/status-pages', category: 'status_page' },
{ keyword: '/ssh-keys', category: 'ssh_key' },
{ keyword: '/api-keys', category: 'settings' },
{ keyword: '/servers', category: 'server' },
{ keyword: '/chat', category: 'chat' },
{ keyword: '/export', category: 'export' },
{ keyword: '/import', category: 'export' },
];
function categorizeRoute(path) {
for (const rule of ROUTE_CATEGORIES) {
if (rule.prefix && path.startsWith(rule.prefix)) return rule.category;
if (rule.keyword && path.includes(rule.keyword)) return rule.category;
}
return 'general';
}
function getAuthLabel(path) {
if (path.includes('login')) return 'Connexion';
if (path.includes('register')) return 'Inscription';
if (path.includes('logout')) return 'Deconnexion';
return 'Auth';
}
/**
* Genere un label d'action lisible.
* @param {string} method
* @param {string} path
* @returns {string}
*/
function describeAction(method, path) {
const category = categorizeRoute(path);
const verbs = { POST: 'Creer', PATCH: 'Modifier', PUT: 'Modifier', DELETE: 'Supprimer' };
const verb = verbs[method] || method;
const labels = {
auth: { POST: getAuthLabel(path) },
docker: { POST: 'Action Docker' },
backup: { POST: 'Backup' },
chat: { POST: 'Message chat' },
};
if (labels[category]?.[method]) return labels[category][method];
return `${verb} ${category}`;
}
/**
* Extrait l'IP reelle du client.
* @param {import('express').Request} req
* @returns {string}
*/
function getClientIp(req) {
const forwarded = req.headers['x-forwarded-for'];
if (forwarded) return forwarded.split(',')[0].trim();
return req.socket?.remoteAddress || 'unknown';
}
/**
* Middleware Express — log les actions mutantes (POST, PATCH, PUT, DELETE).
* Se place APRES requireAuth pour avoir req.user.
*/
function auditLog(req, res, next) {
// Ignorer les GET (lecture seule) et les health checks
if (req.method === 'GET' || req.method === 'HEAD' || req.method === 'OPTIONS') {
return next();
}
// Ignorer les routes de refresh token (trop frequent)
if (req.path === '/auth/refresh') {
return next();
}
// Hook sur res.json pour capturer le statut de reponse
const originalJson = res.json.bind(res);
res.json = function auditedJson(body) {
// Fire-and-forget : ne pas attendre l'insert
if (req.user?.id) {
const action = describeAction(req.method, req.originalUrl || req.path);
const category = categorizeRoute(req.originalUrl || req.path);
// Extraire le premier param de route disponible comme target
const params = req.params ? Object.values(req.params).filter(Boolean) : [];
const target = params[0] || null;
const details = sanitizeDetails({
method: req.method,
path: req.originalUrl || req.path,
status: res.statusCode,
body_keys: req.body ? Object.keys(req.body) : [],
});
// Contexte groupe (si disponible via requireGroupContext)
const ownerUserId = req.access?.ownerUserId || null;
const groupId = req.access?.groupId || null;
const resourceType = category !== 'general' ? category : null;
// resource_id doit etre un UUID valide ou null
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
const resourceId = target && uuidRegex.test(target) ? target : null;
db.query(
`INSERT INTO audit_logs (user_id, action, category, target, details, ip, owner_user_id, group_id, resource_type, resource_id)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`,
[req.user.id, action, category, target, JSON.stringify(details), getClientIp(req),
ownerUserId, groupId, resourceType, resourceId],
).catch((err) => {
console.error('[audit] Erreur insert:', err.message);
});
}
return originalJson(body);
};
next();
}
module.exports = { auditLog, sanitizeDetails, categorizeRoute, describeAction, getClientIp };
|