sanitize.test.ts 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  1. import { describe, expect, it } from 'vitest'
  2. import { sanitizeArrayOfObjects } from './sanitize'
  3. describe('sanitizeArrayOfObjects', () => {
  4. it('redacts sensitive keys case-insensitively', () => {
  5. const input = [{ Password: 'hunter2', username: 'alice' }]
  6. const result = sanitizeArrayOfObjects(input) as Array<Record<string, unknown>>
  7. expect(result).toEqual([{ Password: '[REDACTED]', username: 'alice' }])
  8. })
  9. it('honors custom redaction and extra sensitive keys', () => {
  10. const input = [
  11. {
  12. customSensitive: 'value',
  13. token: 'should hide',
  14. nested: { customSensitive: 'also hide' },
  15. },
  16. ]
  17. const result = sanitizeArrayOfObjects(input, {
  18. redaction: '<removed>',
  19. sensitiveKeys: ['customSensitive'],
  20. }) as Array<any>
  21. expect(result[0].customSensitive).toBe('<removed>')
  22. expect(result[0].token).toBe('<removed>')
  23. expect(result[0].nested).toEqual({ customSensitive: '<removed>' })
  24. expect(input[0].nested.customSensitive).toBe('also hide')
  25. })
  26. it('redacts known secret patterns in strings', () => {
  27. const samples = [
  28. { value: '192.168.0.1' },
  29. { value: '2001:0db8:85a3:0000:0000:8a2e:0370:7334' },
  30. { value: 'AKIAIOSFODNN7EXAMPLE' },
  31. { value: 'wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY' },
  32. { value: 'Bearer abcdEFGHijklMNOPqrstUVWXyz0123456789' },
  33. {
  34. value:
  35. 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c',
  36. },
  37. { value: 'A'.repeat(32) },
  38. ]
  39. const result = sanitizeArrayOfObjects(samples) as Array<{ value: string }>
  40. for (const item of result) {
  41. expect(item.value).toBe('[REDACTED]')
  42. }
  43. })
  44. it('limits recursion depth and uses truncation notice', () => {
  45. const input = [
  46. {
  47. level1: {
  48. level2: {
  49. level3: {
  50. password: 'secret',
  51. },
  52. },
  53. },
  54. },
  55. ]
  56. const [result] = sanitizeArrayOfObjects(input, {
  57. maxDepth: 2,
  58. truncationNotice: '<truncated>',
  59. }) as Array<any>
  60. expect(result.level1.level2).toBe('<truncated>')
  61. expect(result.level1).not.toBe(input[0].level1)
  62. expect(input[0].level1.level2.level3.password).toBe('secret')
  63. })
  64. it('handles circular references without crashing', () => {
  65. const obj: any = { name: 'loop' }
  66. obj.self = obj
  67. const [result] = sanitizeArrayOfObjects([obj]) as Array<any>
  68. expect(result.self).toBe('[Circular]')
  69. expect(result.name).toBe('loop')
  70. })
  71. it('sanitizes complex types consistently', () => {
  72. const date = new Date('2024-01-01T00:00:00.000Z')
  73. const regex = /abc/gi
  74. const fn = () => {}
  75. const arrayBuffer = new ArrayBuffer(8)
  76. const typedArray = new Uint8Array([1, 2, 3])
  77. const map = new Map<any, any>()
  78. map.set('password', 'hunter2')
  79. map.set('public', date)
  80. const set = new Set<any>([1, date])
  81. const url = new URL('https://example.com/path')
  82. const error = new Error('Token is Bearer abcdEFGHijklMNOPqrstUVWXyz0123456789')
  83. const custom = new (class Custom {
  84. toString() {
  85. return 'custom-instance'
  86. }
  87. })()
  88. const [result] = sanitizeArrayOfObjects([
  89. {
  90. date,
  91. regex,
  92. fn,
  93. arrayBuffer,
  94. typedArray,
  95. map,
  96. set,
  97. url,
  98. error,
  99. custom,
  100. },
  101. ]) as Array<any>
  102. expect(result.date).toBe('2024-01-01T00:00:00.000Z')
  103. expect(result.regex).toBe('/abc/gi')
  104. expect(result.fn).toBe('[Function]')
  105. expect(result.arrayBuffer).toBe('[ArrayBuffer byteLength=8]')
  106. expect(result.typedArray).toBe('[TypedArray byteLength=3]')
  107. expect(result.map).toEqual([
  108. ['[REDACTED]', '[REDACTED]'],
  109. ['public', '2024-01-01T00:00:00.000Z'],
  110. ])
  111. expect(result.set).toEqual([1, '2024-01-01T00:00:00.000Z'])
  112. expect(result.url).toBe('https://example.com/path')
  113. expect(result.error).toEqual({
  114. name: 'Error',
  115. message: 'Token is [REDACTED]',
  116. stack: '[REDACTED: max depth reached]',
  117. })
  118. expect(result.custom).toBe('custom-instance')
  119. })
  120. it('sanitizes primitive array entries', () => {
  121. const [redacted, number] = sanitizeArrayOfObjects([
  122. 'Bearer abcdEFGHijklMNOPqrstUVWXyz0123456789',
  123. 42,
  124. ]) as Array<any>
  125. expect(redacted).toBe('[REDACTED]')
  126. expect(number).toBe(42)
  127. })
  128. it('applies maxDepth=0 to top-level entries', () => {
  129. const result = sanitizeArrayOfObjects(
  130. [{ password: 'secret', nested: { value: 'test' } }, 'visible'],
  131. {
  132. maxDepth: 0,
  133. truncationNotice: '<blocked>',
  134. }
  135. ) as Array<any>
  136. expect(result[0]).toBe('<blocked>')
  137. expect(result[1]).toBe('visible')
  138. })
  139. })