circle-ir 3.193.0 → 3.195.0

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.
Files changed (53) hide show
  1. package/configs/README.md +39 -0
  2. package/dist/analysis/config-loader.d.ts.map +1 -1
  3. package/dist/analysis/config-loader.js +43 -3
  4. package/dist/analysis/config-loader.js.map +1 -1
  5. package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
  6. package/dist/analysis/passes/language-sources-pass.js +101 -0
  7. package/dist/analysis/passes/language-sources-pass.js.map +1 -1
  8. package/dist/analysis/passes/prompt-injection-safety-gate-pass.d.ts +75 -0
  9. package/dist/analysis/passes/prompt-injection-safety-gate-pass.d.ts.map +1 -0
  10. package/dist/analysis/passes/prompt-injection-safety-gate-pass.js +353 -0
  11. package/dist/analysis/passes/prompt-injection-safety-gate-pass.js.map +1 -0
  12. package/dist/analysis/passes/speculative-prompt-param-source-pass.d.ts +67 -0
  13. package/dist/analysis/passes/speculative-prompt-param-source-pass.d.ts.map +1 -0
  14. package/dist/analysis/passes/speculative-prompt-param-source-pass.js +193 -0
  15. package/dist/analysis/passes/speculative-prompt-param-source-pass.js.map +1 -0
  16. package/dist/analysis/taint-matcher.d.ts.map +1 -1
  17. package/dist/analysis/taint-matcher.js +24 -8
  18. package/dist/analysis/taint-matcher.js.map +1 -1
  19. package/dist/analyzer.d.ts +19 -0
  20. package/dist/analyzer.d.ts.map +1 -1
  21. package/dist/analyzer.js +30 -0
  22. package/dist/analyzer.js.map +1 -1
  23. package/dist/browser/circle-ir.js +587 -597
  24. package/dist/core/circle-ir-core.cjs +90 -15
  25. package/dist/core/circle-ir-core.js +90 -15
  26. package/dist/core/extractors/dfg.js +46 -2
  27. package/dist/core/extractors/dfg.js.map +1 -1
  28. package/dist/languages/plugins/bash.d.ts.map +1 -1
  29. package/dist/languages/plugins/bash.js +12 -16
  30. package/dist/languages/plugins/bash.js.map +1 -1
  31. package/dist/languages/plugins/go.d.ts +4 -0
  32. package/dist/languages/plugins/go.d.ts.map +1 -1
  33. package/dist/languages/plugins/go.js +16 -32
  34. package/dist/languages/plugins/go.js.map +1 -1
  35. package/dist/languages/plugins/java.d.ts +9 -1
  36. package/dist/languages/plugins/java.d.ts.map +1 -1
  37. package/dist/languages/plugins/java.js +16 -127
  38. package/dist/languages/plugins/java.js.map +1 -1
  39. package/dist/languages/plugins/javascript.d.ts +7 -0
  40. package/dist/languages/plugins/javascript.d.ts.map +1 -1
  41. package/dist/languages/plugins/javascript.js +10 -117
  42. package/dist/languages/plugins/javascript.js.map +1 -1
  43. package/dist/languages/plugins/python.d.ts +7 -0
  44. package/dist/languages/plugins/python.d.ts.map +1 -1
  45. package/dist/languages/plugins/python.js +24 -177
  46. package/dist/languages/plugins/python.js.map +1 -1
  47. package/dist/languages/plugins/rust.d.ts +9 -0
  48. package/dist/languages/plugins/rust.d.ts.map +1 -1
  49. package/dist/languages/plugins/rust.js +22 -110
  50. package/dist/languages/plugins/rust.js.map +1 -1
  51. package/dist/types/config.d.ts +15 -0
  52. package/dist/types/config.d.ts.map +1 -1
  53. package/package.json +3 -2
@@ -9935,6 +9935,7 @@ var GO_KEYWORDS = /* @__PURE__ */ new Set([
9935
9935
  function buildGoDFG(tree) {
9936
9936
  const defs = [];
9937
9937
  const uses = [];
9938
+ const extraChains = [];
9938
9939
  let defIdCounter = 1;
9939
9940
  let useIdCounter = 1;
9940
9941
  const scopeStack = [/* @__PURE__ */ new Map()];
@@ -9956,7 +9957,7 @@ function buildGoDFG(tree) {
9956
9957
  }
9957
9958
  const body2 = func2.childForFieldName("body");
9958
9959
  if (body2) {
9959
- processGoBlock(body2, defs, uses, scopeStack, { defId: defIdCounter, useId: useIdCounter });
9960
+ processGoBlock(body2, defs, uses, scopeStack, { defId: defIdCounter, useId: useIdCounter }, extraChains);
9960
9961
  defIdCounter = defs.length + 1;
9961
9962
  useIdCounter = uses.length + 1;
9962
9963
  }
@@ -9971,6 +9972,15 @@ function buildGoDFG(tree) {
9971
9972
  }
9972
9973
  }
9973
9974
  const chains = computeChains(defs, uses);
9975
+ const seen = new Set(chains.map((c) => `${c.from_def}|${c.to_def}|${c.via}`));
9976
+ for (const ch of extraChains) {
9977
+ const key = `${ch.from_def}|${ch.to_def}|${ch.via}`;
9978
+ if (!seen.has(key)) {
9979
+ seen.add(key);
9980
+ chains.push(ch);
9981
+ }
9982
+ }
9983
+ chains.sort((a, b) => a.from_def - b.from_def || a.to_def - b.to_def);
9974
9984
  return { defs, uses, chains };
9975
9985
  }
9976
9986
  function extractGoParamDefs(params, defs, _startId, scopeStack) {
@@ -9995,28 +10005,47 @@ function extractGoParamDefs(params, defs, _startId, scopeStack) {
9995
10005
  }
9996
10006
  }
9997
10007
  }
