From 0706c47a33d1719ee1e4f2b390b8097a3308ff06 Mon Sep 17 00:00:00 2001 From: Mathis HERRIOT <197931332+0x485254@users.noreply.github.com> Date: Mon, 9 Feb 2026 11:05:53 +0100 Subject: [PATCH] feat(logging): hash IP addresses in logs and Sentry integration - Implemented IP hashing using SHA256 in logs for enhanced privacy. - Updated Sentry integration to hash IP addresses before sending events. - Enhanced `AllExceptionsFilter` and `crawler-detection.middleware` to use hashed IPs in logs and error handling. - Refined request logging in `auth.service` to include hashed email instead of plain text email. --- backend/src/auth/auth.service.ts | 5 +- .../filters/http-exception.filter.spec.ts | 88 +++++++++++++++++++ .../common/filters/http-exception.filter.ts | 18 +++- .../crawler-detection.middleware.ts | 13 ++- backend/src/main.ts | 10 +++ 5 files changed, 124 insertions(+), 10 deletions(-) create mode 100644 backend/src/common/filters/http-exception.filter.spec.ts diff --git a/backend/src/auth/auth.service.ts b/backend/src/auth/auth.service.ts index 8b66838..72f11d1 100644 --- a/backend/src/auth/auth.service.ts +++ b/backend/src/auth/auth.service.ts @@ -103,10 +103,9 @@ export class AuthService { } async login(dto: LoginDto, userAgent?: string, ip?: string) { - this.logger.log(`Login attempt for email: ${dto.email}`); + const emailHash = await this.hashingService.hashEmail(dto.email); + this.logger.log(`Login attempt for email hash: ${emailHash}`); const { email, password } = dto; - - const emailHash = await this.hashingService.hashEmail(email); const user = await this.usersService.findByEmailHash(emailHash); if (!user) { diff --git a/backend/src/common/filters/http-exception.filter.spec.ts b/backend/src/common/filters/http-exception.filter.spec.ts new file mode 100644 index 0000000..628873b --- /dev/null +++ b/backend/src/common/filters/http-exception.filter.spec.ts @@ -0,0 +1,88 @@ +import { ArgumentsHost, HttpException, HttpStatus } from "@nestjs/common"; +import { Test, TestingModule } from "@nestjs/testing"; +import * as Sentry from "@sentry/nestjs"; +import { AllExceptionsFilter } from "./http-exception.filter"; + +jest.mock("@sentry/nestjs", () => ({ + captureException: jest.fn(), + withScope: jest.fn((callback) => { + const scope = { + setUser: jest.fn(), + setTag: jest.fn(), + setExtra: jest.fn(), + }; + callback(scope); + return scope; + }), +})); + +describe("AllExceptionsFilter", () => { + let filter: AllExceptionsFilter; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [AllExceptionsFilter], + }).compile(); + + filter = module.get(AllExceptionsFilter); + }); + + it("should hash the IP address and send it to Sentry for 500 errors", () => { + const mockResponse = { + status: jest.fn().mockReturnThis(), + json: jest.fn().mockReturnThis(), + }; + const mockRequest = { + url: "/test", + method: "GET", + ip: "127.0.0.1", + user: { sub: "user-123" }, + }; + const mockArgumentsHost = { + switchToHttp: () => ({ + getResponse: () => mockResponse, + getRequest: () => mockRequest, + }), + } as ArgumentsHost; + + const exception = new Error("Internal Server Error"); + + filter.catch(exception, mockArgumentsHost); + + expect(mockResponse.status).toHaveBeenCalledWith(HttpStatus.INTERNAL_SERVER_ERROR); + expect(Sentry.withScope).toHaveBeenCalled(); + + // Vérifier que captureException a été appelé (via withScope) + expect(Sentry.captureException).toHaveBeenCalledWith(exception); + }); + + it("should include hashed IP in logs", () => { + const loggerSpy = jest.spyOn((filter as any).logger, "warn"); + const mockResponse = { + status: jest.fn().mockReturnThis(), + json: jest.fn().mockReturnThis(), + }; + const mockRequest = { + url: "/test", + method: "GET", + ip: "1.2.3.4", + }; + const mockArgumentsHost = { + switchToHttp: () => ({ + getResponse: () => mockResponse, + getRequest: () => mockRequest, + }), + } as ArgumentsHost; + + const exception = new HttpException("Bad Request", HttpStatus.BAD_REQUEST); + + filter.catch(exception, mockArgumentsHost); + + expect(mockResponse.status).toHaveBeenCalledWith(HttpStatus.BAD_REQUEST); + + // L'IP 1.2.3.4 hachée en SHA256 contient un hash de 64 caractères + const logCall = loggerSpy.mock.calls[0][0]; + expect(logCall).toMatch(/[a-f0-9]{64}/); + expect(logCall).not.toContain("1.2.3.4"); + }); +}); diff --git a/backend/src/common/filters/http-exception.filter.ts b/backend/src/common/filters/http-exception.filter.ts index a93e28c..4ca1ecb 100644 --- a/backend/src/common/filters/http-exception.filter.ts +++ b/backend/src/common/filters/http-exception.filter.ts @@ -6,6 +6,7 @@ import { HttpStatus, Logger, } from "@nestjs/common"; +import { createHash } from "node:crypto"; import * as Sentry from "@sentry/nestjs"; import { Request, Response } from "express"; @@ -39,6 +40,11 @@ export class AllExceptionsFilter implements ExceptionFilter { const userId = request.user?.sub || request.user?.id; const userPart = userId ? `[User: ${userId}] ` : ""; + const ip = request.ip || "unknown"; + const hashedIp = createHash("sha256") + .update(ip as string) + .digest("hex"); + const errorResponse = { statusCode: status, timestamp: new Date().toISOString(), @@ -51,14 +57,20 @@ export class AllExceptionsFilter implements ExceptionFilter { }; if (status === HttpStatus.INTERNAL_SERVER_ERROR) { - Sentry.captureException(exception); + Sentry.withScope((scope) => { + scope.setUser({ + id: userId, + ip_address: hashedIp, + }); + Sentry.captureException(exception); + }); this.logger.error( - `${userPart}${request.method} ${request.url} - Error: ${exception instanceof Error ? exception.message : "Unknown error"}`, + `${userPart}${hashedIp} ${request.method} ${request.url} - Error: ${exception instanceof Error ? exception.message : "Unknown error"}`, exception instanceof Error ? exception.stack : "", ); } else { this.logger.warn( - `${userPart}${request.method} ${request.url} - Status: ${status} - Message: ${JSON.stringify(message)}`, + `${userPart}${hashedIp} ${request.method} ${request.url} - Status: ${status} - Message: ${JSON.stringify(message)}`, ); } diff --git a/backend/src/common/middlewares/crawler-detection.middleware.ts b/backend/src/common/middlewares/crawler-detection.middleware.ts index 8987b61..100c19f 100644 --- a/backend/src/common/middlewares/crawler-detection.middleware.ts +++ b/backend/src/common/middlewares/crawler-detection.middleware.ts @@ -1,6 +1,7 @@ import { CACHE_MANAGER } from "@nestjs/cache-manager"; import { Inject, Injectable, Logger, NestMiddleware } from "@nestjs/common"; import type { Cache } from "cache-manager"; +import { createHash } from "node:crypto"; import type { NextFunction, Request, Response } from "express"; @Injectable() @@ -48,11 +49,15 @@ export class CrawlerDetectionMiddleware implements NestMiddleware { const { method, url, ip } = req; const userAgent = req.get("user-agent") || "unknown"; + const hashedIp = createHash("sha256") + .update(ip as string) + .digest("hex"); + // Vérifier si l'IP est bannie try { const isBanned = await this.cacheManager.get(`banned_ip:${ip}`); if (isBanned) { - this.logger.warn(`Banned IP attempt: ${ip} -> ${method} ${url}`); + this.logger.warn(`Banned IP attempt: ${hashedIp} -> ${method} ${url}`); res.status(403).json({ message: "Access denied: Your IP has been temporarily banned.", }); @@ -60,7 +65,7 @@ export class CrawlerDetectionMiddleware implements NestMiddleware { } } catch (error) { this.logger.error( - `Error checking ban status for IP ${ip}: ${error.message}`, + `Error checking ban status for IP ${hashedIp}: ${error.message}`, ); // On continue même en cas d'erreur Redis pour ne pas bloquer les utilisateurs légitimes } @@ -76,14 +81,14 @@ export class CrawlerDetectionMiddleware implements NestMiddleware { if (isSuspiciousPath || isBotUserAgent) { this.logger.warn( - `Potential crawler detected: [${ip}] ${method} ${url} - User-Agent: ${userAgent}`, + `Potential crawler detected: [${hashedIp}] ${method} ${url} - User-Agent: ${userAgent}`, ); // Bannir l'IP pour 24h via Redis try { await this.cacheManager.set(`banned_ip:${ip}`, true, 86400000); } catch (error) { - this.logger.error(`Error banning IP ${ip}: ${error.message}`); + this.logger.error(`Error banning IP ${hashedIp}: ${error.message}`); } } } diff --git a/backend/src/main.ts b/backend/src/main.ts index 0416fb5..8e6b3f9 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -1,3 +1,4 @@ +import { createHash } from "node:crypto"; import { Logger, ValidationPipe } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; import { NestFactory } from "@nestjs/core"; @@ -24,6 +25,15 @@ async function bootstrap() { tracesSampleRate: 1.0, profilesSampleRate: 1.0, sendDefaultPii: false, // RGPD + beforeSend(event) { + // Hachage de l'IP utilisateur pour Sentry si elle est présente + if (event.user?.ip_address) { + event.user.ip_address = createHash("sha256") + .update(event.user.ip_address) + .digest("hex"); + } + return event; + }, }); }