From 8295afd38be0989d415e6e7a302c2d57f058633c Mon Sep 17 00:00:00 2001 From: arsam Date: Fri, 18 Sep 2026 23:19:40 +0330 Subject: [PATCH] Import source: app/backend/src/lib/security-audit Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality. --- app/backend/src/lib/security-audit.ts | 120 ++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 app/backend/src/lib/security-audit.ts diff --git a/app/backend/src/lib/security-audit.ts b/app/backend/src/lib/security-audit.ts new file mode 100644 index 0000000..0e5bc01 --- /dev/null +++ b/app/backend/src/lib/security-audit.ts @@ -0,0 +1,120 @@ +import { redis } from '../redis.js'; +import { prisma } from '../db.js'; + +export interface SecurityEvent { + type: 'login_failure' | 'login_success' | 'password_change' | 'password_reset' | 'account_locked' | 'account_unlocked' | 'session_invalidated' | '2fa_enabled' | '2fa_disabled' | '2fa_failure' | 'csrf_failure' | 'rate_limited' | 'suspicious_activity'; + userId?: string | null; + ip: string; + userAgent?: string | null; + metadata?: Record; +} + +const SECURITY_EVENT_TTL = 7 * 24 * 60 * 60; + +function eventKey(ip: string, type: string, windowSeconds: number): string { + return `sec:${type}:${ip}`; +} + +export async function logSecurityEvent(event: SecurityEvent): Promise { + const payload = JSON.stringify({ + ...event, + ts: Date.now(), + }); + + await Promise.all([ + redis.lpush(`seclog:${event.type}`, payload), + redis.ltrim(`seclog:${event.type}`, 0, 999), + redis.expire(`seclog:${event.type}`, SECURITY_EVENT_TTL), + event.userId + ? redis.lpush(`seclog:user:${event.userId}`, payload).then(() => { + redis.ltrim(`seclog:user:${event.userId}`, 0, 199); + redis.expire(`seclog:user:${event.userId}`, SECURITY_EVENT_TTL); + }) + : Promise.resolve(), + prisma.activityLog.create({ + data: { + userId: event.userId ?? null, + action: `[SECURITY] ${event.type}`, + target: event.ip, + type: 'security', + }, + }).catch(() => undefined), + ]); +} + +export async function getRecentSecurityEvents( + type?: string, + userId?: string, + limit = 50, +): Promise { + const key = userId + ? `seclog:user:${userId}` + : type + ? `seclog:${type}` + : null; + + if (!key) return []; + const raw = await redis.lrange(key, 0, limit - 1); + return raw.map((r: string) => { + try { return JSON.parse(r) as SecurityEvent; } catch { return null; } + }).filter((e): e is SecurityEvent => e !== null); +} + +export async function getSecurityEventCount( + type: string, + ip: string, + windowSeconds: number, +): Promise { + const key = eventKey(ip, type, windowSeconds); + const count = await redis.incr(key); + if (count === 1) await redis.expire(key, windowSeconds); + return count; +} + +export async function checkAccountLock(userId: string): Promise { + const key = `lock:${userId}`; + const locked = await redis.get(key); + return locked === '1'; +} + +export async function lockAccount(userId: string, durationSeconds: number = 30 * 60): Promise { + const key = `lock:${userId}`; + await redis.set(key, '1', 'EX', durationSeconds); + await logSecurityEvent({ + type: 'account_locked', + userId, + ip: 'system', + metadata: { durationSeconds }, + }); +} + +export async function unlockAccount(userId: string): Promise { + const key = `lock:${userId}`; + await redis.del(key); + await logSecurityEvent({ + type: 'account_unlocked', + userId, + ip: 'system', + }); +} + +export async function recordLoginFailure(ip: string, userId: string | null, userAgent?: string): Promise<{ locked: boolean; failCount: number }> { + const key = `loginfail:${ip}`; + const failCount = await redis.incr(key); + if (failCount === 1) await redis.expire(key, 15 * 60); + + await logSecurityEvent({ + type: 'login_failure', + userId, + ip, + userAgent, + metadata: { failCount }, + }); + + if (failCount >= 20 && userId) { + await lockAccount(userId, 30 * 60); + return { locked: true, failCount }; + } + + return { locked: false, failCount }; +}