auth/src/middleware/dbAccessLogger.js

282 lines
7.4 KiB
JavaScript

// src/middleware/dbAccessLogger.js
// === SECURITY HARDENING: DATABASE ACCESS LOGGING ===
// Logs all database access for security auditing and compliance
/**
* Database Access Logging
*
* Logs all database queries for security auditing:
* - Query type (SELECT, INSERT, UPDATE, DELETE)
* - Table names accessed
* - Timestamp
* - User context (if available from request)
* - IP address
* - Query parameters (sanitized - no sensitive data)
*
* Configuration:
* - DB_ACCESS_LOGGING_ENABLED: Set to 'true' to enable logging (default: false)
* - DB_ACCESS_LOG_LEVEL: 'all' (all queries) or 'sensitive' (only sensitive tables) (default: 'sensitive')
*
* Sensitive tables (always logged if enabled):
* - users
* - otp_codes
* - otp_requests
* - refresh_tokens
* - auth_audit
*/
const db = require('../db');
const LOGGING_ENABLED = process.env.DB_ACCESS_LOGGING_ENABLED === 'true' || process.env.DB_ACCESS_LOGGING_ENABLED === '1';
const LOG_LEVEL = process.env.DB_ACCESS_LOG_LEVEL || 'sensitive'; // 'all' or 'sensitive'
// Tables that contain sensitive data (always logged)
const SENSITIVE_TABLES = [
'users',
'otp_codes',
'otp_requests',
'refresh_tokens',
'auth_audit',
'user_devices',
];
/**
* Extract table names from SQL query
* @param {string} query - SQL query text
* @returns {string[]} - Array of table names
*/
function extractTableNames(query) {
const tables = [];
const upperQuery = query.toUpperCase();
// Match FROM, JOIN, UPDATE, INSERT INTO patterns
const patterns = [
/FROM\s+([a-z_]+)/gi,
/JOIN\s+([a-z_]+)/gi,
/UPDATE\s+([a-z_]+)/gi,
/INSERT\s+INTO\s+([a-z_]+)/gi,
/DELETE\s+FROM\s+([a-z_]+)/gi,
];
patterns.forEach(pattern => {
const matches = query.matchAll(pattern);
for (const match of matches) {
if (match[1]) {
tables.push(match[1].toLowerCase());
}
}
});
return [...new Set(tables)]; // Remove duplicates
}
/**
* Determine query type from SQL
* @param {string} query - SQL query text
* @returns {string} - Query type (SELECT, INSERT, UPDATE, DELETE, etc.)
*/
function getQueryType(query) {
const upperQuery = query.trim().toUpperCase();
if (upperQuery.startsWith('SELECT')) return 'SELECT';
if (upperQuery.startsWith('INSERT')) return 'INSERT';
if (upperQuery.startsWith('UPDATE')) return 'UPDATE';
if (upperQuery.startsWith('DELETE')) return 'DELETE';
if (upperQuery.startsWith('CREATE')) return 'CREATE';
if (upperQuery.startsWith('ALTER')) return 'ALTER';
if (upperQuery.startsWith('DROP')) return 'DROP';
return 'OTHER';
}
/**
* Sanitize query parameters (remove sensitive data)
* @param {Array} params - Query parameters
* @returns {Array} - Sanitized parameters (sensitive values replaced with '[REDACTED]')
*/
function sanitizeParams(params) {
if (!params || !Array.isArray(params)) {
return [];
}
return params.map(param => {
if (typeof param === 'string') {
// Check if it looks like a phone number
if (/^\+?\d{10,15}$/.test(param)) {
return '[REDACTED_PHONE]';
}
// Check if it looks like a token or hash
if (param.length > 50) {
return '[REDACTED_TOKEN]';
}
// Check if it looks like an encrypted value (format: iv:authTag:data)
if (param.includes(':') && param.split(':').length === 3) {
return '[REDACTED_ENCRYPTED]';
}
}
// For other types, return as-is (numbers, booleans, etc.)
return param;
});
}
/**
* Check if query should be logged
* @param {string} query - SQL query text
* @returns {boolean}
*/
function shouldLogQuery(query) {
if (!LOGGING_ENABLED) {
return false;
}
if (LOG_LEVEL === 'all') {
return true;
}
// Log sensitive tables
const tables = extractTableNames(query);
return tables.some(table => SENSITIVE_TABLES.includes(table));
}
/**
* Log database access
* @param {Object} logData - Log data
*/
async function logDbAccess(logData) {
if (!LOGGING_ENABLED) {
return;
}
try {
// Ensure db_access_log table exists
await ensureDbAccessLogTable();
const {
query,
queryType,
tables,
params,
userId = null,
ipAddress = null,
userAgent = null,
duration = null,
} = logData;
await db.query(
`INSERT INTO db_access_log (
query_type, tables_accessed, query_text, params_sanitized,
user_id, ip_address, user_agent, duration_ms, created_at
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, NOW())`,
[
queryType,
JSON.stringify(tables),
query.substring(0, 1000), // Truncate long queries
JSON.stringify(sanitizeParams(params)),
userId,
ipAddress,
userAgent,
duration,
]
);
} catch (err) {
// Don't throw - logging failures should not break the application
console.error('[dbAccessLogger] Failed to log database access:', err.message);
}
}
/**
* Ensure db_access_log table exists
*/
async function ensureDbAccessLogTable() {
try {
await db.query(`
CREATE TABLE IF NOT EXISTS db_access_log (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
query_type VARCHAR(20) NOT NULL,
tables_accessed JSONB,
query_text TEXT NOT NULL,
params_sanitized JSONB,
user_id UUID,
ip_address VARCHAR(45),
user_agent TEXT,
duration_ms INTEGER,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_db_access_log_created_at ON db_access_log(created_at);
CREATE INDEX IF NOT EXISTS idx_db_access_log_user_id ON db_access_log(user_id);
CREATE INDEX IF NOT EXISTS idx_db_access_log_tables ON db_access_log USING GIN(tables_accessed);
CREATE INDEX IF NOT EXISTS idx_db_access_log_query_type ON db_access_log(query_type);
`);
} catch (err) {
// Table might already exist or database might not be ready
// This is fine, the insert will handle it
}
}
/**
* Wrap database query with logging
* @param {string} query - SQL query
* @param {Array} params - Query parameters
* @param {Object} context - Request context (optional)
* @returns {Promise} - Query result
*/
async function loggedQuery(query, params = [], context = {}) {
const startTime = Date.now();
const queryType = getQueryType(query);
const tables = extractTableNames(query);
// Check if we should log this query
if (!shouldLogQuery(query)) {
// Execute query without logging
return db.query(query, params);
}
try {
// Execute query
const result = await db.query(query, params);
const duration = Date.now() - startTime;
// Log access (fire and forget)
logDbAccess({
query,
queryType,
tables,
params,
userId: context.userId || null,
ipAddress: context.ipAddress || null,
userAgent: context.userAgent || null,
duration,
}).catch(err => {
console.error('[dbAccessLogger] Failed to log (non-blocking):', err.message);
});
return result;
} catch (err) {
// Log failed query too
const duration = Date.now() - startTime;
logDbAccess({
query,
queryType,
tables,
params,
userId: context.userId || null,
ipAddress: context.ipAddress || null,
userAgent: context.userAgent || null,
duration,
error: err.message,
}).catch(() => {
// Ignore logging errors
});
throw err;
}
}
module.exports = {
loggedQuery,
logDbAccess,
shouldLogQuery,
isLoggingEnabled: () => LOGGING_ENABLED,
};