mirror of
https://github.com/simstudioai/sim.git
synced 2026-02-19 02:34:37 -05:00
* progress * improvement(execution): update execution for passing base64 strings * fix types * cleanup comments * path security vuln * reject promise correctly * fix redirect case * remove proxy routes * fix tests * use ipaddr
772 lines
24 KiB
TypeScript
772 lines
24 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import {
|
|
isLargeDataKey,
|
|
isSensitiveKey,
|
|
REDACTED_MARKER,
|
|
redactApiKeys,
|
|
redactSensitiveValues,
|
|
sanitizeEventData,
|
|
sanitizeForLogging,
|
|
TRUNCATED_MARKER,
|
|
} from './redaction'
|
|
|
|
/**
|
|
* Security-focused edge case tests for redaction utilities
|
|
*/
|
|
|
|
describe('REDACTED_MARKER', () => {
|
|
it.concurrent('should be the standard marker', () => {
|
|
expect(REDACTED_MARKER).toBe('[REDACTED]')
|
|
})
|
|
})
|
|
|
|
describe('TRUNCATED_MARKER', () => {
|
|
it.concurrent('should be the standard marker', () => {
|
|
expect(TRUNCATED_MARKER).toBe('[TRUNCATED]')
|
|
})
|
|
})
|
|
|
|
describe('isLargeDataKey', () => {
|
|
it.concurrent('should identify base64 as large data key', () => {
|
|
expect(isLargeDataKey('base64')).toBe(true)
|
|
})
|
|
|
|
it.concurrent('should not identify other keys as large data', () => {
|
|
expect(isLargeDataKey('content')).toBe(false)
|
|
expect(isLargeDataKey('data')).toBe(false)
|
|
expect(isLargeDataKey('base')).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('isSensitiveKey', () => {
|
|
describe('exact matches', () => {
|
|
it.concurrent('should match apiKey variations', () => {
|
|
expect(isSensitiveKey('apiKey')).toBe(true)
|
|
expect(isSensitiveKey('api_key')).toBe(true)
|
|
expect(isSensitiveKey('api-key')).toBe(true)
|
|
expect(isSensitiveKey('APIKEY')).toBe(true)
|
|
expect(isSensitiveKey('API_KEY')).toBe(true)
|
|
})
|
|
|
|
it.concurrent('should match token variations', () => {
|
|
expect(isSensitiveKey('access_token')).toBe(true)
|
|
expect(isSensitiveKey('refresh_token')).toBe(true)
|
|
expect(isSensitiveKey('auth_token')).toBe(true)
|
|
expect(isSensitiveKey('accessToken')).toBe(true)
|
|
})
|
|
|
|
it.concurrent('should match secret variations', () => {
|
|
expect(isSensitiveKey('client_secret')).toBe(true)
|
|
expect(isSensitiveKey('clientSecret')).toBe(true)
|
|
expect(isSensitiveKey('secret')).toBe(true)
|
|
})
|
|
|
|
it.concurrent('should match other sensitive keys', () => {
|
|
expect(isSensitiveKey('private_key')).toBe(true)
|
|
expect(isSensitiveKey('authorization')).toBe(true)
|
|
expect(isSensitiveKey('bearer')).toBe(true)
|
|
expect(isSensitiveKey('private')).toBe(true)
|
|
expect(isSensitiveKey('auth')).toBe(true)
|
|
expect(isSensitiveKey('password')).toBe(true)
|
|
expect(isSensitiveKey('credential')).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('suffix matches', () => {
|
|
it.concurrent('should match keys ending in secret', () => {
|
|
expect(isSensitiveKey('clientSecret')).toBe(true)
|
|
expect(isSensitiveKey('appSecret')).toBe(true)
|
|
expect(isSensitiveKey('mySecret')).toBe(true)
|
|
})
|
|
|
|
it.concurrent('should match keys ending in password', () => {
|
|
expect(isSensitiveKey('userPassword')).toBe(true)
|
|
expect(isSensitiveKey('dbPassword')).toBe(true)
|
|
expect(isSensitiveKey('adminPassword')).toBe(true)
|
|
})
|
|
|
|
it.concurrent('should match keys ending in token', () => {
|
|
expect(isSensitiveKey('accessToken')).toBe(true)
|
|
expect(isSensitiveKey('refreshToken')).toBe(true)
|
|
expect(isSensitiveKey('bearerToken')).toBe(true)
|
|
})
|
|
|
|
it.concurrent('should match keys ending in credential', () => {
|
|
expect(isSensitiveKey('userCredential')).toBe(true)
|
|
expect(isSensitiveKey('dbCredential')).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('non-sensitive keys (no false positives)', () => {
|
|
it.concurrent('should not match keys with sensitive words as prefix only', () => {
|
|
expect(isSensitiveKey('tokenCount')).toBe(false)
|
|
expect(isSensitiveKey('tokenizer')).toBe(false)
|
|
expect(isSensitiveKey('secretKey')).toBe(false)
|
|
expect(isSensitiveKey('passwordStrength')).toBe(false)
|
|
expect(isSensitiveKey('authMethod')).toBe(false)
|
|
})
|
|
|
|
it.concurrent('should match keys ending with sensitive words (intentional)', () => {
|
|
expect(isSensitiveKey('hasSecret')).toBe(true)
|
|
expect(isSensitiveKey('userPassword')).toBe(true)
|
|
expect(isSensitiveKey('sessionToken')).toBe(true)
|
|
})
|
|
|
|
it.concurrent('should not match normal field names', () => {
|
|
expect(isSensitiveKey('name')).toBe(false)
|
|
expect(isSensitiveKey('email')).toBe(false)
|
|
expect(isSensitiveKey('id')).toBe(false)
|
|
expect(isSensitiveKey('value')).toBe(false)
|
|
expect(isSensitiveKey('data')).toBe(false)
|
|
expect(isSensitiveKey('count')).toBe(false)
|
|
expect(isSensitiveKey('status')).toBe(false)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('redactSensitiveValues', () => {
|
|
it.concurrent('should redact Bearer tokens', () => {
|
|
const input = 'Authorization: Bearer abc123xyz456'
|
|
const result = redactSensitiveValues(input)
|
|
expect(result).toBe('Authorization: Bearer [REDACTED]')
|
|
expect(result).not.toContain('abc123xyz456')
|
|
})
|
|
|
|
it.concurrent('should redact Basic auth', () => {
|
|
const input = 'Authorization: Basic dXNlcjpwYXNz'
|
|
const result = redactSensitiveValues(input)
|
|
expect(result).toBe('Authorization: Basic [REDACTED]')
|
|
})
|
|
|
|
it.concurrent('should redact API key prefixes', () => {
|
|
const input = 'Using key sk-1234567890abcdefghijklmnop'
|
|
const result = redactSensitiveValues(input)
|
|
expect(result).toContain('[REDACTED]')
|
|
expect(result).not.toContain('sk-1234567890abcdefghijklmnop')
|
|
})
|
|
|
|
it.concurrent('should redact JSON-style password fields', () => {
|
|
const input = 'password: "mysecretpass123"'
|
|
const result = redactSensitiveValues(input)
|
|
expect(result).toContain('[REDACTED]')
|
|
expect(result).not.toContain('mysecretpass123')
|
|
})
|
|
|
|
it.concurrent('should redact JSON-style token fields', () => {
|
|
const input = 'token: "tokenvalue123"'
|
|
const result = redactSensitiveValues(input)
|
|
expect(result).toContain('[REDACTED]')
|
|
expect(result).not.toContain('tokenvalue123')
|
|
})
|
|
|
|
it.concurrent('should redact JSON-style api_key fields', () => {
|
|
const input = 'api_key: "key123456"'
|
|
const result = redactSensitiveValues(input)
|
|
expect(result).toContain('[REDACTED]')
|
|
expect(result).not.toContain('key123456')
|
|
})
|
|
|
|
it.concurrent('should not modify safe strings', () => {
|
|
const input = 'This is a normal string with no secrets'
|
|
const result = redactSensitiveValues(input)
|
|
expect(result).toBe(input)
|
|
})
|
|
|
|
it.concurrent('should handle empty strings', () => {
|
|
expect(redactSensitiveValues('')).toBe('')
|
|
})
|
|
|
|
it.concurrent('should handle null/undefined gracefully', () => {
|
|
expect(redactSensitiveValues(null as any)).toBe(null)
|
|
expect(redactSensitiveValues(undefined as any)).toBe(undefined)
|
|
})
|
|
})
|
|
|
|
describe('redactApiKeys', () => {
|
|
describe('object redaction', () => {
|
|
it.concurrent('should redact sensitive keys in flat objects', () => {
|
|
const obj = {
|
|
apiKey: 'secret-key',
|
|
api_key: 'another-secret',
|
|
access_token: 'token-value',
|
|
secret: 'secret-value',
|
|
password: 'password-value',
|
|
normalField: 'normal-value',
|
|
}
|
|
|
|
const result = redactApiKeys(obj)
|
|
|
|
expect(result.apiKey).toBe('[REDACTED]')
|
|
expect(result.api_key).toBe('[REDACTED]')
|
|
expect(result.access_token).toBe('[REDACTED]')
|
|
expect(result.secret).toBe('[REDACTED]')
|
|
expect(result.password).toBe('[REDACTED]')
|
|
expect(result.normalField).toBe('normal-value')
|
|
})
|
|
|
|
it.concurrent('should redact sensitive keys in nested objects', () => {
|
|
const obj = {
|
|
config: {
|
|
apiKey: 'secret-key',
|
|
normalField: 'normal-value',
|
|
},
|
|
}
|
|
|
|
const result = redactApiKeys(obj)
|
|
|
|
expect(result.config.apiKey).toBe('[REDACTED]')
|
|
expect(result.config.normalField).toBe('normal-value')
|
|
})
|
|
|
|
it.concurrent('should redact sensitive keys in arrays', () => {
|
|
const arr = [{ apiKey: 'secret-key-1' }, { apiKey: 'secret-key-2' }]
|
|
|
|
const result = redactApiKeys(arr)
|
|
|
|
expect(result[0].apiKey).toBe('[REDACTED]')
|
|
expect(result[1].apiKey).toBe('[REDACTED]')
|
|
})
|
|
|
|
it.concurrent('should handle deeply nested structures', () => {
|
|
const obj = {
|
|
users: [
|
|
{
|
|
name: 'John',
|
|
credentials: {
|
|
apiKey: 'secret-key',
|
|
username: 'john_doe',
|
|
},
|
|
},
|
|
],
|
|
config: {
|
|
database: {
|
|
password: 'db-password',
|
|
host: 'localhost',
|
|
},
|
|
},
|
|
}
|
|
|
|
const result = redactApiKeys(obj)
|
|
|
|
expect(result.users[0].name).toBe('John')
|
|
expect(result.users[0].credentials.apiKey).toBe('[REDACTED]')
|
|
expect(result.users[0].credentials.username).toBe('john_doe')
|
|
expect(result.config.database.password).toBe('[REDACTED]')
|
|
expect(result.config.database.host).toBe('localhost')
|
|
})
|
|
|
|
it.concurrent('should truncate base64 fields', () => {
|
|
const obj = {
|
|
id: 'file-123',
|
|
name: 'document.pdf',
|
|
base64: 'VGhpcyBpcyBhIHZlcnkgbG9uZyBiYXNlNjQgc3RyaW5n...',
|
|
size: 12345,
|
|
}
|
|
|
|
const result = redactApiKeys(obj)
|
|
|
|
expect(result.id).toBe('file-123')
|
|
expect(result.name).toBe('document.pdf')
|
|
expect(result.base64).toBe('[TRUNCATED]')
|
|
expect(result.size).toBe(12345)
|
|
})
|
|
|
|
it.concurrent('should truncate base64 in nested UserFile objects', () => {
|
|
const obj = {
|
|
files: [
|
|
{
|
|
id: 'file-1',
|
|
name: 'doc1.pdf',
|
|
url: 'http://example.com/file1',
|
|
size: 1000,
|
|
base64: 'base64content1...',
|
|
},
|
|
{
|
|
id: 'file-2',
|
|
name: 'doc2.pdf',
|
|
url: 'http://example.com/file2',
|
|
size: 2000,
|
|
base64: 'base64content2...',
|
|
},
|
|
],
|
|
}
|
|
|
|
const result = redactApiKeys(obj)
|
|
|
|
expect(result.files[0].id).toBe('file-1')
|
|
expect(result.files[0].base64).toBe('[TRUNCATED]')
|
|
expect(result.files[1].base64).toBe('[TRUNCATED]')
|
|
})
|
|
|
|
it.concurrent('should filter UserFile objects to only expose allowed fields', () => {
|
|
const obj = {
|
|
processedFiles: [
|
|
{
|
|
id: 'file-123',
|
|
name: 'document.pdf',
|
|
url: 'http://localhost/api/files/serve/...',
|
|
size: 12345,
|
|
type: 'application/pdf',
|
|
key: 'execution/workspace/workflow/file.pdf',
|
|
context: 'execution',
|
|
base64: 'VGhpcyBpcyBhIGJhc2U2NCBzdHJpbmc=',
|
|
},
|
|
],
|
|
}
|
|
|
|
const result = redactApiKeys(obj)
|
|
|
|
// Exposed fields should be present
|
|
expect(result.processedFiles[0].id).toBe('file-123')
|
|
expect(result.processedFiles[0].name).toBe('document.pdf')
|
|
expect(result.processedFiles[0].url).toBe('http://localhost/api/files/serve/...')
|
|
expect(result.processedFiles[0].size).toBe(12345)
|
|
expect(result.processedFiles[0].type).toBe('application/pdf')
|
|
expect(result.processedFiles[0].base64).toBe('[TRUNCATED]')
|
|
|
|
// Internal fields should be filtered out
|
|
expect(result.processedFiles[0]).not.toHaveProperty('key')
|
|
expect(result.processedFiles[0]).not.toHaveProperty('context')
|
|
})
|
|
})
|
|
|
|
describe('primitive handling', () => {
|
|
it.concurrent('should return primitives unchanged', () => {
|
|
expect(redactApiKeys('string')).toBe('string')
|
|
expect(redactApiKeys(123)).toBe(123)
|
|
expect(redactApiKeys(true)).toBe(true)
|
|
expect(redactApiKeys(null)).toBe(null)
|
|
expect(redactApiKeys(undefined)).toBe(undefined)
|
|
})
|
|
})
|
|
|
|
describe('no false positives', () => {
|
|
it.concurrent('should not redact keys with sensitive words as prefix only', () => {
|
|
const obj = {
|
|
tokenCount: 100,
|
|
secretKey: 'not-actually-secret',
|
|
passwordStrength: 'strong',
|
|
authMethod: 'oauth',
|
|
}
|
|
|
|
const result = redactApiKeys(obj)
|
|
|
|
expect(result.tokenCount).toBe(100)
|
|
expect(result.secretKey).toBe('not-actually-secret')
|
|
expect(result.passwordStrength).toBe('strong')
|
|
expect(result.authMethod).toBe('oauth')
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('sanitizeForLogging', () => {
|
|
it.concurrent('should truncate long strings', () => {
|
|
const longString = 'a'.repeat(200)
|
|
const result = sanitizeForLogging(longString, 50)
|
|
expect(result.length).toBe(50)
|
|
})
|
|
|
|
it.concurrent('should use default max length of 100', () => {
|
|
const longString = 'a'.repeat(200)
|
|
const result = sanitizeForLogging(longString)
|
|
expect(result.length).toBe(100)
|
|
})
|
|
|
|
it.concurrent('should redact sensitive patterns', () => {
|
|
const input = 'Bearer abc123xyz456'
|
|
const result = sanitizeForLogging(input)
|
|
expect(result).toContain('[REDACTED]')
|
|
expect(result).not.toContain('abc123xyz456')
|
|
})
|
|
|
|
it.concurrent('should handle empty strings', () => {
|
|
expect(sanitizeForLogging('')).toBe('')
|
|
})
|
|
|
|
it.concurrent('should not modify safe short strings', () => {
|
|
const input = 'Safe string'
|
|
const result = sanitizeForLogging(input)
|
|
expect(result).toBe(input)
|
|
})
|
|
})
|
|
|
|
describe('sanitizeEventData', () => {
|
|
describe('object sanitization', () => {
|
|
it.concurrent('should remove sensitive keys entirely', () => {
|
|
const event = {
|
|
action: 'login',
|
|
apiKey: 'secret-key',
|
|
password: 'secret-pass',
|
|
userId: '123',
|
|
}
|
|
|
|
const result = sanitizeEventData(event)
|
|
|
|
expect(result.action).toBe('login')
|
|
expect(result.userId).toBe('123')
|
|
expect(result).not.toHaveProperty('apiKey')
|
|
expect(result).not.toHaveProperty('password')
|
|
})
|
|
|
|
it.concurrent('should redact sensitive patterns in string values', () => {
|
|
const event = {
|
|
message: 'Auth: Bearer abc123token',
|
|
normal: 'normal value',
|
|
}
|
|
|
|
const result = sanitizeEventData(event)
|
|
|
|
expect(result.message).toContain('[REDACTED]')
|
|
expect(result.message).not.toContain('abc123token')
|
|
expect(result.normal).toBe('normal value')
|
|
})
|
|
|
|
it.concurrent('should handle nested objects', () => {
|
|
const event = {
|
|
user: {
|
|
id: '123',
|
|
accessToken: 'secret-token',
|
|
},
|
|
}
|
|
|
|
const result = sanitizeEventData(event)
|
|
|
|
expect(result.user.id).toBe('123')
|
|
expect(result.user).not.toHaveProperty('accessToken')
|
|
})
|
|
|
|
it.concurrent('should handle arrays', () => {
|
|
const event = {
|
|
items: [
|
|
{ id: 1, apiKey: 'key1' },
|
|
{ id: 2, apiKey: 'key2' },
|
|
],
|
|
}
|
|
|
|
const result = sanitizeEventData(event)
|
|
|
|
expect(result.items[0].id).toBe(1)
|
|
expect(result.items[0]).not.toHaveProperty('apiKey')
|
|
expect(result.items[1].id).toBe(2)
|
|
expect(result.items[1]).not.toHaveProperty('apiKey')
|
|
})
|
|
})
|
|
|
|
describe('primitive handling', () => {
|
|
it.concurrent('should return primitives appropriately', () => {
|
|
expect(sanitizeEventData(null)).toBe(null)
|
|
expect(sanitizeEventData(undefined)).toBe(undefined)
|
|
expect(sanitizeEventData(123)).toBe(123)
|
|
expect(sanitizeEventData(true)).toBe(true)
|
|
})
|
|
|
|
it.concurrent('should redact sensitive patterns in top-level strings', () => {
|
|
const result = sanitizeEventData('Bearer secrettoken123')
|
|
expect(result).toContain('[REDACTED]')
|
|
})
|
|
|
|
it.concurrent('should not redact normal strings', () => {
|
|
const result = sanitizeEventData('normal string')
|
|
expect(result).toBe('normal string')
|
|
})
|
|
})
|
|
|
|
describe('no false positives', () => {
|
|
it.concurrent('should not remove keys with sensitive words in middle', () => {
|
|
const event = {
|
|
tokenCount: 500,
|
|
isAuthenticated: true,
|
|
hasSecretFeature: false,
|
|
}
|
|
|
|
const result = sanitizeEventData(event)
|
|
|
|
expect(result.tokenCount).toBe(500)
|
|
expect(result.isAuthenticated).toBe(true)
|
|
expect(result.hasSecretFeature).toBe(false)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('Security edge cases', () => {
|
|
describe('redactApiKeys security', () => {
|
|
it.concurrent('should handle objects with prototype-like key names safely', () => {
|
|
const obj = {
|
|
protoField: { isAdmin: true },
|
|
name: 'test',
|
|
apiKey: 'secret',
|
|
}
|
|
const result = redactApiKeys(obj)
|
|
|
|
expect(result.name).toBe('test')
|
|
expect(result.protoField).toEqual({ isAdmin: true })
|
|
expect(result.apiKey).toBe('[REDACTED]')
|
|
})
|
|
|
|
it.concurrent('should handle objects with constructor key', () => {
|
|
const obj = {
|
|
constructor: 'test-value',
|
|
normalField: 'normal',
|
|
}
|
|
|
|
const result = redactApiKeys(obj)
|
|
|
|
expect(result.constructor).toBe('test-value')
|
|
expect(result.normalField).toBe('normal')
|
|
})
|
|
|
|
it.concurrent('should handle objects with toString key', () => {
|
|
const obj = {
|
|
toString: 'custom-tostring',
|
|
valueOf: 'custom-valueof',
|
|
apiKey: 'secret',
|
|
}
|
|
|
|
const result = redactApiKeys(obj)
|
|
|
|
expect(result.toString).toBe('custom-tostring')
|
|
expect(result.valueOf).toBe('custom-valueof')
|
|
expect(result.apiKey).toBe('[REDACTED]')
|
|
})
|
|
|
|
it.concurrent('should not mutate original object', () => {
|
|
const original = {
|
|
apiKey: 'secret-key',
|
|
nested: {
|
|
password: 'secret-password',
|
|
},
|
|
}
|
|
|
|
const originalCopy = JSON.parse(JSON.stringify(original))
|
|
redactApiKeys(original)
|
|
|
|
expect(original).toEqual(originalCopy)
|
|
})
|
|
|
|
it.concurrent('should handle very deeply nested structures', () => {
|
|
let obj: any = { data: 'value' }
|
|
for (let i = 0; i < 50; i++) {
|
|
obj = { nested: obj, apiKey: `secret-${i}` }
|
|
}
|
|
|
|
const result = redactApiKeys(obj)
|
|
|
|
expect(result.apiKey).toBe('[REDACTED]')
|
|
expect(result.nested.apiKey).toBe('[REDACTED]')
|
|
})
|
|
|
|
it.concurrent('should handle arrays with mixed types', () => {
|
|
const arr = [
|
|
{ apiKey: 'secret' },
|
|
'string',
|
|
123,
|
|
null,
|
|
undefined,
|
|
true,
|
|
[{ password: 'nested' }],
|
|
]
|
|
|
|
const result = redactApiKeys(arr)
|
|
|
|
expect(result[0].apiKey).toBe('[REDACTED]')
|
|
expect(result[1]).toBe('string')
|
|
expect(result[2]).toBe(123)
|
|
expect(result[3]).toBe(null)
|
|
expect(result[4]).toBe(undefined)
|
|
expect(result[5]).toBe(true)
|
|
expect(result[6][0].password).toBe('[REDACTED]')
|
|
})
|
|
|
|
it.concurrent('should handle empty arrays', () => {
|
|
const result = redactApiKeys([])
|
|
expect(result).toEqual([])
|
|
})
|
|
|
|
it.concurrent('should handle empty objects', () => {
|
|
const result = redactApiKeys({})
|
|
expect(result).toEqual({})
|
|
})
|
|
})
|
|
|
|
describe('redactSensitiveValues security', () => {
|
|
it.concurrent('should handle multiple API key patterns in one string', () => {
|
|
const input = 'Keys: sk-abc123defghijklmnopqr and pk-xyz789abcdefghijklmnop'
|
|
const result = redactSensitiveValues(input)
|
|
|
|
expect(result).not.toContain('sk-abc123defghijklmnopqr')
|
|
expect(result).not.toContain('pk-xyz789abcdefghijklmnop')
|
|
expect(result.match(/\[REDACTED\]/g)?.length).toBeGreaterThanOrEqual(2)
|
|
})
|
|
|
|
it.concurrent('should handle multiline strings with sensitive data', () => {
|
|
const input = `Line 1: Bearer token123abc456def
|
|
Line 2: password: "secretpass"
|
|
Line 3: Normal content`
|
|
|
|
const result = redactSensitiveValues(input)
|
|
|
|
expect(result).toContain('[REDACTED]')
|
|
expect(result).not.toContain('token123abc456def')
|
|
expect(result).not.toContain('secretpass')
|
|
expect(result).toContain('Normal content')
|
|
})
|
|
|
|
it.concurrent('should handle unicode in strings', () => {
|
|
const input = 'Bearer abc123'
|
|
const result = redactSensitiveValues(input)
|
|
|
|
expect(result).toContain('[REDACTED]')
|
|
expect(result).not.toContain('abc123')
|
|
})
|
|
|
|
it.concurrent('should handle very long strings', () => {
|
|
const longSecret = 'a'.repeat(10000)
|
|
const input = `Bearer ${longSecret}`
|
|
const result = redactSensitiveValues(input)
|
|
|
|
expect(result).toContain('[REDACTED]')
|
|
expect(result.length).toBeLessThan(input.length)
|
|
})
|
|
|
|
it.concurrent('should not match partial patterns', () => {
|
|
const input = 'This is a Bear without er suffix'
|
|
const result = redactSensitiveValues(input)
|
|
|
|
expect(result).toBe(input)
|
|
})
|
|
|
|
it.concurrent('should handle special regex characters safely', () => {
|
|
const input = 'Test with special chars: $^.*+?()[]{}|'
|
|
const result = redactSensitiveValues(input)
|
|
|
|
expect(result).toBe(input)
|
|
})
|
|
})
|
|
|
|
describe('sanitizeEventData security', () => {
|
|
it.concurrent('should strip sensitive keys entirely (not redact)', () => {
|
|
const event = {
|
|
action: 'login',
|
|
apiKey: 'should-be-stripped',
|
|
password: 'should-be-stripped',
|
|
userId: '123',
|
|
}
|
|
|
|
const result = sanitizeEventData(event)
|
|
|
|
expect(result).not.toHaveProperty('apiKey')
|
|
expect(result).not.toHaveProperty('password')
|
|
expect(Object.keys(result)).not.toContain('apiKey')
|
|
expect(Object.keys(result)).not.toContain('password')
|
|
})
|
|
|
|
it.concurrent('should handle Symbol keys gracefully', () => {
|
|
const sym = Symbol('test')
|
|
const event: any = {
|
|
[sym]: 'symbol-value',
|
|
normalKey: 'normal-value',
|
|
}
|
|
|
|
expect(() => sanitizeEventData(event)).not.toThrow()
|
|
})
|
|
|
|
it.concurrent('should handle Date objects as objects', () => {
|
|
const date = new Date('2024-01-01')
|
|
const event = {
|
|
createdAt: date,
|
|
apiKey: 'secret',
|
|
}
|
|
|
|
const result = sanitizeEventData(event)
|
|
|
|
expect(result.createdAt).toBeDefined()
|
|
expect(result).not.toHaveProperty('apiKey')
|
|
})
|
|
|
|
it.concurrent('should handle objects with numeric keys', () => {
|
|
const event: any = {
|
|
0: 'first',
|
|
1: 'second',
|
|
apiKey: 'secret',
|
|
}
|
|
|
|
const result = sanitizeEventData(event)
|
|
|
|
expect(result[0]).toBe('first')
|
|
expect(result[1]).toBe('second')
|
|
expect(result).not.toHaveProperty('apiKey')
|
|
})
|
|
})
|
|
|
|
describe('isSensitiveKey security', () => {
|
|
it.concurrent('should handle case variations', () => {
|
|
expect(isSensitiveKey('APIKEY')).toBe(true)
|
|
expect(isSensitiveKey('ApiKey')).toBe(true)
|
|
expect(isSensitiveKey('apikey')).toBe(true)
|
|
expect(isSensitiveKey('API_KEY')).toBe(true)
|
|
expect(isSensitiveKey('api_key')).toBe(true)
|
|
expect(isSensitiveKey('Api_Key')).toBe(true)
|
|
})
|
|
|
|
it.concurrent('should handle empty string', () => {
|
|
expect(isSensitiveKey('')).toBe(false)
|
|
})
|
|
|
|
it.concurrent('should handle very long key names', () => {
|
|
const longKey = `${'a'.repeat(10000)}password`
|
|
expect(isSensitiveKey(longKey)).toBe(true)
|
|
})
|
|
|
|
it.concurrent('should handle keys with special characters', () => {
|
|
expect(isSensitiveKey('api-key')).toBe(true)
|
|
expect(isSensitiveKey('api_key')).toBe(true)
|
|
})
|
|
|
|
it.concurrent('should detect oauth tokens', () => {
|
|
expect(isSensitiveKey('access_token')).toBe(true)
|
|
expect(isSensitiveKey('refresh_token')).toBe(true)
|
|
expect(isSensitiveKey('accessToken')).toBe(true)
|
|
expect(isSensitiveKey('refreshToken')).toBe(true)
|
|
})
|
|
|
|
it.concurrent('should detect various credential patterns', () => {
|
|
expect(isSensitiveKey('userCredential')).toBe(true)
|
|
expect(isSensitiveKey('dbCredential')).toBe(true)
|
|
expect(isSensitiveKey('appCredential')).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('sanitizeForLogging edge cases', () => {
|
|
it.concurrent('should handle string with only sensitive content', () => {
|
|
const input = 'Bearer abc123xyz456'
|
|
const result = sanitizeForLogging(input)
|
|
|
|
expect(result).toContain('[REDACTED]')
|
|
expect(result).not.toContain('abc123xyz456')
|
|
})
|
|
|
|
it.concurrent('should truncate strings to specified length', () => {
|
|
const longString = 'a'.repeat(200)
|
|
const result = sanitizeForLogging(longString, 60)
|
|
|
|
expect(result.length).toBe(60)
|
|
})
|
|
|
|
it.concurrent('should handle maxLength of 0', () => {
|
|
const result = sanitizeForLogging('test', 0)
|
|
expect(result).toBe('')
|
|
})
|
|
|
|
it.concurrent('should handle negative maxLength gracefully', () => {
|
|
const result = sanitizeForLogging('test', -5)
|
|
expect(result).toBe('')
|
|
})
|
|
|
|
it.concurrent('should handle maxLength larger than string', () => {
|
|
const input = 'short'
|
|
const result = sanitizeForLogging(input, 1000)
|
|
expect(result).toBe(input)
|
|
})
|
|
})
|
|
})
|