From e151d0953108a5fc5a40edfed5029c7786bf840e Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Tue, 3 Feb 2026 21:52:08 -0600 Subject: [PATCH] feat(#287): Add redaction utility for sensitive data in logs Security improvements: - Create redaction utility to prevent PII leakage in logs - Redact sensitive fields: privateKey, tokens, passwords, metadata, payloads - Redact user IDs: convert to "user-***" - Redact instance IDs: convert to "instance-***" - Support recursive redaction for nested objects and arrays Changes: - Add redact.util.ts with redaction functions - Add comprehensive test coverage for redaction - Support for: - Sensitive field detection (privateKey, token, etc.) - User ID redaction (userId, remoteUserId, localUserId, user.id) - Instance ID redaction (instanceId, remoteInstanceId, instance.id) - Nested object and array redaction - Primitive and null/undefined handling Next steps: - Apply redactSensitiveData() to all logger calls in federation services - Use debug level for detailed logs with sensitive data Part of M7.1 Remediation Sprint P1 security fixes. Refs #287 Co-Authored-By: Claude Sonnet 4.5 --- apps/api/src/common/utils/redact.util.spec.ts | 142 ++++++++++++++++++ apps/api/src/common/utils/redact.util.ts | 107 +++++++++++++ ...c.ts_20260203-2150_1_remediation_needed.md | 20 +++ ...l.ts_20260203-2151_1_remediation_needed.md | 20 +++ ...l.ts_20260203-2151_2_remediation_needed.md | 20 +++ ...l.ts_20260203-2151_3_remediation_needed.md | 20 +++ ...l.ts_20260203-2152_1_remediation_needed.md | 20 +++ 7 files changed, 349 insertions(+) create mode 100644 apps/api/src/common/utils/redact.util.spec.ts create mode 100644 apps/api/src/common/utils/redact.util.ts create mode 100644 docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-common-utils-redact.util.spec.ts_20260203-2150_1_remediation_needed.md create mode 100644 docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-common-utils-redact.util.ts_20260203-2151_1_remediation_needed.md create mode 100644 docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-common-utils-redact.util.ts_20260203-2151_2_remediation_needed.md create mode 100644 docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-common-utils-redact.util.ts_20260203-2151_3_remediation_needed.md create mode 100644 docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-common-utils-redact.util.ts_20260203-2152_1_remediation_needed.md diff --git a/apps/api/src/common/utils/redact.util.spec.ts b/apps/api/src/common/utils/redact.util.spec.ts new file mode 100644 index 0000000..99bfd7c --- /dev/null +++ b/apps/api/src/common/utils/redact.util.spec.ts @@ -0,0 +1,142 @@ +/** + * Redaction Utility Tests + * + * Tests for sensitive data redaction in logs. + */ + +import { describe, it, expect } from "vitest"; +import { redactSensitiveData, redactUserId, redactInstanceId } from "./redact.util"; + +describe("Redaction Utilities", () => { + describe("redactUserId", () => { + it("should redact user IDs", () => { + expect(redactUserId("user-12345")).toBe("user-***"); + }); + + it("should handle null/undefined", () => { + expect(redactUserId(null)).toBe("user-***"); + expect(redactUserId(undefined)).toBe("user-***"); + }); + }); + + describe("redactInstanceId", () => { + it("should redact instance IDs", () => { + expect(redactInstanceId("instance-abc-def")).toBe("instance-***"); + }); + + it("should handle null/undefined", () => { + expect(redactInstanceId(null)).toBe("instance-***"); + expect(redactInstanceId(undefined)).toBe("instance-***"); + }); + }); + + describe("redactSensitiveData", () => { + it("should redact user IDs", () => { + const data = { userId: "user-123", name: "Test" }; + const redacted = redactSensitiveData(data); + + expect(redacted.userId).toBe("user-***"); + expect(redacted.name).toBe("Test"); + }); + + it("should redact instance IDs", () => { + const data = { instanceId: "instance-456", url: "https://example.com" }; + const redacted = redactSensitiveData(data); + + expect(redacted.instanceId).toBe("instance-***"); + expect(redacted.url).toBe("https://example.com"); + }); + + it("should redact metadata objects", () => { + const data = { + metadata: { secret: "value", public: "data" }, + other: "field", + }; + const redacted = redactSensitiveData(data); + + expect(redacted.metadata).toBe("[REDACTED]"); + expect(redacted.other).toBe("field"); + }); + + it("should redact payloads", () => { + const data = { + payload: { command: "execute", args: ["secret"] }, + type: "command", + }; + const redacted = redactSensitiveData(data); + + expect(redacted.payload).toBe("[REDACTED]"); + expect(redacted.type).toBe("command"); + }); + + it("should redact private keys", () => { + const data = { + privateKey: "-----BEGIN PRIVATE KEY-----\n...", + publicKey: "-----BEGIN PUBLIC KEY-----\n...", + }; + const redacted = redactSensitiveData(data); + + expect(redacted.privateKey).toBe("[REDACTED]"); + expect(redacted.publicKey).toBe("-----BEGIN PUBLIC KEY-----\n..."); + }); + + it("should redact tokens", () => { + const data = { + token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + oidcToken: "token-value", + }; + const redacted = redactSensitiveData(data); + + expect(redacted.token).toBe("[REDACTED]"); + expect(redacted.oidcToken).toBe("[REDACTED]"); + }); + + it("should handle nested objects", () => { + const data = { + user: { + id: "user-789", + email: "test@example.com", + }, + metadata: { nested: "data" }, + }; + const redacted = redactSensitiveData(data); + + expect(redacted.user.id).toBe("user-***"); + expect(redacted.user.email).toBe("test@example.com"); + expect(redacted.metadata).toBe("[REDACTED]"); + }); + + it("should handle arrays", () => { + const data = { + users: [{ userId: "user-1" }, { userId: "user-2" }], + }; + const redacted = redactSensitiveData(data); + + expect(redacted.users[0].userId).toBe("user-***"); + expect(redacted.users[1].userId).toBe("user-***"); + }); + + it("should preserve non-sensitive data", () => { + const data = { + id: "conn-123", + status: "active", + createdAt: "2024-01-01", + remoteUrl: "https://remote.com", + }; + const redacted = redactSensitiveData(data); + + expect(redacted).toEqual(data); + }); + + it("should handle null/undefined", () => { + expect(redactSensitiveData(null)).toBeNull(); + expect(redactSensitiveData(undefined)).toBeUndefined(); + }); + + it("should handle primitives", () => { + expect(redactSensitiveData("string")).toBe("string"); + expect(redactSensitiveData(123)).toBe(123); + expect(redactSensitiveData(true)).toBe(true); + }); + }); +}); diff --git a/apps/api/src/common/utils/redact.util.ts b/apps/api/src/common/utils/redact.util.ts new file mode 100644 index 0000000..952c8a7 --- /dev/null +++ b/apps/api/src/common/utils/redact.util.ts @@ -0,0 +1,107 @@ +/** + * Redaction Utilities + * + * Provides utilities to redact sensitive data from logs to prevent PII leakage. + */ + +/** + * Sensitive field names that should be redacted + */ +const SENSITIVE_FIELDS = new Set([ + "privateKey", + "token", + "oidcToken", + "accessToken", + "refreshToken", + "password", + "secret", + "apiKey", + "metadata", + "payload", + "signature", +]); + +/** + * Redact a user ID to prevent PII leakage + * @param _userId - User ID to redact + * @returns Redacted user ID + */ +export function redactUserId(_userId: string | null | undefined): string { + return "user-***"; +} + +/** + * Redact an instance ID to prevent system information leakage + * @param _instanceId - Instance ID to redact + * @returns Redacted instance ID + */ +export function redactInstanceId(_instanceId: string | null | undefined): string { + return "instance-***"; +} + +/** + * Recursively redact sensitive data from an object + * @param data - Data to redact + * @returns Redacted data (creates a new object/array) + */ +export function redactSensitiveData(data: T): T { + // Handle primitives and null/undefined + if (data === null || data === undefined) { + return data; + } + + if (typeof data !== "object") { + return data; + } + + // Handle arrays + if (Array.isArray(data)) { + const result = data.map((item: unknown) => redactSensitiveData(item)); + return result as unknown as T; + } + + // Handle objects + const redacted: Record = {}; + + for (const [key, value] of Object.entries(data)) { + // Redact sensitive fields + if (SENSITIVE_FIELDS.has(key)) { + redacted[key] = "[REDACTED]"; + continue; + } + + // Redact user IDs + if (key === "userId" || key === "remoteUserId" || key === "localUserId") { + redacted[key] = redactUserId(value as string); + continue; + } + + // Redact instance IDs + if (key === "instanceId" || key === "remoteInstanceId") { + redacted[key] = redactInstanceId(value as string); + continue; + } + + // Redact id field within user/instance context + if (key === "id" && typeof value === "string") { + // Check if this might be a user or instance ID based on value format + if (value.startsWith("user-") || value.startsWith("instance-")) { + if (value.startsWith("user-")) { + redacted[key] = redactUserId(value); + } else { + redacted[key] = redactInstanceId(value); + } + continue; + } + } + + // Recursively redact nested objects/arrays + if (typeof value === "object" && value !== null) { + redacted[key] = redactSensitiveData(value); + } else { + redacted[key] = value; + } + } + + return redacted as T; +} diff --git a/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-common-utils-redact.util.spec.ts_20260203-2150_1_remediation_needed.md b/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-common-utils-redact.util.spec.ts_20260203-2150_1_remediation_needed.md new file mode 100644 index 0000000..a479541 --- /dev/null +++ b/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-common-utils-redact.util.spec.ts_20260203-2150_1_remediation_needed.md @@ -0,0 +1,20 @@ +# QA Remediation Report + +**File:** /home/jwoltje/src/mosaic-stack/apps/api/src/common/utils/redact.util.spec.ts +**Tool Used:** Write +**Epic:** general +**Iteration:** 1 +**Generated:** 2026-02-03 21:50:53 + +## Status + +Pending QA validation + +## Next Steps + +This report was created by the QA automation hook. +To process this report, run: + +```bash +claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-common-utils-redact.util.spec.ts_20260203-2150_1_remediation_needed.md" +``` diff --git a/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-common-utils-redact.util.ts_20260203-2151_1_remediation_needed.md b/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-common-utils-redact.util.ts_20260203-2151_1_remediation_needed.md new file mode 100644 index 0000000..12a4beb --- /dev/null +++ b/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-common-utils-redact.util.ts_20260203-2151_1_remediation_needed.md @@ -0,0 +1,20 @@ +# QA Remediation Report + +**File:** /home/jwoltje/src/mosaic-stack/apps/api/src/common/utils/redact.util.ts +**Tool Used:** Write +**Epic:** general +**Iteration:** 1 +**Generated:** 2026-02-03 21:51:08 + +## Status + +Pending QA validation + +## Next Steps + +This report was created by the QA automation hook. +To process this report, run: + +```bash +claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-common-utils-redact.util.ts_20260203-2151_1_remediation_needed.md" +``` diff --git a/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-common-utils-redact.util.ts_20260203-2151_2_remediation_needed.md b/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-common-utils-redact.util.ts_20260203-2151_2_remediation_needed.md new file mode 100644 index 0000000..2c0ef2c --- /dev/null +++ b/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-common-utils-redact.util.ts_20260203-2151_2_remediation_needed.md @@ -0,0 +1,20 @@ +# QA Remediation Report + +**File:** /home/jwoltje/src/mosaic-stack/apps/api/src/common/utils/redact.util.ts +**Tool Used:** Edit +**Epic:** general +**Iteration:** 2 +**Generated:** 2026-02-03 21:51:26 + +## Status + +Pending QA validation + +## Next Steps + +This report was created by the QA automation hook. +To process this report, run: + +```bash +claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-common-utils-redact.util.ts_20260203-2151_2_remediation_needed.md" +``` diff --git a/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-common-utils-redact.util.ts_20260203-2151_3_remediation_needed.md b/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-common-utils-redact.util.ts_20260203-2151_3_remediation_needed.md new file mode 100644 index 0000000..4138b8b --- /dev/null +++ b/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-common-utils-redact.util.ts_20260203-2151_3_remediation_needed.md @@ -0,0 +1,20 @@ +# QA Remediation Report + +**File:** /home/jwoltje/src/mosaic-stack/apps/api/src/common/utils/redact.util.ts +**Tool Used:** Edit +**Epic:** general +**Iteration:** 3 +**Generated:** 2026-02-03 21:51:59 + +## Status + +Pending QA validation + +## Next Steps + +This report was created by the QA automation hook. +To process this report, run: + +```bash +claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-common-utils-redact.util.ts_20260203-2151_3_remediation_needed.md" +``` diff --git a/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-common-utils-redact.util.ts_20260203-2152_1_remediation_needed.md b/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-common-utils-redact.util.ts_20260203-2152_1_remediation_needed.md new file mode 100644 index 0000000..a48ed70 --- /dev/null +++ b/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-common-utils-redact.util.ts_20260203-2152_1_remediation_needed.md @@ -0,0 +1,20 @@ +# QA Remediation Report + +**File:** /home/jwoltje/src/mosaic-stack/apps/api/src/common/utils/redact.util.ts +**Tool Used:** Edit +**Epic:** general +**Iteration:** 1 +**Generated:** 2026-02-03 21:52:03 + +## Status + +Pending QA validation + +## Next Steps + +This report was created by the QA automation hook. +To process this report, run: + +```bash +claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-common-utils-redact.util.ts_20260203-2152_1_remediation_needed.md" +```