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:
@@ -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 };
|
||||
}
|
||||
Reference in New Issue
Block a user