Files
livedash-node/lib/securityAuditLogger.ts
Kaj Kowalski dd145686e6 fix: resolve all TypeScript compilation errors and enable production build
- Fixed missing type imports in lib/api/index.ts
- Updated Zod error property from 'errors' to 'issues' for compatibility
- Added missing lru-cache dependency for performance caching
- Fixed LRU Cache generic type constraints for TypeScript compliance
- Resolved Map iteration ES5 compatibility issues using Array.from()
- Fixed Redis configuration by removing unsupported socket options
- Corrected Prisma relationship naming (auditLogs vs securityAuditLogs)
- Applied type casting for missing database schema fields
- Created missing security types file for enhanced security service
- Disabled deprecated ESLint during build (using Biome for linting)
- Removed deprecated critters dependency and disabled CSS optimization
- Achieved successful production build with all 47 pages generated
2025-07-13 11:52:53 +02:00

450 lines
11 KiB
TypeScript

import type { NextRequest } from "next/server";
import { prisma } from "./prisma";
import { extractClientIP } from "./rateLimiter";
export interface AuditLogContext {
userId?: string;
companyId?: string;
platformUserId?: string;
sessionId?: string;
requestId?: string;
userAgent?: string;
ipAddress?: string;
country?: string;
metadata?: Record<string, unknown>;
}
export interface AuditLogEntry {
eventType: SecurityEventType;
action: string;
outcome: AuditOutcome;
severity?: AuditSeverity;
errorMessage?: string;
context?: AuditLogContext;
}
/* eslint-disable no-unused-vars */
export enum SecurityEventType {
AUTHENTICATION = "AUTHENTICATION",
AUTHORIZATION = "AUTHORIZATION",
USER_MANAGEMENT = "USER_MANAGEMENT",
COMPANY_MANAGEMENT = "COMPANY_MANAGEMENT",
RATE_LIMITING = "RATE_LIMITING",
CSRF_PROTECTION = "CSRF_PROTECTION",
SECURITY_HEADERS = "SECURITY_HEADERS",
PASSWORD_RESET = "PASSWORD_RESET",
PLATFORM_ADMIN = "PLATFORM_ADMIN",
DATA_PRIVACY = "DATA_PRIVACY",
SYSTEM_CONFIG = "SYSTEM_CONFIG",
API_SECURITY = "API_SECURITY",
}
/* eslint-enable no-unused-vars */
/* eslint-disable no-unused-vars */
export enum AuditOutcome {
SUCCESS = "SUCCESS",
FAILURE = "FAILURE",
BLOCKED = "BLOCKED",
RATE_LIMITED = "RATE_LIMITED",
SUSPICIOUS = "SUSPICIOUS",
}
/* eslint-enable no-unused-vars */
/* eslint-disable no-unused-vars */
export enum AuditSeverity {
INFO = "INFO",
LOW = "LOW",
MEDIUM = "MEDIUM",
HIGH = "HIGH",
CRITICAL = "CRITICAL",
}
/* eslint-enable no-unused-vars */
class SecurityAuditLogger {
private isEnabled: boolean;
constructor() {
this.isEnabled = process.env.AUDIT_LOGGING_ENABLED !== "false";
}
async log(entry: AuditLogEntry): Promise<void> {
if (!this.isEnabled) {
return;
}
try {
await prisma.securityAuditLog.create({
data: {
eventType: entry.eventType,
action: entry.action,
outcome: entry.outcome,
severity: entry.severity || AuditSeverity.INFO,
userId: entry.context?.userId || null,
companyId: entry.context?.companyId || null,
platformUserId: entry.context?.platformUserId || null,
ipAddress: entry.context?.ipAddress || null,
userAgent: entry.context?.userAgent || null,
country: entry.context?.country || null,
sessionId: entry.context?.sessionId || null,
requestId: entry.context?.requestId || null,
metadata: (entry.context?.metadata as any) || undefined,
errorMessage: entry.errorMessage || null,
},
});
} catch (error) {
console.error("Failed to write audit log:", error);
}
}
async logAuthentication(
action: string,
outcome: AuditOutcome,
context: AuditLogContext,
errorMessage?: string
): Promise<void> {
const severity = this.getAuthenticationSeverity(outcome);
await this.log({
eventType: SecurityEventType.AUTHENTICATION,
action,
outcome,
severity,
errorMessage,
context,
});
}
async logAuthorization(
action: string,
outcome: AuditOutcome,
context: AuditLogContext,
errorMessage?: string
): Promise<void> {
const severity =
outcome === AuditOutcome.BLOCKED
? AuditSeverity.MEDIUM
: AuditSeverity.INFO;
await this.log({
eventType: SecurityEventType.AUTHORIZATION,
action,
outcome,
severity,
errorMessage,
context,
});
}
async logUserManagement(
action: string,
outcome: AuditOutcome,
context: AuditLogContext,
errorMessage?: string
): Promise<void> {
const severity = this.getUserManagementSeverity(action, outcome);
await this.log({
eventType: SecurityEventType.USER_MANAGEMENT,
action,
outcome,
severity,
errorMessage,
context,
});
}
async logCompanyManagement(
action: string,
outcome: AuditOutcome,
context: AuditLogContext,
errorMessage?: string
): Promise<void> {
const severity = this.getCompanyManagementSeverity(action, outcome);
await this.log({
eventType: SecurityEventType.COMPANY_MANAGEMENT,
action,
outcome,
severity,
errorMessage,
context,
});
}
async logRateLimiting(
action: string,
outcome: AuditOutcome,
context: AuditLogContext,
errorMessage?: string
): Promise<void> {
const severity =
outcome === AuditOutcome.RATE_LIMITED
? AuditSeverity.MEDIUM
: AuditSeverity.LOW;
await this.log({
eventType: SecurityEventType.RATE_LIMITING,
action,
outcome,
severity,
errorMessage,
context,
});
}
async logCSRFProtection(
action: string,
outcome: AuditOutcome,
context: AuditLogContext,
errorMessage?: string
): Promise<void> {
const severity =
outcome === AuditOutcome.BLOCKED
? AuditSeverity.HIGH
: AuditSeverity.MEDIUM;
await this.log({
eventType: SecurityEventType.CSRF_PROTECTION,
action,
outcome,
severity,
errorMessage,
context,
});
}
async logSecurityHeaders(
action: string,
outcome: AuditOutcome,
context: AuditLogContext,
errorMessage?: string
): Promise<void> {
const severity =
outcome === AuditOutcome.BLOCKED
? AuditSeverity.MEDIUM
: AuditSeverity.LOW;
await this.log({
eventType: SecurityEventType.SECURITY_HEADERS,
action,
outcome,
severity,
errorMessage,
context,
});
}
async logPasswordReset(
action: string,
outcome: AuditOutcome,
context: AuditLogContext,
errorMessage?: string
): Promise<void> {
const severity = this.getPasswordResetSeverity(action, outcome);
await this.log({
eventType: SecurityEventType.PASSWORD_RESET,
action,
outcome,
severity,
errorMessage,
context,
});
}
async logPlatformAdmin(
action: string,
outcome: AuditOutcome,
context: AuditLogContext,
errorMessage?: string
): Promise<void> {
const severity = AuditSeverity.HIGH; // All platform admin actions are high severity
await this.log({
eventType: SecurityEventType.PLATFORM_ADMIN,
action,
outcome,
severity,
errorMessage,
context,
});
}
async logDataPrivacy(
action: string,
outcome: AuditOutcome,
context: AuditLogContext,
errorMessage?: string
): Promise<void> {
const severity = AuditSeverity.HIGH; // Data privacy events are always high severity
await this.log({
eventType: SecurityEventType.DATA_PRIVACY,
action,
outcome,
severity,
errorMessage,
context,
});
}
async logAPIStatus(
action: string,
outcome: AuditOutcome,
context: AuditLogContext,
errorMessage?: string
): Promise<void> {
const severity = this.getAPISecuritySeverity(outcome);
await this.log({
eventType: SecurityEventType.API_SECURITY,
action,
outcome,
severity,
errorMessage,
context,
});
}
private getAuthenticationSeverity(outcome: AuditOutcome): AuditSeverity {
switch (outcome) {
case AuditOutcome.SUCCESS:
return AuditSeverity.INFO;
case AuditOutcome.FAILURE:
return AuditSeverity.MEDIUM;
case AuditOutcome.BLOCKED:
case AuditOutcome.RATE_LIMITED:
return AuditSeverity.HIGH;
case AuditOutcome.SUSPICIOUS:
return AuditSeverity.MEDIUM;
default:
return AuditSeverity.INFO;
}
}
private getUserManagementSeverity(
action: string,
outcome: AuditOutcome
): AuditSeverity {
const privilegedActions = ["delete", "suspend", "elevate", "grant"];
const isPrivilegedAction = privilegedActions.some((pa) =>
action.toLowerCase().includes(pa)
);
if (isPrivilegedAction) {
return outcome === AuditOutcome.SUCCESS
? AuditSeverity.HIGH
: AuditSeverity.MEDIUM;
}
return outcome === AuditOutcome.SUCCESS
? AuditSeverity.MEDIUM
: AuditSeverity.LOW;
}
private getCompanyManagementSeverity(
action: string,
outcome: AuditOutcome
): AuditSeverity {
const criticalActions = ["suspend", "delete", "archive"];
const isCriticalAction = criticalActions.some((ca) =>
action.toLowerCase().includes(ca)
);
if (isCriticalAction) {
return outcome === AuditOutcome.SUCCESS
? AuditSeverity.CRITICAL
: AuditSeverity.HIGH;
}
return outcome === AuditOutcome.SUCCESS
? AuditSeverity.HIGH
: AuditSeverity.MEDIUM;
}
private getPasswordResetSeverity(
action: string,
outcome: AuditOutcome
): AuditSeverity {
if (action.toLowerCase().includes("complete")) {
return outcome === AuditOutcome.SUCCESS
? AuditSeverity.MEDIUM
: AuditSeverity.LOW;
}
return AuditSeverity.INFO;
}
private getAPISecuritySeverity(outcome: AuditOutcome): AuditSeverity {
switch (outcome) {
case AuditOutcome.BLOCKED:
return AuditSeverity.HIGH;
case AuditOutcome.SUSPICIOUS:
return AuditSeverity.MEDIUM;
case AuditOutcome.RATE_LIMITED:
return AuditSeverity.MEDIUM;
default:
return AuditSeverity.LOW;
}
}
static extractContextFromRequest(
request: NextRequest
): Partial<AuditLogContext> {
return {
ipAddress: extractClientIP(request),
userAgent: request.headers.get("user-agent") || undefined,
requestId: request.headers.get("x-request-id") || crypto.randomUUID(),
};
}
static createSessionContext(sessionId?: string): Partial<AuditLogContext> {
return {
sessionId,
requestId: crypto.randomUUID(),
};
}
}
export const securityAuditLogger = new SecurityAuditLogger();
export async function createAuditContext(
request?: NextRequest,
session?: { user?: { id?: string; email?: string } },
additionalContext?: Partial<AuditLogContext>
): Promise<AuditLogContext> {
const context: AuditLogContext = {
requestId: crypto.randomUUID(),
...additionalContext,
};
if (request) {
const requestContext =
SecurityAuditLogger.extractContextFromRequest(request);
Object.assign(context, requestContext);
}
if (session?.user) {
context.userId = session.user.id;
context.companyId = (session.user as any).companyId;
if ((session.user as any).isPlatformUser) {
context.platformUserId = session.user.id;
}
}
return context;
}
export function createAuditMetadata(
data: Record<string, unknown>
): Record<string, unknown> {
const sanitized: Record<string, unknown> = {};
for (const [key, value] of Object.entries(data)) {
if (
typeof value === "string" ||
typeof value === "number" ||
typeof value === "boolean"
) {
sanitized[key] = value;
} else if (Array.isArray(value)) {
sanitized[key] = value.map((item) =>
typeof item === "object" ? "[Object]" : item
);
} else if (typeof value === "object" && value !== null) {
sanitized[key] = "[Object]";
}
}
return sanitized;
}