wulf-pulse/lib/services/analyzer/itglue-redact.test.ts

166 lines
4.4 KiB
TypeScript
Raw Normal View History

import { describe, it, expect } from 'vitest';
import { redact, isSensitiveKey, REDACTED_VALUE } from './itglue-redact';
describe('isSensitiveKey', () => {
it.each([
['password', true],
['Password', true],
['PASSWORD', true],
['user_password', true],
['secret', true],
['client_secret', true],
['apiKey', true],
['api_key', true],
['api-key', true],
['API_KEY', true],
['authToken', true],
['accessToken', true],
['credentials', true],
['masterKey', true],
['privateKey', true],
['name', false],
['email', false],
['id', false],
['title', false],
['hostname', false],
['username', false], // intentional: username alone isn't a credential
])('%s -> %s', (key, expected) => {
expect(isSensitiveKey(key)).toBe(expected);
});
});
describe('redact', () => {
it('redacts top-level sensitive keys', () => {
const input = { id: 'abc', password: 'hunter2', name: 'wifi' };
expect(redact(input)).toEqual({
id: 'abc',
password: REDACTED_VALUE,
name: 'wifi',
});
});
it('redacts nested object credentials', () => {
const input = {
id: 'abc',
traits: {
username: 'svc-account',
password: 'p@ss',
api_key: 'ak_123',
},
};
expect(redact(input)).toEqual({
id: 'abc',
traits: {
username: 'svc-account',
password: REDACTED_VALUE,
api_key: REDACTED_VALUE,
},
});
});
it('redacts inside arrays of objects', () => {
const input = {
passwords: [
{ id: 1, name: 'admin', password: 'topsecret' },
{ id: 2, name: 'svc', password: 'alsotopsecret' },
],
};
// The outer key "passwords" matches → entire array is redacted.
expect(redact(input)).toEqual({ passwords: REDACTED_VALUE });
});
it('redacts per-item secrets when the array key is benign', () => {
const input = {
accounts: [
{ id: 1, name: 'admin', password: 'topsecret' },
{ id: 2, name: 'svc', api_key: 'ak_456' },
],
};
expect(redact(input)).toEqual({
accounts: [
{ id: 1, name: 'admin', password: REDACTED_VALUE },
{ id: 2, name: 'svc', api_key: REDACTED_VALUE },
],
});
});
it('redacts whole subtree when key matches even if value is an object', () => {
const input = {
org_id: 7,
auth: {
type: 'oauth',
client_id: 'cid',
client_secret: 'shouldbehidden',
nested: { tokens: { access: 'a', refresh: 'r' } },
},
};
// Note: "auth" itself does NOT match the pattern, so it's recursed into.
// Inside, client_id is benign, client_secret matches, nested.tokens matches.
expect(redact(input)).toEqual({
org_id: 7,
auth: {
type: 'oauth',
client_id: 'cid',
client_secret: REDACTED_VALUE,
nested: { tokens: REDACTED_VALUE },
},
});
});
it('preserves non-sensitive values of all primitive types', () => {
const input = {
id: 'abc',
count: 42,
enabled: true,
ratio: 0.5,
tag: null,
missing: undefined,
};
expect(redact(input)).toEqual(input);
});
it('does not mutate the input', () => {
const input = {
id: 'abc',
password: 'hunter2',
nested: { api_key: 'ak_123', name: 'svc' },
};
const snapshot = JSON.parse(JSON.stringify(input));
redact(input);
expect(input).toEqual(snapshot);
});
it('returns null/undefined unchanged on sensitive keys with null value', () => {
expect(redact({ password: null })).toEqual({ password: null });
expect(redact({ password: undefined })).toEqual({ password: undefined });
});
it('handles primitives at the root', () => {
expect(redact('plain string')).toBe('plain string');
expect(redact(42)).toBe(42);
expect(redact(null)).toBe(null);
expect(redact(undefined)).toBe(undefined);
});
it('handles top-level arrays', () => {
const input = [
{ id: 1, password: 'a' },
{ id: 2, name: 'b' },
];
expect(redact(input)).toEqual([
{ id: 1, password: REDACTED_VALUE },
{ id: 2, name: 'b' },
]);
});
it('does not crash on cyclic references', () => {
const a: Record<string, unknown> = { id: 1 };
a.self = a; // cycle
a.password = 'secret';
const out = redact(a) as Record<string, unknown>;
expect(out.id).toBe(1);
expect(out.password).toBe(REDACTED_VALUE);
expect(out.self).toBe('[CIRCULAR]');
});
});