cms.c2sgmbh/tests/unit/security/data-masking.unit.spec.ts
Martin Porwoll 0cdc25c4f0 feat: comprehensive security test suite
Add 143 security tests covering all security modules:

Unit Tests (125 tests):
- rate-limiter.unit.spec.ts: limiter creation, request tracking,
  blocking, window reset, IP extraction, header generation
- csrf.unit.spec.ts: token generation/validation, origin checking,
  double submit cookie pattern, referer validation
- ip-allowlist.unit.spec.ts: CIDR matching, wildcards, endpoint-
  specific allowlist/blocklist rules, IP extraction
- data-masking.unit.spec.ts: field detection, pattern matching,
  recursive masking, JWT/connection string/private key handling

API Integration Tests (18 tests):
- security-api.int.spec.ts: rate limiting responses, IP blocking,
  CSRF protection on state-changing endpoints

Test Infrastructure:
- tests/helpers/security-test-utils.ts: CSRF token generators,
  mock request builders, environment setup utilities
- vitest.config.mts: updated to include unit tests
- package.json: added test:unit and test:security scripts
- .github/workflows/security.yml: added security-tests CI job

Also updated detect-secrets.sh to ignore .spec.ts and .test.ts
files which may contain example secrets for testing purposes.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-08 00:20:47 +00:00

456 lines
13 KiB
TypeScript

