fix(db): close round-7 review evasions in writer-coverage assertion
ci/woodpecker/pr/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful
The text-only template-key rule fires on any computed member — access or call — not only the call form, so template-keyed write targets, export expressions, and receivers fail closed at their origin. Declaration exports and derivations tolerate a type annotation and prior declarators via shared TYPE_ANN/DECL_LIST shapes at all five declarator sites. Invoking a write/exec verb through .apply/.call/.bind fails closed anywhere (both member names statically visible, unlike the method-extraction residual). The evasion test counts violations across the whole synthetic chain, since some routes fail closed at the helper. Controls E82-E92; clean control pinning the interpolated-key discriminator.
This commit is contained in:
@@ -15,7 +15,10 @@
|
||||
* (`export { companies as c } from`), an export of a locally bound
|
||||
* alias (`import { companies }; export { companies as co }`), and a
|
||||
* binding derived from a namespace (`export const co = ns.companies`)
|
||||
* all propagate symbol identity to the consumer. The DEFAULT-export
|
||||
* all propagate symbol identity to the consumer. Declaration exports
|
||||
* tolerate a type annotation and prior declarators
|
||||
* (`export const co: typeof companies = companies`,
|
||||
* `export const dummy = 0, co = companies`). The DEFAULT-export
|
||||
* slot is an export name like any other (`export { companies as
|
||||
* default } from`, `export default companies`), so a bare default
|
||||
* import of a conduit resolves against the map too; namespace-member
|
||||
@@ -79,12 +82,15 @@
|
||||
* residual is accepted and reviews of DI provider modules carry it.
|
||||
* - Computed member access with a NON-literal name (obj[verb]()) is not
|
||||
* statically resolvable; literal computed access (obj['insert']()) is
|
||||
* flagged, and a computed member CALL whose key is a text-only template
|
||||
* literal (obj[`insert`]()) fails closed anywhere — template text never
|
||||
* reaches the lexer's code output, so such a call is indistinguishable
|
||||
* from a runtime-constructed verb (a template key WITH interpolation is
|
||||
* a non-literal computed member, above). Constructing the verb at
|
||||
* runtime is adjacent to eval and is expected to be caught in review.
|
||||
* flagged, and a computed member — access or call, in any position —
|
||||
* whose key is a text-only template literal (obj[`insert`](),
|
||||
* ns[`companies`]) fails closed anywhere — template text never reaches
|
||||
* the lexer's code output, so such a member is indistinguishable from a
|
||||
* runtime-constructed one (a template key WITH interpolation is a
|
||||
* non-literal computed member, above). Invoking a write/exec verb via
|
||||
* `.apply`/`.call`/`.bind` likewise fails closed anywhere. Constructing
|
||||
* the member at runtime is adjacent to eval and is expected to be caught
|
||||
* in review.
|
||||
* - The DB_FACTORY_IMPORTERS enumeration counts the import edges
|
||||
* hasSymbolImportEdge can see (named import, literal dynamic package
|
||||
* import with symbol use, namespace member use). Destructuring a factory
|
||||
@@ -492,6 +498,19 @@ function stripExprDressing(raw: string): string {
|
||||
/** Call-open shape tolerant of the optional-call form: `f(…)` or `f?.(…)`. */
|
||||
const CALL_OPEN = `\\s*(?:\\?\\.)?\\s*\\(`;
|
||||
|
||||
/**
|
||||
* Declaration-head shapes: a declarator's initializer may sit behind a type
|
||||
* annotation (`export const co: typeof companies = companies`) or behind
|
||||
* prior declarators (`export const dummy = 0, co = companies`). TYPE_ANN
|
||||
* admits `=>` inside the type text but stops at a bare `=` (the
|
||||
* initializer); DECL_LIST skips prior declarators whose initializers are
|
||||
* comma-free. Both are approximations of the declarator grammar — exotic
|
||||
* prior initializers (an array or call containing a comma) fall to the
|
||||
* value-flow residual.
|
||||
*/
|
||||
const TYPE_ANN = `(?:\\s*:\\s*(?:[^=;\\n]|=>)*?)?`;
|
||||
const DECL_LIST = `(?:[\\w$]+${TYPE_ANN}\\s*=\\s*[^,;\\n]*,\\s*)*`;
|
||||
|
||||
interface FileFacts {
|
||||
rel: string;
|
||||
code: string;
|
||||
@@ -591,7 +610,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*\\(*\\s*${ns}${MEMBER_SEG}*${memberTail(CLASS_SYMBOLS.join('|'))}`,
|
||||
`export\\s+(?:const|let|var)\\s+${DECL_LIST}(\\w+)${TYPE_ANN}\\s*=\\s*\\(*\\s*${ns}${MEMBER_SEG}*${memberTail(CLASS_SYMBOLS.join('|'))}`,
|
||||
'g',
|
||||
),
|
||||
)) {
|
||||
@@ -600,7 +619,10 @@ 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*\\(*\\s*${local}\\b`, 'g'),
|
||||
new RegExp(
|
||||
`export\\s+(?:const|let|var)\\s+${DECL_LIST}(\\w+)${TYPE_ANN}\\s*=\\s*\\(*\\s*${local}\\b`,
|
||||
'g',
|
||||
),
|
||||
)) {
|
||||
mine.named.add(m[1]!);
|
||||
}
|
||||
@@ -734,7 +756,7 @@ 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*\\(*\\s*${ns}${MEMBER_SEG}*(?:${DOT}(\\w+)\\b|${BRACKET_OPEN}\\s*['"\`](\\w+)['"\`]\\s*\\])`,
|
||||
`(?:const|let|var)\\s+${DECL_LIST}(\\w+)${TYPE_ANN}\\s*=\\s*\\(*\\s*${ns}${MEMBER_SEG}*(?:${DOT}(\\w+)\\b|${BRACKET_OPEN}\\s*['"\`](\\w+)['"\`]\\s*\\])`,
|
||||
'g',
|
||||
),
|
||||
)) {
|
||||
@@ -851,7 +873,7 @@ function computeCapabilityConduits(
|
||||
for (const [ns, src] of nss) {
|
||||
for (const am of f.code.matchAll(
|
||||
new RegExp(
|
||||
`(?:const|let|var)\\s+(\\w+)\\s*=\\s*\\(*\\s*${ns}(?:${DOT}(\\w+)\\b|${BRACKET_OPEN}\\s*['"\`](\\w+)['"\`]\\s*\\])`,
|
||||
`(?:const|let|var)\\s+${DECL_LIST}(\\w+)${TYPE_ANN}\\s*=\\s*\\(*\\s*${ns}(?:${DOT}(\\w+)\\b|${BRACKET_OPEN}\\s*['"\`](\\w+)['"\`]\\s*\\])`,
|
||||
'g',
|
||||
),
|
||||
)) {
|
||||
@@ -868,13 +890,20 @@ function computeCapabilityConduits(
|
||||
}
|
||||
}
|
||||
for (const local of locals) {
|
||||
if (new RegExp(`export\\s+(?:const|let|var|function)\\s+${local}\\b`).test(f.code)) {
|
||||
if (
|
||||
new RegExp(`export\\s+(?:const|let|var|function)\\s+${DECL_LIST}${local}\\b`).test(
|
||||
f.code,
|
||||
)
|
||||
) {
|
||||
mine.add(local);
|
||||
}
|
||||
// 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'),
|
||||
new RegExp(
|
||||
`export\\s+(?:const|let|var)\\s+${DECL_LIST}(\\w+)${TYPE_ANN}\\s*=\\s*\\(*\\s*${local}\\b`,
|
||||
'g',
|
||||
),
|
||||
)) {
|
||||
mine.add(dm[1]!);
|
||||
}
|
||||
@@ -958,18 +987,41 @@ function analyzeFile(f: FileFacts, ctx: AnalysisCtx): Violation[] {
|
||||
) {
|
||||
violations.push({ file: rel, prong: 'code-construction', detail: 'eval/new Function' });
|
||||
}
|
||||
// A computed member CALL whose key is a text-only template literal is
|
||||
// invisible to every verb matcher (the lexer routes template text to
|
||||
// spans, so the key survives in code as two ADJACENT backticks — the
|
||||
// discriminator against backticks inside quoted-string prose, whose text
|
||||
// stays in code). Runtime code construction's sibling: fail-closed
|
||||
// anywhere. A template key WITH interpolation is a non-literal computed
|
||||
// member (documented residual above).
|
||||
if (new RegExp(`\\[\\s*\`\`\\s*\\]${CALL_OPEN}`).test(code)) {
|
||||
// A computed member — access or call — whose key is a text-only template
|
||||
// literal is invisible to every member and verb matcher (the lexer routes
|
||||
// template text to spans, so the key survives in code as two ADJACENT
|
||||
// backticks — the discriminator against backticks inside quoted-string
|
||||
// prose, whose text stays in code, and against interpolated keys, whose
|
||||
// `${…}` expression stays in code between the backticks). Runtime code
|
||||
// construction's sibling: fail-closed anywhere, in any position (write
|
||||
// target, export expression, receiver, call). A template key WITH
|
||||
// interpolation is a non-literal computed member (documented residual
|
||||
// above).
|
||||
if (new RegExp(`\\[\\s*\`\`\\s*\\]`).test(code)) {
|
||||
violations.push({
|
||||
file: rel,
|
||||
prong: 'code-construction',
|
||||
detail: 'template-literal computed member call',
|
||||
detail: 'template-literal computed member',
|
||||
});
|
||||
}
|
||||
// Invoking a write/exec verb through Function.prototype indirection
|
||||
// (`db.insert.apply(db, [companies])`, `d.execute.call(d, s)`,
|
||||
// `db.insert.bind(db)`) hides the argument shape from every verb matcher
|
||||
// while both member names stay statically visible — unlike the
|
||||
// method-EXTRACTION residual, where the verb never appears as a member.
|
||||
// Fail-closed anywhere: no legitimate site invokes a builder verb this way
|
||||
// (calibrated clean over the production tree).
|
||||
if (
|
||||
new RegExp(
|
||||
`(?:${DOT}(?:insert|update|delete|execute|query|unsafe|raw)\\b|` +
|
||||
`\\[\\s*['"](?:insert|update|delete|execute|query|unsafe|raw)['"]\\s*\\])` +
|
||||
`${DOT}(?:apply|call|bind)${CALL_OPEN}`,
|
||||
).test(code)
|
||||
) {
|
||||
violations.push({
|
||||
file: rel,
|
||||
prong: 'code-construction',
|
||||
detail: 'verb apply/call/bind indirection',
|
||||
});
|
||||
}
|
||||
// Dynamic import whose specifier is not a single string literal: the
|
||||
@@ -1900,6 +1952,86 @@ describe('hierarchy writer coverage (contract 1 §6.3b)', () => {
|
||||
name: 'E81 template-literal computed member call',
|
||||
src: `import { companies } from '@mosaicstack/db';\nimport { db } from './x.js';\nexport async function f() { await db[\`insert\`](companies).values({}); }`,
|
||||
},
|
||||
{
|
||||
name: 'E82 template-literal computed member as 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: 'E83 template-literal computed member in schema default export',
|
||||
src: `import c from './evasion-mid35.js';\nimport { db } from './x.js';\nexport async function f() { await db.insert(c).values({}); }`,
|
||||
extras: [
|
||||
{
|
||||
rel: 'packages/db/src/evasion-mid35.ts',
|
||||
src: `import * as ns from '@mosaicstack/db';\nexport default ns[\`companies\`];`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'E84 template-literal computed member in factory extraction',
|
||||
src: `import { mk } from './evasion-mid36.js';\nexport async function f(t: string) { await mk('u').execute('DELETE FROM ' + t); }`,
|
||||
extras: [
|
||||
{
|
||||
rel: 'packages/db/src/evasion-mid36.ts',
|
||||
src: `import * as ns from '@mosaicstack/db';\nexport const mk = ns[\`createDb\`];`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'E85 type-annotated re-export of schema binding',
|
||||
src: `import { co } from './evasion-mid37.js';\nimport { db } from './x.js';\nexport async function f() { await db.insert(co).values({}); }`,
|
||||
extras: [
|
||||
{
|
||||
rel: 'packages/db/src/evasion-mid37.ts',
|
||||
src: `import { companies } from '@mosaicstack/db';\nexport const co: typeof companies = companies;`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'E86 second-declarator re-export of schema binding',
|
||||
src: `import { co } from './evasion-mid38.js';\nimport { db } from './x.js';\nexport async function f() { await db.insert(co).values({}); }`,
|
||||
extras: [
|
||||
{
|
||||
rel: 'packages/db/src/evasion-mid38.ts',
|
||||
src: `import { companies } from '@mosaicstack/db';\nexport const dummy = 0,\n co = companies;`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'E87 type-annotated re-export of factory binding',
|
||||
src: `import { mk } from './evasion-mid39.js';\nexport async function f(t: string) { await mk('u').execute('DELETE FROM ' + t); }`,
|
||||
extras: [
|
||||
{
|
||||
rel: 'packages/db/src/evasion-mid39.ts',
|
||||
src: `import { createDb } from '@mosaicstack/db';\nexport const mk: typeof createDb = createDb;`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'E88 second-declarator re-export of factory binding',
|
||||
src: `import { mk } from './evasion-mid40.js';\nexport async function f(t: string) { await mk('u').execute('DELETE FROM ' + t); }`,
|
||||
extras: [
|
||||
{
|
||||
rel: 'packages/db/src/evasion-mid40.ts',
|
||||
src: `import { createDb } from '@mosaicstack/db';\nexport const d0 = 0,\n mk = createDb;`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'E89 apply-invoked write verb',
|
||||
src: `import { companies } from '@mosaicstack/db';\nimport { db } from './x.js';\nexport async function f() { await db.insert.apply(db, [companies]).values({}); }`,
|
||||
},
|
||||
{
|
||||
name: 'E90 call-invoked write verb',
|
||||
src: `import { companies } from '@mosaicstack/db';\nimport { db } from './x.js';\nexport async function f() { await db.insert.call(db, companies).values({}); }`,
|
||||
},
|
||||
{
|
||||
name: 'E91 apply-invoked bracket verb in capability-free file',
|
||||
src: `export class R { constructor(private pool: { execute(s: string): Promise<unknown> }) {}\n async f(t: string) { await this.pool['execute'].apply(this.pool, ['TRUNCATE ' + t]); } }`,
|
||||
},
|
||||
{
|
||||
name: 'E92 template-keyed receiver verb in capability-free file',
|
||||
src: `export class R { constructor(private pool: { execute(s: string): Promise<unknown> }) {}\n async f(t: string) { await this.pool[\`execute\`].apply(this.pool, ['TRUNCATE ' + t]); } }`,
|
||||
},
|
||||
];
|
||||
const CLEAN_CONTROLS: Array<{ name: string; src: string }> = [
|
||||
{
|
||||
@@ -1910,6 +2042,10 @@ describe('hierarchy writer coverage (contract 1 §6.3b)', () => {
|
||||
name: 'clean: comment mentioning a table is not SQL',
|
||||
src: `// syncs hierarchy_grants downstream\nexport const n = 1;`,
|
||||
},
|
||||
{
|
||||
name: 'clean: interpolated template key is a non-literal computed member, not a text-only key',
|
||||
src: `export function g(o: Record<string, () => void>, k: string) { o[\`\${k}\`](); }`,
|
||||
},
|
||||
];
|
||||
|
||||
it('analyzer flags every known evasion form', () => {
|
||||
@@ -1934,7 +2070,11 @@ describe('hierarchy writer coverage (contract 1 §6.3b)', () => {
|
||||
driverConduits: synthCap.driver,
|
||||
factoryConduits: synthCap.factory,
|
||||
};
|
||||
const v = analyzeFile(synthetic, synthCtx);
|
||||
// An evasion is caught when ANY file in the chain is flagged: some
|
||||
// laundering routes fail closed at the HELPER (origin), not the
|
||||
// consumer — e.g. a template-keyed conduit export — which keeps the
|
||||
// chain out of the tree just as effectively.
|
||||
const v = [synthetic, ...extraFacts].flatMap((ff) => analyzeFile(ff, synthCtx));
|
||||
expect(v.length, `evasion not caught: ${e.name}`).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user