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 <noreply@anthropic.com>
This commit is contained in:
142
apps/api/src/common/utils/redact.util.spec.ts
Normal file
142
apps/api/src/common/utils/redact.util.spec.ts
Normal file
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
107
apps/api/src/common/utils/redact.util.ts
Normal file
107
apps/api/src/common/utils/redact.util.ts
Normal file
@@ -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<T>(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<string, unknown> = {};
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
```
|
||||||
@@ -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"
|
||||||
|
```
|
||||||
@@ -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"
|
||||||
|
```
|
||||||
@@ -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"
|
||||||
|
```
|
||||||
@@ -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"
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user