Spaces:
Running
Running
File size: 9,117 Bytes
aa89cb5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 | import { SelectQuery } from '../src/core/ast';
import { UserContext } from '../src/core/context';
import { executeRelationFacets } from '../src/core/facet';
import { PLAINTEXT_LOG_ACK, PLAINTEXT_LOG_ENV } from '../src/core/log-privacy';
import { AbstractSQLTeaQLClient, EntitySchema, SQLExecutionEvidenceStore,
SQLExecutionMetadata, TextDiagnosticSQLLogSink } from '../src/sql/core';
import { SQLiteDriver } from '../src/sql/sqlite';
const secret = 'FACET-PRIVATE-SCHOOL';
const futureSecret = 'FACET-PRIVATE-PLATFORM';
const column = (name: string, text = false) => ({ columnName: name,
logicalType: text ? 'text' as const : 'integer' as const,
decode: 'string' as const, logPolicy: 'plain' as const });
const schemas: Record<string, EntitySchema> = {
School: { table: 'like_school', auditMaskFields: ['name'], columns: {
id: column('id'), version: column('version'), platform: column('platform'),
name: column('name', true), publicName: column('public_name', true),
password: column('password', true), unknownName: { ...column('unknown_name', true), logPolicy: 'unknown' },
}, relations: { platform: { targetEntity: 'Platform', localKey: 'platform', foreignKey: 'id', many: false } } },
Platform: { table: 'like_platform', auditMaskFields: ['name'], columns: {
id: column('id'), version: column('version'), name: column('name', true),
} },
};
class Driver extends SQLiteDriver {
readonly reads: { sql: string; values: unknown[] }[] = [];
async query(sql: string, values: any[] = []) {
if (sql.startsWith('SELECT')) this.reads.push({ sql, values: [...values] });
return super.query(sql, values);
}
}
class Client extends AbstractSQLTeaQLClient {
constructor(readonly transport = new Driver(':memory:')) { super(transport, schemas); }
}
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;
});
async function fixture(logging: boolean, value = secret) {
const client = new Client().setDiagnosticSQLLogSink(undefined);
await new UserContext().insertResource('dataService', client).ensureSchema();
await client.executeMutation({ entity: 'Platform', action: 'Create', id: '1',
payload: { name: `${futureSecret}-tail` }, comment: 'seed LIKE privacy fixture' });
for (const [id, name] of [['1', `${value}-tail`], ['2', `head-${value}`]]) {
await client.executeMutation({ entity: 'School', action: 'Create', id,
payload: { name, publicName: name, password: name, unknownName: name, platform: '1' },
comment: 'seed LIKE privacy fixture' });
}
const evidence = new SQLExecutionEvidenceStore();
const diagnostics: SQLExecutionMetadata[] = [], text: string[] = [];
client.setRuntimeTelemetrySink(evidence).setQueryLoggingEnabled(logging)
.setDiagnosticSQLLogSink({ write(entry) {
diagnostics.push(entry); new TextDiagnosticSQLLogSink(line => text.push(line)).write(entry);
} });
client.transport.reads.length = 0;
return { client, evidence, diagnostics, text };
}
it.each(['$startsWith', '$notStartsWith', '$endsWith', '$notEndsWith'].flatMap(operator =>
[false, true].map(logging => [operator, logging] as const)))(
'masks original LIKE operand at real safe sinks: %s logging=%p', async (operator, logging) => {
const f = await fixture(logging);
const query = new SelectQuery('School').filter({ name: { [operator]: secret } }).limit(10)
.comment(`load ${secret}`).purpose(`inspect ${secret}`);
const before = JSON.stringify(query);
try {
const rows = await f.client.executeQuery<any>(query);
const expectFirst = operator === '$startsWith' || operator === '$notEndsWith';
expect(rows.map(row => row.id)).toEqual([expectFirst ? '1' : '2']);
expect(f.client.transport.reads).toHaveLength(1);
expect(f.client.transport.reads[0].values).toEqual([
operator.toLowerCase().includes('startswith') ? `${secret}%` : `%${secret}`, 10,
]);
expect(f.evidence.snapshot()).toHaveLength(1);
const entry = f.evidence.snapshot()[0];
expect(entry.comment).toBe('load [REDACTED]');
expect(entry.purpose).toBe('inspect [REDACTED]');
expect(entry.executionOutcome).toBe('success');
expect(entry.resultCount).toBe(1);
expect(entry.logMode).toBe('masked');
expect(entry.parameterLogPolicies).toEqual(['masked', 'plain']);
expect(entry.tracePath.map(node => node.name)).toEqual(['School', 'School', 'sqlite', 'select']);
expect(JSON.stringify([f.evidence.snapshot(), f.diagnostics, f.text])).not.toContain(secret);
expect(f.diagnostics).toHaveLength(logging ? 1 : 0);
expect(JSON.stringify(query)).toBe(before);
await f.client.executeQuery(new SelectQuery('Platform').limit(1)
.comment(`independent ${secret}`).purpose('no inherited binding'));
expect(f.evidence.snapshot()[1].comment).toBe(`independent ${secret}`);
} finally { await f.client.close(); }
});
it.each([false, true])('carries LIKE operand provenance through future Facets, logging=%p', async logging => {
const f = await fixture(logging);
try {
const child = new SelectQuery('Platform').filter({ name: { $startsWith: futureSecret } })
.limit(10).aggregate('Count', 'id', 'schoolCount');
const query = new SelectQuery('School').filter({ name: { $startsWith: secret } }).limit(10)
.facetBy('platforms', 'platform', { toQuery: () => child }, false)
.comment(`load ${secret} via ${futureSecret}`).purpose(`inspect ${secret} ${futureSecret}`);
const before = JSON.stringify(query);
expect((await f.client.executeQuery<any>(query)).map(row => row.id)).toEqual(['1']);
const facets = await executeRelationFacets(f.client, value => value, query, query.facets);
expect(facets.platforms.map(row => [row.id, row.schoolCount])).toEqual([['1', 1]]);
const entries = f.evidence.snapshot();
expect(entries).toHaveLength(3);
expect(entries.map(entry => entry.tracePath.filter(node => node.kind === 'relation').map(node => node.name)))
.toEqual([[], [], ['platform']]);
for (const entry of entries) {
expect(entry.comment).toBe('load [REDACTED] via [REDACTED]');
expect(entry.purpose).toBe('inspect [REDACTED] [REDACTED]');
}
expect(f.client.transport.reads[0].values).toEqual([`${secret}%`, 10]);
expect(f.client.transport.reads[2].values).toContain(`${futureSecret}%`);
expect(f.diagnostics).toHaveLength(logging ? 3 : 0);
expect(JSON.stringify([entries, f.diagnostics, f.text])).not.toMatch(/FACET-PRIVATE-(SCHOOL|PLATFORM)/);
expect(JSON.stringify(query)).toBe(before);
} finally { await f.client.close(); }
});
it.each(['$eq', '$startsWith'])('does not infer sensitive substrings by stripping literal wildcards: %s', async operator => {
const literal = '%LITERAL_WILDCARD%\\';
const f = await fixture(true, literal);
try {
const operand = operator === '$eq' ? `${literal}-tail` : literal;
const query = new SelectQuery('School').filter({ name: { [operator]: operand } }).limit(10)
.comment(`private ${operand}; public LITERAL_WILDCARD`).purpose('preserve exact operand');
// Preserve the existing SQL LIKE semantics: the caller's leading % is
// still a wildcard, so the prefix form matches both seeded rows.
expect(await f.client.executeQuery(query)).toHaveLength(operator === '$eq' ? 1 : 2);
expect(f.client.transport.reads[0].values).toEqual([operator === '$eq' ? operand : `${operand}%`, 10]);
expect(f.evidence.snapshot()[0].comment).toBe('private [REDACTED]; public LITERAL_WILDCARD');
expect(query.commentText).toBe(`private ${operand}; public LITERAL_WILDCARD`);
} finally { await f.client.close(); }
});
it.each(['name', 'publicName', 'password', 'unknownName'].flatMap(field =>
[false, true].map(debug => [field, debug] as const)))(
'retains original field policy for LIKE operand: %s debug=%p', async (field, debug) => {
const f = await fixture(true);
if (debug) process.env[PLAINTEXT_LOG_ENV] = PLAINTEXT_LOG_ACK;
try {
const query = new SelectQuery('School').filter({ [field]: { $startsWith: secret } }).limit(10)
.comment(`load ${secret}`).purpose('verify source policy');
expect(await f.client.executeQuery(query)).toHaveLength(1);
const visible = field === 'publicName' || (field === 'name' && debug);
expect(f.evidence.snapshot()[0].comment).toBe(`load ${visible ? secret : '[REDACTED]'}`);
expect(f.client.transport.reads[0].values).toEqual([`${secret}%`, 10]);
if (!visible) expect(JSON.stringify([f.evidence.snapshot(), f.diagnostics, f.text])).not.toContain(secret);
if (debug && field === 'name') {
delete process.env[PLAINTEXT_LOG_ENV];
const safeText: string[] = [];
new TextDiagnosticSQLLogSink(line => safeText.push(line)).write(f.diagnostics[0]);
expect(safeText.join('\n')).not.toContain(secret);
}
} finally { await f.client.close(); }
});
|