9998
- function processGoBlock(node, defs, uses, scopeStack, counters) {
10008
+ function processGoBlock(node, defs, uses, scopeStack, counters, extraChains = []) {
10009
+ const linkMultiLineRhs = (right, useStart, useEnd, defStart, defEnd) => {
10010
+ if (right.endPosition.row <= right.startPosition.row) return;
10011
+ for (let ui = useStart; ui < useEnd; ui++) {
10012
+ const u = uses[ui];
10013
+ if (!u || u.def_id === null) continue;
10014
+ for (let di = defStart; di < defEnd; di++) {
10015
+ const d = defs[di];
10016
+ if (d && d.kind === "local" && d.id !== u.def_id) {
10017
+ extraChains.push({ from_def: u.def_id, to_def: d.id, via: u.variable });
10018
+ }
10019
+ }
10020
+ }
10021
+ };
9999
10022
  walkTree(node, (child) => {
10000
10023
  if (child.type === "short_var_declaration") {
10001
10024
  const left = child.childForFieldName("left");
10002
10025
  const right = child.childForFieldName("right");
10026
+ const usesBefore = uses.length;
10003
10027
  if (right) {
10004
10028
  extractGoUses(right, uses, scopeStack);
10005
10029
  }
10030
+ const defsBefore = defs.length;
10006
10031
  if (left) {
10007
10032
  extractGoLhsDefs(left, defs, scopeStack, child.startPosition.row + 1);
10008
10033
  }
10034
+ if (right) linkMultiLineRhs(right, usesBefore, uses.length, defsBefore, defs.length);
10009
10035
  } else if (child.type === "var_declaration") {
10010
10036
  processGoVarDecl(child, defs, scopeStack, counters);
10011
10037
  } else if (child.type === "assignment_statement") {
10012
10038
  const left = child.childForFieldName("left");
10013
10039
  const right = child.childForFieldName("right");
10040
+ const usesBefore = uses.length;
10014
10041
  if (right) {
10015
10042
  extractGoUses(right, uses, scopeStack);
10016
10043
  }
10044
+ const defsBefore = defs.length;
10017
10045
  if (left) {
10018
10046
  extractGoLhsDefs(left, defs, scopeStack, child.startPosition.row + 1);
10019
10047
  }
10048
+ if (right) linkMultiLineRhs(right, usesBefore, uses.length, defsBefore, defs.length);
10020
10049
  } else if (child.type === "for_statement") {
10021
10050
  const rangeClause = findChildByTypeGo(child, "range_clause");
10022
10051
  if (rangeClause) {
@@ -11331,7 +11360,11 @@ var DEFAULT_SOURCES = [
11331
11360
  { method: "Json", type: "http_body", severity: "high", return_tainted: true, languages: ["rust"] },
11332
11361
  { method: "Query", type: "http_param", severity: "high", return_tainted: true, languages: ["rust"] },
11333
11362
  { method: "Path", type: "http_path", severity: "high", return_tainted: true, languages: ["rust"] },
11334
- { method: "Form", type: "http_param", severity: "high", return_tainted: true, languages: ["rust"] },
11363
+ // `Form<T>` decodes the urlencoded request *body* (`Query<T>` above is the
11364
+ // query-string extractor), so it is http_body — retyped from http_param
11365
+ // when the RustPlugin's http_body duplicate was removed, so the two
11366
+ // surfaces no longer disagree. Matches Gin's `Context.PostForm`.
11367
+ { method: "Form", type: "http_body", severity: "high", return_tainted: true, languages: ["rust"] },
11335
11368
  // Rust std library
11336
11369
  { method: "var", class: "env", type: "env_input", severity: "medium", return_tainted: true },
11337
11370
  { method: "var_os", class: "env", type: "env_input", severity: "medium", return_tainted: true },
@@ -12609,7 +12642,11 @@ var DEFAULT_SINKS = [
12609
12642
  // Also match without receiver (destructured imports: const { exec } = require('child_process'))
12610
12643
  // `exec` is intentionally classless: catches Node.js child_process.exec AND
12611
12644
  // Java Runtime.exec (via `r.exec()` where heuristic can't resolve r → Runtime).
12612
- { method: "exec", type: "command_injection", cwe: "CWE-78", severity: "high", arg_positions: [0] },
12645
+ // Excluded from Python: there, bare `exec(code)` is the code-execution
12646
+ // builtin, registered below as code_injection / CWE-94. Before the exclusion
12647
+ // a Python `exec(user_code)` matched both patterns and emitted two findings
12648
+ // for one call — CWE-78 (wrong: no shell is involved) and CWE-94 (right).
12649
+ { method: "exec", type: "command_injection", cwe: "CWE-78", severity: "high", arg_positions: [0], exclude_languages: ["python"] },
12613
12650
  // `execSync`/`spawn`/`spawnSync`/`execFile` are Node-specific — language-scope them.
12614
12651
  { method: "execSync", type: "command_injection", cwe: "CWE-78", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
12615
12652
  { method: "spawn", type: "command_injection", cwe: "CWE-78", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
@@ -12637,7 +12674,11 @@ var DEFAULT_SINKS = [
12637
12674
  // Note: classless { method: 'query' } removed — too many FPs (UriComponentsBuilder.query(), etc.)
12638
12675
  // SQL query calls are covered by class-specific patterns above (Connection, Pool, Client, JdbcTemplate)
12639
12676
  // Note: `raw` is shared with Python (Django ORM) — scoped to JS+TS to avoid leaking.
12640
- { method: "raw", type: "sql_injection", cwe: "CWE-89", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
12677
+ // Severity `critical` (not `high`) preserves what the JS plugin's duplicate
12678
+ // builtin emitted before the #4 follow-up consolidation removed it: the
12679
+ // plugin copy carried `critical`, which won the `findSinks` dedup slot via
12680
+ // the +0.1 critical-severity confidence boost.
12681
+ { method: "raw", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["javascript", "typescript"] },
12641
12682
  // sqlite3 (npm) — Database/Statement methods. The JS plugin resolves
12642
12683
  // `const db = new sqlite3.Database(...); db.all(sql)` to the resolution
12643
12684
  // target `Connection.all`, so class-scoped patterns matching `Connection`
@@ -13196,10 +13237,30 @@ var DEFAULT_SINKS = [
13196
13237
  { method: "Sprintf", class: "fmt", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [0], languages: ["go"] },
13197
13238
  { method: "Printf", class: "fmt", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [0], languages: ["go"] },
13198
13239
  { method: "Errorf", class: "fmt", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [0], languages: ["go"] },
13240
+ // `fmt.Fprintf` is deliberately dual-classified: this CWE-134 entry and the
13241
+ // Go plugin's xss (CWE-79) entry both cover arg[1], so a tainted format
13242
+ // string emits two findings. Unlike the logger case above, neither is
13243
+ // redundant — CWE-134 holds for any writer, while CWE-79 is the actionable
13244
+ // finding when the writer is an http.ResponseWriter (`fmt.Fprintf(w, taint)`
13245
+ // is the standard Go reflected-XSS shape). Collapsing it requires deciding
13246
+ // by the *type of arg[0]*, which sink patterns cannot express today: the
13247
+ // options are a Go-specific gate (mirroring isSafeGoJsonUnmarshalCall) that
13248
+ // emits xss only for a resolved ResponseWriter, or dropping one CWE and
13249
+ // accepting either lost XSS recall or a mis-classified stderr write.
13250
+ // Left as-is pending the Go corpus run — see tasks.md.
13199
13251
  { method: "Fprintf", class: "fmt", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [1], languages: ["go"] },
13200
13252
  // cognium-dev #264 — Go stdlib `log` package format-string entry points.
13201
13253
  // log.Printf / Fatalf / Panicf take the format string at arg[0]; tainted
13202
13254
  // format string reaches the same fmt.Sprintf machinery internally.
13255
+ //
13256
+ // These deliberately coexist with the Go plugin's log_injection (CWE-117)
13257
+ // entries for the same methods, so such a call emits both. The Logger
13258
+ // decision note in the Java log_injection section above does NOT apply
13259
+ // here: it reasons about SLF4J-style APIs where the format string is a
13260
+ // literal with `{}` placeholders, whereas Go's `log.Printf(fmt, ...)` takes
13261
+ // a real format string that an attacker can control. CWE-117 covers the
13262
+ // forged log line, CWE-134 the format string itself. Locked by
13263
+ // tests/analysis/passes/format-string-additions.test.ts.
13203
13264
  { method: "Printf", class: "log", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [0], languages: ["go"] },
13204
13265
  { method: "Fatalf", class: "log", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [0], languages: ["go"] },
13205
13266
  { method: "Panicf", class: "log", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [0], languages: ["go"] },
@@ -13213,6 +13274,14 @@ var DEFAULT_SINKS = [
13213
13274
  { method: "cookie", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1], languages: ["javascript", "typescript"] },
13214
13275
  // Express: res.location(url) and res.redirect(url) — Location header.
13215
13276
  { method: "location", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [0], languages: ["javascript", "typescript"] },
13277
+ // `redirect` is deliberately dual-classified: this crlf (CWE-113) entry
13278
+ // coexists with the open_redirect (CWE-601) entry for js/ts, so a tainted
13279
+ // redirect target emits two flows. That is intentional, not duplication —
13280
+ // #189 Sprint 82 shipped a sink-type-aware flow dedup specifically so both
13281
+ // survive ("res.redirect is both open_redirect AND crlf"), and #132 added a
13282
+ // crlf recall test for the bare `res.redirect(req.query.url)` shape. Locked
13283
+ // by issue-189-sprint82-open-redirect-cluster.test.ts and
13284
+ // crlf-stage8-fp.test.ts — do not collapse without revisiting both tickets.
13216
13285
  { method: "redirect", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [0], languages: ["javascript", "typescript"] },
13217
13286
  // Go net/http: w.Header().Set(k, v) / Add(k, v) — first arg is the value
13218
13287
  // (Header is a map; the actual `value` is arg 1 of the call). We flag the
@@ -14133,18 +14202,28 @@ function analyzeTaint(calls, types, config = getDefaultConfig(), typeHierarchy,
14133
14202
  const sanitizers = findSanitizers(calls, types, config.sanitizers, sourceLines);
14134
14203
  return { sources, sinks, sanitizers };
14135
14204
  }
14205
+ function sinkPatternAppliesTo(pattern, language) {
14206
+ if (language === void 0) return true;
14207
+ if (pattern.languages && pattern.languages.length > 0 && !pattern.languages.includes(language)) {
14208
+ return false;
14209
+ }
14210
+ if (pattern.exclude_languages && pattern.exclude_languages.includes(language)) {
14211
+ return false;
14212
+ }
14213
+ return true;
14214
+ }
14136
14215
  function expandPromisifyAliases(patterns, sourceLines, language) {
14137
14216
  if (!sourceLines || sourceLines.length === 0) return patterns;
14138
14217
  if (language !== "javascript" && language !== "typescript") return patterns;
14139
14218
  const PROMISIFY_RE = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:util\s*\.\s*)?promisify\s*\(\s*([A-Za-z_$][\w$]*)\s*\)/;
14140
14219
  const innerToPattern = /* @__PURE__ */ new Map();
14141
14220
  for (const p of patterns) {
14142
- if (p.languages && !p.languages.includes(language)) continue;
14221
+ if (!sinkPatternAppliesTo(p, language)) continue;
14143
14222
  if (p.class !== void 0) continue;
14144
14223
  if (!innerToPattern.has(p.method)) innerToPattern.set(p.method, p);
14145
14224
  }
14146
14225
  for (const p of patterns) {
14147
- if (p.languages && !p.languages.includes(language)) continue;
14226
+ if (!sinkPatternAppliesTo(p, language)) continue;
14148
14227
  if (innerToPattern.has(p.method)) continue;
14149
14228
  innerToPattern.set(p.method, p);
14150
14229
  }
@@ -14178,7 +14257,7 @@ function expandIndirectEvalAliases(patterns, sourceLines, language) {
14178
14257
  const INDIRECT_EVAL_RE = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*(?::[^=]+)?=\s*(eval|Function)\b(?!\s*\()/;
14179
14258
  const innerToPattern = /* @__PURE__ */ new Map();
14180
14259
  for (const p of patterns) {
14181
- if (p.languages && !p.languages.includes(language)) continue;
14260
+ if (!sinkPatternAppliesTo(p, language)) continue;
14182
14261
  if (p.class !== void 0) continue;
14183
14262
  if (p.method !== "eval" && p.method !== "Function") continue;
14184
14263
  if (!innerToPattern.has(p.method)) innerToPattern.set(p.method, p);
@@ -14886,9 +14965,7 @@ function isSafeJinjaRenderCall(call, pattern, language, sourceLines) {
14886
14965
  return false;
14887
14966
  }
14888
14967
  function findSinks(calls, patterns, typeHierarchy, language, sourceLines, types) {
14889
- const patternsForLanguage = language === void 0 ? patterns : patterns.filter(
14890
- (p) => !p.languages || p.languages.length === 0 || p.languages.includes(language)
14891
- );
14968
+ const patternsForLanguage = language === void 0 ? patterns : patterns.filter((p) => sinkPatternAppliesTo(p, language));
14892
14969
  const sinkMap = /* @__PURE__ */ new Map();
14893
14970
  for (const call of calls) {
14894
14971
  for (const pattern of patternsForLanguage) {
@@ -15233,10 +15310,8 @@ var SINK_FQN_EXCLUSIONS = {
15233
15310
  ]
15234
15311
  };
15235
15312
  function matchesSinkPattern(call, pattern, typeHierarchy, language) {
15236
- if (pattern.languages && pattern.languages.length > 0 && language !== void 0) {
15237
- if (!pattern.languages.includes(language)) {
15238
- return false;
15239
- }
15313
+ if (!sinkPatternAppliesTo(pattern, language)) {
15314
+ return false;
15240
15315
  }
15241
15316
  const callMethodName = call.method_name;
15242
15317
  const patternMethod = pattern.method;
@@ -22117,107 +22192,30 @@ var JavaPlugin = class extends BaseLanguagePlugin {
22117
22192
  severity: "high",
22118
22193
  confidence: 0.9,
22119
22194
  returnTainted: true
22120
- },
22121
- // Servlet API
22122
- {
22123
- method: "getParameter",
22124
- class: "HttpServletRequest",
22125
- type: "http_param",
22126
- severity: "high",
22127
- confidence: 0.95,
22128
- returnTainted: true
22129
- },
22130
- {
22131
- method: "getParameterValues",
22132
- class: "HttpServletRequest",
22133
- type: "http_param",
22134
- severity: "high",
22135
- confidence: 0.95,
22136
- returnTainted: true
22137
- },
22138
- {
22139
- method: "getHeader",
22140
- class: "HttpServletRequest",
22141
- type: "http_header",
22142
- severity: "high",
22143
- confidence: 0.9,
22144
- returnTainted: true
22145
- },
22146
- {
22147
- method: "getCookies",
22148
- class: "HttpServletRequest",
22149
- type: "http_cookie",
22150
- severity: "high",
22151
- confidence: 0.9,
22152
- returnTainted: true
22153
- },
22154
- {
22155
- method: "getInputStream",
22156
- class: "HttpServletRequest",
22157
- type: "http_body",
22158
- severity: "high",
22159
- confidence: 0.85,
22160
- returnTainted: true
22161
- },
22162
- {
22163
- method: "getReader",
22164
- class: "HttpServletRequest",
22165
- type: "http_body",
22166
- severity: "high",
22167
- confidence: 0.85,
22168
- returnTainted: true
22169
22195
  }
22196
+ // NOTE: the Servlet API call sources (`HttpServletRequest.getParameter`,
22197
+ // `getParameterValues`, `getHeader`, `getCookies`, `getInputStream`,
22198
+ // `getReader`) are registered identically in DEFAULT_SOURCES. Because
22199
+ // `findSources` pushes one TaintSource per matching pattern with no
22200
+ // dedup, the duplicates emitted a second, byte-identical source at every
22201
+ // call site. Removed in the #4 follow-up consolidation — DEFAULT_SOURCES
22202
+ // is the canonical registry (ADR-004).
22170
22203
  ];
22171
22204
  }
22172
22205
  /**
22173
22206
  * Java-specific taint sink patterns.
22174
- * These supplement the YAML configuration.
22207
+ *
22208
+ * Only patterns that have NO counterpart in `DEFAULT_SINKS` belong here —
22209
+ * `DEFAULT_SINKS` (`analysis/config-loader.ts`) is the canonical registry
22210
+ * (ADR-004). Duplicated entries were dead weight: `TaintMatcherPass` merges
22211
+ * plugin builtins *after* the config patterns, and `findSinks` dedupes by
22212
+ * `location:line:cwe` keeping the higher-confidence match, so the config
22213
+ * copy always won the slot. Removed in the #4 follow-up consolidation
22214
+ * (`Statement.execute*`, `Runtime.exec`, `ProcessBuilder.start`,
22215
+ * `PrintWriter.write/println`, `DirContext.search`, `XPath.evaluate`).
22175
22216
  */
22176
22217
  getBuiltinSinks() {
22177
22218
  return [
22178
- // SQL Injection
22179
- {
22180
- method: "executeQuery",
22181
- class: "Statement",
22182
- type: "sql_injection",
22183
- cwe: "CWE-89",
22184
- severity: "critical",
22185
- argPositions: [0]
22186
- },
22187
- {
22188
- method: "executeUpdate",
22189
- class: "Statement",
22190
- type: "sql_injection",
22191
- cwe: "CWE-89",
22192
- severity: "critical",
22193
- argPositions: [0]
22194
- },
22195
- {
22196
- method: "execute",
22197
- class: "Statement",
22198
- type: "sql_injection",
22199
- cwe: "CWE-89",
22200
- severity: "critical",
22201
- argPositions: [0]
22202
- },
22203
- // Command Injection
22204
- {
22205
- method: "exec",
22206
- class: "Runtime",
22207
- type: "command_injection",
22208
- cwe: "CWE-78",
22209
- severity: "critical",
22210
- argPositions: [0]
22211
- },
22212
- {
22213
- method: "start",
22214
- class: "ProcessBuilder",
22215
- type: "command_injection",
22216
- cwe: "CWE-78",
22217
- severity: "critical",
22218
- argPositions: []
22219
- // Constructor args are dangerous
22220
- },
22221
22219
  // Path Traversal
22222
22220
  {
22223
22221
  method: "FileInputStream",
@@ -22233,41 +22231,6 @@ var JavaPlugin = class extends BaseLanguagePlugin {
22233
22231
  severity: "high",
22234
22232
  argPositions: [0]
22235
22233
  },
22236
- // XSS
22237
- {
22238
- method: "write",
22239
- class: "PrintWriter",
22240
- type: "xss",
22241
- cwe: "CWE-79",
22242
- severity: "high",
22243
- argPositions: [0]
22244
- },
22245
- {
22246
- method: "println",
22247
- class: "PrintWriter",
22248
- type: "xss",
22249
- cwe: "CWE-79",
22250
- severity: "high",
22251
- argPositions: [0]
22252
- },
22253
- // LDAP Injection
22254
- {
22255
- method: "search",
22256
- class: "DirContext",
22257
- type: "ldap_injection",
22258
- cwe: "CWE-90",
22259
- severity: "high",
22260
- argPositions: [0, 1]
22261
- },
22262
- // XPath Injection
22263
- {
22264
- method: "evaluate",
22265
- class: "XPath",
22266
- type: "xpath_injection",
22267
- cwe: "CWE-643",
22268
- severity: "high",
22269
- argPositions: [0]
22270
- },
22271
22234
  // Deserialization
22272
22235
  {
22273
22236
  method: "readObject",
@@ -22826,42 +22789,17 @@ var JavaScriptPlugin = class extends BaseLanguagePlugin {
22826
22789
  }
22827
22790
  /**
22828
22791
  * JavaScript/TypeScript taint sink patterns.
22792
+ *
22793
+ * Only patterns that have NO counterpart in `DEFAULT_SINKS` belong here —
22794
+ * `DEFAULT_SINKS` (`analysis/config-loader.ts`) is the canonical registry
22795
+ * (ADR-004). Duplicated entries were dead weight: `TaintMatcherPass` merges
22796
+ * plugin builtins *after* the config patterns, and `findSinks` dedupes by
22797
+ * `location:line:cwe` keeping the higher-confidence match, so the config
22798
+ * copy always won the slot. Removed in the #4 follow-up consolidation.
22829
22799
  */
22830
22800
  getBuiltinSinks() {
22831
22801
  return [
22832
- // Command Injection
22833
- {
22834
- method: "exec",
22835
- class: "child_process",
22836
- type: "command_injection",
22837
- cwe: "CWE-78",
22838
- severity: "critical",
22839
- argPositions: [0]
22840
- },
22841
- {
22842
- method: "execSync",
22843
- class: "child_process",
22844
- type: "command_injection",
22845
- cwe: "CWE-78",
22846
- severity: "critical",
22847
- argPositions: [0]
22848
- },
22849
- {
22850
- method: "spawn",
22851
- class: "child_process",
22852
- type: "command_injection",
22853
- cwe: "CWE-78",
22854
- severity: "critical",
22855
- argPositions: [0, 1]
22856
- },
22857
22802
  // Code Injection
22858
- {
22859
- method: "eval",
22860
- type: "code_injection",
22861
- cwe: "CWE-94",
22862
- severity: "critical",
22863
- argPositions: [0]
22864
- },
22865
22803
  {
22866
22804
  method: "Function",
22867
22805
  type: "code_injection",
@@ -22885,46 +22823,7 @@ var JavaScriptPlugin = class extends BaseLanguagePlugin {
22885
22823
  argPositions: [0]
22886
22824
  // When first arg is string
22887
22825
  },
22888
- // Path Traversal
22889
- {
22890
- method: "readFileSync",
22891
- class: "fs",
22892
- type: "path_traversal",
22893
- cwe: "CWE-22",
22894
- severity: "high",
22895
- argPositions: [0]
22896
- },
22897
- {
22898
- method: "writeFileSync",
22899
- class: "fs",
22900
- type: "path_traversal",
22901
- cwe: "CWE-22",
22902
- severity: "high",
22903
- argPositions: [0]
22904
- },
22905
- {
22906
- method: "createReadStream",
22907
- class: "fs",
22908
- type: "path_traversal",
22909
- cwe: "CWE-22",
22910
- severity: "high",
22911
- argPositions: [0]
22912
- },
22913
22826
  // XSS (DOM)
22914
- {
22915
- method: "innerHTML",
22916
- type: "xss",
22917
- cwe: "CWE-79",
22918
- severity: "high",
22919
- argPositions: [0]
22920
- },
22921
- {
22922
- method: "outerHTML",
22923
- type: "xss",
22924
- cwe: "CWE-79",
22925
- severity: "high",
22926
- argPositions: [0]
22927
- },
22928
22827
  {
22929
22828
  method: "document.write",
22930
22829
  type: "xss",
@@ -22976,13 +22875,6 @@ var JavaScriptPlugin = class extends BaseLanguagePlugin {
22976
22875
  severity: "critical",
22977
22876
  argPositions: [0]
22978
22877
  },
22979
- {
22980
- method: "raw",
22981
- type: "sql_injection",
22982
- cwe: "CWE-89",
22983
- severity: "critical",
22984
- argPositions: [0]
22985
- },
22986
22878
  // Prisma ORM - unsafe raw query methods ($executeRaw/$queryRaw with template literals are safe/parameterized)
22987
22879
  {
22988
22880
  method: "$executeRawUnsafe",
@@ -22998,30 +22890,6 @@ var JavaScriptPlugin = class extends BaseLanguagePlugin {
22998
22890
  severity: "critical",
22999
22891
  argPositions: [0]
23000
22892
  },
23001
- // SSRF
23002
- {
23003
- method: "fetch",
23004
- type: "ssrf",
23005
- cwe: "CWE-918",
23006
- severity: "high",
23007
- argPositions: [0]
23008
- },
23009
- {
23010
- method: "get",
23011
- class: "axios",
23012
- type: "ssrf",
23013
- cwe: "CWE-918",
23014
- severity: "high",
23015
- argPositions: [0]
23016
- },
23017
- {
23018
- method: "request",
23019
- class: "http",
23020
- type: "ssrf",
23021
- cwe: "CWE-918",
23022
- severity: "high",
23023
- argPositions: [0]
23024
- },
23025
22893
  // NoSQL Injection
23026
22894
  {
23027
22895
  method: "find",
@@ -23030,13 +22898,6 @@ var JavaScriptPlugin = class extends BaseLanguagePlugin {
23030
22898
  severity: "high",
23031
22899
  argPositions: [0]
23032
22900
  },
23033
- {
23034
- method: "findOne",
23035
- type: "nosql_injection",
23036
- cwe: "CWE-943",
23037
- severity: "high",
23038
- argPositions: [0]
23039
- },
23040
22901
  // Prototype Pollution (JS-specific)
23041
22902
  {
23042
22903
  method: "merge",
@@ -23130,14 +22991,9 @@ var JavaScriptPlugin = class extends BaseLanguagePlugin {
23130
22991
  // =========================================================
23131
22992
  // Next.js Sinks
23132
22993
  // =========================================================
23133
- {
23134
- // Server-side redirect
23135
- method: "redirect",
23136
- type: "open_redirect",
23137
- cwe: "CWE-601",
23138
- severity: "high",
23139
- argPositions: [0]
23140
- },
22994
+ // NOTE: classless `redirect` (CWE-601) lives in DEFAULT_SINKS, scoped to
22995
+ // javascript+typescript — the duplicate builtin here never won the dedup
22996
+ // slot (equal confidence, config pattern iterates first).
23141
22997
  {
23142
22998
  // Router push with user-controlled URL
23143
22999
  method: "push",
@@ -23419,13 +23275,15 @@ var PythonPlugin = class extends BaseLanguagePlugin {
23419
23275
  returnTainted: true
23420
23276
  },
23421
23277
  // Standard library sources
23422
- {
23423
- method: "input",
23424
- type: "user_input",
23425
- severity: "high",
23426
- confidence: 0.95,
23427
- returnTainted: true
23428
- },
23278
+ //
23279
+ // `input()` and `os.getenv()` are registered in DEFAULT_SOURCES as
23280
+ // `io_input` / `env_input`. The duplicates that used to sit here typed
23281
+ // them `user_input` / `env_var` — strings outside the published
23282
+ // `SourceType` union, which reach consumers only because the merge in
23283
+ // TaintMatcherPass casts `TaintSourcePattern.type` (a bare `string`) to
23284
+ // `SourceType`. Both entries fired, so one call emitted two sources with
23285
+ // two different type strings. Removed in favour of the canonical
23286
+ // registry (ADR-004).
23429
23287
  {
23430
23288
  method: "argv",
23431
23289
  class: "sys",
@@ -23441,137 +23299,28 @@ var PythonPlugin = class extends BaseLanguagePlugin {
23441
23299
  severity: "medium",
23442
23300
  confidence: 0.85,
23443
23301
  returnTainted: true
23444
- },
23445
- {
23446
- method: "getenv",
23447
- class: "os",
23448
- type: "env_var",
23449
- severity: "medium",
23450
- confidence: 0.85,
23451
- returnTainted: true
23452
- },
23453
- // File reading
23454
- {
23455
- method: "read",
23456
- type: "file_input",
23457
- severity: "medium",
23458
- confidence: 0.8,
23459
- returnTainted: true
23460
- },
23461
- {
23462
- method: "readline",
23463
- type: "file_input",
23464
- severity: "medium",
23465
- confidence: 0.8,
23466
- returnTainted: true
23467
- },
23468
- {
23469
- method: "readlines",
23470
- type: "file_input",
23471
- severity: "medium",
23472
- confidence: 0.8,
23473
- returnTainted: true
23474
23302
  }
23303
+ // NOTE: the file-reading sources (`read`, `readline`, `readlines` as
23304
+ // file_input) are registered identically in DEFAULT_SOURCES. Since
23305
+ // `findSources` pushes one TaintSource per matching pattern with no
23306
+ // dedup, these duplicates emitted a second, byte-identical source at
23307
+ // every call site. Removed in the #4 follow-up consolidation —
23308
+ // DEFAULT_SOURCES is the canonical registry (ADR-004).
23475
23309
  ];
23476
23310
  }
23477
23311
  /**
23478
23312
  * Python taint sink patterns.
23313
+ *
23314
+ * Only patterns that have NO counterpart in `DEFAULT_SINKS` belong here —
23315
+ * `DEFAULT_SINKS` (`analysis/config-loader.ts`) is the canonical registry
23316
+ * (ADR-004). Duplicated entries were dead weight: `TaintMatcherPass` merges
23317
+ * plugin builtins *after* the config patterns, and `findSinks` dedupes by
23318
+ * `location:line:cwe` keeping the higher-confidence match, so the config
23319
+ * copy always won the slot. Removed in the #4 follow-up consolidation.
23479
23320
  */
23480
23321
  getBuiltinSinks() {
23481
23322
  return [
23482
- // Command Injection
23483
- {
23484
- method: "system",
23485
- class: "os",
23486
- type: "command_injection",
23487
- cwe: "CWE-78",
23488
- severity: "critical",
23489
- argPositions: [0]
23490
- },
23491
- {
23492
- method: "popen",
23493
- class: "os",
23494
- type: "command_injection",
23495
- cwe: "CWE-78",
23496
- severity: "critical",
23497
- argPositions: [0]
23498
- },
23499
- {
23500
- method: "run",
23501
- class: "subprocess",
23502
- type: "command_injection",
23503
- cwe: "CWE-78",
23504
- severity: "critical",
23505
- argPositions: [0]
23506
- },
23507
- {
23508
- method: "call",
23509
- class: "subprocess",
23510
- type: "command_injection",
23511
- cwe: "CWE-78",
23512
- severity: "critical",
23513
- argPositions: [0]
23514
- },
23515
- {
23516
- method: "Popen",
23517
- class: "subprocess",
23518
- type: "command_injection",
23519
- cwe: "CWE-78",
23520
- severity: "critical",
23521
- argPositions: [0]
23522
- },
23523
- // Code Injection
23524
- {
23525
- method: "eval",
23526
- type: "code_injection",
23527
- cwe: "CWE-94",
23528
- severity: "critical",
23529
- argPositions: [0]
23530
- },
23531
- {
23532
- method: "exec",
23533
- type: "code_injection",
23534
- cwe: "CWE-94",
23535
- severity: "critical",
23536
- argPositions: [0]
23537
- },
23538
- {
23539
- method: "compile",
23540
- type: "code_injection",
23541
- cwe: "CWE-94",
23542
- severity: "high",
23543
- argPositions: [0]
23544
- },
23545
- // SQL Injection
23546
- {
23547
- method: "execute",
23548
- type: "sql_injection",
23549
- cwe: "CWE-89",
23550
- severity: "critical",
23551
- argPositions: [0]
23552
- },
23553
- {
23554
- method: "executemany",
23555
- type: "sql_injection",
23556
- cwe: "CWE-89",
23557
- severity: "critical",
23558
- argPositions: [0]
23559
- },
23560
- {
23561
- method: "raw",
23562
- type: "sql_injection",
23563
- cwe: "CWE-89",
23564
- severity: "critical",
23565
- argPositions: [0]
23566
- },
23567
23323
  // Path Traversal
23568
- {
23569
- method: "open",
23570
- type: "path_traversal",
23571
- cwe: "CWE-22",
23572
- severity: "high",
23573
- argPositions: [0]
23574
- },
23575
23324
  {
23576
23325
  method: "read",
23577
23326
  class: "pathlib",
@@ -23581,13 +23330,6 @@ var PythonPlugin = class extends BaseLanguagePlugin {
23581
23330
  argPositions: [0]
23582
23331
  },
23583
23332
  // XSS (template injection)
23584
- {
23585
- method: "Markup",
23586
- type: "xss",
23587
- cwe: "CWE-79",
23588
- severity: "high",
23589
- argPositions: [0]
23590
- },
23591
23333
  {
23592
23334
  method: "safe",
23593
23335
  type: "xss",
@@ -23596,22 +23338,6 @@ var PythonPlugin = class extends BaseLanguagePlugin {
23596
23338
  argPositions: [0]
23597
23339
  },
23598
23340
  // SSRF
23599
- {
23600
- method: "get",
23601
- class: "requests",
23602
- type: "ssrf",
23603
- cwe: "CWE-918",
23604
- severity: "high",
23605
- argPositions: [0]
23606
- },
23607
- {
23608
- method: "post",
23609
- class: "requests",
23610
- type: "ssrf",
23611
- cwe: "CWE-918",
23612
- severity: "high",
23613
- argPositions: [0]
23614
- },
23615
23341
  {
23616
23342
  method: "urlopen",
23617
23343
  class: "urllib",
@@ -23639,34 +23365,11 @@ var PythonPlugin = class extends BaseLanguagePlugin {
23639
23365
  argPositions: [1]
23640
23366
  },
23641
23367
  // Deserialization
23642
- {
23643
- method: "loads",
23644
- class: "pickle",
23645
- type: "deserialization",
23646
- cwe: "CWE-502",
23647
- severity: "critical",
23648
- argPositions: [0]
23649
- },
23650
- {
23651
- method: "load",
23652
- class: "pickle",
23653
- type: "deserialization",
23654
- cwe: "CWE-502",
23655
- severity: "critical",
23656
- argPositions: [0]
23657
- },
23658
23368
  // NOTE: yaml.safe_load / yaml.SafeLoader are intentionally NOT sinks.
23659
23369
  // safe_load constructs only standard scalar/list/dict types and cannot
23660
23370
  // instantiate arbitrary Python objects, so it is not a CWE-502 sink
23661
- // (unlike yaml.load / yaml.unsafe_load / yaml.full_load below).
23662
- {
23663
- method: "load",
23664
- class: "yaml",
23665
- type: "deserialization",
23666
- cwe: "CWE-502",
23667
- severity: "critical",
23668
- argPositions: [0]
23669
- },
23371
+ // (unlike yaml.load — registered in DEFAULT_SINKS — and
23372
+ // yaml.unsafe_load / yaml.full_load below).
23670
23373
  {
23671
23374
  method: "unsafe_load",
23672
23375
  class: "yaml",
@@ -23825,34 +23528,17 @@ var RustPlugin = class extends BaseLanguagePlugin {
23825
23528
  getBuiltinSources() {
23826
23529
  return [
23827
23530
  // Actix-web request extractors
23828
- {
23829
- method: "Query",
23830
- type: "http_param",
23831
- severity: "high",
23832
- confidence: 0.95,
23833
- returnTainted: true
23834
- },
23835
- {
23836
- method: "Json",
23837
- type: "http_body",
23838
- severity: "high",
23839
- confidence: 0.95,
23840
- returnTainted: true
23841
- },
23842
- {
23843
- method: "Path",
23844
- type: "http_path",
23845
- severity: "high",
23846
- confidence: 0.95,
23847
- returnTainted: true
23848
- },
23849
- {
23850
- method: "Form",
23851
- type: "http_body",
23852
- severity: "high",
23853
- confidence: 0.95,
23854
- returnTainted: true
23855
- },
23531
+ // NOTE: `Query` (http_param), `Json` (http_body) and `Path` (http_path)
23532
+ // are registered identically in DEFAULT_SOURCES; the duplicates here
23533
+ // emitted a second, byte-identical source at every call site
23534
+ // (`findSources` has no dedup). Removed in the #4 follow-up
23535
+ // consolidation — DEFAULT_SOURCES is the canonical registry (ADR-004).
23536
+ // `Form` stays because the two surfaces disagree on its type
23537
+ // `Form` is registered in DEFAULT_SOURCES, retyped there to `http_body`
23538
+ // to match this entry (actix/axum `Form<T>` decodes the urlencoded
23539
+ // request body, not the query string — `Query<T>` is the query one).
23540
+ // The duplicate that used to sit here made every extractor emit two
23541
+ // sources with different types.
23856
23542
  // Standard library sources
23857
23543
  {
23858
23544
  method: "args",
@@ -23902,20 +23588,22 @@ var RustPlugin = class extends BaseLanguagePlugin {
23902
23588
  severity: "medium",
23903
23589
  confidence: 0.8,
23904
23590
  returnTainted: true
23905
- },
23906
- // Network input
23907
- {
23908
- method: "read",
23909
- class: "TcpStream",
23910
- type: "network_input",
23911
- severity: "high",
23912
- confidence: 0.85,
23913
- returnTainted: true
23914
23591
  }
23592
+ // NOTE: `TcpStream.read` (network_input) is registered identically in
23593
+ // DEFAULT_SOURCES — duplicate removed in the #4 follow-up consolidation.
23915
23594
  ];
23916
23595
  }
23917
23596
  /**
23918
23597
  * Rust taint sink patterns.
23598
+ *
23599
+ * Only patterns that have NO counterpart in `DEFAULT_SINKS` belong here —
23600
+ * `DEFAULT_SINKS` (`analysis/config-loader.ts`) is the canonical registry
23601
+ * (ADR-004). Duplicated entries were dead weight: `TaintMatcherPass` merges
23602
+ * plugin builtins *after* the config patterns, and `findSinks` dedupes by
23603
+ * `location:line:cwe` keeping the higher-confidence match, so the config
23604
+ * copy always won the slot. Removed in the #4 follow-up consolidation
23605
+ * (`Command.arg/args`, classless `execute`, `File.open/create`,
23606
+ * `serde_json.from_str/from_slice`, `reqwest.get/post`).
23919
23607
  */
23920
23608
  getBuiltinSinks() {
23921
23609
  return [
@@ -23928,30 +23616,7 @@ var RustPlugin = class extends BaseLanguagePlugin {
23928
23616
  severity: "critical",
23929
23617
  argPositions: [0]
23930
23618
  },
23931
- {
23932
- method: "arg",
23933
- class: "Command",
23934
- type: "command_injection",
23935
- cwe: "CWE-78",
23936
- severity: "critical",
23937
- argPositions: [0]
23938
- },
23939
- {
23940
- method: "args",
23941
- class: "Command",
23942
- type: "command_injection",
23943
- cwe: "CWE-78",
23944
- severity: "critical",
23945
- argPositions: [0]
23946
- },
23947
23619
  // SQL Injection
23948
- {
23949
- method: "execute",
23950
- type: "sql_injection",
23951
- cwe: "CWE-89",
23952
- severity: "critical",
23953
- argPositions: [0]
23954
- },
23955
23620
  {
23956
23621
  method: "query",
23957
23622
  type: "sql_injection",
@@ -23967,22 +23632,6 @@ var RustPlugin = class extends BaseLanguagePlugin {
23967
23632
  argPositions: [0]
23968
23633
  },
23969
23634
  // Path Traversal
23970
- {
23971
- method: "open",
23972
- class: "File",
23973
- type: "path_traversal",
23974
- cwe: "CWE-22",
23975
- severity: "high",
23976
- argPositions: [0]
23977
- },
23978
- {
23979
- method: "create",
23980
- class: "File",
23981
- type: "path_traversal",
23982
- cwe: "CWE-22",
23983
- severity: "high",
23984
- argPositions: [0]
23985
- },
23986
23635
  {
23987
23636
  method: "read_to_string",
23988
23637
  class: "std::fs",
@@ -24022,40 +23671,6 @@ var RustPlugin = class extends BaseLanguagePlugin {
24022
23671
  severity: "critical",
24023
23672
  argPositions: [0]
24024
23673
  },
24025
- // Deserialization (with serde)
24026
- {
24027
- method: "from_str",
24028
- class: "serde_json",
24029
- type: "deserialization",
24030
- cwe: "CWE-502",
24031
- severity: "medium",
24032
- argPositions: [0]
24033
- },
24034
- {
24035
- method: "from_slice",
24036
- class: "serde_json",
24037
- type: "deserialization",
24038
- cwe: "CWE-502",
24039
- severity: "medium",
24040
- argPositions: [0]
24041
- },
24042
- // SSRF
24043
- {
24044
- method: "get",
24045
- class: "reqwest",
24046
- type: "ssrf",
24047
- cwe: "CWE-918",
24048
- severity: "high",
24049
- argPositions: [0]
24050
- },
24051
- {
24052
- method: "post",
24053
- class: "reqwest",
24054
- type: "ssrf",
24055
- cwe: "CWE-918",
24056
- severity: "high",
24057
- argPositions: [0]
24058
- },
24059
23674
  // Regex DoS
24060
23675
  {
24061
23676
  method: "new",
@@ -24170,14 +23785,13 @@ var BashPlugin = class extends BaseLanguagePlugin {
24170
23785
  */
24171
23786
  getBuiltinSources() {
24172
23787
  return [
24173
- // read built-in reads user input from stdin.
24174
- {
24175
- method: "read",
24176
- type: "io_input",
24177
- severity: "high",
24178
- confidence: 0.9,
24179
- returnTainted: true
24180
- },
23788
+ // NOTE: no `read` entry here. The `read` builtin is covered precisely by
23789
+ // `findBashTaintSources` (language-sources-pass, #213 slice 3), which
23790
+ // emits `io_input` *with the bound variable name* for `read x`,
23791
+ // `read -p "…" x`, `read -a arr`, bare `read` → `$REPLY`, etc. This
23792
+ // pattern-based duplicate added a second, variable-less source on the
23793
+ // same line, and typed it differently from the classless `read`
23794
+ // (file_input) entry in DEFAULT_SOURCES that it collided with.
24181
23795
  // curl/wget output as taint sources for supply-chain attack patterns
24182
23796
  // (e.g., curl ... | sh, $(curl ...) piped to eval/bash).
24183
23797
  {
@@ -24203,14 +23817,11 @@ var BashPlugin = class extends BaseLanguagePlugin {
24203
23817
  */
24204
23818
  getBuiltinSinks() {
24205
23819
  return [
24206
- // Code / command injection via eval
24207
- {
24208
- method: "eval",
24209
- type: "code_injection",
24210
- cwe: "CWE-94",
24211
- severity: "critical",
24212
- argPositions: [0]
24213
- },
23820
+ // NOTE: classless `eval` (CWE-94, critical) is registered unscoped in
23821
+ // DEFAULT_SINKS and already covers Bash; the duplicate builtin that used
23822
+ // to sit here never won the findSinks dedup slot (equal confidence, the
23823
+ // config pattern iterates first). Removed in the #4 follow-up
23824
+ // consolidation — DEFAULT_SINKS is the canonical registry (ADR-004).
24214
23825
  // Command injection: spawning a sub-shell with -c flag
24215
23826
  {
24216
23827
  method: "bash",
@@ -24652,30 +24263,15 @@ var GoPlugin = class extends BaseLanguagePlugin {
24652
24263
  returnTainted: true
24653
24264
  },
24654
24265
  // Gin framework
24655
- {
24656
- method: "Query",
24657
- class: "Context",
24658
- type: "http_param",
24659
- severity: "high",
24660
- confidence: 0.95,
24661
- returnTainted: true
24662
- },
24663
- {
24664
- method: "Param",
24665
- class: "Context",
24666
- type: "http_path",
24667
- severity: "high",
24668
- confidence: 0.95,
24669
- returnTainted: true
24670
- },
24671
- {
24672
- method: "PostForm",
24673
- class: "Context",
24674
- type: "http_param",
24675
- severity: "high",
24676
- confidence: 0.95,
24677
- returnTainted: true
24678
- },
24266
+ // NOTE: `Context.Query` (http_param) and `Context.Param` (http_path) are
24267
+ // registered identically in DEFAULT_SOURCES; the duplicates here emitted
24268
+ // a second, byte-identical source at every call site (`findSources` has
24269
+ // no dedup). Removed in the #4 follow-up consolidation — DEFAULT_SOURCES
24270
+ // is the canonical registry (ADR-004). `PostForm` stays because the two
24271
+ // `Context.PostForm` is registered in DEFAULT_SOURCES as `http_body`,
24272
+ // which is the accurate type — Gin reads it from the POST body, not the
24273
+ // query string. The duplicate that used to sit here typed it
24274
+ // `http_param`, so one call emitted two sources with different types.
24679
24275
  {
24680
24276
  method: "GetRawData",
24681
24277
  class: "Context",
@@ -24721,6 +24317,10 @@ var GoPlugin = class extends BaseLanguagePlugin {
24721
24317
  }
24722
24318
  /**
24723
24319
  * Go taint sink patterns.
24320
+ *
24321
+ * Only patterns that have NO counterpart in `DEFAULT_SINKS` belong here —
24322
+ * `DEFAULT_SINKS` (`analysis/config-loader.ts`) is the canonical registry
24323
+ * (ADR-004). See the #4 follow-up consolidation note on the other plugins.
24724
24324
  */
24725
24325
  getBuiltinSinks() {
24726
24326
  return [
@@ -24897,14 +24497,9 @@ var GoPlugin = class extends BaseLanguagePlugin {
24897
24497
  severity: "medium",
24898
24498
  argPositions: [0]
24899
24499
  },
24900
- {
24901
- method: "Decode",
24902
- class: "Decoder",
24903
- type: "deserialization",
24904
- cwe: "CWE-502",
24905
- severity: "medium",
24906
- argPositions: [0]
24907
- },
24500
+ // NOTE: `Decoder.Decode` (CWE-502) lives in DEFAULT_SINKS at severity
24501
+ // `high`, which outranked this duplicate's `medium` in the findSinks
24502
+ // dedup — removed in the #4 follow-up consolidation.
24908
24503
  // Log Injection — `log.Printf("event=%s", userInput)` allows
24909
24504
  // CRLF/log forging when the format string interpolates user data.
24910
24505
  // CWE-117. (cognium-dev #107)
@@ -26254,6 +25849,7 @@ var LanguageSourcesPass = class {
26254
25849
  additionalSanitizers.push(...findPythonRangeCheckGuardSanitizers(code));
26255
25850
  additionalSanitizers.push(...findPythonRegexAllowlistWrapperSanitizers(code));
26256
25851
  additionalSanitizers.push(...findPythonSetMembershipXssGuardSanitizers(code));
25852
+ additionalSanitizers.push(...findPythonTraversalRejectGuardSanitizers(code));
26257
25853
  additionalSanitizers.push(...findPythonDefusedXmlSanitizers(code));
26258
25854
  additionalSanitizers.push(...findPythonJinjaAutoescapeSanitizers(code));
26259
25855
  const pyMisconfigFindings = findPythonPatternFindings(code, graph.ir.meta.file);
@@ -28038,6 +27634,59 @@ function findPythonSetMembershipXssGuardSanitizers(code) {
28038
27634
  }
28039
27635
  return sanitizers;
28040
27636
  }
27637
+ function findPythonTraversalRejectGuardSanitizers(code) {
27638
+ const sanitizers = [];
27639
+ const lines = code.split("\n");
27640
+ const guardOpen = /^(\s*)if\s+(['"])(\.\.[/\\]?)\2\s+in\s+([A-Za-z_][A-Za-z0-9_]*)\s*:\s*$/;
27641
+ const terminator = /\b(return|raise|abort\s*\(|sys\.exit\s*\()/;
27642
+ for (let i2 = 0; i2 < lines.length; i2++) {
27643
+ const m = guardOpen.exec(lines[i2]);
27644
+ if (!m) continue;
27645
+ const guardIndent = m[1].length;
27646
+ const guardedVar = m[4];
27647
+ let bodyHasTerminator = false;
27648
+ let blockEnd = -1;
27649
+ const maxScan = Math.min(lines.length, i2 + 26);
27650
+ for (let j = i2 + 1; j < maxScan; j++) {
27651
+ const line = lines[j];
27652
+ if (line.trim() === "") continue;
27653
+ const indent = line.length - line.trimStart().length;
27654
+ if (indent <= guardIndent) {
27655
+ blockEnd = j - 1;
27656
+ break;
27657
+ }
27658
+ if (terminator.test(line)) bodyHasTerminator = true;
27659
+ }
27660
+ if (blockEnd === -1) blockEnd = Math.min(lines.length - 1, i2 + 25);
27661
+ if (!bodyHasTerminator) continue;
27662
+ const guarded = /* @__PURE__ */ new Set([guardedVar]);
27663
+ const assignRe = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=(?!=)\s*(.*)$/;
27664
+ const mentionsGuarded = (text) => {
27665
+ for (const name2 of guarded) {
27666
+ if (new RegExp(`\\b${name2}\\b`).test(text)) return true;
27667
+ }
27668
+ return false;
27669
+ };
27670
+ for (let l = blockEnd + 2; l <= lines.length; l++) {
27671
+ const lineText = lines[l - 1];
27672
+ const assign = assignRe.exec(lineText);
27673
+ if (assign) {
27674
+ const target = assign[1];
27675
+ const rhs = assign[2];
27676
+ if (mentionsGuarded(rhs)) guarded.add(target);
27677
+ else if (target !== guardedVar) guarded.delete(target);
27678
+ }
27679
+ if (!mentionsGuarded(lineText)) continue;
27680
+ sanitizers.push({
27681
+ type: "python_traversal_reject_guard",
27682
+ method: "if",
27683
+ line: l,
27684
+ sanitizes: ["path_traversal"]
27685
+ });
27686
+ }
27687
+ }
27688
+ return sanitizers;
27689
+ }
28041
27690
  function findPythonDefusedXmlSanitizers(code) {
28042
27691
  const sanitizers = [];
28043
27692
  const lines = code.split("\n");
@@ -28591,12 +28240,12 @@ function findJavaPathNormalizeStartsWithGuardSanitizers(code) {
28591
28240
  function findJavaPathGetFileNameSanitizers(code) {
28592
28241
  const sanitizers = [];
28593
28242
  const lines = code.split("\n");
28594
- const assignmentRe = /^\s*(?:(?:final\s+)?[A-Za-z_][\w.<>?,\s\[\]]*?\s+)?([A-Za-z_]\w*)\s*=\s*(.+?);\s*$/;
28243
+ const assignmentRe2 = /^\s*(?:(?:final\s+)?[A-Za-z_][\w.<>?,\s\[\]]*?\s+)?([A-Za-z_]\w*)\s*=\s*(.+?);\s*$/;
28595
28244
  const rhsHasGetFileNameRe = /\.\s*getFileName\s*\(\s*\)/;
28596
28245
  const rhsHasPathsChainRe = /\b(?:Paths\s*\.\s*get|Path\s*\.\s*of)\s*\(/;
28597
28246
  const candidates = [];
28598
28247
  for (let i2 = 0; i2 < lines.length; i2++) {
28599
- const m = assignmentRe.exec(lines[i2]);
28248
+ const m = assignmentRe2.exec(lines[i2]);
28600
28249
  if (!m) continue;
28601
28250
  const rhs = m[2];
28602
28251
  if (!rhsHasGetFileNameRe.test(rhs)) continue;
@@ -33831,6 +33480,333 @@ var DeserializationSafetyGatePass = class {
33831
33480
  }
33832
33481
  };
33833
33482
 
33483
+ // src/analysis/passes/prompt-injection-safety-gate-pass.ts
33484
+ function isDelimiterLiteral(lit) {
33485
+ const s = lit.trim();
33486
+ if (s.length === 0) return true;
33487
+ return /^<\/?[\w.-]+\s*\/?>$/.test(s) || /^\[\/?[\w.-]+\]$/.test(s) || /^`{1,3}[\w-]*$/.test(s) || /^"""[\w-]*$/.test(s) || /^[#*=_~-]{2,}$/.test(s) || /^<{2,}[\w-]*$/.test(s) || /^[\w-]*>{2,}$/.test(s);
33488
+ }
33489
+ function scanValue(code, start2) {
33490
+ let depth = 0;
33491
+ let str = null;
33492
+ let out2 = "";
33493
+ for (let i2 = start2; i2 < code.length; i2++) {
33494
+ const c = code[i2];
33495
+ if (str) {
33496
+ out2 += c;
33497
+ if (c === str && code[i2 - 1] !== "\\") str = null;
33498
+ continue;
33499
+ }
33500
+ if (c === '"' || c === "'" || c === "`") {
33501
+ str = c;
33502
+ out2 += c;
33503
+ continue;
33504
+ }
33505
+ if (c === "(" || c === "[" || c === "{") {
33506
+ depth++;
33507
+ out2 += c;
33508
+ continue;
33509
+ }
33510
+ if (c === ")" || c === "]" || c === "}") {
33511
+ if (depth === 0) break;
33512
+ depth--;
33513
+ out2 += c;
33514
+ continue;
33515
+ }
33516
+ if (c === "," && depth === 0) break;
33517
+ out2 += c;
33518
+ }
33519
+ return out2.trim();
33520
+ }
33521
+ function stringLiterals(expr) {
33522
+ return [...expr.matchAll(/"([^"]*)"|'([^']*)'/g)].map((m) => m[1] ?? m[2] ?? "");
33523
+ }
33524
+ function hasIdentifier(expr) {
33525
+ return /[A-Za-z_$][\w$]*/.test(expr.replace(/"[^"]*"|'[^']*'|`[^`]*`/g, " "));
33526
+ }
33527
+ function isPureLiteral(valueExpr) {
33528
+ const stripped = valueExpr.replace(/"[^"]*"|'[^']*'|`[^`]*`/g, " ");
33529
+ return !/[A-Za-z_$][\w$]*/.test(stripped);
33530
+ }
33531
+ function isInstructionConcat(valueExpr) {
33532
+ if (!valueExpr.includes("+")) return false;
33533
+ if (!hasIdentifier(valueExpr)) return false;
33534
+ const lits = stringLiterals(valueExpr);
33535
+ if (lits.length === 0) return false;
33536
+ return !lits.every(isDelimiterLiteral);
33537
+ }
33538
+ function isMixedTemplate(v) {
33539
+ const fstr = v.match(/\bf(["'])((?:\\.|(?!\1).)*)\1/);
33540
+ if (fstr) {
33541
+ const body2 = fstr[2];
33542
+ if (/\{[^}]+\}/.test(body2) && /\w/.test(body2.replace(/\{[^}]*\}/g, ""))) return true;
33543
+ }
33544
+ const tmpl = v.match(/`([^`]*)`/);
33545
+ if (tmpl) {
33546
+ const body2 = tmpl[1];
33547
+ if (/\$\{[^}]+\}/.test(body2) && /\w/.test(body2.replace(/\$\{[^}]*\}/g, ""))) return true;
33548
+ }
33549
+ return false;
33550
+ }
33551
+ function assignmentRe(varName, flags2 = "") {
33552
+ return new RegExp(`\\b${varName}\\s*(?::=|(?<![=<>!])=(?!=))\\s*`, flags2);
33553
+ }
33554
+ function resolveAssignmentRHS(varName, codeLines) {
33555
+ const re = assignmentRe(varName);
33556
+ for (const line of codeLines) {
33557
+ const m = line.match(re);
33558
+ if (m && m.index !== void 0) {
33559
+ return scanValue(line, m.index + m[0].length);
33560
+ }
33561
+ }
33562
+ return void 0;
33563
+ }
33564
+ function classifyExpr(v) {
33565
+ if (isMixedTemplate(v)) return "unsafe";
33566
+ if (v.includes("+")) {
33567
+ if (isInstructionConcat(v)) return "unsafe";
33568
+ const lits = stringLiterals(v);
33569
+ if (lits.length > 0 && lits.every(isDelimiterLiteral) && hasIdentifier(v)) return "safe";
33570
+ return "unknown";
33571
+ }
33572
+ return "unknown";
33573
+ }
33574
+ function classifyContentPlacement(val, codeLines) {
33575
+ const v = val.trim();
33576
+ if (/^[A-Za-z_$][\w$]*$/.test(v)) {
33577
+ const rhs = resolveAssignmentRHS(v, codeLines);
33578
+ if (rhs === void 0) return "unknown";
33579
+ return classifyExpr(rhs);
33580
+ }
33581
+ return classifyExpr(v);
33582
+ }
33583
+ function classifyPromptCall(callCode, codeLines) {
33584
+ const contentRe = /["']?[Cc]ontent["']?\s*[:=]\s*/g;
33585
+ let m;
33586
+ let sawDynamic = false;
33587
+ let allSafe = true;
33588
+ while ((m = contentRe.exec(callCode)) !== null) {
33589
+ const val = scanValue(callCode, m.index + m[0].length);
33590
+ if (val === "" || isPureLiteral(val)) continue;
33591
+ sawDynamic = true;
33592
+ const ctx = callCode.slice(Math.max(0, m.index - 80), m.index);
33593
+ const roleM = ctx.match(/["']?[Rr]ole["']?\s*[:=]\s*["'](\w+)["'][^"']*$/);
33594
+ const role = roleM ? roleM[1].toLowerCase() : "user";
33595
+ if (role === "system" || role === "assistant") return "unsafe";
33596
+ const placement = classifyContentPlacement(val, codeLines);
33597
+ if (placement === "unsafe") return "unsafe";
33598
+ if (placement !== "safe") allSafe = false;
33599
+ }
33600
+ if (!sawDynamic) return "unknown";
33601
+ return allSafe ? "safe" : "unknown";
33602
+ }
33603
+ function scanRhsAcrossLines(text, start2) {
33604
+ let depth = 0;
33605
+ let str = null;
33606
+ let out2 = "";
33607
+ for (let i2 = start2; i2 < text.length; i2++) {
33608
+ const c = text[i2];
33609
+ if (str) {
33610
+ out2 += c;
33611
+ if (c === str && text[i2 - 1] !== "\\") str = null;
33612
+ continue;
33613
+ }
33614
+ if (c === '"' || c === "'" || c === "`") {
33615
+ str = c;
33616
+ out2 += c;
33617
+ continue;
33618
+ }
33619
+ if (c === "(" || c === "[" || c === "{") {
33620
+ depth++;
33621
+ out2 += c;
33622
+ continue;
33623
+ }
33624
+ if (c === ")" || c === "]" || c === "}") {
33625
+ if (depth === 0) break;
33626
+ depth--;
33627
+ out2 += c;
33628
+ continue;
33629
+ }
33630
+ if (c === "\n" && depth === 0) break;
33631
+ out2 += c;
33632
+ }
33633
+ return out2.trim();
33634
+ }
33635
+ function resolveBuilderRhs(varName, joined) {
33636
+ const re = assignmentRe(varName, "g");
33637
+ const m = re.exec(joined);
33638
+ if (m) return scanRhsAcrossLines(joined, m.index + m[0].length);
33639
+ return void 0;
33640
+ }
33641
+ function collectBuilderRegion(callCode, joined) {
33642
+ const parts2 = [];
33643
+ const seen = /* @__PURE__ */ new Set();
33644
+ const stack = [...callCode.matchAll(/[A-Za-z_$][\w$]*/g)].map((x) => x[0]);
33645
+ let budget = 40;
33646
+ while (stack.length > 0 && budget-- > 0) {
33647
+ const v = stack.pop();
33648
+ if (seen.has(v)) continue;
33649
+ seen.add(v);
33650
+ const rhs = resolveBuilderRhs(v, joined);
33651
+ if (rhs) {
33652
+ parts2.push(rhs);
33653
+ for (const mm of rhs.matchAll(/[A-Za-z_$][\w$]*/g)) stack.push(mm[0]);
33654
+ }
33655
+ }
33656
+ return parts2.join("\n");
33657
+ }
33658
+ function classifyPromptSink(callCode, codeLines) {
33659
+ const direct = classifyPromptCall(callCode, codeLines);
33660
+ if (direct !== "unknown") return direct;
33661
+ const region = collectBuilderRegion(callCode, codeLines.join("\n"));
33662
+ if (!region) return "unknown";
33663
+ return classifyPromptCall(region, codeLines);
33664
+ }
33665
+ var PromptInjectionSafetyGatePass = class {
33666
+ name = "prompt-injection-safety-gate";
33667
+ category = "security";
33668
+ run(ctx) {
33669
+ const { graph, code } = ctx;
33670
+ const sinks = ctx.hasResult("sink-filter") ? ctx.getResult("sink-filter").sinks : graph.ir.taint.sinks;
33671
+ if (sinks.length === 0) return { droppedSafe: 0 };
33672
+ const codeLines = code.split("\n");
33673
+ let droppedSafe = 0;
33674
+ const kept = sinks.filter((sink) => {
33675
+ if (sink.type !== "prompt_injection") return true;
33676
+ const callCode = sink.code ?? codeLines[sink.line - 1] ?? "";
33677
+ if (!callCode) return true;
33678
+ if (classifyPromptSink(callCode, codeLines) === "safe") {
33679
+ droppedSafe++;
33680
+ return false;
33681
+ }
33682
+ return true;
33683
+ });
33684
+ if (droppedSafe > 0) {
33685
+ sinks.length = 0;
33686
+ sinks.push(...kept);
33687
+ }
33688
+ return { droppedSafe };
33689
+ }
33690
+ };
33691
+
33692
+ // src/analysis/passes/speculative-prompt-param-source-pass.ts
33693
+ function returnOrAssignRhs(text) {
33694
+ const t = text.trim();
33695
+ const ret = t.match(/^return\s+(.+?);?$/);
33696
+ if (ret) return ret[1];
33697
+ const asg = t.match(/^[A-Za-z_$][\w$.]*\s*(?::=|(?<![=<>!])=(?!=))\s*(.+?);?$/);
33698
+ if (asg) return asg[1];
33699
+ return void 0;
33700
+ }
33701
+ function looksLikePromptText(lit) {
33702
+ const s = lit.trim();
33703
+ if (/\b(you are|you're|assistant|system prompt|follow (the )?(policy|instructions|rules)|act as|your task|instructions?\s*:|ignore (all )?previous|do not reveal|respond (to|with|as)|answer the (question|user)|you (must|should|will)|helpful (ai|assistant))\b/i.test(s)) {
33704
+ return true;
33705
+ }
33706
+ const words = s.split(/\s+/).filter(Boolean);
33707
+ return words.length >= 4 && s.length >= 20;
33708
+ }
33709
+ function literalsOf(expr) {
33710
+ return [...expr.matchAll(/"([^"]*)"|'([^']*)'|`([^`]*)`/g)].map((m) => m[1] ?? m[2] ?? m[3] ?? "");
33711
+ }
33712
+ function detectPromptConstructionFlows(codeLines, types) {
33713
+ const flows = [];
33714
+ const seen = /* @__PURE__ */ new Set();
33715
+ for (const type of types) {
33716
+ for (const method of type.methods) {
33717
+ const params = (method.parameters ?? []).map((p) => ({ name: p.name, line: p.line ?? method.start_line })).filter((p) => p.name && p.name !== "_");
33718
+ if (params.length === 0) continue;
33719
+ for (let line = method.start_line; line <= method.end_line; line++) {
33720
+ const text = codeLines[line - 1] ?? "";
33721
+ const rhs = returnOrAssignRhs(text);
33722
+ if (!rhs || !isInstructionConcat(rhs)) continue;
33723
+ if (!literalsOf(rhs).some(looksLikePromptText)) continue;
33724
+ const param = params.find((p) => new RegExp(`\\b${p.name}\\b`).test(rhs));
33725
+ if (!param) continue;
33726
+ const key = `${param.line}|${line}`;
33727
+ if (seen.has(key)) continue;
33728
+ seen.add(key);
33729
+ flows.push({
33730
+ source_line: param.line,
33731
+ sink_line: line,
33732
+ source_type: "http_body",
33733
+ sink_type: "prompt_injection",
33734
+ path: [
33735
+ { variable: param.name, line: param.line, type: "source" },
33736
+ { variable: param.name, line, type: "sink" }
33737
+ ],
33738
+ confidence: 1,
33739
+ sanitized: false
33740
+ });
33741
+ }
33742
+ }
33743
+ }
33744
+ return flows;
33745
+ }
33746
+ function looksLikeTextParam(type) {
33747
+ if (!type) return true;
33748
+ const t = type.trim();
33749
+ if (t.startsWith("*") || t.startsWith("&")) return false;
33750
+ if (/\b(Client|Context|Writer|Reader|Conn|DB|Logger|Handler|Server|Request|Response|Pool|Session|Engine|Service|Repository|Config)\b/.test(t)) {
33751
+ return false;
33752
+ }
33753
+ return /(?:^|\b)(string|str|String|text|any|object|interface\{\}|\[\]byte|\[\]string|List\[str\]|Optional\[str\])(?:\b|$)/i.test(t) || // TS unions / plain identifiers without a handle keyword default to text.
33754
+ /^[A-Za-z_$][\w$<>[\], |]*$/.test(t);
33755
+ }
33756
+ var SpeculativePromptParamSourcePass = class {
33757
+ constructor(enabled) {
33758
+ this.enabled = enabled;
33759
+ }
33760
+ enabled;
33761
+ name = "speculative-prompt-param-source";
33762
+ category = "security";
33763
+ run(ctx) {
33764
+ if (!this.enabled) return { added: 0 };
33765
+ if (!ctx.hasResult("sink-filter")) return { added: 0 };
33766
+ const sinkFilter = ctx.getResult("sink-filter");
33767
+ const { types } = ctx.graph.ir;
33768
+ const promptSinks = sinkFilter.sinks.filter(
33769
+ (s) => s.type === "prompt_injection"
33770
+ );
33771
+ if (promptSinks.length === 0) return { added: 0 };
33772
+ const existing = new Set(
33773
+ sinkFilter.sources.filter((s) => typeof s.variable === "string").map((s) => `${s.variable}:${s.line}`)
33774
+ );
33775
+ const added = [];
33776
+ for (const type of types) {
33777
+ for (const method of type.methods) {
33778
+ const hasPromptSink = promptSinks.some(
33779
+ (s) => s.line >= method.start_line && s.line <= method.end_line
33780
+ );
33781
+ if (!hasPromptSink) continue;
33782
+ for (const param of method.parameters) {
33783
+ if (!param.name || param.name === "_") continue;
33784
+ if (!looksLikeTextParam(param.type)) continue;
33785
+ const line = param.line ?? method.start_line;
33786
+ const key = `${param.name}:${line}`;
33787
+ if (existing.has(key)) continue;
33788
+ existing.add(key);
33789
+ added.push({
33790
+ // `http_body` reaches `prompt_injection` via the flow
33791
+ // generators; a generic untrusted request-shaped type.
33792
+ type: "http_body",
33793
+ location: `speculative untrusted parameter '${param.name}' in ${method.name}`,
33794
+ severity: "high",
33795
+ line,
33796
+ confidence: 1,
33797
+ variable: param.name,
33798
+ in_method: method.name
33799
+ });
33800
+ }
33801
+ }
33802
+ }
33803
+ if (added.length > 0) {
33804
+ sinkFilter.sources.push(...added);
33805
+ }
33806
+ return { added: added.length };
33807
+ }
33808
+ };
33809
+
33834
33810
  // src/analysis/passes/cli-main-reflection-suppress-pass.ts
33835
33811
  var REFLECTION_SINK_METHODS = /* @__PURE__ */ new Set([
33836
33812
  "forName",
@@ -44077,6 +44053,9 @@ async function analyze(code, filePath, language, options = {}) {
44077
44053
  if (!disabledPasses.has("sink-semantics")) pipeline.add(new SinkSemanticsPass());
44078
44054
  if (!disabledPasses.has("deserialization-safety-gate"))
44079
44055
  pipeline.add(new DeserializationSafetyGatePass(options.dependencyContext));
44056
+ if (!disabledPasses.has("prompt-injection-safety-gate"))
44057
+ pipeline.add(new PromptInjectionSafetyGatePass());
44058
+ pipeline.add(new SpeculativePromptParamSourcePass(options.speculativeParamSources === true));
44080
44059
  if (!disabledPasses.has("cli-main-reflection-suppress"))
44081
44060
  pipeline.add(new CliMainReflectionSuppressPass());
44082
44061
  if (!disabledPasses.has("library-profile-sink-gate"))
@@ -44156,6 +44135,17 @@ async function analyze(code, filePath, language, options = {}) {
44156
44135
  flows: interProc.additionalFlows,
44157
44136
  interprocedural: interProc.interprocedural
44158
44137
  };
44138
+ if (options.speculativeParamSources === true) {
44139
+ const existingFlowKeys = new Set(
44140
+ (taint.flows ?? []).map((f) => `${f.source_line}|${f.sink_line}|${f.sink_type}`)
44141
+ );
44142
+ const constructionFlows = detectPromptConstructionFlows(code.split("\n"), types).filter(
44143
+ (f) => !existingFlowKeys.has(`${f.source_line}|${f.sink_line}|${f.sink_type}`)
44144
+ );
44145
+ if (constructionFlows.length > 0) {
44146
+ taint.flows = [...taint.flows ?? [], ...constructionFlows];
44147
+ }
44148
+ }
44159
44149
  if (taint.flows && taint.flows.length > 0 && taint.sinks.length > 0) {
44160
44150
  const sinkTagsByKey = /* @__PURE__ */ new Map();
44161
44151
  for (const s of taint.sinks) {