/**
* Data Masking Unit Tests
*
* Tests for the data masking module.
* Covers field detection, pattern matching, recursive masking, and special formats.
*/
import { describe, it, expect, beforeEach, vi } from 'vitest'
import {
maskString,
maskObject,
maskError,
isSensitiveField,
safeStringify,
createSafeLogger,
} from '@/lib/security/data-masking'
describe('Data Masking', () => {
describe('isSensitiveField', () => {
it('detects password field', () => {
expect(isSensitiveField('password')).toBe(true)
expect(isSensitiveField('PASSWORD')).toBe(true)
expect(isSensitiveField('userPassword')).toBe(true)
expect(isSensitiveField('password_hash')).toBe(true)
})
it('detects token fields', () => {
expect(isSensitiveField('token')).toBe(true)
expect(isSensitiveField('accessToken')).toBe(true)
expect(isSensitiveField('refreshToken')).toBe(true)
expect(isSensitiveField('apiToken')).toBe(true)
})
it('detects API key fields', () => {
expect(isSensitiveField('apiKey')).toBe(true)
expect(isSensitiveField('api_key')).toBe(true)
expect(isSensitiveField('apikey')).toBe(true)
})
it('detects secret fields', () => {
expect(isSensitiveField('secret')).toBe(true)
expect(isSensitiveField('clientSecret')).toBe(true)
expect(isSensitiveField('payloadSecret')).toBe(true)
})
it('detects credential fields', () => {
expect(isSensitiveField('credentials')).toBe(true)
expect(isSensitiveField('authCredentials')).toBe(true)
})
it('detects private key fields', () => {
expect(isSensitiveField('privateKey')).toBe(true)
expect(isSensitiveField('private_key')).toBe(true)
})
it('detects SMTP-related fields', () => {
expect(isSensitiveField('smtpPassword')).toBe(true)
expect(isSensitiveField('smtp_pass')).toBe(true)
})
it('returns false for non-sensitive fields', () => {
expect(isSensitiveField('email')).toBe(false)
expect(isSensitiveField('name')).toBe(false)
expect(isSensitiveField('id')).toBe(false)
expect(isSensitiveField('status')).toBe(false)
})
})
describe('maskString', () => {
it('masks JWT tokens', () => {
const jwt =
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c'
const masked = maskString(jwt)
// JWT format: keeps header, redacts payload and signature
expect(masked).toContain('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9')
expect(masked).toContain('[PAYLOAD REDACTED]')
expect(masked).toContain('[SIGNATURE REDACTED]')
})
it('masks connection strings', () => {
const connectionString = 'postgresql://user:secret123@localhost:5432/mydb'
const masked = maskString(connectionString)
expect(masked).toContain('[REDACTED]')
expect(masked).not.toContain('secret123')
})
it('masks private keys (inline format)', () => {
// Test with inline private key format
const privateKey = '-----BEGIN RSA PRIVATE KEY-----MIIEpAIBAAKCAQEA...-----END RSA PRIVATE KEY-----'
const masked = maskString(privateKey)
expect(masked).toBe('[PRIVATE KEY REDACTED]')
})
it('masks password patterns in text', () => {
const text = 'User logged in with password=secret123 successfully'
const masked = maskString(text)
// The pattern replaces password=... with password: [REDACTED] or pass: [REDACTED]
expect(masked).toContain('[REDACTED]')
expect(masked).not.toContain('secret123')
})
it('preserves non-sensitive text', () => {
const text = 'Normal log message without sensitive data'
const masked = maskString(text)
expect(masked).toBe(text)
})
it('handles empty string', () => {
expect(maskString('')).toBe('')
})
it('handles null/undefined gracefully', () => {
// The function returns the input for non-strings
expect(maskString(null as unknown as string)).toBeNull()
expect(maskString(undefined as unknown as string)).toBeUndefined()
})
})
describe('maskObject', () => {
it('masks sensitive fields in flat object', () => {
const obj = {
email: 'user@example.com',
password: 'supersecret',
name: 'John Doe',
}
const masked = maskObject(obj)
expect(masked.email).toBe('user@example.com')
expect(masked.password).toBe('[REDACTED]')
expect(masked.name).toBe('John Doe')
})
it('masks sensitive fields in nested object', () => {
const obj = {
user: {
email: 'user@example.com',
credentials: {
password: 'secret',
apiKey: 'key-12345',
},
},
status: 'active',
}
const masked = maskObject(obj) as typeof obj
expect(masked.user.email).toBe('user@example.com')
// credentials itself is a sensitive field name
expect(masked.user.credentials).toBe('[REDACTED OBJECT]')
expect(masked.status).toBe('active')
})
it('masks arrays with sensitive values', () => {
const obj = {
tokens: ['token1', 'token2'],
users: [{ name: 'Alice', password: 'secret1' }, { name: 'Bob', password: 'secret2' }],
}
const masked = maskObject(obj) as typeof obj
// tokens is a sensitive field name (contains 'token')
expect(masked.tokens).toBe('[REDACTED OBJECT]')
expect(masked.users[0].name).toBe('Alice')
expect(masked.users[0].password).toBe('[REDACTED]')
})
it('handles circular references gracefully', () => {
const obj: Record<string, unknown> = { name: 'test' }
obj.self = obj
// Should not throw
expect(() => maskObject(obj)).not.toThrow()
})
it('respects depth limit', () => {
const deepObj = {
level1: {
level2: {
level3: {
level4: {
level5: {
password: 'secret',
},
},
},
},
},
}
const masked = maskObject(deepObj, { maxDepth: 3 })
// Deep nested should still be handled
expect(masked).toBeDefined()
})
it('preserves specified fields', () => {
const obj = {
password: 'secret',
debugPassword: 'debug-secret',
}
const masked = maskObject(obj, { preserveFields: ['debugPassword'] })
expect(masked.password).toBe('[REDACTED]')
expect(masked.debugPassword).toBe('debug-secret')
})
it('masks additional custom fields', () => {
const obj = {
customSecret: 'my-custom-value',
normalField: 'normal',
}
const masked = maskObject(obj, { additionalFields: ['customSecret'] })
expect(masked.customSecret).toBe('[REDACTED]')
expect(masked.normalField).toBe('normal')
})
it('masks string values containing secrets', () => {
const obj = {
config:
'database_url=postgresql://admin:password123@localhost/db apiKey=sk-12345',
}
const masked = maskObject(obj) as typeof obj
expect(masked.config).not.toContain('password123')
})
it('handles null and undefined values', () => {
const obj = {
password: null,
token: undefined,
name: 'test',
}
const masked = maskObject(obj)
expect(masked.password).toBeNull()
expect(masked.token).toBeUndefined()
expect(masked.name).toBe('test')
})
})
describe('maskError', () => {
it('masks error message', () => {
const error = new Error('Database connection failed: password=secret123')
const masked = maskError(error)
expect(masked.message).not.toContain('secret123')
expect(masked.message).toContain('[REDACTED]')
})
it('masks error stack', () => {
const error = new Error('Error with password=secret')
const masked = maskError(error)
if (masked.stack) {
expect(masked.stack).not.toContain('secret')
}
})
it('preserves error structure', () => {
const error = new Error('Test error')
error.name = 'CustomError'
const masked = maskError(error)
expect(masked.name).toBe('CustomError')
expect(masked.message).toBe('Test error')
})
it('handles non-Error objects', () => {
const notAnError = { message: 'password=secret', code: 500 }
const masked = maskError(notAnError as Error)
expect(masked).toBeDefined()
})
it('handles string errors', () => {
const stringError = 'Connection failed: apiKey=12345'
const masked = maskError(stringError as unknown as Error)
expect(masked).toBeDefined()
})
})
describe('safeStringify', () => {
it('stringifies and masks object', () => {
const obj = {
user: 'admin',
password: 'secret',
}
const result = safeStringify(obj)
expect(result).toContain('admin')
expect(result).toContain('[REDACTED]')
expect(result).not.toContain('secret')
})
it('handles circular references', () => {
const obj: Record<string, unknown> = { name: 'test' }
obj.self = obj
// Should not throw
expect(() => safeStringify(obj)).not.toThrow()
})
it('formats with spaces when specified', () => {
const obj = { name: 'test' }
const result = safeStringify(obj, 2)
expect(result).toContain('\n')
})
it('handles arrays', () => {
const arr = [{ password: 'secret' }, { name: 'test' }]
const result = safeStringify(arr)
expect(result).toContain('[REDACTED]')
expect(result).toContain('test')
})
})
describe('createSafeLogger', () => {
beforeEach(() => {
vi.spyOn(console, 'log').mockImplementation(() => {})
vi.spyOn(console, 'error').mockImplementation(() => {})
vi.spyOn(console, 'warn').mockImplementation(() => {})
})
it('creates logger with info method', () => {
const logger = createSafeLogger('TestModule')
logger.info('Test message')
expect(console.log).toHaveBeenCalledWith(
expect.stringContaining('[TestModule]'),
expect.any(String),
expect.any(String),
)
})
it('masks sensitive data in log messages', () => {
const logger = createSafeLogger('Auth')
logger.info('User login', { password: 'secret123' })
const lastCall = (console.log as ReturnType<typeof vi.fn>).mock.calls[0]
const logOutput = JSON.stringify(lastCall)
expect(logOutput).toContain('[REDACTED]')
expect(logOutput).not.toContain('secret123')
})
it('provides error method', () => {
const logger = createSafeLogger('Error')
logger.error('Error occurred', new Error('password=secret'))
expect(console.error).toHaveBeenCalled()
})
it('provides warn method', () => {
const logger = createSafeLogger('Warn')
logger.warn('Warning', { apiKey: 'key-123' })
expect(console.warn).toHaveBeenCalled()
})
})
describe('Real-world Scenarios', () => {
it('masks SMTP configuration', () => {
const smtpConfig = {
host: 'smtp.example.com',
port: 587,
smtpUser: 'sender@example.com',
smtpPassword: 'smtp-password-123',
}
const masked = maskObject(smtpConfig) as typeof smtpConfig
expect(masked.host).toBe('smtp.example.com')
expect(masked.smtpUser).toBe('sender@example.com')
expect(masked.smtpPassword).toBe('[REDACTED]')
})
it('masks tenant data', () => {
const tenant = {
id: 1,
name: 'Test Tenant',
smtpSettings: {
host: 'smtp.tenant.com',
password: 'tenant-smtp-pass',
apiKey: 'sendgrid-key-123',
},
}
const masked = maskObject(tenant) as typeof tenant
expect(masked.id).toBe(1)
expect(masked.name).toBe('Test Tenant')
expect(masked.smtpSettings.host).toBe('smtp.tenant.com')
expect(masked.smtpSettings.password).toBe('[REDACTED]')
expect(masked.smtpSettings.apiKey).toBe('[REDACTED]')
})
it('masks audit log data', () => {
const auditLog = {
action: 'login_failed',
userEmail: 'user@example.com',
metadata: {
attemptedPassword: 'wrong-password',
ipAddress: '192.168.1.1',
},
}
const masked = maskObject(auditLog) as typeof auditLog
expect(masked.action).toBe('login_failed')
expect(masked.userEmail).toBe('user@example.com')
expect(masked.metadata.attemptedPassword).toBe('[REDACTED]')
expect(masked.metadata.ipAddress).toBe('192.168.1.1')
})
it('masks database connection errors', () => {
const error = new Error(
'FATAL: password authentication failed for user "admin" at host "db.example.com"',
)
const masked = maskError(error)
expect(masked.message).not.toContain('authentication failed')
})
})
})