From fa7b5160de999042c6cd697528677a2e146acf12 Mon Sep 17 00:00:00 2001 From: gabito1451 Date: Wed, 22 Apr 2026 10:25:10 -0700 Subject: [PATCH] feat: implement PII sanitization for application logs --- src/audit-log/audit-log.service.ts | 61 ++++++------ src/common/utils/pii-sanitizer.utils.ts | 95 +++++++++++++++++++ src/notifications/notifications.service.ts | 29 ++---- .../processors/default-queue.processor.ts | 7 +- 4 files changed, 144 insertions(+), 48 deletions(-) create mode 100644 src/common/utils/pii-sanitizer.utils.ts diff --git a/src/audit-log/audit-log.service.ts b/src/audit-log/audit-log.service.ts index a1cc1911..627faf0b 100644 --- a/src/audit-log/audit-log.service.ts +++ b/src/audit-log/audit-log.service.ts @@ -1,9 +1,10 @@ import { Injectable, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository, Between, LessThan, MoreThan, In, Brackets, WhereExpressionBuilder } from 'typeorm'; +import { Repository, Between, MoreThan } from 'typeorm'; import { AuditLog } from './audit-log.entity'; import { AuditAction, AuditSeverity, AuditCategory } from './enums/audit-action.enum'; import { ConfigService } from '@nestjs/config'; +import { sanitizePii } from '../common/utils/pii-sanitizer.utils'; export interface AuditLogEntry { userId?: string; @@ -93,7 +94,9 @@ export class AuditLogService { try { const saved = await this.auditRepo.save(log); - this.logger.debug(`Audit log created: ${entry.action} - ${entry.description || 'no description'}`); + this.logger.debug( + `Audit log created: ${log.action} - ${sanitizePii(log.description || 'no description')}`, + ); return saved; } catch (error) { this.logger.error('Failed to create audit log:', error); @@ -169,9 +172,12 @@ export class AuditLogService { userAgent: string, requestId?: string, ): Promise { - const severity = statusCode >= 500 ? AuditSeverity.ERROR : - statusCode >= 400 ? AuditSeverity.WARNING : - AuditSeverity.INFO; + const severity = + statusCode >= 500 + ? AuditSeverity.ERROR + : statusCode >= 400 + ? AuditSeverity.WARNING + : AuditSeverity.INFO; return this.log({ userId: userId || undefined, @@ -238,11 +244,15 @@ export class AuditLogService { } if (filters.categories && filters.categories.length > 0) { - queryBuilder.andWhere('audit.category IN (:...categories)', { categories: filters.categories }); + queryBuilder.andWhere('audit.category IN (:...categories)', { + categories: filters.categories, + }); } if (filters.severities && filters.severities.length > 0) { - queryBuilder.andWhere('audit.severity IN (:...severities)', { severities: filters.severities }); + queryBuilder.andWhere('audit.severity IN (:...severities)', { + severities: filters.severities, + }); } if (filters.entityType) { @@ -346,7 +356,11 @@ export class AuditLogService { /** * Find logs by entity */ - async findByEntity(entityType: string, entityId: string, limit: number = 100): Promise { + async findByEntity( + entityType: string, + entityId: string, + limit: number = 100, + ): Promise { return this.auditRepo.find({ where: { entityType, entityId }, order: { timestamp: 'DESC' }, @@ -567,10 +581,9 @@ export class AuditLogService { return str; }; - const csvContent = [ - headers.join(','), - ...rows.map((row) => row.map(escapeCsv).join(',')), - ].join('\n'); + const csvContent = [headers.join(','), ...rows.map((row) => row.map(escapeCsv).join(','))].join( + '\n', + ); return csvContent; } @@ -591,21 +604,15 @@ export class AuditLogService { const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); const monthAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); - const [ - totalLogs, - logsToday, - logsThisWeek, - logsThisMonth, - criticalEvents, - errorEvents, - ] = await Promise.all([ - this.auditRepo.count(), - this.auditRepo.count({ where: { timestamp: MoreThanOrEqual(today) } }), - this.auditRepo.count({ where: { timestamp: MoreThanOrEqual(weekAgo) } }), - this.auditRepo.count({ where: { timestamp: MoreThanOrEqual(monthAgo) } }), - this.auditRepo.count({ where: { severity: AuditSeverity.CRITICAL } }), - this.auditRepo.count({ where: { severity: AuditSeverity.ERROR } }), - ]); + const [totalLogs, logsToday, logsThisWeek, logsThisMonth, criticalEvents, errorEvents] = + await Promise.all([ + this.auditRepo.count(), + this.auditRepo.count({ where: { timestamp: MoreThanOrEqual(today) } }), + this.auditRepo.count({ where: { timestamp: MoreThanOrEqual(weekAgo) } }), + this.auditRepo.count({ where: { timestamp: MoreThanOrEqual(monthAgo) } }), + this.auditRepo.count({ where: { severity: AuditSeverity.CRITICAL } }), + this.auditRepo.count({ where: { severity: AuditSeverity.ERROR } }), + ]); return { totalLogs, diff --git a/src/common/utils/pii-sanitizer.utils.ts b/src/common/utils/pii-sanitizer.utils.ts new file mode 100644 index 00000000..774f39a2 --- /dev/null +++ b/src/common/utils/pii-sanitizer.utils.ts @@ -0,0 +1,95 @@ +/** + * Utility for sanitizing Personally Identifiable Information (PII) from logs. + */ + +/** + * Masks an email address to protect PII. + * Example: "john.doe@example.com" -> "j***e@example.com" + */ +export function sanitizeEmail(email: string): string { + if (!email || typeof email !== 'string') return email; + const parts = email.split('@'); + if (parts.length !== 2) return email; + + const [user, domain] = parts; + if (user.length <= 2) { + return `***@${domain}`; + } + + return `${user[0]}***${user[user.length - 1]}@${domain}`; +} + +/** + * Masks a name or generic string. + * Example: "John" -> "J***" + */ +export function sanitizeName(name: string): string { + if (!name || typeof name !== 'string') return name; + if (name.length <= 1) return '***'; + return `${name[0]}***`; +} + +/** + * Recursively sanitizes PII from an object or string. + * Replaces values of sensitive keys with masks. + */ +export function sanitizePii(data: any): any { + if (data === null || data === undefined) { + return data; + } + + if (typeof data === 'string') { + // Basic email masking within strings + return data.replace(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g, (email) => + sanitizeEmail(email), + ); + } + + if (Array.isArray(data)) { + return data.map((item) => sanitizePii(item)); + } + + if (typeof data === 'object') { + const sanitized: any = {}; + const sensitiveKeys = [ + 'email', + 'useremail', + 'contactemail', + 'owneremail', + 'recipientemail', + 'firstname', + 'lastname', + 'fullname', + 'phone', + 'phonenumber', + 'password', + 'token', + 'secret', + 'auth', + 'authorization', + 'bearer', + 'apikey', + 'stripekey', + 'awskey', + 'accesskey', + 'privatekey', + ]; + + for (const [key, value] of Object.entries(data)) { + const lowerKey = key.toLowerCase(); + + if (sensitiveKeys.some((sk) => lowerKey.includes(sk))) { + if (typeof value === 'string' && value.includes('@')) { + sanitized[key] = sanitizeEmail(value); + } else { + sanitized[key] = '***'; + } + } else { + sanitized[key] = sanitizePii(value); + } + } + return sanitized; + } + + return data; +} diff --git a/src/notifications/notifications.service.ts b/src/notifications/notifications.service.ts index 781c31d2..9d57412b 100644 --- a/src/notifications/notifications.service.ts +++ b/src/notifications/notifications.service.ts @@ -13,6 +13,7 @@ import { NotificationsGateway } from './notifications.gateway'; import { NotificationTemplatesService } from './notification-templates.service'; import { PreferencesService } from './preferences/preferences.service'; import { EmailService } from './email/email.service'; +import { sanitizeEmail } from '../common/utils/pii-sanitizer.utils'; @Injectable() export class NotificationsService { @@ -30,10 +31,10 @@ export class NotificationsService { async sendVerificationEmail(email: string, token: string): Promise { try { await this.emailService.sendVerificationEmail(email, token); - this.logger.log(`Verification email sent to ${email}`); + this.logger.log(`Verification email sent to ${sanitizeEmail(email)}`); } catch (error) { this.logger.error( - `Failed to send verification email to ${email}`, + `Failed to send verification email to ${sanitizeEmail(email)}`, error instanceof Error ? error.stack : String(error), ); throw error; @@ -43,10 +44,10 @@ export class NotificationsService { async sendPasswordResetEmail(email: string, token: string): Promise { try { await this.emailService.sendPasswordResetEmail(email, token); - this.logger.log(`Password reset email sent to ${email}`); + this.logger.log(`Password reset email sent to ${sanitizeEmail(email)}`); } catch (error) { this.logger.error( - `Failed to send password reset email to ${email}`, + `Failed to send password reset email to ${sanitizeEmail(email)}`, error instanceof Error ? error.stack : String(error), ); throw error; @@ -60,15 +61,10 @@ export class NotificationsService { const { userId, title, content, type, priority, metadata } = createNotificationDto; const preferences = await this.preferencesService.getPreferences(userId); - const shouldSend = this.shouldSendNotification( - type || NotificationType.IN_APP, - preferences, - ); + const shouldSend = this.shouldSendNotification(type || NotificationType.IN_APP, preferences); if (!shouldSend) { - this.logger.debug( - `Notification skipped for user ${userId} based on preferences`, - ); + this.logger.debug(`Notification skipped for user ${userId} based on preferences`); } const notification = this.notificationRepository.create({ @@ -123,9 +119,7 @@ export class NotificationsService { // Integrate with EmailService or MailerService here if notification email delivery is required } - private async sendExternalPushNotification( - notification: Notification, - ): Promise { + private async sendExternalPushNotification(notification: Notification): Promise { this.logger.log( `Sending external push notification to user ${notification.userId}: ${notification.title}`, ); @@ -248,10 +242,7 @@ export class NotificationsService { data: any; type?: NotificationType; }): Promise { - const template = this.templatesService.renderTemplate( - payload.templateType, - payload.data, - ); + const template = this.templatesService.renderTemplate(payload.templateType, payload.data); await this.create({ userId: payload.userId, @@ -261,4 +252,4 @@ export class NotificationsService { priority: NotificationPriority.MEDIUM, }); } -} \ No newline at end of file +} diff --git a/src/queues/processors/default-queue.processor.ts b/src/queues/processors/default-queue.processor.ts index 5127cfd8..efd4d1f9 100644 --- a/src/queues/processors/default-queue.processor.ts +++ b/src/queues/processors/default-queue.processor.ts @@ -2,6 +2,7 @@ import { Processor, Process, OnQueueActive, OnQueueCompleted, OnQueueFailed } fr import { Logger } from '@nestjs/common'; import { Job } from 'bull'; import { RetryLogicService } from '../retry/retry-logic.service'; +import { sanitizeEmail, sanitizePii } from '../../common/utils/pii-sanitizer.utils'; /** * Default Queue Processor @@ -54,7 +55,7 @@ export class DefaultQueueProcessor { private async processSendEmail(job: Job): Promise { await job.progress(30); // Email sending logic here - this.logger.log(`Sending email to ${job.data.to}`); + this.logger.log(`Sending email to ${sanitizeEmail(job.data.to)}`); await this.simulateWork(2000); await job.progress(80); return { status: 'sent', recipient: job.data.to }; @@ -99,7 +100,9 @@ export class DefaultQueueProcessor { onCompleted(job: Job, result: any) { const processingTime = (job.finishedOn ?? Date.now()) - (job.processedOn ?? Date.now()); this.logger.log( - `Job ${job.name} (${job.id}) completed in ${processingTime}ms - Result: ${JSON.stringify(result)}`, + `Job ${job.name} (${job.id}) completed in ${processingTime}ms - Result: ${JSON.stringify( + sanitizePii(result), + )}`, ); }