code-auditor-mcp 4.0.0 → 4.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +80 -0
- package/dist/__tests__/fixtures/spec-52/item-04-storage-sql-exec-in-loop.d.ts.map +1 -1
- package/dist/__tests__/fixtures/spec-52/item-05-upsert-forms.d.ts +4 -4
- package/dist/__tests__/fixtures/spec-52/item-05-upsert-forms.d.ts.map +1 -1
- package/dist/analyzers/analyzerUtils.d.ts.map +1 -1
- package/dist/analyzers/analyzerUtils.js +2 -2
- package/dist/analyzers/analyzerUtils.js.map +1 -1
- package/dist/analyzers/applicability.d.ts +13 -0
- package/dist/analyzers/applicability.d.ts.map +1 -1
- package/dist/analyzers/applicability.js +1 -1
- package/dist/analyzers/applicability.js.map +1 -1
- package/dist/analyzers/cross-language/APIContractAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/cross-language/DependencyGraphBuilder.d.ts +1 -1
- package/dist/analyzers/cross-language/DependencyGraphBuilder.d.ts.map +1 -1
- package/dist/analyzers/cross-language/DependencyGraphBuilder.js +3 -3
- package/dist/analyzers/cross-language/DependencyGraphBuilder.js.map +1 -1
- package/dist/analyzers/cross-language/SchemaValidator.d.ts.map +1 -1
- package/dist/analyzers/cross-language/SchemaValidator.js.map +1 -1
- package/dist/analyzers/crossDomain/CrossDomainAnalyzer.d.ts +1 -1
- package/dist/analyzers/crossDomain/CrossDomainAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/crossDomain/CrossDomainAnalyzer.js +28 -7
- package/dist/analyzers/crossDomain/CrossDomainAnalyzer.js.map +1 -1
- package/dist/analyzers/documentationAnalyzer.js +5 -5
- package/dist/analyzers/documentationAnalyzer.js.map +1 -1
- package/dist/analyzers/orm/adapterRegistry.d.ts.map +1 -1
- package/dist/analyzers/orm/drizzleAdapter.d.ts.map +1 -1
- package/dist/analyzers/orm/prismaAdapter.d.ts.map +1 -1
- package/dist/analyzers/provenance.d.ts +10 -1
- package/dist/analyzers/provenance.d.ts.map +1 -1
- package/dist/analyzers/provenance.js +138 -1
- package/dist/analyzers/provenance.js.map +1 -1
- package/dist/analyzers/reactAnalyzer.js +8 -8
- package/dist/analyzers/reactAnalyzer.js.map +1 -1
- package/dist/analyzers/ruleRegistry.js +2 -2
- package/dist/analyzers/ruleRegistry.js.map +1 -1
- package/dist/analyzers/universal/UniversalConventionsAnalyzer.d.ts +2 -2
- package/dist/analyzers/universal/UniversalConventionsAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalConventionsAnalyzer.js +7 -7
- package/dist/analyzers/universal/UniversalConventionsAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalDRYAnalyzer.d.ts +4 -4
- package/dist/analyzers/universal/UniversalDRYAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalDRYAnalyzer.js +10 -10
- package/dist/analyzers/universal/UniversalDRYAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalDataAccessAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js +283 -39
- package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalDocumentationAnalyzer.d.ts +1 -1
- package/dist/analyzers/universal/UniversalDocumentationAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalDocumentationAnalyzer.js +7 -7
- package/dist/analyzers/universal/UniversalDocumentationAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalSOLIDAnalyzer.d.ts +18 -0
- package/dist/analyzers/universal/UniversalSOLIDAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalSOLIDAnalyzer.js +57 -11
- package/dist/analyzers/universal/UniversalSOLIDAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalSchemaAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalSchemaAnalyzer.js +1 -0
- package/dist/analyzers/universal/UniversalSchemaAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalSecretsAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalStylesAnalyzer.d.ts +1 -1
- package/dist/analyzers/universal/UniversalStylesAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalStylesAnalyzer.js +66 -31
- package/dist/analyzers/universal/UniversalStylesAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/schema/codeAnalysis.d.ts +21 -21
- package/dist/analyzers/universal/schema/codeAnalysis.d.ts.map +1 -1
- package/dist/analyzers/universal/schema/codeAnalysis.js +199 -45
- package/dist/analyzers/universal/schema/codeAnalysis.js.map +1 -1
- package/dist/analyzers/universal/schema/config.d.ts +6 -6
- package/dist/analyzers/universal/schema/config.d.ts.map +1 -1
- package/dist/analyzers/universal/schema/jsonSchema.js +1 -1
- package/dist/analyzers/universal/schema/jsonSchema.js.map +1 -1
- package/dist/analyzers/universal/schema/types.d.ts +10 -0
- package/dist/analyzers/universal/schema/types.d.ts.map +1 -1
- package/dist/analyzers/universal/schema/violations.d.ts +1 -1
- package/dist/analyzers/universal/schema/violations.d.ts.map +1 -1
- package/dist/auditRouter.js +2 -2
- package/dist/auditRouter.js.map +1 -1
- package/dist/auditRunner.d.ts +1 -1
- package/dist/auditRunner.d.ts.map +1 -1
- package/dist/auditRunner.js +46 -13
- package/dist/auditRunner.js.map +1 -1
- package/dist/churn/churnExtractor.js.map +1 -1
- package/dist/cli.js +93 -28
- package/dist/cli.js.map +1 -1
- package/dist/codeIndexDB.d.ts.map +1 -1
- package/dist/codeIndexDB.js +19 -3
- package/dist/codeIndexDB.js.map +1 -1
- package/dist/codeIndexService.d.ts.map +1 -1
- package/dist/codeIndexService.js.map +1 -1
- package/dist/componentScanner.js.map +1 -1
- package/dist/config/configLoader.js +1 -1
- package/dist/config/configLoader.js.map +1 -1
- package/dist/config/defaults.d.ts +2 -2
- package/dist/config/defaults.d.ts.map +1 -1
- package/dist/config/defaults.js +10 -3
- package/dist/config/defaults.js.map +1 -1
- package/dist/constants.d.ts +1 -1
- package/dist/conventions/conventionMiner.js.map +1 -1
- package/dist/coverage/lcovParser.js.map +1 -1
- package/dist/daemon/args.js.map +1 -1
- package/dist/daemon/core.d.ts.map +1 -1
- package/dist/daemon/core.js +18 -9
- package/dist/daemon/core.js.map +1 -1
- package/dist/daemon/lspServer.d.ts.map +1 -1
- package/dist/daemon/lspServer.js +1 -1
- package/dist/daemon/lspServer.js.map +1 -1
- package/dist/daemon/main.js.map +1 -1
- package/dist/daemon/resolve.js.map +1 -1
- package/dist/daemon/socketServer.d.ts.map +1 -1
- package/dist/enforcement/gate.js +2 -2
- package/dist/functionScanner.d.ts.map +1 -1
- package/dist/generators/AWSQConfigGenerator.d.ts.map +1 -1
- package/dist/generators/AiderConfigGenerator.d.ts.map +1 -1
- package/dist/generators/BaseConfigGenerator.d.ts.map +1 -1
- package/dist/generators/BaseConfigGenerator.js.map +1 -1
- package/dist/generators/ClaudeConfigGenerator.d.ts.map +1 -1
- package/dist/generators/ClineConfigGenerator.d.ts.map +1 -1
- package/dist/generators/CodeiumConfigGenerator.d.ts.map +1 -1
- package/dist/generators/CodexConfigGenerator.d.ts.map +1 -1
- package/dist/generators/ConfigGeneratorFactory.d.ts.map +1 -1
- package/dist/generators/ConfigGeneratorFactory.js.map +1 -1
- package/dist/generators/ContinueConfigGenerator.d.ts.map +1 -1
- package/dist/generators/CopilotConfigGenerator.d.ts.map +1 -1
- package/dist/generators/CursorConfigGenerator.d.ts.map +1 -1
- package/dist/generators/GeminiConfigGenerator.d.ts.map +1 -1
- package/dist/generators/JetBrainsConfigGenerator.d.ts.map +1 -1
- package/dist/generators/VSCodeConfigGenerator.d.ts.map +1 -1
- package/dist/generators/ZCodeConfigGenerator.d.ts.map +1 -1
- package/dist/graph/importClassification.d.ts +46 -8
- package/dist/graph/importClassification.d.ts.map +1 -1
- package/dist/graph/importClassification.js +207 -35
- package/dist/graph/importClassification.js.map +1 -1
- package/dist/graph/outputFormatter.js.map +1 -1
- package/dist/hooks/codex.d.ts.map +1 -1
- package/dist/hooks/codex.js +1 -1
- package/dist/hooks/codex.js.map +1 -1
- package/dist/hooks/core.d.ts +1 -1
- package/dist/hooks/core.d.ts.map +1 -1
- package/dist/hooks/core.js +4 -4
- package/dist/hooks/core.js.map +1 -1
- package/dist/hooks/cursor.d.ts.map +1 -1
- package/dist/hooks/cursor.js +2 -2
- package/dist/hooks/cursor.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/installer.js.map +1 -1
- package/dist/invariants/invariant-rules.schema.json +1 -1
- package/dist/invariants/ruleEngine.js.map +1 -1
- package/dist/invariants/ruleValidator.js +1 -1
- package/dist/invariants/ruleValidator.js.map +1 -1
- package/dist/invariants/types.d.ts +1 -1
- package/dist/invariants/types.d.ts.map +1 -1
- package/dist/languages/LanguageOrchestrator.d.ts +2 -2
- package/dist/languages/LanguageOrchestrator.d.ts.map +1 -1
- package/dist/languages/LanguageOrchestrator.js.map +1 -1
- package/dist/languages/LanguageRegistry.d.ts.map +1 -1
- package/dist/languages/RuntimeManager.d.ts.map +1 -1
- package/dist/languages/RuntimeManager.js +2 -2
- package/dist/languages/RuntimeManager.js.map +1 -1
- package/dist/languages/UniversalAnalyzer.d.ts.map +1 -1
- package/dist/languages/UniversalAnalyzer.js.map +1 -1
- package/dist/languages/adapterBridge.js.map +1 -1
- package/dist/languages/go/GoAdapter.d.ts.map +1 -1
- package/dist/languages/go/GoAdapter.js.map +1 -1
- package/dist/languages/go/analyzer +0 -0
- package/dist/languages/go/analyzer-src/analyzer.go +3 -3
- package/dist/languages/go/analyzer-src/solid.go +5 -5
- package/dist/languages/goBinary.js.map +1 -1
- package/dist/languages/tree-sitter/TreeSitterCssAdapter.d.ts.map +1 -1
- package/dist/languages/tree-sitter/TreeSitterCssAdapter.js.map +1 -1
- package/dist/languages/tree-sitter/converter.js.map +1 -1
- package/dist/languages/tree-sitter/parser.js.map +1 -1
- package/dist/languages/types.d.ts +15 -0
- package/dist/languages/types.d.ts.map +1 -1
- package/dist/languages/typescript/TreeSitterTypeScriptAdapter.d.ts +11 -0
- package/dist/languages/typescript/TreeSitterTypeScriptAdapter.d.ts.map +1 -1
- package/dist/languages/typescript/TreeSitterTypeScriptAdapter.js +34 -0
- package/dist/languages/typescript/TreeSitterTypeScriptAdapter.js.map +1 -1
- package/dist/ledger.js +1 -1
- package/dist/ledger.js.map +1 -1
- package/dist/mcp-tools/projectTasks.d.ts +37 -37
- package/dist/mcp-tools/projectTasks.js +2 -2
- package/dist/mcp-tools/projectTasks.js.map +1 -1
- package/dist/mcp-tools/whitelistTools.d.ts +17 -17
- package/dist/mcp-tools/whitelistTools.d.ts.map +1 -1
- package/dist/mcp-tools/workflowGuide.js +3 -3
- package/dist/mcp-tools/workflowGuide.js.map +1 -1
- package/dist/mcp-tools-shared.d.ts.map +1 -1
- package/dist/mcp-tools-shared.js +13 -13
- package/dist/mcp-tools-shared.js.map +1 -1
- package/dist/mcp-ui-simple.js +6 -6
- package/dist/mcp-ui-simple.js.map +1 -1
- package/dist/mcp.js +14 -14
- package/dist/mcp.js.map +1 -1
- package/dist/mcpAuditJobs.js +7 -7
- package/dist/mcpAuditJobs.js.map +1 -1
- package/dist/mcpAutoIndex.js +1 -1
- package/dist/mcpAutoIndex.js.map +1 -1
- package/dist/mcpToolErrors.d.ts.map +1 -1
- package/dist/mcpToolErrors.js.map +1 -1
- package/dist/native-bootstrap.js.map +1 -1
- package/dist/nextFile.d.ts +2 -2
- package/dist/nextFile.d.ts.map +1 -1
- package/dist/nextFile.js +3 -3
- package/dist/nextFile.js.map +1 -1
- package/dist/nextFileIncremental.d.ts +9 -2
- package/dist/nextFileIncremental.d.ts.map +1 -1
- package/dist/nextFileIncremental.js +41 -23
- package/dist/nextFileIncremental.js.map +1 -1
- package/dist/pipeline.d.ts +2 -2
- package/dist/pipeline.d.ts.map +1 -1
- package/dist/pipeline.js +45 -0
- package/dist/pipeline.js.map +1 -1
- package/dist/pipelineAdapters.d.ts +39 -2
- package/dist/pipelineAdapters.d.ts.map +1 -1
- package/dist/pipelineAdapters.js +230 -29
- package/dist/pipelineAdapters.js.map +1 -1
- package/dist/reporting/csvReportGenerator.d.ts.map +1 -1
- package/dist/reporting/csvReportGenerator.js +6 -6
- package/dist/reporting/csvReportGenerator.js.map +1 -1
- package/dist/reporting/htmlReportGenerator.d.ts.map +1 -1
- package/dist/reporting/htmlReportGenerator.js +11 -11
- package/dist/reporting/htmlReportGenerator.js.map +1 -1
- package/dist/reporting/jsonReportGenerator.d.ts.map +1 -1
- package/dist/reporting/jsonReportGenerator.js +1 -1
- package/dist/reporting/jsonReportGenerator.js.map +1 -1
- package/dist/reporting/reportGenerator.d.ts.map +1 -1
- package/dist/reporting/sarifReportGenerator.d.ts.map +1 -1
- package/dist/reporting/sarifReportGenerator.js +1 -1
- package/dist/reporting/sarifReportGenerator.js.map +1 -1
- package/dist/reporting/sizeDistribution.d.ts +17 -0
- package/dist/reporting/sizeDistribution.d.ts.map +1 -0
- package/dist/reporting/sizeDistribution.js +93 -0
- package/dist/reporting/sizeDistribution.js.map +1 -0
- package/dist/scripts/benchmarkWorkers.js +1 -1
- package/dist/scripts/benchmarkWorkers.js.map +1 -1
- package/dist/search/QueryParser.d.ts.map +1 -1
- package/dist/search/QueryParser.js.map +1 -1
- package/dist/services/CodeMapGenerator.d.ts.map +1 -1
- package/dist/services/CodeMapGenerator.js.map +1 -1
- package/dist/services/ProjectTaskRepository.d.ts.map +1 -1
- package/dist/services/ProjectTaskRepository.js.map +1 -1
- package/dist/services/auditJobService.js.map +1 -1
- package/dist/services/fileAccounting.d.ts.map +1 -1
- package/dist/services/projectTaskHelpers.js.map +1 -1
- package/dist/services/whitelistService.d.ts.map +1 -1
- package/dist/sqlite/betterSqlite3.d.ts.map +1 -1
- package/dist/sqlite/betterSqlite3.js.map +1 -1
- package/dist/sqlite/nodeSqlite.d.ts.map +1 -1
- package/dist/sqlite/nodeSqlite.js.map +1 -1
- package/dist/structuralSignature.js.map +1 -1
- package/dist/styles/styleExtractor.js.map +1 -1
- package/dist/styles/styleIndexer.js.map +1 -1
- package/dist/styles/tailwindConfigLoader.js.map +1 -1
- package/dist/styles/tailwindProbe.d.ts.map +1 -1
- package/dist/styles/tailwindUtilityExpander.d.ts.map +1 -1
- package/dist/styles/tailwindUtilityExpander.js.map +1 -1
- package/dist/telemetry.d.ts +1 -1
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js.map +1 -1
- package/dist/tool-registry.d.ts.map +1 -1
- package/dist/types/crossLanguage.d.ts +1 -1
- package/dist/types/crossLanguage.d.ts.map +1 -1
- package/dist/types.d.ts +99 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +3 -3
- package/dist/types.js.map +1 -1
- package/dist/ui/InteractivePrompts.d.ts.map +1 -1
- package/dist/ui/InteractivePrompts.js.map +1 -1
- package/dist/utils/componentPatterns.js.map +1 -1
- package/dist/utils/fileDiscovery.js.map +1 -1
- package/dist/utils/jsDocParser.js.map +1 -1
- package/dist/version.generated.d.ts +1 -1
- package/dist/version.generated.js +1 -1
- package/dist/workers/auditWorker.js.map +1 -1
- package/package.json +4 -3
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/skills/code-auditor/SKILL-RULE-KINDS.md +7 -7
- package/plugin/skills/code-auditor/SKILL.md +9 -9
- package/server.json +2 -2
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { UniversalAnalyzer } from '../../languages/UniversalAnalyzer.js';
|
|
7
7
|
import { withRuleTiming } from '../ruleTiming.js';
|
|
8
|
-
import { buildProvenanceContext, isDBProvenanced, DB_CALL_METHODS, } from '../provenance.js';
|
|
8
|
+
import { buildProvenanceContext, isDBProvenanced, getCallExpressionCallee, DB_CALL_METHODS, ORM_METHODS, } from '../provenance.js';
|
|
9
9
|
import { DB_RECEIVER_NAMES, DB_CALL_METHOD_NAMES, DB_BINDING_NAMES, DB_WRAPPER_NAMES, } from './UniversalSchemaAnalyzer.js';
|
|
10
10
|
import { isSqlKeyword, findEnclosingFunctionIdentity, functionIdentityLabel } from './schema/codeAnalysis.js';
|
|
11
11
|
import { isTestOrSpecPath } from '../../languages/testConventions.js';
|
|
@@ -209,6 +209,7 @@ function buildDatabaseCall(node, ast, scan) {
|
|
|
209
209
|
hasFilter,
|
|
210
210
|
hasParameterizedQuery: security.parameterized,
|
|
211
211
|
hasSqlInjectionRisk: security.injectionRisk,
|
|
212
|
+
sqlEscaped: security.escaped,
|
|
212
213
|
enclosingFunction: enclosingIdentity(node, adapter, ast.filePath),
|
|
213
214
|
};
|
|
214
215
|
}
|
|
@@ -307,20 +308,39 @@ function checkViolations(call, analysis, ctx) {
|
|
|
307
308
|
const violations = [];
|
|
308
309
|
const symbol = nextSymbol(call.enclosingFunction ?? 'top-level', call.method, symbolOrdinals);
|
|
309
310
|
const push = (message, opts) => violations.push(makeViolation(filePath, { line: call.line, column: call.column }, message, { ...opts, symbol }));
|
|
310
|
-
// Security: SQL injection
|
|
311
|
-
// now
|
|
311
|
+
// Security: SQL injection. Raw unescaped interpolation of input is a live
|
|
312
|
+
// vulnerability now → `critical`. Manual quote-escaping
|
|
313
|
+
// (`.replace(/'/g, "''")`) is *defended* — single-quote doubling handles only
|
|
314
|
+
// the single-quote vector, not backslash escapes or unicode quote variants —
|
|
315
|
+
// so it downgrades to `high` ("verify escaping") rather than asserting a
|
|
316
|
+
// certified vulnerability.
|
|
312
317
|
if (config.checkSQLInjection && call.hasSqlInjectionRisk) {
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
318
|
+
if (call.sqlEscaped) {
|
|
319
|
+
push(`Interpolated SQL in ${call.method} — verify escaping is sufficient. Use parameterized queries.`, {
|
|
320
|
+
severity: 'high',
|
|
321
|
+
rule: 'sql-injection-risk',
|
|
322
|
+
resolution: {
|
|
323
|
+
action: 'parameterize',
|
|
324
|
+
summary: `The SQL in ${call.method} interpolates a manually quote-escaped value. Quote-doubling defends only the single-quote case (not backslash escapes, unicode quote variants, or numeric/identifier positions) — replace the interpolation with a parameterized query (\`?\`, \`$1\`, or \`:name\`) for a full guarantee.`,
|
|
325
|
+
symbols: [call.method],
|
|
326
|
+
files: [filePath],
|
|
327
|
+
lines: [call.line],
|
|
328
|
+
},
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
push(`Potential SQL injection risk in ${call.method}. Use parameterized queries.`, {
|
|
333
|
+
severity: 'critical',
|
|
334
|
+
rule: 'sql-injection-risk',
|
|
335
|
+
resolution: {
|
|
336
|
+
action: 'parameterize',
|
|
337
|
+
summary: `Replace the string-interpolated SQL in ${call.method} with a parameterized query — bind values via the driver's placeholder form (\`?\`, \`$1\`, or \`:name\`) instead of concatenating them into the statement.`,
|
|
338
|
+
symbols: [call.method],
|
|
339
|
+
files: [filePath],
|
|
340
|
+
lines: [call.line],
|
|
341
|
+
},
|
|
342
|
+
});
|
|
343
|
+
}
|
|
324
344
|
}
|
|
325
345
|
// Security: Missing Organization Filter — a query on a tenant-scoped table with
|
|
326
346
|
// no org/tenant filter is a live data-isolation breach: one tenant reads
|
|
@@ -331,14 +351,14 @@ function checkViolations(call, analysis, ctx) {
|
|
|
331
351
|
// Performance: Complex Query — a join-heavy query (many tables). Spec 55 R5:
|
|
332
352
|
// a subquery alone is no longer "complex" — it is an ordinary SQLite/D1 idiom.
|
|
333
353
|
if (analysis.performanceRisk === 'high') {
|
|
334
|
-
push(`Query references ${call.tables.length} tables`, { severity: '
|
|
354
|
+
push(`Query references ${call.tables.length} tables`, { severity: 'high', rule: 'complex-query' });
|
|
335
355
|
}
|
|
336
356
|
// Performance: Unfiltered Query — an unfiltered write (DELETE/UPDATE with no
|
|
337
357
|
// row-limiting clause), the classic mass-mutation foot-gun. Spec 55 R3: it is
|
|
338
358
|
// a query-shape rule excluded from test files; R5: it is about writes now, not
|
|
339
359
|
// reads (an unfiltered SELECT is often an intentional full-set load).
|
|
340
360
|
if (!skipTestRules && analysis.performanceRisk === 'medium') {
|
|
341
|
-
push(`Unfiltered write on ${call.tables.join(', ')} has no WHERE/HAVING/LIMIT`, { severity: '
|
|
361
|
+
push(`Unfiltered write on ${call.tables.join(', ')} has no WHERE/HAVING/LIMIT`, { severity: 'high', rule: 'unfiltered-query' });
|
|
342
362
|
}
|
|
343
363
|
return violations;
|
|
344
364
|
}
|
|
@@ -662,6 +682,64 @@ function isStatementConstructionOnly(node, adapter, sourceCode) {
|
|
|
662
682
|
const method = dbCallMethodName(node, adapter, sourceCode);
|
|
663
683
|
return (method === 'prepare' || method === 'bind') && !hasEagerMethodInCallChain(node, adapter, sourceCode);
|
|
664
684
|
}
|
|
685
|
+
/**
|
|
686
|
+
* True when a db-call node is SQL-string construction inside a query compiler:
|
|
687
|
+
* an ORM *builder* method (e.g. `aggregate`, `select`, `where` — never an eager
|
|
688
|
+
* I/O method) invoked on an unprovenanced `this` receiver whose result is
|
|
689
|
+
* accumulated into an array via `.push(...)`. knex's query compilers build SQL
|
|
690
|
+
* exactly this way — `sql.push(...this.aggregate(stmt))` inside a
|
|
691
|
+
* column-iteration loop — and the call returns a string fragment, not a query
|
|
692
|
+
* result, so the loop is not an N+1 (Spec 52 R1 extension). Eager methods
|
|
693
|
+
* (`run`/`all`/`exec`/`query`/…) are excluded, so a genuine
|
|
694
|
+
* `results.push(db.query(...))` still fires.
|
|
695
|
+
*/
|
|
696
|
+
function isSqlStringConstruction(node, adapter, sourceCode) {
|
|
697
|
+
const method = dbCallMethodName(node, adapter, sourceCode);
|
|
698
|
+
if (!method || EAGER_DB_METHODS.has(method))
|
|
699
|
+
return false;
|
|
700
|
+
if (!ORM_METHODS.has(method))
|
|
701
|
+
return false;
|
|
702
|
+
const call = resolveDbCallNode(node, adapter);
|
|
703
|
+
if (!call)
|
|
704
|
+
return false;
|
|
705
|
+
const memberExpr = findMemberCallee(call, adapter);
|
|
706
|
+
if (!memberExpr)
|
|
707
|
+
return false;
|
|
708
|
+
// The receiver must be an unprovenanced `this`/`super` — the query-compiler
|
|
709
|
+
// object itself. `this.db.aggregate(...)` (receiver `this.db`) would be a real
|
|
710
|
+
// ORM query on a provenanced `db`, not a compiler fragment.
|
|
711
|
+
const objectText = memberObjectText(memberExpr, adapter, sourceCode);
|
|
712
|
+
if (objectText !== 'this' && objectText !== 'super')
|
|
713
|
+
return false;
|
|
714
|
+
return isConsumedByArrayPush(call, adapter, sourceCode);
|
|
715
|
+
}
|
|
716
|
+
/** The object text of a member_expression (`this.aggregate` → `this`). */
|
|
717
|
+
function memberObjectText(node, adapter, sourceCode) {
|
|
718
|
+
const object = adapter.getChildren(node).find((c) => {
|
|
719
|
+
const t = adapter.getNodeType(c);
|
|
720
|
+
return t !== 'property_identifier' && t !== 'field_identifier' && t !== '.';
|
|
721
|
+
});
|
|
722
|
+
return object ? adapter.getNodeText(object, sourceCode) : null;
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* True when a call's result is consumed by an array `.push(...)` — the call is
|
|
726
|
+
* an argument (possibly spread) of a `push` invocation. The accumulation
|
|
727
|
+
* signature (`sql.push(...this.aggregate(stmt))`) marks the value as a string
|
|
728
|
+
* fragment being collected, not a query result being read.
|
|
729
|
+
*/
|
|
730
|
+
function isConsumedByArrayPush(call, adapter, sourceCode) {
|
|
731
|
+
let cur = adapter.getParent(call);
|
|
732
|
+
if (cur && adapter.getNodeType(cur) === 'spread_element') {
|
|
733
|
+
cur = adapter.getParent(cur);
|
|
734
|
+
}
|
|
735
|
+
if (!cur || adapter.getNodeType(cur) !== 'arguments')
|
|
736
|
+
return false;
|
|
737
|
+
const pushCall = adapter.getParent(cur);
|
|
738
|
+
if (!pushCall || adapter.getNodeType(pushCall) !== 'call_expression')
|
|
739
|
+
return false;
|
|
740
|
+
const pushMember = findMemberCallee(pushCall, adapter);
|
|
741
|
+
return !!pushMember && memberPropertyName(pushMember, adapter, sourceCode) === 'push';
|
|
742
|
+
}
|
|
665
743
|
/** True when a call's arguments contain a spread_element (…binds). */
|
|
666
744
|
function hasSpreadArgument(call, adapter) {
|
|
667
745
|
const args = adapter.getChildren(call).find(c => adapter.getNodeType(c) === 'arguments');
|
|
@@ -756,10 +834,12 @@ function isWrapperFunctionWithBindParams(node, adapter, sourceCode, wrapperNames
|
|
|
756
834
|
if (!call || adapter.getNodeType(call) !== 'call_expression')
|
|
757
835
|
return false;
|
|
758
836
|
// Check if the callee is a simple identifier (not member expression)
|
|
759
|
-
// matching one of the wrapper names.
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
837
|
+
// matching one of the wrapper names. Use the shared callee extractor so an
|
|
838
|
+
// `await fn<T>(...)` call — whose callee is an `await_expression` wrapping the
|
|
839
|
+
// identifier, plus a `type_arguments` child — resolves the same way the
|
|
840
|
+
// provenance detector does. A naive `children.find(identifier)` misses it.
|
|
841
|
+
const callee = getCallExpressionCallee(call, adapter);
|
|
842
|
+
if (!callee || adapter.getNodeType(callee) !== 'identifier')
|
|
763
843
|
return false;
|
|
764
844
|
const calleeName = adapter.getNodeText(callee, sourceCode);
|
|
765
845
|
if (!wrapperNames.includes(calleeName))
|
|
@@ -1050,17 +1130,35 @@ function isUnfilteredWrite(call) {
|
|
|
1050
1130
|
* wrapper function with bind params). All four short-circuit checkQuerySecurity
|
|
1051
1131
|
* to a "safe" result.
|
|
1052
1132
|
*/
|
|
1053
|
-
function isParameterizedByChain(node, adapter, sourceCode, config) {
|
|
1133
|
+
function isParameterizedByChain(node, adapter, sourceCode, config, wrapperNames) {
|
|
1054
1134
|
if (isInPrepareBindChain(node, adapter, sourceCode))
|
|
1055
1135
|
return true;
|
|
1056
1136
|
if (isInExecChain(node, adapter, sourceCode))
|
|
1057
1137
|
return true;
|
|
1058
1138
|
if (isD1ConvenienceCall(node, adapter, sourceCode))
|
|
1059
1139
|
return true;
|
|
1060
|
-
if (isWrapperFunctionWithBindParams(node, adapter, sourceCode,
|
|
1140
|
+
if (isWrapperFunctionWithBindParams(node, adapter, sourceCode, wrapperNames))
|
|
1061
1141
|
return true;
|
|
1062
1142
|
return false;
|
|
1063
1143
|
}
|
|
1144
|
+
/**
|
|
1145
|
+
* The effective set of DB-wrapper names the FP guards recognize as
|
|
1146
|
+
* parameterized. This is the union of the static `config.dbWrapperNames`
|
|
1147
|
+
* (d1Query, d1Exec) and the names the provenance detector *learned* at
|
|
1148
|
+
* file-scan time (`reason: 'wrapper'` — e.g. a bare `d1(sql, params)` D1 REST
|
|
1149
|
+
* helper). Keeping the two in sync is what prevents a wrapper the detector
|
|
1150
|
+
* already treats as DB-bound from being re-flagged as raw interpolation by
|
|
1151
|
+
* `checkQuerySecurity`.
|
|
1152
|
+
*/
|
|
1153
|
+
function effectiveWrapperNames(scan) {
|
|
1154
|
+
const staticNames = scan.config.dbWrapperNames ?? [];
|
|
1155
|
+
const learned = scan.provenanceContext
|
|
1156
|
+
? [...scan.provenanceContext.dbProvenanced.values()]
|
|
1157
|
+
.filter(ev => ev.reason === 'wrapper')
|
|
1158
|
+
.map(ev => ev.identifier)
|
|
1159
|
+
: [];
|
|
1160
|
+
return [...new Set([...staticNames, ...learned])];
|
|
1161
|
+
}
|
|
1064
1162
|
/**
|
|
1065
1163
|
* True when a single dynamic interpolation part is provably safe — via a
|
|
1066
1164
|
* config-driven sanitizer allowlist, the adapter's cross-function safety
|
|
@@ -1106,30 +1204,33 @@ function checkQuerySecurity(node, text, ast, scan) {
|
|
|
1106
1204
|
const { adapter, sourceCode, config } = scan;
|
|
1107
1205
|
// Parameterized chains (.prepare().bind(), .exec() spread, D1 convenience,
|
|
1108
1206
|
// DB wrappers) and explicit parameterization are always safe.
|
|
1109
|
-
if (isParameterizedByChain(node, adapter, sourceCode, config)) {
|
|
1110
|
-
return { parameterized: true, injectionRisk: false };
|
|
1207
|
+
if (isParameterizedByChain(node, adapter, sourceCode, config, effectiveWrapperNames(scan))) {
|
|
1208
|
+
return { parameterized: true, injectionRisk: false, escaped: false };
|
|
1111
1209
|
}
|
|
1112
1210
|
if ((config.securityPatterns?.parameterizedQueries || []).some(p => text.includes(p))) {
|
|
1113
|
-
return { parameterized: true, injectionRisk: false };
|
|
1211
|
+
return { parameterized: true, injectionRisk: false, escaped: false };
|
|
1114
1212
|
}
|
|
1115
1213
|
// No dynamic-string capability → can't prove unsafe; err quiet.
|
|
1116
1214
|
if (!adapter.isDynamicStringConstruction || !adapter.getDynamicParts) {
|
|
1117
|
-
return { parameterized: false, injectionRisk: false };
|
|
1215
|
+
return { parameterized: false, injectionRisk: false, escaped: false };
|
|
1118
1216
|
}
|
|
1119
1217
|
// Only a dynamically-constructed string carrying SQL keywords can inject.
|
|
1120
1218
|
if (!adapter.isDynamicStringConstruction(node) || !containsSQLKeywords(text)) {
|
|
1121
|
-
return { parameterized: false, injectionRisk: false };
|
|
1219
|
+
return { parameterized: false, injectionRisk: false, escaped: false };
|
|
1122
1220
|
}
|
|
1123
1221
|
const unresolved = adapter.getDynamicParts(node, sourceCode)
|
|
1124
|
-
.filter(part => !isSafeDynamicPart(part, ast, scan))
|
|
1125
|
-
.map(part => part.text);
|
|
1222
|
+
.filter(part => !isSafeDynamicPart(part, ast, scan));
|
|
1126
1223
|
if (unresolved.length === 0) {
|
|
1127
|
-
return { parameterized: false, injectionRisk: false };
|
|
1224
|
+
return { parameterized: false, injectionRisk: false, escaped: false };
|
|
1128
1225
|
}
|
|
1226
|
+
// Manual quote-escaping (`.replace(/'/g, "''")`) is defended, not raw — every
|
|
1227
|
+
// unresolved part being quote-escaped downgrades the finding to high.
|
|
1228
|
+
const escaped = unresolved.every(part => !!part.node && !!adapter.isEscapedInterpolation && adapter.isEscapedInterpolation(part.node, ast, sourceCode));
|
|
1129
1229
|
return {
|
|
1130
1230
|
parameterized: false,
|
|
1131
1231
|
injectionRisk: true,
|
|
1132
|
-
|
|
1232
|
+
escaped,
|
|
1233
|
+
message: `Cannot protect interpolated content: ${unresolved.map(part => '${' + part.text + '}').join(', ')}`,
|
|
1133
1234
|
};
|
|
1134
1235
|
}
|
|
1135
1236
|
function extractCallExpressionMethod(callExpr, adapter, sourceCode) {
|
|
@@ -1139,11 +1240,10 @@ function extractCallExpressionMethod(callExpr, adapter, sourceCode) {
|
|
|
1139
1240
|
// keywords (COUNT, JOIN, WHERE, ...) that appear inside template literals
|
|
1140
1241
|
// in the call arguments, which a regex scan of the full call-expression
|
|
1141
1242
|
// text would incorrectly match.
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
});
|
|
1243
|
+
// Use the shared callee extractor: it recurses through `await_expression` and
|
|
1244
|
+
// skips `type_arguments`, so `await d1<{...}>(...)` resolves to `d1` rather
|
|
1245
|
+
// than falling through to the "unknown" regex fallback.
|
|
1246
|
+
const callee = getCallExpressionCallee(callExpr, adapter);
|
|
1147
1247
|
if (callee) {
|
|
1148
1248
|
const calleeType = adapter.getNodeType(callee);
|
|
1149
1249
|
if (calleeType === 'member_expression') {
|
|
@@ -1246,6 +1346,42 @@ function isConnectionString(text) {
|
|
|
1246
1346
|
return patterns.some(pattern => pattern.test(text));
|
|
1247
1347
|
}
|
|
1248
1348
|
// ── R4.1: Loop-query detection ──────────────────────────────────────
|
|
1349
|
+
/**
|
|
1350
|
+
* LLM provider / SDK namespaces. A callee whose text carries one of these is
|
|
1351
|
+
* an unambiguous LLM/agent invocation (e.g. `gatewayDeepseekModel`,
|
|
1352
|
+
* `anthropic.messages.create`, `openai.chat`).
|
|
1353
|
+
*/
|
|
1354
|
+
const LLM_PROVIDER_RE = /(?:anthropic|openai|deepseek|claude|gpt|gemini|cohere|mistral|bedrock|vertex|ollama|llm|gateway)/i;
|
|
1355
|
+
/**
|
|
1356
|
+
* LLM action verbs — AI SDK methods and the wrapper names that survive the
|
|
1357
|
+
* provider-token check (e.g. `aiEmbed`, `chatCompletion`, `thesisEngine`).
|
|
1358
|
+
* Deliberately narrower than `generate`/`extract` (those collide with ordinary
|
|
1359
|
+
* helpers like `insertSingleGeneratedBuild`); the provider namespace and the
|
|
1360
|
+
* model-client argument signal cover the rest.
|
|
1361
|
+
*/
|
|
1362
|
+
const LLM_ACTION_RE = /(?:embed|reembed|completion|chat|prompt|synthesize|thesis|agent|classify|summarize|translate)/i;
|
|
1363
|
+
/**
|
|
1364
|
+
* Identifier names that denote an LLM/model client when they appear as a call
|
|
1365
|
+
* argument (e.g. `extractBuildsFromCorpus(db, model, …)`) or as the object of a
|
|
1366
|
+
* member callee (e.g. `model.chat(…)`). Deliberately excludes `client`/`ai`
|
|
1367
|
+
* (ambiguous — a DB client is also a `client`) so we never suppress a real N+1
|
|
1368
|
+
* on a generic variable name.
|
|
1369
|
+
*/
|
|
1370
|
+
const LLM_CLIENT_ARG_NAMES = new Set([
|
|
1371
|
+
'model', 'llm', 'embedder', 'gateway',
|
|
1372
|
+
'anthropic', 'openai', 'deepseek', 'claude', 'gpt', 'gemini', 'cohere', 'mistral', 'bedrock',
|
|
1373
|
+
]);
|
|
1374
|
+
/**
|
|
1375
|
+
* Message-lifecycle methods a queue consumer invokes per message (`msg.ack()`,
|
|
1376
|
+
* `msg.retry()`, `msg.nack()`, …). A loop whose body acknowledges or retries the
|
|
1377
|
+
* item it iterates over is a queue-consumer message loop: each message is an
|
|
1378
|
+
* independent job that must be acked/nacked/retried in isolation, so a
|
|
1379
|
+
* per-iteration query is the contract, not a batchable N+1. Batching or joining
|
|
1380
|
+
* would regress the retry semantics. `ack`/`nack`/`acknowledge`/`deleteMessage`
|
|
1381
|
+
* are unambiguous queue vocabulary; `retry` is queue-contextual (a lone
|
|
1382
|
+
* `.retry()` inside a collection loop is overwhelmingly a per-item queue retry).
|
|
1383
|
+
*/
|
|
1384
|
+
const MESSAGE_LIFECYCLE_METHODS = new Set(['ack', 'nack', 'acknowledge', 'deleteMessage', 'retry']);
|
|
1249
1385
|
/**
|
|
1250
1386
|
* R4.1: Find database queries inside loops and flag them as N+1 risks.
|
|
1251
1387
|
* Each finding carries the query call location (never line 1).
|
|
@@ -1275,12 +1411,35 @@ function checkLoopQueries(ast, scan) {
|
|
|
1275
1411
|
// an eager call or a prepare chained into one still fires.
|
|
1276
1412
|
if (isStatementConstructionOnly(node, adapter, sourceCode))
|
|
1277
1413
|
continue;
|
|
1414
|
+
// Spec 52 R1 (extension) — skip SQL-string construction inside a query
|
|
1415
|
+
// compiler: an ORM builder method (e.g. `aggregate`) on an unprovenanced
|
|
1416
|
+
// `this` whose result is accumulated via `.push(...)` returns a string
|
|
1417
|
+
// fragment, not a query result (knex `sql.push(...this.aggregate(stmt))`).
|
|
1418
|
+
if (isSqlStringConstruction(node, adapter, sourceCode))
|
|
1419
|
+
continue;
|
|
1278
1420
|
const loopInfo = findEnclosingLoop(node, adapter);
|
|
1279
1421
|
if (!loopInfo)
|
|
1280
1422
|
continue;
|
|
1281
|
-
// R4.1
|
|
1282
|
-
|
|
1283
|
-
|
|
1423
|
+
// R4.1 (Spec 46): LLM-pipeline discriminator. A loop whose body invokes an
|
|
1424
|
+
// LLM/agent (embedding, model completion, corpus extraction) is an intentional
|
|
1425
|
+
// *sequential pipeline* — its per-item DB calls are persistence steps gated by
|
|
1426
|
+
// rate limits and per-item crash-recovery, not a batchable N+1. Batching or
|
|
1427
|
+
// joining would regress those semantics, so suppress the finding rather than
|
|
1428
|
+
// emit an unfixable `severe`.
|
|
1429
|
+
if (loopBodyContainsLlmCall(loopInfo.loopNode, adapter, sourceCode))
|
|
1430
|
+
continue;
|
|
1431
|
+
// R4.1 (queue consumer): a loop whose body acks/nacks/retries the message it
|
|
1432
|
+
// iterates over is a queue consumer, not a batchable N+1. Each message is an
|
|
1433
|
+
// independent job that must ack/retry in isolation, so batching or joining the
|
|
1434
|
+
// per-iteration queries would break the retry contract — suppress the finding.
|
|
1435
|
+
if (loopBodyContainsMessageLifecycleCall(loopInfo.loopNode, adapter, sourceCode))
|
|
1436
|
+
continue;
|
|
1437
|
+
// R4.1: one finding per *loop*, not per query — an N+1 is a property of the
|
|
1438
|
+
// loop, so a loop issuing several queries is still one violation (defect #51).
|
|
1439
|
+
// Key on the loop node's start byte offset so distinct loops (including
|
|
1440
|
+
// nested ones) never collapse, while every query in the same loop dedups to
|
|
1441
|
+
// the first. The finding anchors to that first query's location below.
|
|
1442
|
+
const dedupKey = String(loopInfo.loopNode.range[0]);
|
|
1284
1443
|
if (reported.has(dedupKey))
|
|
1285
1444
|
continue;
|
|
1286
1445
|
reported.add(dedupKey);
|
|
@@ -1293,6 +1452,91 @@ function checkLoopQueries(ast, scan) {
|
|
|
1293
1452
|
}
|
|
1294
1453
|
return violations;
|
|
1295
1454
|
}
|
|
1455
|
+
/**
|
|
1456
|
+
* R4.1 (Spec 46): True when the loop's subtree contains an LLM/agent invocation.
|
|
1457
|
+
* Used as the discriminator between a batchable N+1 (no LLM call — the queries
|
|
1458
|
+
* should be batched/joined) and an intentional sequential pipeline (an LLM call
|
|
1459
|
+
* — the per-item DB calls are persistence steps behind rate limits and
|
|
1460
|
+
* crash-recovery). Walking the whole loop subtree (header + body) is safe: a
|
|
1461
|
+
* loop header rarely hosts an LLM call, and a body-level one is exactly the
|
|
1462
|
+
* signal we want.
|
|
1463
|
+
*/
|
|
1464
|
+
function loopBodyContainsLlmCall(loopNode, adapter, sourceCode) {
|
|
1465
|
+
let found = false;
|
|
1466
|
+
walkSubtree(loopNode, adapter, (node) => {
|
|
1467
|
+
if (found || adapter.getNodeType(node) !== 'call_expression')
|
|
1468
|
+
return;
|
|
1469
|
+
if (isLlmCallNode(node, adapter, sourceCode))
|
|
1470
|
+
found = true;
|
|
1471
|
+
});
|
|
1472
|
+
return found;
|
|
1473
|
+
}
|
|
1474
|
+
/**
|
|
1475
|
+
* R4.1 (queue consumer): True when the loop's subtree contains a
|
|
1476
|
+
* message-lifecycle call — a member call whose property is a queue lifecycle
|
|
1477
|
+
* method (`ack`, `nack`, `acknowledge`, `deleteMessage`, `retry`). The signature
|
|
1478
|
+
* of a queue consumer (`for (const msg of batch.messages) { …; msg.ack(); }`)
|
|
1479
|
+
* marks per-message independent jobs, not a batchable N+1. Walking the whole loop
|
|
1480
|
+
* subtree is safe: the header (`batch.messages`) carries no lifecycle property,
|
|
1481
|
+
* and a body-level `msg.ack()`/`msg.retry()` is exactly the signal we want.
|
|
1482
|
+
*/
|
|
1483
|
+
function loopBodyContainsMessageLifecycleCall(loopNode, adapter, sourceCode) {
|
|
1484
|
+
let found = false;
|
|
1485
|
+
walkSubtree(loopNode, adapter, (node) => {
|
|
1486
|
+
if (found || adapter.getNodeType(node) !== 'member_expression')
|
|
1487
|
+
return;
|
|
1488
|
+
const prop = memberPropertyName(node, adapter, sourceCode);
|
|
1489
|
+
if (prop && MESSAGE_LIFECYCLE_METHODS.has(prop))
|
|
1490
|
+
found = true;
|
|
1491
|
+
});
|
|
1492
|
+
return found;
|
|
1493
|
+
}
|
|
1494
|
+
/** Depth-first walk over an ASTNode subtree (children only, no parent links). */
|
|
1495
|
+
function walkSubtree(node, adapter, visitor) {
|
|
1496
|
+
visitor(node);
|
|
1497
|
+
for (const child of adapter.getChildren(node)) {
|
|
1498
|
+
walkSubtree(child, adapter, visitor);
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
/**
|
|
1502
|
+
* Classify a call_expression as an LLM/agent invocation via three unambiguous
|
|
1503
|
+
* signals: a provider/SDK namespace in the callee text, an LLM action verb in the
|
|
1504
|
+
* callee text, or a model-client identifier passed as an argument / used as the
|
|
1505
|
+
* member-callee object.
|
|
1506
|
+
*/
|
|
1507
|
+
function isLlmCallNode(node, adapter, sourceCode) {
|
|
1508
|
+
const children = adapter.getChildren(node);
|
|
1509
|
+
const callee = children.find(c => adapter.getNodeType(c) !== 'arguments');
|
|
1510
|
+
if (callee) {
|
|
1511
|
+
const calleeText = adapter.getNodeText(callee, sourceCode);
|
|
1512
|
+
if (LLM_PROVIDER_RE.test(calleeText) || LLM_ACTION_RE.test(calleeText))
|
|
1513
|
+
return true;
|
|
1514
|
+
// Member callee whose object is an LLM client: `model.chat(…)`, `llm.invoke(…)`.
|
|
1515
|
+
if (adapter.getNodeType(callee) === 'member_expression') {
|
|
1516
|
+
const obj = adapter.getChildren(callee).find(c => adapter.getNodeType(c) === 'identifier');
|
|
1517
|
+
if (obj && LLM_CLIENT_ARG_NAMES.has(adapter.getNodeText(obj, sourceCode)))
|
|
1518
|
+
return true;
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
// Argument signal: an LLM client identifier passed into the call, e.g.
|
|
1522
|
+
// `extractBuildsFromCorpus(db, model, …)`.
|
|
1523
|
+
const args = children.find(c => adapter.getNodeType(c) === 'arguments');
|
|
1524
|
+
if (args) {
|
|
1525
|
+
for (const arg of adapter.getChildren(args)) {
|
|
1526
|
+
if (adapter.getNodeType(arg) === 'identifier' &&
|
|
1527
|
+
LLM_CLIENT_ARG_NAMES.has(adapter.getNodeText(arg, sourceCode))) {
|
|
1528
|
+
return true;
|
|
1529
|
+
}
|
|
1530
|
+
// Member access argument (`client.messages`) — check its object identifier.
|
|
1531
|
+
if (adapter.getNodeType(arg) === 'member_expression') {
|
|
1532
|
+
const obj = adapter.getChildren(arg).find(c => adapter.getNodeType(c) === 'identifier');
|
|
1533
|
+
if (obj && LLM_CLIENT_ARG_NAMES.has(adapter.getNodeText(obj, sourceCode)))
|
|
1534
|
+
return true;
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
return false;
|
|
1539
|
+
}
|
|
1296
1540
|
/** True when `node` is a function call whose callee is DB-provenanced. */
|
|
1297
1541
|
function isDBProvenancedFunctionCall(node, adapter, sourceCode, provenanceContext) {
|
|
1298
1542
|
return isFunctionCall(node, adapter) &&
|