fix(db): close round-5 review evasions in writer-coverage assertion
ci/woodpecker/pr/ci Pipeline was successful

Unify member-access matching on shared DOT/BRACKET_OPEN/MEMBER_SEG shapes
(dot, bracket, ?. and ! forms) across both conduit computations, the write
targets, the factory-import clause, and the DI backstop. Treat export-default
as an expression at both ends: strip parens/assertions, classify bare-word vs
namespace-member chain, on the schema AND capability sides. Add the
capability-side export-default ns-member pass (round-5 finding 2). Admit bare
SQL qualifier words (TABLE, ONLY, IF EXISTS) between keyword and table name in
prong (ii). Controls E54-E68.
This commit is contained in:
fred
2026-08-27 17:50:44 -05:00
parent 3dcfb264c5
commit 743d884bfc
+210 -33
View File
@@ -87,7 +87,9 @@
* function returns, or method extraction. Demonstrated escapes in this
* class: `const u = this.client.unsafe; u.call(this.client, s)` (the
* verb never appears as a member call), an alias chain three or more
* steps deep, and a helper function that returns a schema symbol.
* steps deep, a helper function that returns a schema symbol, and an
* export whose expression COMPUTES the value (a ternary, a call result)
* rather than naming a binding or member.
* Closing it requires data-flow analysis (a type-checker-backed rewrite,
* tracked for M4-1b consideration); the counterfactual — flagging every
* bare identifier call — false-positives on essentially all callback
@@ -444,6 +446,33 @@ const IMPORT_RE =
const EXPORT_FROM_RE =
/export\s*(type\s+)?(?:\{([^}]*)\}|\*(?:\s*as\s+\w+)?)\s*from\s*['"]([^'"]+)['"]/g;
// Member-access shapes, tolerant of optional chaining and non-null
// assertions (ns?.companies, ns!.companies, ns?.['companies']) — a `?.` or
// `!` between receiver and member must not break a match.
const DOT = `\\s*(?:\\?\\.|!\\s*\\.|\\.)\\s*`;
const BRACKET_OPEN = `\\s*(?:\\?\\.|!)?\\s*\\[`;
const MEMBER_SEG = `(?:${DOT}\\w+|${BRACKET_OPEN}\\s*['"\`]\\w+['"\`]\\s*\\])`;
/** Terminal member access matching one of `alt`'s names, dot or bracket form. */
const memberTail = (alt: string): string =>
`(?:${DOT}(?:${alt})\\b|${BRACKET_OPEN}\\s*['"\`](?:${alt})['"\`]\\s*\\])`;
/**
* Reduce an exported expression to its core: strip parenthesization and
* trailing type assertions (`(companies)`, `companies as unknown as object`)
* so `export default <expr>` passes see the binding under the dressing.
*/
function stripExprDressing(raw: string): string {
let expr = raw.trim();
for (let prev = ''; prev !== expr; ) {
prev = expr;
expr = expr
.replace(/^\(\s*/, '')
.replace(/\s*\)$/, '')
.replace(/\s+as\s+[^()]+$/, '')
.trim();
}
return expr;
}
interface FileFacts {
rel: string;
code: string;
@@ -543,7 +572,7 @@ function computeSchemaConduits(files: FileFacts[], fileSet: Set<string>): Schema
for (const ns of aliases.namespaces) {
for (const m of f.code.matchAll(
new RegExp(
`export\\s+(?:const|let|var)\\s+(\\w+)\\s*=\\s*${ns}(?:\\s*\\.\\s*\\w+)*\\s*\\.\\s*(?:${CLASS_SYMBOLS.join('|')})\\b`,
`export\\s+(?:const|let|var)\\s+(\\w+)\\s*=\\s*${ns}${MEMBER_SEG}*${memberTail(CLASS_SYMBOLS.join('|'))}`,
'g',
),
)) {
@@ -552,24 +581,28 @@ function computeSchemaConduits(files: FileFacts[], fileSet: Set<string>): Schema
}
for (const local of aliases.named) {
for (const m of f.code.matchAll(
new RegExp(`export\\s+(?:const|let|var)\\s+(\\w+)\\s*=\\s*${local}\\b`, 'g'),
new RegExp(`export\\s+(?:const|let|var)\\s+(\\w+)\\s*=\\s*\\(*\\s*${local}\\b`, 'g'),
)) {
mine.named.add(m[1]!);
}
}
// The default slot is an export name like any other:
// `export default companies;` / `export default mod.companies;`
for (const m of f.code.matchAll(/export\s+default\s+(\w+)\s*;/g)) {
if (aliases.named.includes(m[1]!)) mine.named.add('default');
if (aliases.namespaces.includes(m[1]!)) mine.ns.add('default');
}
for (const ns of aliases.namespaces) {
if (
new RegExp(
`export\\s+default\\s+${ns}(?:\\s*\\.\\s*\\w+)*\\s*\\.\\s*(?:${CLASS_SYMBOLS.join('|')})\\b`,
).test(f.code)
) {
mine.named.add('default');
// The default slot is an export name like any other, whatever the
// expression dressing: bare local, parenthesized, type-asserted, or
// a namespace member chain in dot or bracket form, semicolon or not.
for (const m of f.code.matchAll(/export\s+default\s+([^;\n]+)/g)) {
const expr = stripExprDressing(m[1]!);
if (/^\w+$/.test(expr)) {
if (aliases.named.includes(expr)) mine.named.add('default');
if (aliases.namespaces.includes(expr)) mine.ns.add('default');
} else {
for (const ns of aliases.namespaces) {
if (
new RegExp(`^${ns}${MEMBER_SEG}*${memberTail(CLASS_SYMBOLS.join('|'))}$`).test(expr)
) {
mine.named.add('default');
break;
}
}
}
}
}
@@ -681,11 +714,11 @@ function classAliases(f: FileFacts, conduits: SchemaConduits, fileSet: Set<strin
}
for (const m of f.code.matchAll(
new RegExp(
`(?:const|let|var)\\s+(\\w+)\\s*=\\s*${ns}(?:\\s*\\.\\s*\\w+)*\\s*\\.\\s*(\\w+)\\b`,
`(?:const|let|var)\\s+(\\w+)\\s*=\\s*${ns}${MEMBER_SEG}*(?:${DOT}(\\w+)\\b|${BRACKET_OPEN}\\s*['"\`](\\w+)['"\`]\\s*\\])`,
'g',
),
)) {
if (memberSyms.has(m[2]!)) named.add(m[1]!);
if (memberSyms.has((m[2] ?? m[3])!)) named.add(m[1]!);
else namespaces.add(m[1]!);
}
}
@@ -797,9 +830,12 @@ function computeCapabilityConduits(
}
for (const [ns, src] of nss) {
for (const am of f.code.matchAll(
new RegExp(`(?:const|let|var)\\s+(\\w+)\\s*=\\s*${ns}\\s*\\.\\s*(\\w+)\\b`, 'g'),
new RegExp(
`(?:const|let|var)\\s+(\\w+)\\s*=\\s*${ns}(?:${DOT}(\\w+)\\b|${BRACKET_OPEN}\\s*['"\`](\\w+)['"\`]\\s*\\])`,
'g',
),
)) {
if (src.has(am[2]!)) locals.add(am[1]!);
if (src.has((am[2] ?? am[3])!)) locals.add(am[1]!);
}
}
if (locals.size > 0) {
@@ -815,8 +851,32 @@ function computeCapabilityConduits(
if (new RegExp(`export\\s+(?:const|let|var|function)\\s+${local}\\b`).test(f.code)) {
mine.add(local);
}
if (new RegExp(`export\\s+default\\s+${local}\\b`).test(f.code)) {
mine.add('default');
// A derived binding exported under a NEW name re-exports the
// capability under that name: `export const mk2 = (mk);`
for (const dm of f.code.matchAll(
new RegExp(`export\\s+(?:const|let|var)\\s+(\\w+)\\s*=\\s*\\(*\\s*${local}\\b`, 'g'),
)) {
mine.add(dm[1]!);
}
}
}
// The default slot, under any expression dressing — a local factory
// binding (`export default (createDb)`) or a namespace factory member
// (`export default mod.createDb`, `export default mod['createDb']`) —
// mirrors the schema side's default-export pass.
for (const m of f.code.matchAll(/export\s+default\s+([^;\n]+)/g)) {
const expr = stripExprDressing(m[1]!);
if (/^\w+$/.test(expr)) {
if (locals.has(expr)) mine.add('default');
} else {
for (const [ns, src] of nss) {
const t = new RegExp(
`^${ns}(?:${DOT}(\\w+)\\b|${BRACKET_OPEN}\\s*['"\`](\\w+)['"\`]\\s*\\])$`,
).exec(expr);
if (t && src.has((t[1] ?? t[2])!)) {
mine.add('default');
break;
}
}
}
}
@@ -837,12 +897,13 @@ function hasSymbolImportEdge(code: string, symbols: string[], packageName: strin
).test(code);
const usesSymbol = new RegExp(`\\b(${symbols.join('|')})\\b`).test(code);
const dynamicEdge = usesSymbol && new RegExp(`import\\s*\\(\\s*['"]${pkg}['"]\\s*\\)`).test(code);
// Namespace form: `import * as ns from '<pkg>'` + `ns.<symbol>` usage.
// Namespace form: `import * as ns from '<pkg>'` + `ns.<symbol>` usage
// (dot or bracket member, optional-chain/non-null tolerant).
let nsEdge = false;
for (const m of code.matchAll(
new RegExp(`import\\s*\\*\\s*as\\s+(\\w+)\\s*from\\s*['"]${pkg}['"]`, 'g'),
)) {
if (new RegExp(`\\b${m[1]}\\s*\\.\\s*(${symbols.join('|')})\\b`).test(code)) nsEdge = true;
if (new RegExp(`\\b${m[1]}${memberTail(symbols.join('|'))}`).test(code)) nsEdge = true;
}
return staticEdge || dynamicEdge || nsEdge;
}
@@ -964,9 +1025,9 @@ function analyzeFile(f: FileFacts, ctx: AnalysisCtx): Violation[] {
const symAlt = [...aliases.memberSyms].join('|');
for (const ns of aliases.namespaces) {
// Allow intermediate property segments (ns.schema.companies — nested
// namespace re-exports) and literal computed access (ns['companies']).
targets.push(`${ns}(?:\\s*\\.\\s*\\w+)*\\s*\\.\\s*(?:${symAlt})\\b`);
targets.push(`${ns}(?:\\s*\\.\\s*\\w+)*\\s*\\[\\s*['"\`](?:${symAlt})['"\`]\\s*\\]`);
// namespace re-exports), literal computed access (ns['companies']),
// and optional-chain/non-null markers (ns?.companies, ns!.companies).
targets.push(`${ns}${MEMBER_SEG}*${memberTail(symAlt)}`);
}
if (targets.length > 0) {
// The argument prefix tolerates parenthesization, spread, and array
@@ -1013,15 +1074,19 @@ function analyzeFile(f: FileFacts, ctx: AnalysisCtx): Violation[] {
// The SQL keyword must be ADJACENT to the table name — co-residence
// anywhere in one span over-matches prose ("pnpm workspaces" plus an
// unrelated "from" in an embedded doc string is not SQL). Adjacency
// tolerates schema qualification (public.hierarchy_grants), interposed
// block comments, and quoting (including escaped quotes in span text).
// tolerates schema qualification (public.hierarchy_grants), bare
// qualifier words (TABLE, ONLY, IF EXISTS), interposed block comments,
// and quoting (including escaped quotes in span text).
if (!inAllowlist && !isSchemaDefinition) {
const kw =
'(?:insert\\s+into|update|delete\\s+from|from|join|truncate(?:\\s+table)?|alter\\s+table|drop\\s+table|references|into|copy|on|lock(?:\\s+table)?)';
const gap = '(?:\\s|/\\*[\\s\\S]*?\\*/|--[^\\n]*\\n)+';
// Bare qualifier words may sit between keyword and table name:
// GRANT … ON TABLE t, DELETE FROM ONLY t, DROP TABLE IF EXISTS t.
const qual = `(?:(?:table|only|if\\s+exists)${gap})*`;
const q = `(?:\\\\?["'\`])?`;
const sqlAdjacentRe = new RegExp(
`\\b${kw}${gap}${q}(?:${q}\\w+${q}\\s*\\.\\s*)?${q}(${CLASS_TABLES.join('|')})\\b`,
`\\b${kw}${gap}${qual}${q}(?:${q}\\w+${q}\\s*\\.\\s*)?${q}(${CLASS_TABLES.join('|')})\\b`,
'i',
);
for (const span of f.spans) {
@@ -1066,7 +1131,7 @@ function analyzeFile(f: FileFacts, ctx: AnalysisCtx): Violation[] {
hasSymbolImportEdge(code, DB_FACTORY_SYMBOLS, '@mosaicstack/db') ||
(aliases.namespaces.length > 0 &&
new RegExp(
`\\b(${aliases.namespaces.join('|')})(?:\\s*\\.\\s*\\w+)*\\s*\\.\\s*(createDb|createPgliteDb)\\b`,
`\\b(?:${aliases.namespaces.join('|')})${MEMBER_SEG}*${memberTail('createDb|createPgliteDb')}`,
).test(code)) ||
// Destructuring a factory symbol OUT of a schema namespace confers
// capability whatever the local rename: const { createDb: mk } = dbns.
@@ -1102,9 +1167,13 @@ function analyzeFile(f: FileFacts, ctx: AnalysisCtx): Violation[] {
// DI residual backstop: db/client-shaped receivers fire regardless of
// detected capability (a handle can arrive by injection).
// The receiver may be a dotted name OR a literal bracketed member with
// a conventional name: this['db'].query(…).
// a conventional name this['db'].query(…) — and the final member
// access tolerates ?. and ! markers (this.db?.query(…)).
for (const m of code.matchAll(
/(?:\b(?:db|database|client|conn|connection|pool|pg|pglite|tx|trx)|\[\s*['"](?:db|database|client|conn|connection|pool|pg|pglite|tx|trx)['"]\s*\])\s*\.\s*(execute|query|unsafe)\s*\(/g,
new RegExp(
`(?:\\b(?:db|database|client|conn|connection|pool|pg|pglite|tx|trx)|\\[\\s*['"](?:db|database|client|conn|connection|pool|pg|pglite|tx|trx)['"]\\s*\\])${DOT}(execute|query|unsafe)\\s*\\(`,
'g',
),
)) {
violations.push({
file: rel,
@@ -1572,6 +1641,114 @@ describe('hierarchy writer coverage (contract 1 §6.3b)', () => {
name: 'E53 bracketed conventional receiver',
src: `export class R { async f(t: string) { await this['db'].query('TRUNCATE ' + t); } }`,
},
{
name: 'E54 parenthesized default export of schema symbol',
src: `import c from './evasion-mid18.js';\nimport { db } from './x.js';\nexport async function f() { await db.insert(c).values({}); }`,
extras: [
{
rel: 'packages/db/src/evasion-mid18.ts',
src: `import { companies } from '@mosaicstack/db';\nexport default (companies);`,
},
],
},
{
name: 'E55 bracket-member default export over dynamic namespace',
src: `import c from './evasion-mid19.js';\nimport { db } from './x.js';\nexport async function f() { await db.insert(c).values({}); }`,
extras: [
{
rel: 'packages/db/src/evasion-mid19.ts',
src: `const mod = await import('@mosaicstack/db');\nexport default mod['companies'];`,
},
],
},
{
name: 'E56 type-asserted default export of schema symbol',
src: `import c from './evasion-mid20.js';\nimport { db } from './x.js';\nexport async function f() { await db.insert(c).values({}); }`,
extras: [
{
rel: 'packages/db/src/evasion-mid20.ts',
src: `import { companies } from '@mosaicstack/db';\nexport default companies as unknown as object;`,
},
],
},
{
name: 'E57 export-const bracket member of schema namespace',
src: `import { co } from './evasion-mid21.js';\nimport { db } from './x.js';\nexport async function f() { await db.insert(co).values({}); }`,
extras: [
{
rel: 'packages/db/src/evasion-mid21.ts',
src: `import * as ns from '@mosaicstack/db';\nexport const co = ns['companies'];`,
},
],
},
{
name: 'E58 default export of dynamic namespace factory member',
src: `import mk from './evasion-mid22.js';\nexport async function f(t: string) { await mk('u').execute('DELETE FROM ' + t); }`,
extras: [
{
rel: 'packages/db/src/evasion-mid22.ts',
src: `const mod = await import('@mosaicstack/db');\nexport default mod.createDb;`,
},
],
},
{
name: 'E59 default export of static namespace factory member',
src: `import mk from './evasion-mid23.js';\nexport async function f(t: string) { await mk('u').execute('DELETE FROM ' + t); }`,
extras: [
{
rel: 'packages/db/src/evasion-mid23.ts',
src: `import * as ns from '@mosaicstack/db';\nexport default ns.createDb;`,
},
],
},
{
name: 'E60 export-const bracket factory member',
src: `import { mk } from './evasion-mid24.js';\nexport async function f(t: string) { await mk('u').execute('DELETE FROM ' + t); }`,
extras: [
{
rel: 'packages/db/src/evasion-mid24.ts',
src: `import * as ns from '@mosaicstack/db';\nexport const mk = ns['createDb'];`,
},
],
},
{
name: 'E61 parenthesized default export of factory local',
src: `import mk from './evasion-mid25.js';\nexport async function f(t: string) { await mk('u').execute('DELETE FROM ' + t); }`,
extras: [
{
rel: 'packages/db/src/evasion-mid25.ts',
src: `import { createDb } from '@mosaicstack/db';\nexport default (createDb);`,
},
],
},
{
name: 'E62 GRANT ON TABLE class table in SQL span',
src: `export const q = 'GRANT SELECT ON TABLE hierarchy_grants TO auditor';`,
},
{
name: 'E63 DROP TABLE IF EXISTS class table in SQL span',
src: `export const q = 'DROP TABLE IF EXISTS hierarchy_grants';`,
},
{
name: 'E64 DELETE FROM ONLY class table in SQL span',
src: `export const q = 'DELETE FROM ONLY hierarchy_grants WHERE id = $1';`,
},
{
name: 'E65 optional-chained namespace write target',
src: `import * as ns from '@mosaicstack/db';\nimport { db } from './x.js';\nexport async function f() { await db.insert(ns?.companies).values({}); }`,
},
{
name: 'E66 non-null-asserted namespace write target',
src: `import * as ns from '@mosaicstack/db';\nimport { db } from './x.js';\nexport async function f() { await db.insert(ns!.companies).values({}); }`,
},
{
name: 'E67 optional-chained DI receiver',
src: `export class R { constructor(private db?: { query(s: string): Promise<unknown> }) {}\n async f(t: string) { await this.db?.query('TRUNCATE ' + t); } }`,
},
{
name: 'E68 optional-chained bracketed DI receiver',
src: `export class R { async f(t: string) { await this['db']?.query('TRUNCATE ' + t); } }`,
},
];
const CLEAN_CONTROLS: Array<{ name: string; src: string }> = [
{