Files
ldap-auth-service/lib/auditLog.js
2026-08-21 00:34:47 +05:00

285 lines
8.9 KiB
JavaScript

'use strict';
const fs = require('fs');
const path = require('path');
const { DatabaseSync } = require('node:sqlite');
const dbPath = path.join(__dirname, '..', 'data', 'journal.db');
const RETENTION_DAYS = 30;
const IPV4_RE = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
let db = null;
function getDb() {
if (db) return db;
fs.mkdirSync(path.dirname(dbPath), { recursive: true });
db = new DatabaseSync(dbPath);
db.exec(`
PRAGMA journal_mode = WAL;
CREATE TABLE IF NOT EXISTS admin_login_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ts INTEGER NOT NULL,
ip TEXT NOT NULL,
username TEXT NOT NULL DEFAULT '',
success INTEGER NOT NULL,
reason TEXT NOT NULL DEFAULT ''
);
CREATE INDEX IF NOT EXISTS idx_admin_ts ON admin_login_log(ts);
CREATE INDEX IF NOT EXISTS idx_admin_ip ON admin_login_log(ip);
CREATE INDEX IF NOT EXISTS idx_admin_success ON admin_login_log(success);
CREATE TABLE IF NOT EXISTS auth_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ts INTEGER NOT NULL,
ip TEXT NOT NULL,
username TEXT NOT NULL DEFAULT '',
success INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_auth_ts ON auth_log(ts);
CREATE INDEX IF NOT EXISTS idx_auth_ip ON auth_log(ip);
CREATE INDEX IF NOT EXISTS idx_auth_success ON auth_log(success);
CREATE TABLE IF NOT EXISTS block_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ts INTEGER NOT NULL,
ip TEXT NOT NULL,
blocked_until INTEGER NOT NULL,
attempts INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_block_ts ON block_events(ts);
CREATE TABLE IF NOT EXISTS active_blocks (
ip TEXT PRIMARY KEY,
blocked_until INTEGER NOT NULL,
attempts INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS ip_whitelist (
id INTEGER PRIMARY KEY AUTOINCREMENT,
pattern TEXT NOT NULL UNIQUE,
comment TEXT NOT NULL DEFAULT ''
);
`);
return db;
}
// ================= IP helpers (IPv4) =================
function ipToInt(ip) {
const m = IPV4_RE.exec(String(ip || ''));
if (!m) return null;
const parts = m.slice(1).map(Number);
if (parts.some((p) => p > 255)) return null;
return ((parts[0] << 24) | (parts[1] << 16) | (parts[2] << 8) | parts[3]) >>> 0;
}
function isValidPattern(pattern) {
const value = String(pattern || '').trim();
if (!value) return false;
const parts = value.split('/');
if (parts.length === 1) {
return ipToInt(value) !== null;
}
if (parts.length === 2) {
const prefix = Number(parts[1]);
return Number.isInteger(prefix) && prefix >= 0 && prefix <= 32 && ipToInt(parts[0]) !== null;
}
return false;
}
function patternContains(pattern, ipInt) {
const parts = String(pattern).trim().split('/');
const base = ipToInt(parts[0]);
if (base === null || ipInt === null) return false;
if (parts.length === 1) return base === ipInt;
const prefix = Number(parts[1]);
if (prefix === 0) return true;
const mask = prefix === 32 ? 0xffffffff : (0xffffffff << (32 - prefix)) >>> 0;
return (base & mask) === (ipInt & mask);
}
// ================= Whitelist =================
function listWhitelist() {
return getDb()
.prepare('SELECT id, pattern, comment FROM ip_whitelist ORDER BY id')
.all()
.map((r) => ({ id: r.id, pattern: r.pattern, comment: r.comment }));
}
function addWhitelist(pattern, comment) {
const value = String(pattern || '').trim();
if (!isValidPattern(value)) {
const err = new Error('Некорректный IP или маска (пример: 192.168.1.0/24)');
err.status = 400;
throw err;
}
try {
const info = getDb()
.prepare('INSERT INTO ip_whitelist (pattern, comment) VALUES (?, ?)')
.run(value, String(comment || '').trim());
return { id: Number(info.lastInsertRowid), pattern: value, comment: String(comment || '').trim() };
} catch (err) {
if (String(err.message || '').includes('UNIQUE')) {
const e = new Error('Этот IP/маска уже в списке');
e.status = 409;
throw e;
}
throw err;
}
}
function removeWhitelist(id) {
getDb().prepare('DELETE FROM ip_whitelist WHERE id = ?').run(Number(id));
}
function isWhitelisted(ip) {
if (!ip) return false;
const ipInt = ipToInt(ip);
if (ipInt === null) return false;
const rows = getDb().prepare('SELECT pattern FROM ip_whitelist').all();
return rows.some((r) => patternContains(r.pattern, ipInt));
}
// ================= Journals =================
function logAdminEntry({ ip, username, success, reason }) {
getDb()
.prepare('INSERT INTO admin_login_log (ts, ip, username, success, reason) VALUES (?, ?, ?, ?, ?)')
.run(Date.now(), String(ip || ''), String(username || ''), success ? 1 : 0, String(reason || ''));
}
function logAuthEntry({ ip, username, success }) {
getDb()
.prepare('INSERT INTO auth_log (ts, ip, username, success) VALUES (?, ?, ?, ?)')
.run(Date.now(), String(ip || ''), String(username || ''), success ? 1 : 0);
}
// ================= Blocks =================
function blockIp(ip, blockMs, attempts) {
const now = Date.now();
const blockedUntil = now + blockMs;
getDb()
.prepare('INSERT INTO active_blocks (ip, blocked_until, attempts) VALUES (?, ?, ?) ON CONFLICT(ip) DO UPDATE SET blocked_until = excluded.blocked_until, attempts = excluded.attempts')
.run(String(ip), blockedUntil, attempts);
getDb()
.prepare('INSERT INTO block_events (ts, ip, blocked_until, attempts) VALUES (?, ?, ?, ?)')
.run(now, String(ip), blockedUntil, attempts);
}
function getActiveBlock(ip) {
const row = getDb().prepare('SELECT ip, blocked_until, attempts FROM active_blocks WHERE ip = ?').get(String(ip || ''));
if (!row) return null;
return { ip: row.ip, blockedUntil: row.blocked_until, attempts: row.attempts };
}
function removeActiveBlock(ip) {
getDb().prepare('DELETE FROM active_blocks WHERE ip = ?').run(String(ip || ''));
}
function getActiveBlocks() {
const now = Date.now();
const rows = getDb()
.prepare('SELECT ip, blocked_until, attempts FROM active_blocks WHERE blocked_until > ? ORDER BY blocked_until')
.all(now);
return rows.map((r) => ({
ip: r.ip,
blockedUntil: r.blocked_until,
attempts: r.attempts,
remainingMs: r.blocked_until - now
}));
}
function getBlockHistory(limit) {
const n = Math.min(Math.max(Number(limit) || 200, 1), 1000);
return getDb()
.prepare('SELECT ts, ip, blocked_until, attempts FROM block_events ORDER BY ts DESC LIMIT ?')
.all(n)
.map((r) => ({ ts: r.ts, ip: r.ip, blockedUntil: r.blocked_until, attempts: r.attempts }));
}
// ================= Queries =================
const JOURNAL_TABLES = {
admin: 'admin_login_log',
auth: 'auth_log'
};
function queryJournal(type, filters) {
const table = JOURNAL_TABLES[type];
if (!table) {
const err = new Error('Неизвестный тип журнала');
err.status = 400;
throw err;
}
const where = [];
const params = [];
const f = filters || {};
if (f.success === '1' || f.success === 'true' || f.success === 1) {
where.push('success = 1');
} else if (f.success === '0' || f.success === 'false' || f.success === 0) {
where.push('success = 0');
}
if (f.from) {
const from = Date.parse(String(f.from));
if (!Number.isNaN(from)) {
where.push('ts >= ?');
params.push(from);
}
}
if (f.to) {
const to = Date.parse(String(f.to));
if (!Number.isNaN(to)) {
where.push('ts <= ?');
params.push(to);
}
}
if (f.ip) {
where.push('ip LIKE ?');
params.push(`%${String(f.ip).trim()}%`);
}
if (f.username) {
where.push('username LIKE ?');
params.push(`%${String(f.username).trim()}%`);
}
const limit = Math.min(Math.max(Number(f.limit) || 200, 1), 1000);
const reasonCol = table === 'admin_login_log' ? ', reason' : '';
const sql = `SELECT id, ts, ip, username, success${reasonCol} FROM ${table} ${where.length ? 'WHERE ' + where.join(' AND ') : ''} ORDER BY ts DESC LIMIT ${limit}`;
return getDb().prepare(sql).all(...params).map((r) => ({
id: r.id,
ts: r.ts,
ip: r.ip,
username: r.username,
success: !!r.success,
reason: r.reason || ''
}));
}
// ================= Retention =================
function cleanup() {
const cutoff = Date.now() - RETENTION_DAYS * 24 * 60 * 60 * 1000;
const database = getDb();
database.prepare('DELETE FROM admin_login_log WHERE ts < ?').run(cutoff);
database.prepare('DELETE FROM auth_log WHERE ts < ?').run(cutoff);
database.prepare('DELETE FROM block_events WHERE ts < ?').run(cutoff);
database.prepare('DELETE FROM active_blocks WHERE blocked_until <= ?').run(Date.now());
}
setInterval(cleanup, 60 * 60 * 1000).unref();
module.exports = {
dbPath,
RETENTION_DAYS,
isValidPattern,
isWhitelisted,
listWhitelist,
addWhitelist,
removeWhitelist,
logAdminEntry,
logAuthEntry,
blockIp,
getActiveBlock,
removeActiveBlock,
getActiveBlocks,
getBlockHistory,
queryJournal
};