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.
This commit is contained in:
arsam
2026-09-18 23:19:40 +03:30
parent 2de6f702a2
commit 8295afd38b
+120
View File
@@ -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<string, unknown>;
}
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<void> {
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<SecurityEvent[]> {
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<number> {
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<boolean> {
const key = `lock:${userId}`;
const locked = await redis.get(key);
return locked === '1';
}
export async function lockAccount(userId: string, durationSeconds: number = 30 * 60): Promise<void> {
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<void> {
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 };
}