WS-7 PR-3 commit 2.
- scrubber.spec.ts (18 cases): mirrors backend PiiScrubbingTest semantics.
Body/header/query scrubbing, form_values wholesale replacement, all
SENSITIVE_BODY_KEYS at top + nested levels, max_depth guard, cookies +
storage + user.cookies sanitisation.
- contextBinding.spec.ts (11 cases): exercises the Vue Router beforeEach
guard against a real router with mocked Sentry scope (capturing every
setTag/setUser call into a per-test buffer). Cases:
- portal-token zone — actor_scope=portal, no user_id
- platform route + super_admin — actor_scope=platform
- platform route without super_admin — does NOT tag platform
- organizer route with active org — actor_scope=organisation +
organisation_id
- organizer route without active org — actor_scope=user, no org tag
- unauthenticated public — actor_scope=anonymous
- actor_type role hierarchy
- RFC §3.8 ULID-only user identity (no email leakage)
- route_name + app=app baseline tags
- cross-zone leak guard: navigating from organizer to portal-token
calls scope.clear() and does not bind user
Frontend test count 223 to 252. Typecheck clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
207 lines
7.1 KiB
TypeScript
207 lines
7.1 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import type { ErrorEvent as SentryErrorEvent } from '@sentry/vue'
|
|
import { scrubEvent } from '../scrubber'
|
|
|
|
function makeEvent(overrides: Partial<SentryErrorEvent> = {}): SentryErrorEvent {
|
|
return {
|
|
type: undefined,
|
|
...overrides,
|
|
} as SentryErrorEvent
|
|
}
|
|
|
|
describe('scrubEvent', () => {
|
|
describe('request body', () => {
|
|
it('scrubs password in request body', () => {
|
|
const event = makeEvent({ request: { data: { email: 'a@b.test', password: 'p@ss' } } })
|
|
const result = scrubEvent(event) as SentryErrorEvent
|
|
const data = result.request?.data as Record<string, unknown>
|
|
|
|
expect(data.password).toBe('[scrubbed]')
|
|
expect(data.email).toBe('a@b.test')
|
|
})
|
|
|
|
it('scrubs password_confirmation', () => {
|
|
const event = makeEvent({ request: { data: { password_confirmation: 'x', current_password: 'y' } } })
|
|
const result = scrubEvent(event) as SentryErrorEvent
|
|
const data = result.request?.data as Record<string, unknown>
|
|
|
|
expect(data.password_confirmation).toBe('[scrubbed]')
|
|
expect(data.current_password).toBe('[scrubbed]')
|
|
})
|
|
|
|
it('scrubs every SENSITIVE_BODY_KEY at top level', () => {
|
|
const allKeys = {
|
|
password: 'a',
|
|
token: 'b',
|
|
api_key: 'c',
|
|
secret: 'd',
|
|
webhook_secret: 'e',
|
|
dsn: 'f',
|
|
signature: 'g',
|
|
authorization: 'h',
|
|
cookie: 'i',
|
|
bearer: 'j',
|
|
iban: 'k',
|
|
bic: 'l',
|
|
passport_number: 'm',
|
|
bsn: 'n',
|
|
}
|
|
|
|
const event = makeEvent({ request: { data: allKeys } })
|
|
const result = scrubEvent(event) as SentryErrorEvent
|
|
const data = result.request?.data as Record<string, unknown>
|
|
for (const key of Object.keys(allKeys))
|
|
expect(data[key]).toBe('[scrubbed]')
|
|
})
|
|
|
|
it('scrubs sensitive keys at nested levels (recursive)', () => {
|
|
const event = makeEvent({
|
|
request: {
|
|
data: { profile: { address: { iban: 'NL91...', street: 'Damrak 1' } } },
|
|
},
|
|
})
|
|
|
|
const result = scrubEvent(event) as SentryErrorEvent
|
|
const data = result.request?.data as Record<string, Record<string, Record<string, unknown>>>
|
|
|
|
expect(data.profile.address.iban).toBe('[scrubbed]')
|
|
expect(data.profile.address.street).toBe('Damrak 1')
|
|
})
|
|
|
|
it('replaces form_values payload wholesale', () => {
|
|
const event = makeEvent({
|
|
request: { data: { form_values: { email: 'x@y.com', dietary: 'vegan' } } },
|
|
})
|
|
|
|
const result = scrubEvent(event) as SentryErrorEvent
|
|
const data = result.request?.data as Record<string, unknown>
|
|
|
|
expect(data.form_values).toBe('[scrubbed_form_values]')
|
|
|
|
const serialised = JSON.stringify(data)
|
|
|
|
expect(serialised).not.toContain('x@y.com')
|
|
expect(serialised).not.toContain('vegan')
|
|
})
|
|
|
|
it('does not leak email or other non-sensitive keys', () => {
|
|
const event = makeEvent({ request: { data: { email: 'x@y.com', name: 'Bob' } } })
|
|
const result = scrubEvent(event) as SentryErrorEvent
|
|
const data = result.request?.data as Record<string, unknown>
|
|
|
|
expect(data.email).toBe('x@y.com')
|
|
expect(data.name).toBe('Bob')
|
|
})
|
|
|
|
it('hits max_depth guard at depth 11', () => {
|
|
let deep: unknown = { v: 'leaf' }
|
|
for (let i = 0; i < 15; i++)
|
|
deep = { nest: deep }
|
|
|
|
const event = makeEvent({ request: { data: deep as Record<string, unknown> } })
|
|
const result = scrubEvent(event) as SentryErrorEvent
|
|
const serialised = JSON.stringify(result.request?.data)
|
|
|
|
expect(serialised).toContain('[max_depth]')
|
|
})
|
|
|
|
it('returns event unchanged when request is undefined', () => {
|
|
const event = makeEvent()
|
|
const result = scrubEvent(event)
|
|
|
|
expect(result).toBe(event)
|
|
expect(result?.request).toBeUndefined()
|
|
})
|
|
|
|
it('returns event unchanged when request.data is null', () => {
|
|
const event = makeEvent({ request: { data: null as unknown as undefined } })
|
|
const result = scrubEvent(event) as SentryErrorEvent
|
|
|
|
expect(result.request?.data).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('headers', () => {
|
|
it('scrubs Authorization header', () => {
|
|
const event = makeEvent({ request: { headers: { Authorization: 'Bearer abc' } } })
|
|
const result = scrubEvent(event) as SentryErrorEvent
|
|
|
|
expect((result.request?.headers as Record<string, string>).Authorization).toBe('[scrubbed]')
|
|
})
|
|
|
|
it('scrubs Cookie header', () => {
|
|
const event = makeEvent({ request: { headers: { Cookie: 'sess=abc' } } })
|
|
const result = scrubEvent(event) as SentryErrorEvent
|
|
|
|
expect((result.request?.headers as Record<string, string>).Cookie).toBe('[scrubbed]')
|
|
})
|
|
|
|
it('scrubs case-insensitive header names', () => {
|
|
const event = makeEvent({ request: { headers: { 'X-API-KEY': 'k', 'x-impersonation-token': 't' } } })
|
|
const result = scrubEvent(event) as SentryErrorEvent
|
|
const headers = result.request?.headers as Record<string, string>
|
|
|
|
expect(headers['X-API-KEY']).toBe('[scrubbed]')
|
|
expect(headers['x-impersonation-token']).toBe('[scrubbed]')
|
|
})
|
|
})
|
|
|
|
describe('query string', () => {
|
|
it('scrubs token query param', () => {
|
|
const event = makeEvent({ request: { query_string: 'token=abc&keep=me' } })
|
|
const result = scrubEvent(event) as SentryErrorEvent
|
|
const qs = result.request?.query_string as string
|
|
|
|
expect(qs).toContain('token=%5Bscrubbed%5D')
|
|
expect(qs).toContain('keep=me')
|
|
})
|
|
|
|
it('scrubs api_key query param', () => {
|
|
const event = makeEvent({ request: { query_string: 'api_key=xyz&page=2' } })
|
|
const result = scrubEvent(event) as SentryErrorEvent
|
|
const qs = result.request?.query_string as string
|
|
|
|
expect(qs).toContain('api_key=%5Bscrubbed%5D')
|
|
expect(qs).toContain('page=2')
|
|
})
|
|
|
|
it('preserves non-sensitive query params unchanged', () => {
|
|
const event = makeEvent({ request: { query_string: 'page=2&sort=name' } })
|
|
const result = scrubEvent(event) as SentryErrorEvent
|
|
const qs = result.request?.query_string as string
|
|
|
|
expect(qs).toBe('page=2&sort=name')
|
|
})
|
|
})
|
|
|
|
describe('cookies + storage', () => {
|
|
it('scrubs cookies wholesale', () => {
|
|
const event = makeEvent({ request: { cookies: { sess: 'abc', tracking: 'xyz' } } })
|
|
const result = scrubEvent(event) as SentryErrorEvent
|
|
|
|
expect(result.request?.cookies).toEqual({ scrubbed: '[scrubbed]' })
|
|
})
|
|
|
|
it('strips storage context if present (RFC §3.7 frontend point 2)', () => {
|
|
const event = makeEvent({
|
|
contexts: { storage: { local: { token: 'abc' } } } as unknown as SentryErrorEvent['contexts'],
|
|
})
|
|
|
|
const result = scrubEvent(event) as SentryErrorEvent
|
|
|
|
expect(result.contexts).not.toHaveProperty('storage')
|
|
})
|
|
|
|
it('strips user.cookies if present (RFC §3.7 frontend point 1)', () => {
|
|
const event = makeEvent({
|
|
user: { id: 'ulid', cookies: 'session=...' } as unknown as SentryErrorEvent['user'],
|
|
})
|
|
|
|
const result = scrubEvent(event) as SentryErrorEvent
|
|
|
|
expect(result.user).not.toHaveProperty('cookies')
|
|
expect(result.user?.id).toBe('ulid')
|
|
})
|
|
})
|
|
})
|