Spaces:
Running
Running
Download source/teaql-ts/tests/sql-masking-policy.test.ts from teaql/browser-sqlite-playground: direct link, hf CLI and curl.
- Browser
- Download file 13.9 kB
-
https://huggingface.co/spaces/teaql/browser-sqlite-playground/resolve/main/source/teaql-ts/tests/sql-masking-policy.test.ts
- Command line
-
hf download hf://spaces/teaql/browser-sqlite-playground/source/teaql-ts/tests/sql-masking-policy.test.ts
-
curl -L -o sql-masking-policy.test.ts https://huggingface.co/spaces/teaql/browser-sqlite-playground/resolve/main/source/teaql-ts/tests/sql-masking-policy.test.ts
13.9 kB
| import { UserContext } from '../src/core/context'; | |
| import { SelectQuery } from '../src/core/ast'; | |
| import { PLAINTEXT_LOG_ENV, PLAINTEXT_LOG_ACK, projectSQLLog } from '../src/core/log-privacy'; | |
| import { EntitySchema, SQLExecutionMetadata, TextDiagnosticSQLLogSink } from '../src/sql/core'; | |
| import { SQLiteTeaQLClient } from '../src/sql/sqlite'; | |
| const previous = process.env[PLAINTEXT_LOG_ENV]; | |
| beforeEach(() => { delete process.env[PLAINTEXT_LOG_ENV]; }); | |
| afterEach(() => { | |
| if (previous === undefined) delete process.env[PLAINTEXT_LOG_ENV]; | |
| else process.env[PLAINTEXT_LOG_ENV] = previous; | |
| }); | |
| function metadata(sql: string, parameters: unknown[]): SQLExecutionMetadata { | |
| return { operation: 'select', tracePath: [], parameterizedSQL: sql, parameters, | |
| debugSQL: '', elapsedMicros: 1, resultSummary: 'one row', | |
| comment: 'what: load a bounded customer', purpose: 'why: verify safe diagnostics' }; | |
| } | |
| it('renders mixed field policies through the existing dialect renderer', () => { | |
| const raw = { ...metadata('SELECT $1, $2, $1', ["O'Reilly", true]), | |
| databaseKind: 'postgresql' as const, parameterLogPolicies: ['masked', 'plain'] as const }; | |
| const safe = projectSQLLog(raw); | |
| expect(safe.debugSQL).toContain("'O''****ly' /* masked */, TRUE, 'O''****ly' /* masked */"); | |
| expect(safe.maskedParameters).toEqual([true, false]); | |
| expect(safe.debugSQL).not.toContain("O''Reilly"); | |
| expect(raw.parameters).toEqual(["O'Reilly", true]); | |
| expect(projectSQLLog(safe)).toBe(safe); | |
| }); | |
| it.each([1, 'customer'])('preserves the compiled SQL structure through a copied projection: %s', value => { | |
| const sql = 'SELECT id FROM customer WHERE name = ? LIMIT 10000'; | |
| const safe = projectSQLLog({...metadata(sql,[value]),sqlOrigin:'generated'}); | |
| expect(safe.parameterizedSQL).toBe(sql); | |
| expect(projectSQLLog({...safe}).debugSQL).toContain('FROM customer'); | |
| expect(projectSQLLog({...safe}).debugSQL).toContain('LIMIT 10000'); | |
| }); | |
| it.each([false,true])('projects inherited unknown/nested credentials without retaining their source (debug=%s)', debug => { | |
| if(debug) process.env[PLAINTEXT_LOG_ENV] = PLAINTEXT_LOG_ACK; | |
| const source = {parameterizedSQL:'UPDATE customer SET a=?, b=?, c=?, d=?',sqlOrigin:'generated' as const, | |
| parameters:['Riverside','UNKNOWN-CANARY',{apiKey:'NESTED-CANARY'},12345], | |
| parameterLogPolicies:['masked','unknown','plain','masked'] as const}; | |
| const raw = {...metadata('SELECT id FROM customer WHERE id=?',['1']),sqlOrigin:'generated' as const, | |
| parameterLogPolicies:['plain'] as const,auditReason:'what: inspect Riverside UNKNOWN-CANARY NESTED-CANARY 12345'}; | |
| const projected = projectSQLLog(raw,source); | |
| expect(projected.auditReason?.includes('Riverside')).toBe(debug); | |
| expect(projected.auditReason?.includes('12345')).toBe(debug); | |
| expect(JSON.stringify(projected)).not.toMatch(/UNKNOWN-CANARY|NESTED-CANARY/); | |
| expect(Object.getOwnPropertySymbols(projected)).toHaveLength(0); | |
| expect(Object.keys(projected)).not.toContain('inherited'); | |
| expect(source.parameters[0]).toBe('Riverside'); | |
| if(!debug) { | |
| process.env[PLAINTEXT_LOG_ENV] = PLAINTEXT_LOG_ACK; | |
| expect(JSON.stringify(projectSQLLog({...projected}))).not.toMatch(/Riverside|UNKNOWN-CANARY|NESTED-CANARY/); | |
| expect(projectSQLLog({...projected}).logMode).toBe('masked'); | |
| } | |
| }); | |
| it('preserves typed plain literals while masking derived and collection values', () => { | |
| const raw = { ...metadata('SELECT ?, ?, ?, ?', [ | |
| { type: 'Date', value: '2024-02-29' }, { type: 'Timestamp', value: -123 }, | |
| ['Riverside', '12345678'], null]), databaseKind: 'postgresql' as const, | |
| parameterLogPolicies: ['plain', 'plain', 'masked', 'masked'] as const }; | |
| const safe = projectSQLLog(raw); | |
| expect(safe.debugSQL).toContain("DATE '2024-02-29'"); | |
| expect(safe.debugSQL).toContain("TIMESTAMPTZ '1969-12-31T23:59:59.877Z'"); | |
| expect(safe.debugSQL).toContain('Ri*****de'); | |
| expect(safe.debugSQL).not.toContain('Riverside'); | |
| expect(safe.debugSQL).toContain('NULL /* masked */'); | |
| }); | |
| it('uses trusted compiler identifiers and ignores fake placeholders in literals/comments', () => { | |
| const raw = { ...metadata("SELECT `field?`, 'constant?', ? /* static ? */", ['Riverside']), | |
| sqlOrigin: 'generated' as const, parameterLogPolicies: ['masked'] as const }; | |
| expect(projectSQLLog(raw).debugSQL).toContain("`field?`, 'constant?', 'Ri*****de' /* masked */"); | |
| }); | |
| it.each(['SELECT $0', 'SELECT $2', 'SELECT ?, ?', 'SELECT @p0', 'SELECT 1'])( | |
| 'fails closed for bind mismatch: %s', sql => { | |
| const safe = projectSQLLog(metadata(sql, ['CANARY-MISSING-BIND'])); | |
| expect(safe.debugSQL).toContain('NOT REPLAYABLE'); | |
| expect(safe.debugSQL).not.toContain('CANARY-MISSING-BIND'); | |
| expect(safe.debugSQL).not.toContain('SELECT'); | |
| }); | |
| it('cannot expose credentials with a forged plain policy during debug', () => { | |
| process.env[PLAINTEXT_LOG_ENV] = PLAINTEXT_LOG_ACK; | |
| const safe = projectSQLLog({ ...metadata('UPDATE customer SET password = ?', ['CREDENTIAL-CANARY']), | |
| parameterLogPolicies: ['plain'] }); | |
| expect(JSON.stringify(safe)).not.toContain('CREDENTIAL-CANARY'); | |
| expect(safe.debugSQL).toContain('masked'); | |
| expect(safe.maskedParameters).toEqual([true]); | |
| }); | |
| it.each([false, true])('preserves generated field policies beside credential columns (debug=%s)', debug => { | |
| if (debug) process.env[PLAINTEXT_LOG_ENV] = PLAINTEXT_LOG_ACK; | |
| const raw: SQLExecutionMetadata = { | |
| ...metadata('SELECT password_hash FROM customer WHERE display_name = ? AND public_address = ? AND password_hash = ?', | |
| ['Riverside', '1 Runtime Road', 'PASSWORD-CANARY']), | |
| sqlOrigin: 'generated', parameterLogPolicies: ['masked', 'plain', 'credential'], | |
| }; | |
| const safe = projectSQLLog(raw); | |
| expect(safe.parameterLogPolicies).toEqual(raw.parameterLogPolicies); | |
| expect(safe.debugSQL).toContain(debug ? 'Riverside' : 'Ri*****de'); | |
| expect(safe.debugSQL).toContain('1 Runtime Road'); | |
| expect(JSON.stringify(safe)).not.toContain('PASSWORD-CANARY'); | |
| expect(projectSQLLog(safe)).toBe(safe); | |
| expect(raw.parameters[2]).toBe('PASSWORD-CANARY'); | |
| }); | |
| it.each([undefined, ['unknown'], ['unsupported-policy']])( | |
| 'keeps unclassified bindings private even with explicit debug opt-in: %s', supplied => { | |
| process.env[PLAINTEXT_LOG_ENV] = PLAINTEXT_LOG_ACK; | |
| const raw = { ...metadata('SELECT ?', ['UNKNOWN-BINDING-CANARY']), | |
| parameterLogPolicies: supplied as SQLExecutionMetadata['parameterLogPolicies'], | |
| comment: 'what: locate UNKNOWN-BINDING-CANARY' }; | |
| const safe = projectSQLLog(raw); | |
| expect(JSON.stringify(safe)).not.toContain('UNKNOWN-BINDING-CANARY'); | |
| expect(safe.maskedParameters).toEqual([true]); | |
| expect(safe.debugSQL).toContain('SELECT'); | |
| expect(safe.debugSQL).toContain('NOT REPLAYABLE'); | |
| expect(raw.parameters).toEqual(['UNKNOWN-BINDING-CANARY']); | |
| }); | |
| it('only explicit business policies permit plaintext debug, never unknown or credential values', () => { | |
| process.env[PLAINTEXT_LOG_ENV] = PLAINTEXT_LOG_ACK; | |
| const safe = projectSQLLog({ ...metadata('SELECT ?, ?, ?, ?', | |
| ['Ordinary', 'Riverside', 'UNKNOWN-BINDING-CANARY', 'CREDENTIAL-CANARY']), | |
| parameterLogPolicies: ['plain', 'masked', 'unknown', 'credential'] }); | |
| expect(safe.parameters).toEqual(['Ordinary', 'Riverside', '[REDACTED]', '[REDACTED]']); | |
| expect(safe.maskedParameters).toEqual([false, false, true, true]); | |
| }); | |
| it('omits untrusted inline credentials even during debug and explains the omission', () => { | |
| process.env[PLAINTEXT_LOG_ENV] = PLAINTEXT_LOG_ACK; | |
| const raw = metadata("UPDATE customer SET password = 'INLINE-CREDENTIAL-CANARY'", []); | |
| const output: string[] = []; | |
| new TextDiagnosticSQLLogSink(line => output.push(line)).write(raw); | |
| expect(output.join('\n')).not.toContain('INLINE-CREDENTIAL-CANARY'); | |
| expect(output.join('\n')).toContain('SQL omitted: untrusted-literal-sql'); | |
| expect(JSON.stringify(projectSQLLog(raw))).not.toContain('INLINE-CREDENTIAL-CANARY'); | |
| }); | |
| it('preserves operational counts even when their digits match a masked binding', () => { | |
| const safe = projectSQLLog({ ...metadata('SELECT ?', [1]), resultCount: 1 }); | |
| expect(safe.resultSummary).toBe('1 rows returned'); | |
| expect(safe.parameters).toEqual(['[REDACTED]']); | |
| }); | |
| it('explains invalid policy/bind counts without leaking values or driver errors', () => { | |
| const raw = { ...metadata('SELECT ?', ['COUNT-CANARY']), parameterLogPolicies: [] }; | |
| expect(projectSQLLog(raw).omissionReason).toBe('policy-count-mismatch'); | |
| expect(projectSQLLog(metadata('SELECT ?, ?', ['COUNT-CANARY'])).omissionReason) | |
| .toBe('unsupported-or-mismatched-bindings'); | |
| expect(JSON.stringify(projectSQLLog(raw))).not.toContain('COUNT-CANARY'); | |
| }); | |
| it('reprojects when debug is disabled and never recovers plaintext from a safe projection', () => { | |
| process.env[PLAINTEXT_LOG_ENV] = PLAINTEXT_LOG_ACK; | |
| const raw = { ...metadata('SELECT ?', ['Riverside']), parameterLogPolicies: ['masked'] as const }; | |
| const debug = projectSQLLog(raw); | |
| expect(debug.debugSQL).toContain('Riverside'); | |
| delete process.env[PLAINTEXT_LOG_ENV]; | |
| const safe = projectSQLLog(debug); | |
| expect(JSON.stringify(safe)).not.toContain('Riverside'); | |
| process.env[PLAINTEXT_LOG_ENV] = PLAINTEXT_LOG_ACK; | |
| expect(projectSQLLog(safe)).toBe(safe); | |
| expect(raw.parameters).toEqual(['Riverside']); | |
| }); | |
| it('compiles trusted field policies on real SQLite mutations and predicates before logging', async () => { | |
| const schemas: Record<string, EntitySchema> = { Customer: { | |
| table: 'customer_data', auditMaskFields: ['display_name'], columns: { | |
| id: { columnName: 'id', logicalType: 'integer', decode: 'string' }, | |
| version: { columnName: 'version', logicalType: 'integer', decode: 'number' }, | |
| displayName: { columnName: 'display_name', modelName: 'display_name', logicalType: 'text', decode: 'native' }, | |
| active: { columnName: 'active', logicalType: 'boolean', decode: 'native', logPolicy: 'plain' }, | |
| passwordHash: { columnName: 'password_hash', logicalType: 'text', decode: 'native', logPolicy: 'plain' }, | |
| }, | |
| } }; | |
| const captured: SQLExecutionMetadata[] = []; | |
| const output: string[] = []; | |
| const text = new TextDiagnosticSQLLogSink(line => output.push(line)); | |
| const client = new SQLiteTeaQLClient(':memory:', schemas).setDiagnosticSQLLogSink({ | |
| write: entry => { captured.push(entry); text.write(entry); }, | |
| }); | |
| try { | |
| await new UserContext().insertResource('dataService', client).ensureSchema(); | |
| const created = await client.executeMutation({ entity: 'Customer', action: 'Create', id: '1', | |
| payload: { display_name: 'Riverside', active: true, passwordHash: 'PASSWORD-CANARY' }, comment: 'seed a customer' }); | |
| const query = new SelectQuery('Customer').filter({ displayName: { $in: ['Riverside', 'Other'] }, active: { $eq: true } }) | |
| .limit(1).comment('what: find the customer').purpose('why: verify field policy propagation'); | |
| const rows = await client.executeQuery(query); | |
| expect(rows[0].displayName).toBe('Riverside'); | |
| expect(captured[0].parameterLogPolicies).toContain('masked'); | |
| expect(output.join('\n')).toContain('Ri*****de'); | |
| expect(output.join('\n')).toContain('LIMIT 1'); | |
| expect(output.join('\n')).not.toContain('Riverside'); | |
| expect(output.join('\n')).not.toContain('Parameterized SQL:'); | |
| expect(output.join('\n')).not.toContain('REDACTED SQL'); | |
| const selects = captured.filter(entry => entry.operation === 'select'); | |
| expect(selects).toHaveLength(2); | |
| expect(selects[0].purpose).toBe('verify persisted mutation result'); | |
| const sql = selects[1]; | |
| expect(sql.maskedParameters).toEqual([true, true, false, false]); | |
| await client.executeMutation({ entity: 'Customer', action: 'Update', id: '1', version: created.version, | |
| payload: { display_name: "O'Reilly" }, comment: 'rename customer' }); | |
| expect(output.join('\n')).toContain("O''****ly"); | |
| expect(output.join('\n')).not.toContain("O''Reilly"); | |
| } finally { await client.close(); } | |
| }); | |
| it.each([false, true])('distinguishes old missing mask metadata from explicit empty (declared=%s)', async declared => { | |
| const customer: EntitySchema = { | |
| table: 'customer_data', | |
| columns: { | |
| id: { columnName: 'id', logicalType: 'integer', decode: 'string', logPolicy: 'plain' }, | |
| version: { columnName: 'version', logicalType: 'integer', decode: 'number', logPolicy: 'plain' }, | |
| displayName: { columnName: 'display_name', logicalType: 'text', decode: 'native', logPolicy: 'plain' }, | |
| }, | |
| }; | |
| if (declared) customer.auditMaskFields = []; | |
| const captured: SQLExecutionMetadata[] = []; | |
| const output: string[] = []; | |
| const text = new TextDiagnosticSQLLogSink(line => output.push(line)); | |
| const client = new SQLiteTeaQLClient(':memory:', { Customer: customer }).setDiagnosticSQLLogSink({ | |
| write: entry => { captured.push(entry); text.write(entry); }, | |
| }); | |
| try { | |
| await new UserContext().insertResource('dataService', client).ensureSchema(); | |
| await client.executeMutation({ entity: 'Customer', action: 'Create', id: '1', | |
| payload: { displayName: 'Riverside' }, comment: 'seed old descriptor fixture' }); | |
| const rows = await client.executeQuery(new SelectQuery('Customer') | |
| .filter({ displayName: { $eq: 'Riverside' } }).limit(1) | |
| .comment('what: find old descriptor').purpose('why: verify fail-closed upgrade')); | |
| expect(rows[0].displayName).toBe('Riverside'); | |
| const logged = captured.find(entry => entry.operation === 'select' && | |
| entry.comment === 'what: find old descriptor'); | |
| expect(logged?.parameterLogPolicies).toContain(declared ? 'plain' : 'unknown'); | |
| if (declared) expect(output.join('\n')).toContain('Riverside'); | |
| else { | |
| expect(output.join('\n')).toContain('[REDACTED]'); | |
| expect(output.join('\n')).not.toContain('Riverside'); | |
| } | |
| } finally { await client.close(); } | |
| }); | |