All checks were successful
ci/woodpecker/push/api Pipeline was successful
Co-authored-by: Jason Woltje <jason@diversecanvas.com> Co-committed-by: Jason Woltje <jason@diversecanvas.com>
312 lines
9.7 KiB
TypeScript
312 lines
9.7 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import { sanitizeForLogging } from "./log-sanitizer";
|
|
|
|
describe("sanitizeForLogging", () => {
|
|
describe("String sanitization", () => {
|
|
it("should redact API keys", () => {
|
|
const input = "Error with API key: sk_live_1234567890abcdef";
|
|
const result = sanitizeForLogging(input);
|
|
expect(result).toBe("Error with API key: [REDACTED]");
|
|
});
|
|
|
|
it("should redact bearer tokens", () => {
|
|
const input = "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9";
|
|
const result = sanitizeForLogging(input);
|
|
expect(result).toBe("Authorization: Bearer [REDACTED]");
|
|
});
|
|
|
|
it("should redact Discord bot tokens", () => {
|
|
const input = "Bot token: MTk4NjIyNDgzNDcxOTI1MjQ4.Cl2FMQ.ZnCjm1XVW7vRze4b7Cq4se7kKWs";
|
|
const result = sanitizeForLogging(input);
|
|
expect(result).toBe("Bot token: [REDACTED]");
|
|
});
|
|
|
|
it("should redact passwords in strings", () => {
|
|
const input = 'Connection failed with password="secret123"';
|
|
const result = sanitizeForLogging(input);
|
|
expect(result).toBe('Connection failed with password="[REDACTED]"');
|
|
});
|
|
|
|
it("should redact email addresses", () => {
|
|
const input = "User email: user@example.com failed to authenticate";
|
|
const result = sanitizeForLogging(input);
|
|
expect(result).toBe("User email: [REDACTED] failed to authenticate");
|
|
});
|
|
|
|
it("should redact database connection strings", () => {
|
|
const input = "postgresql://user:password123@localhost:5432/mydb";
|
|
const result = sanitizeForLogging(input);
|
|
expect(result).toBe("postgresql://user:[REDACTED]@localhost:5432/mydb");
|
|
});
|
|
|
|
it("should redact authorization headers", () => {
|
|
const input = "Authorization: Basic dXNlcjpwYXNzd29yZA==";
|
|
const result = sanitizeForLogging(input);
|
|
expect(result).toBe("Authorization: Basic [REDACTED]");
|
|
});
|
|
|
|
it("should preserve non-sensitive strings", () => {
|
|
const input = "This is a regular log message without secrets";
|
|
const result = sanitizeForLogging(input);
|
|
expect(result).toBe("This is a regular log message without secrets");
|
|
});
|
|
|
|
it("should redact environment variable style secrets", () => {
|
|
const input = "API_KEY=abc123def456 failed";
|
|
const result = sanitizeForLogging(input);
|
|
expect(result).toBe("API_KEY=[REDACTED] failed");
|
|
});
|
|
|
|
it("should redact multiple secrets in one string", () => {
|
|
const input = "token=xyz123 and password=secret456";
|
|
const result = sanitizeForLogging(input);
|
|
expect(result).toBe("token=[REDACTED] and password=[REDACTED]");
|
|
});
|
|
});
|
|
|
|
describe("Object sanitization", () => {
|
|
it("should redact secrets in flat objects", () => {
|
|
const input = {
|
|
message: "Error occurred",
|
|
apiKey: "sk_live_1234567890",
|
|
token: "Bearer abc123",
|
|
};
|
|
const result = sanitizeForLogging(input);
|
|
expect(result).toEqual({
|
|
message: "Error occurred",
|
|
apiKey: "[REDACTED]",
|
|
token: "[REDACTED]",
|
|
});
|
|
});
|
|
|
|
it("should redact secrets in nested objects", () => {
|
|
const input = {
|
|
error: {
|
|
message: "Auth failed",
|
|
credentials: {
|
|
username: "admin",
|
|
password: "secret123",
|
|
},
|
|
},
|
|
};
|
|
const result = sanitizeForLogging(input);
|
|
expect(result).toEqual({
|
|
error: {
|
|
message: "Auth failed",
|
|
credentials: {
|
|
username: "admin",
|
|
password: "[REDACTED]",
|
|
},
|
|
},
|
|
});
|
|
});
|
|
|
|
it("should redact secrets based on key names", () => {
|
|
const input = {
|
|
apiKey: "secret",
|
|
api_key: "secret",
|
|
API_KEY: "secret",
|
|
bearerToken: "token",
|
|
accessToken: "token",
|
|
password: "pass",
|
|
secret: "secret",
|
|
client_secret: "secret",
|
|
};
|
|
const result = sanitizeForLogging(input);
|
|
expect(result).toEqual({
|
|
apiKey: "[REDACTED]",
|
|
api_key: "[REDACTED]",
|
|
API_KEY: "[REDACTED]",
|
|
bearerToken: "[REDACTED]",
|
|
accessToken: "[REDACTED]",
|
|
password: "[REDACTED]",
|
|
secret: "[REDACTED]",
|
|
client_secret: "[REDACTED]",
|
|
});
|
|
});
|
|
|
|
it("should preserve non-sensitive object properties", () => {
|
|
const input = {
|
|
message: "Test message",
|
|
statusCode: 500,
|
|
timestamp: new Date("2024-01-01"),
|
|
count: 42,
|
|
};
|
|
const result = sanitizeForLogging(input);
|
|
expect(result).toEqual({
|
|
message: "Test message",
|
|
statusCode: 500,
|
|
timestamp: new Date("2024-01-01"),
|
|
count: 42,
|
|
});
|
|
});
|
|
|
|
it("should handle objects with null and undefined values", () => {
|
|
const input = {
|
|
message: "Error",
|
|
token: null,
|
|
apiKey: undefined,
|
|
data: "value",
|
|
};
|
|
const result = sanitizeForLogging(input);
|
|
expect(result).toEqual({
|
|
message: "Error",
|
|
token: null,
|
|
apiKey: undefined,
|
|
data: "value",
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("Array sanitization", () => {
|
|
it("should sanitize strings in arrays", () => {
|
|
const input = ["normal message", "token=abc123", "another message"];
|
|
const result = sanitizeForLogging(input);
|
|
expect(result).toEqual(["normal message", "token=[REDACTED]", "another message"]);
|
|
});
|
|
|
|
it("should sanitize objects in arrays", () => {
|
|
const input = [
|
|
{ message: "ok" },
|
|
{ message: "error", apiKey: "secret123" },
|
|
{ message: "info" },
|
|
];
|
|
const result = sanitizeForLogging(input);
|
|
expect(result).toEqual([
|
|
{ message: "ok" },
|
|
{ message: "error", apiKey: "[REDACTED]" },
|
|
{ message: "info" },
|
|
]);
|
|
});
|
|
|
|
it("should handle nested arrays", () => {
|
|
const input = [["token=abc"], ["password=xyz"]];
|
|
const result = sanitizeForLogging(input);
|
|
expect(result).toEqual([["token=[REDACTED]"], ["password=[REDACTED]"]]);
|
|
});
|
|
});
|
|
|
|
describe("Error object sanitization", () => {
|
|
it("should sanitize Error objects", () => {
|
|
const error = new Error("Auth failed with token abc123");
|
|
const result = sanitizeForLogging(error);
|
|
expect(result.message).toBe("Auth failed with token [REDACTED]");
|
|
expect(result.name).toBe("Error");
|
|
});
|
|
|
|
it("should sanitize custom error properties", () => {
|
|
const error = new Error("Request failed");
|
|
(error as any).config = {
|
|
headers: {
|
|
Authorization: "Bearer secret123",
|
|
},
|
|
};
|
|
const result = sanitizeForLogging(error);
|
|
expect(result.config.headers.Authorization).toBe("[REDACTED]");
|
|
});
|
|
|
|
it("should handle errors with nested objects", () => {
|
|
const error = new Error("Discord error");
|
|
(error as any).response = {
|
|
status: 401,
|
|
data: {
|
|
message: "Invalid token",
|
|
token: "abc123",
|
|
},
|
|
};
|
|
const result = sanitizeForLogging(error);
|
|
expect(result.response.data.token).toBe("[REDACTED]");
|
|
});
|
|
});
|
|
|
|
describe("Edge cases", () => {
|
|
it("should handle null input", () => {
|
|
const result = sanitizeForLogging(null);
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it("should handle undefined input", () => {
|
|
const result = sanitizeForLogging(undefined);
|
|
expect(result).toBeUndefined();
|
|
});
|
|
|
|
it("should handle numbers", () => {
|
|
const result = sanitizeForLogging(42);
|
|
expect(result).toBe(42);
|
|
});
|
|
|
|
it("should handle booleans", () => {
|
|
const result = sanitizeForLogging(true);
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
it("should handle empty objects", () => {
|
|
const result = sanitizeForLogging({});
|
|
expect(result).toEqual({});
|
|
});
|
|
|
|
it("should handle empty arrays", () => {
|
|
const result = sanitizeForLogging([]);
|
|
expect(result).toEqual([]);
|
|
});
|
|
|
|
it("should handle circular references", () => {
|
|
const obj: any = { name: "test" };
|
|
obj.self = obj;
|
|
const result = sanitizeForLogging(obj);
|
|
expect(result.name).toBe("test");
|
|
expect(result.self).toBe("[Circular Reference]");
|
|
});
|
|
|
|
it("should handle large objects without performance issues", () => {
|
|
const largeObj: any = {};
|
|
for (let i = 0; i < 1000; i++) {
|
|
largeObj[`key${i}`] = `value${i}`;
|
|
}
|
|
largeObj.password = "secret123";
|
|
|
|
const start = Date.now();
|
|
const result = sanitizeForLogging(largeObj);
|
|
const duration = Date.now() - start;
|
|
|
|
expect(result.password).toBe("[REDACTED]");
|
|
expect(duration).toBeLessThan(500); // Should complete in under 500ms
|
|
});
|
|
});
|
|
|
|
describe("Discord-specific cases", () => {
|
|
it("should sanitize Discord bot token format", () => {
|
|
const input = {
|
|
error: "Failed to connect",
|
|
token: "MTk4NjIyNDgzNDcxOTI1MjQ4.Cl2FMQ.ZnCjm1XVW7vRze4b7Cq4se7kKWs",
|
|
};
|
|
const result = sanitizeForLogging(input);
|
|
expect(result.token).toBe("[REDACTED]");
|
|
});
|
|
|
|
it("should sanitize Discord error with config", () => {
|
|
const error = {
|
|
message: "Request failed",
|
|
config: {
|
|
headers: {
|
|
Authorization: "Bot MTk4NjIyNDgzNDcxOTI1MjQ4.Cl2FMQ.ZnCjm1XVW7vRze4b7Cq4se7kKWs",
|
|
},
|
|
},
|
|
};
|
|
const result = sanitizeForLogging(error);
|
|
expect(result.config.headers.Authorization).toBe("[REDACTED]");
|
|
});
|
|
|
|
it("should sanitize workspace IDs if configured", () => {
|
|
const input = {
|
|
message: "Job dispatched",
|
|
workspaceId: "ws_123456789",
|
|
};
|
|
const result = sanitizeForLogging(input);
|
|
// Workspace IDs are preserved by default (not considered sensitive)
|
|
// Can be redacted if needed in future
|
|
expect(result.workspaceId).toBe("ws_123456789");
|
|
});
|
|
});
|
|
});
|