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
@@ -9981,6 +9981,7 @@ var GO_KEYWORDS = /* @__PURE__ */ new Set([
9981
9981
  function buildGoDFG(tree) {
9982
9982
  const defs = [];
9983
9983
  const uses = [];
9984
+ const extraChains = [];
9984
9985
  let defIdCounter = 1;
9985
9986
  let useIdCounter = 1;
9986
9987
  const scopeStack = [/* @__PURE__ */ new Map()];
@@ -10002,7 +10003,7 @@ function buildGoDFG(tree) {
10002
10003
  }
10003
10004
  const body2 = func2.childForFieldName("body");
10004
10005
  if (body2) {
10005
- processGoBlock(body2, defs, uses, scopeStack, { defId: defIdCounter, useId: useIdCounter });
10006
+ processGoBlock(body2, defs, uses, scopeStack, { defId: defIdCounter, useId: useIdCounter }, extraChains);
10006
10007
  defIdCounter = defs.length + 1;
10007
10008
  useIdCounter = uses.length + 1;
10008
10009
  }
@@ -10017,6 +10018,15 @@ function buildGoDFG(tree) {
10017
10018
  }
10018
10019
  }
10019
10020
  const chains = computeChains(defs, uses);
10021
+ const seen = new Set(chains.map((c) => `${c.from_def}|${c.to_def}|${c.via}`));
10022
+ for (const ch of extraChains) {
10023
+ const key = `${ch.from_def}|${ch.to_def}|${ch.via}`;
10024
+ if (!seen.has(key)) {
10025
+ seen.add(key);
10026
+ chains.push(ch);
10027
+ }
10028
+ }
10029
+ chains.sort((a, b) => a.from_def - b.from_def || a.to_def - b.to_def);
10020
10030
  return { defs, uses, chains };
10021
10031
  }
10022
10032
  function extractGoParamDefs(params, defs, _startId, scopeStack) {
@@ -10041,28 +10051,47 @@ function extractGoParamDefs(params, defs, _startId, scopeStack) {
10041
10051
  }
10042
10052
  }
10043
10053
  }
10044
- function processGoBlock(node, defs, uses, scopeStack, counters) {
10054
+ function processGoBlock(node, defs, uses, scopeStack, counters, extraChains = []) {
10055
+ const linkMultiLineRhs = (right, useStart, useEnd, defStart, defEnd) => {
10056
+ if (right.endPosition.row <= right.startPosition.row) return;
10057
+ for (let ui = useStart; ui < useEnd; ui++) {
10058
+ const u = uses[ui];
10059
+ if (!u || u.def_id === null) continue;
10060
+ for (let di = defStart; di < defEnd; di++) {
10061
+ const d = defs[di];
10062
+ if (d && d.kind === "local" && d.id !== u.def_id) {
10063
+ extraChains.push({ from_def: u.def_id, to_def: d.id, via: u.variable });
10064
+ }
10065
+ }
10066
+ }
10067
+ };
10045
10068
  walkTree(node, (child) => {
10046
10069
  if (child.type === "short_var_declaration") {
10047
10070
  const left = child.childForFieldName("left");
10048
10071
  const right = child.childForFieldName("right");
10072
+ const usesBefore = uses.length;
10049
10073
  if (right) {
10050
10074
  extractGoUses(right, uses, scopeStack);
10051
10075
  }
10076
+ const defsBefore = defs.length;
10052
10077
  if (left) {
10053
10078
  extractGoLhsDefs(left, defs, scopeStack, child.startPosition.row + 1);
10054
10079
  }
10080
+ if (right) linkMultiLineRhs(right, usesBefore, uses.length, defsBefore, defs.length);
10055
10081
  } else if (child.type === "var_declaration") {
10056
10082
  processGoVarDecl(child, defs, scopeStack, counters);
10057
10083
  } else if (child.type === "assignment_statement") {
10058
10084
  const left = child.childForFieldName("left");
10059
10085
  const right = child.childForFieldName("right");
10086
+ const usesBefore = uses.length;
10060
10087
  if (right) {
10061
10088
  extractGoUses(right, uses, scopeStack);
10062
10089
  }
10090
+ const defsBefore = defs.length;
10063
10091
  if (left) {
10064
10092
  extractGoLhsDefs(left, defs, scopeStack, child.startPosition.row + 1);
10065
10093
  }
10094
+ if (right) linkMultiLineRhs(right, usesBefore, uses.length, defsBefore, defs.length);
10066
10095
  } else if (child.type === "for_statement") {
10067
10096
  const rangeClause = findChildByTypeGo(child, "range_clause");
10068
10097
  if (rangeClause) {
@@ -10726,7 +10755,11 @@ var DEFAULT_SOURCES = [
10726
10755
  { method: "Json", type: "http_body", severity: "high", return_tainted: true, languages: ["rust"] },
10727
10756
  { method: "Query", type: "http_param", severity: "high", return_tainted: true, languages: ["rust"] },
10728
10757
  { method: "Path", type: "http_path", severity: "high", return_tainted: true, languages: ["rust"] },
10729
- { method: "Form", type: "http_param", severity: "high", return_tainted: true, languages: ["rust"] },
10758
+ // `Form<T>` decodes the urlencoded request *body* (`Query<T>` above is the
10759
+ // query-string extractor), so it is http_body — retyped from http_param
10760
+ // when the RustPlugin's http_body duplicate was removed, so the two
10761
+ // surfaces no longer disagree. Matches Gin's `Context.PostForm`.
10762
+ { method: "Form", type: "http_body", severity: "high", return_tainted: true, languages: ["rust"] },
10730
10763
  // Rust std library
10731
10764
  { method: "var", class: "env", type: "env_input", severity: "medium", return_tainted: true },
10732
10765
  { method: "var_os", class: "env", type: "env_input", severity: "medium", return_tainted: true },
@@ -12004,7 +12037,11 @@ var DEFAULT_SINKS = [
12004
12037
  // Also match without receiver (destructured imports: const { exec } = require('child_process'))
12005
12038
  // `exec` is intentionally classless: catches Node.js child_process.exec AND
12006
12039
  // Java Runtime.exec (via `r.exec()` where heuristic can't resolve r → Runtime).
12007
- { method: "exec", type: "command_injection", cwe: "CWE-78", severity: "high", arg_positions: [0] },
12040
+ // Excluded from Python: there, bare `exec(code)` is the code-execution
12041
+ // builtin, registered below as code_injection / CWE-94. Before the exclusion
12042
+ // a Python `exec(user_code)` matched both patterns and emitted two findings
12043
+ // for one call — CWE-78 (wrong: no shell is involved) and CWE-94 (right).
12044
+ { method: "exec", type: "command_injection", cwe: "CWE-78", severity: "high", arg_positions: [0], exclude_languages: ["python"] },
12008
12045
  // `execSync`/`spawn`/`spawnSync`/`execFile` are Node-specific — language-scope them.
12009
12046
  { method: "execSync", type: "command_injection", cwe: "CWE-78", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
12010
12047
  { method: "spawn", type: "command_injection", cwe: "CWE-78", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
@@ -12032,7 +12069,11 @@ var DEFAULT_SINKS = [
12032
12069
  // Note: classless { method: 'query' } removed — too many FPs (UriComponentsBuilder.query(), etc.)
12033
12070
  // SQL query calls are covered by class-specific patterns above (Connection, Pool, Client, JdbcTemplate)
12034
12071
  // Note: `raw` is shared with Python (Django ORM) — scoped to JS+TS to avoid leaking.
12035
- { method: "raw", type: "sql_injection", cwe: "CWE-89", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
12072
+ // Severity `critical` (not `high`) preserves what the JS plugin's duplicate
12073
+ // builtin emitted before the #4 follow-up consolidation removed it: the
12074
+ // plugin copy carried `critical`, which won the `findSinks` dedup slot via
12075
+ // the +0.1 critical-severity confidence boost.
12076
+ { method: "raw", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["javascript", "typescript"] },
12036
12077
  // sqlite3 (npm) — Database/Statement methods. The JS plugin resolves
12037
12078
  // `const db = new sqlite3.Database(...); db.all(sql)` to the resolution
12038
12079
  // target `Connection.all`, so class-scoped patterns matching `Connection`
@@ -12591,10 +12632,30 @@ var DEFAULT_SINKS = [
12591
12632
  { method: "Sprintf", class: "fmt", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [0], languages: ["go"] },
12592
12633
  { method: "Printf", class: "fmt", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [0], languages: ["go"] },
12593
12634
  { method: "Errorf", class: "fmt", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [0], languages: ["go"] },
12635
+ // `fmt.Fprintf` is deliberately dual-classified: this CWE-134 entry and the
12636
+ // Go plugin's xss (CWE-79) entry both cover arg[1], so a tainted format
12637
+ // string emits two findings. Unlike the logger case above, neither is
12638
+ // redundant — CWE-134 holds for any writer, while CWE-79 is the actionable
12639
+ // finding when the writer is an http.ResponseWriter (`fmt.Fprintf(w, taint)`
12640
+ // is the standard Go reflected-XSS shape). Collapsing it requires deciding
12641
+ // by the *type of arg[0]*, which sink patterns cannot express today: the
12642
+ // options are a Go-specific gate (mirroring isSafeGoJsonUnmarshalCall) that
12643
+ // emits xss only for a resolved ResponseWriter, or dropping one CWE and
12644
+ // accepting either lost XSS recall or a mis-classified stderr write.
12645
+ // Left as-is pending the Go corpus run — see tasks.md.
12594
12646
  { method: "Fprintf", class: "fmt", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [1], languages: ["go"] },
12595
12647
  // cognium-dev #264 — Go stdlib `log` package format-string entry points.
12596
12648
  // log.Printf / Fatalf / Panicf take the format string at arg[0]; tainted
12597
12649
  // format string reaches the same fmt.Sprintf machinery internally.
12650
+ //
12651
+ // These deliberately coexist with the Go plugin's log_injection (CWE-117)
12652
+ // entries for the same methods, so such a call emits both. The Logger
12653
+ // decision note in the Java log_injection section above does NOT apply
12654
+ // here: it reasons about SLF4J-style APIs where the format string is a
12655
+ // literal with `{}` placeholders, whereas Go's `log.Printf(fmt, ...)` takes
12656
+ // a real format string that an attacker can control. CWE-117 covers the
12657
+ // forged log line, CWE-134 the format string itself. Locked by
12658
+ // tests/analysis/passes/format-string-additions.test.ts.
12598
12659
  { method: "Printf", class: "log", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [0], languages: ["go"] },
12599
12660
  { method: "Fatalf", class: "log", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [0], languages: ["go"] },
12600
12661
  { method: "Panicf", class: "log", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [0], languages: ["go"] },
@@ -12608,6 +12669,14 @@ var DEFAULT_SINKS = [
12608
12669
  { method: "cookie", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1], languages: ["javascript", "typescript"] },
12609
12670
  // Express: res.location(url) and res.redirect(url) — Location header.
12610
12671
  { method: "location", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [0], languages: ["javascript", "typescript"] },
12672
+ // `redirect` is deliberately dual-classified: this crlf (CWE-113) entry
12673
+ // coexists with the open_redirect (CWE-601) entry for js/ts, so a tainted
12674
+ // redirect target emits two flows. That is intentional, not duplication —
12675
+ // #189 Sprint 82 shipped a sink-type-aware flow dedup specifically so both
12676
+ // survive ("res.redirect is both open_redirect AND crlf"), and #132 added a
12677
+ // crlf recall test for the bare `res.redirect(req.query.url)` shape. Locked
12678
+ // by issue-189-sprint82-open-redirect-cluster.test.ts and
12679
+ // crlf-stage8-fp.test.ts — do not collapse without revisiting both tickets.
12611
12680
  { method: "redirect", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [0], languages: ["javascript", "typescript"] },
12612
12681
  // Go net/http: w.Header().Set(k, v) / Add(k, v) — first arg is the value
12613
12682
  // (Header is a map; the actual `value` is arg 1 of the call). We flag the
@@ -13433,18 +13502,28 @@ function analyzeTaint(calls, types, config = getDefaultConfig(), typeHierarchy,
13433
13502
  const sanitizers = findSanitizers(calls, types, config.sanitizers, sourceLines);
13434
13503
  return { sources, sinks, sanitizers };
13435
13504
  }
13505
+ function sinkPatternAppliesTo(pattern, language) {
13506
+ if (language === void 0) return true;
13507
+ if (pattern.languages && pattern.languages.length > 0 && !pattern.languages.includes(language)) {
13508
+ return false;
13509
+ }
13510
+ if (pattern.exclude_languages && pattern.exclude_languages.includes(language)) {
13511
+ return false;
13512
+ }
13513
+ return true;
13514
+ }
13436
13515
  function expandPromisifyAliases(patterns, sourceLines, language) {
13437
13516
  if (!sourceLines || sourceLines.length === 0) return patterns;
13438
13517
  if (language !== "javascript" && language !== "typescript") return patterns;
13439
13518
  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*\)/;
13440
13519
  const innerToPattern = /* @__PURE__ */ new Map();
13441
13520
  for (const p of patterns) {
13442
- if (p.languages && !p.languages.includes(language)) continue;
13521
+ if (!sinkPatternAppliesTo(p, language)) continue;
13443
13522
  if (p.class !== void 0) continue;
13444
13523
  if (!innerToPattern.has(p.method)) innerToPattern.set(p.method, p);
13445
13524
  }
13446
13525
  for (const p of patterns) {
13447
- if (p.languages && !p.languages.includes(language)) continue;
13526
+ if (!sinkPatternAppliesTo(p, language)) continue;
13448
13527
  if (innerToPattern.has(p.method)) continue;
13449
13528
  innerToPattern.set(p.method, p);
13450
13529
  }
@@ -13478,7 +13557,7 @@ function expandIndirectEvalAliases(patterns, sourceLines, language) {
13478
13557
  const INDIRECT_EVAL_RE = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*(?::[^=]+)?=\s*(eval|Function)\b(?!\s*\()/;
13479
13558
  const innerToPattern = /* @__PURE__ */ new Map();
13480
13559
  for (const p of patterns) {
13481
- if (p.languages && !p.languages.includes(language)) continue;
13560
+ if (!sinkPatternAppliesTo(p, language)) continue;
13482
13561
  if (p.class !== void 0) continue;
13483
13562
  if (p.method !== "eval" && p.method !== "Function") continue;
13484
13563
  if (!innerToPattern.has(p.method)) innerToPattern.set(p.method, p);
@@ -14186,9 +14265,7 @@ function isSafeJinjaRenderCall(call, pattern, language, sourceLines) {
14186
14265
  return false;
14187
14266
  }
14188
14267
  function findSinks(calls, patterns, typeHierarchy, language, sourceLines, types) {
14189
- const patternsForLanguage = language === void 0 ? patterns : patterns.filter(
14190
- (p) => !p.languages || p.languages.length === 0 || p.languages.includes(language)
14191
- );
14268
+ const patternsForLanguage = language === void 0 ? patterns : patterns.filter((p) => sinkPatternAppliesTo(p, language));
14192
14269
  const sinkMap = /* @__PURE__ */ new Map();
14193
14270
  for (const call of calls) {
14194
14271
  for (const pattern of patternsForLanguage) {
@@ -14533,10 +14610,8 @@ var SINK_FQN_EXCLUSIONS = {
14533
14610
  ]
14534
14611
  };
14535
14612
  function matchesSinkPattern(call, pattern, typeHierarchy, language) {
14536
- if (pattern.languages && pattern.languages.length > 0 && language !== void 0) {
14537
- if (!pattern.languages.includes(language)) {
14538
- return false;
14539
- }
14613
+ if (!sinkPatternAppliesTo(pattern, language)) {
14614
+ return false;
14540
14615
  }
14541
14616
  const callMethodName = call.method_name;
14542
14617
  const patternMethod = pattern.method;
@@ -9915,6 +9915,7 @@ var GO_KEYWORDS = /* @__PURE__ */ new Set([
9915
9915
  function buildGoDFG(tree) {
9916
9916
  const defs = [];
9917
9917
  const uses = [];
9918
+ const extraChains = [];
9918
9919
  let defIdCounter = 1;
9919
9920
  let useIdCounter = 1;
9920
9921
  const scopeStack = [/* @__PURE__ */ new Map()];
@@ -9936,7 +9937,7 @@ function buildGoDFG(tree) {
9936
9937
  }
9937
9938
  const body2 = func2.childForFieldName("body");
9938
9939
  if (body2) {
9939
- processGoBlock(body2, defs, uses, scopeStack, { defId: defIdCounter, useId: useIdCounter });
9940
+ processGoBlock(body2, defs, uses, scopeStack, { defId: defIdCounter, useId: useIdCounter }, extraChains);
9940
9941
  defIdCounter = defs.length + 1;
9941
9942
  useIdCounter = uses.length + 1;
9942
9943
  }
@@ -9951,6 +9952,15 @@ function buildGoDFG(tree) {
9951
9952
  }
9952
9953
  }
9953
9954
  const chains = computeChains(defs, uses);
9955
+ const seen = new Set(chains.map((c) => `${c.from_def}|${c.to_def}|${c.via}`));
9956
+ for (const ch of extraChains) {
9957
+ const key = `${ch.from_def}|${ch.to_def}|${ch.via}`;
9958
+ if (!seen.has(key)) {
9959
+ seen.add(key);
9960
+ chains.push(ch);
9961
+ }
9962
+ }
9963
+ chains.sort((a, b) => a.from_def - b.from_def || a.to_def - b.to_def);
9954
9964
  return { defs, uses, chains };
9955
9965
  }
9956
9966
  function extractGoParamDefs(params, defs, _startId, scopeStack) {
@@ -9975,28 +9985,47 @@ function extractGoParamDefs(params, defs, _startId, scopeStack) {
9975
9985
  }
9976
9986
  }
9977
9987
  }
9978
- function processGoBlock(node, defs, uses, scopeStack, counters) {
9988
+ function processGoBlock(node, defs, uses, scopeStack, counters, extraChains = []) {
9989
+ const linkMultiLineRhs = (right, useStart, useEnd, defStart, defEnd) => {
9990
+ if (right.endPosition.row <= right.startPosition.row) return;
9991
+ for (let ui = useStart; ui < useEnd; ui++) {
9992
+ const u = uses[ui];
9993
+ if (!u || u.def_id === null) continue;
9994
+ for (let di = defStart; di < defEnd; di++) {
9995
+ const d = defs[di];
9996
+ if (d && d.kind === "local" && d.id !== u.def_id) {
9997
+ extraChains.push({ from_def: u.def_id, to_def: d.id, via: u.variable });
9998
+ }
9999
+ }
10000
+ }
10001
+ };
9979
10002
  walkTree(node, (child) => {
9980
10003
  if (child.type === "short_var_declaration") {
9981
10004
  const left = child.childForFieldName("left");
9982
10005
  const right = child.childForFieldName("right");
10006
+ const usesBefore = uses.length;
9983
10007
  if (right) {
9984
10008
  extractGoUses(right, uses, scopeStack);
9985
10009
  }
10010
+ const defsBefore = defs.length;
9986
10011
  if (left) {
9987
10012
  extractGoLhsDefs(left, defs, scopeStack, child.startPosition.row + 1);
9988
10013
  }
10014
+ if (right) linkMultiLineRhs(right, usesBefore, uses.length, defsBefore, defs.length);
9989
10015
  } else if (child.type === "var_declaration") {
9990
10016
  processGoVarDecl(child, defs, scopeStack, counters);
9991
10017
  } else if (child.type === "assignment_statement") {
9992
10018
  const left = child.childForFieldName("left");
9993
10019
  const right = child.childForFieldName("right");
10020
+ const usesBefore = uses.length;
9994
10021
  if (right) {
9995
10022
  extractGoUses(right, uses, scopeStack);
9996
10023
  }
10024
+ const defsBefore = defs.length;
9997
10025
  if (left) {
9998
10026
  extractGoLhsDefs(left, defs, scopeStack, child.startPosition.row + 1);
9999
10027
  }
10028
+ if (right) linkMultiLineRhs(right, usesBefore, uses.length, defsBefore, defs.length);
10000
10029
  } else if (child.type === "for_statement") {
10001
10030
  const rangeClause = findChildByTypeGo(child, "range_clause");
10002
10031
  if (rangeClause) {
@@ -10660,7 +10689,11 @@ var DEFAULT_SOURCES = [
10660
10689
  { method: "Json", type: "http_body", severity: "high", return_tainted: true, languages: ["rust"] },
10661
10690
  { method: "Query", type: "http_param", severity: "high", return_tainted: true, languages: ["rust"] },
10662
10691
  { method: "Path", type: "http_path", severity: "high", return_tainted: true, languages: ["rust"] },
10663
- { method: "Form", type: "http_param", severity: "high", return_tainted: true, languages: ["rust"] },
10692
+ // `Form<T>` decodes the urlencoded request *body* (`Query<T>` above is the
10693
+ // query-string extractor), so it is http_body — retyped from http_param
10694
+ // when the RustPlugin's http_body duplicate was removed, so the two
10695
+ // surfaces no longer disagree. Matches Gin's `Context.PostForm`.
10696
+ { method: "Form", type: "http_body", severity: "high", return_tainted: true, languages: ["rust"] },
10664
10697
  // Rust std library
10665
10698
  { method: "var", class: "env", type: "env_input", severity: "medium", return_tainted: true },
10666
10699
  { method: "var_os", class: "env", type: "env_input", severity: "medium", return_tainted: true },
@@ -11938,7 +11971,11 @@ var DEFAULT_SINKS = [
11938
11971
  // Also match without receiver (destructured imports: const { exec } = require('child_process'))
11939
11972
  // `exec` is intentionally classless: catches Node.js child_process.exec AND
11940
11973
  // Java Runtime.exec (via `r.exec()` where heuristic can't resolve r → Runtime).
11941
- { method: "exec", type: "command_injection", cwe: "CWE-78", severity: "high", arg_positions: [0] },
11974
+ // Excluded from Python: there, bare `exec(code)` is the code-execution
11975
+ // builtin, registered below as code_injection / CWE-94. Before the exclusion
11976
+ // a Python `exec(user_code)` matched both patterns and emitted two findings
11977
+ // for one call — CWE-78 (wrong: no shell is involved) and CWE-94 (right).
11978
+ { method: "exec", type: "command_injection", cwe: "CWE-78", severity: "high", arg_positions: [0], exclude_languages: ["python"] },
11942
11979
  // `execSync`/`spawn`/`spawnSync`/`execFile` are Node-specific — language-scope them.
11943
11980
  { method: "execSync", type: "command_injection", cwe: "CWE-78", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
11944
11981
  { method: "spawn", type: "command_injection", cwe: "CWE-78", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
@@ -11966,7 +12003,11 @@ var DEFAULT_SINKS = [
11966
12003
  // Note: classless { method: 'query' } removed — too many FPs (UriComponentsBuilder.query(), etc.)
11967
12004
  // SQL query calls are covered by class-specific patterns above (Connection, Pool, Client, JdbcTemplate)
11968
12005
  // Note: `raw` is shared with Python (Django ORM) — scoped to JS+TS to avoid leaking.
11969
- { method: "raw", type: "sql_injection", cwe: "CWE-89", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
12006
+ // Severity `critical` (not `high`) preserves what the JS plugin's duplicate
12007
+ // builtin emitted before the #4 follow-up consolidation removed it: the
12008
+ // plugin copy carried `critical`, which won the `findSinks` dedup slot via
12009
+ // the +0.1 critical-severity confidence boost.
12010
+ { method: "raw", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["javascript", "typescript"] },
11970
12011
  // sqlite3 (npm) — Database/Statement methods. The JS plugin resolves
11971
12012
  // `const db = new sqlite3.Database(...); db.all(sql)` to the resolution
11972
12013
  // target `Connection.all`, so class-scoped patterns matching `Connection`
@@ -12525,10 +12566,30 @@ var DEFAULT_SINKS = [
12525
12566
  { method: "Sprintf", class: "fmt", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [0], languages: ["go"] },
12526
12567
  { method: "Printf", class: "fmt", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [0], languages: ["go"] },
12527
12568
  { method: "Errorf", class: "fmt", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [0], languages: ["go"] },
12569
+ // `fmt.Fprintf` is deliberately dual-classified: this CWE-134 entry and the
12570
+ // Go plugin's xss (CWE-79) entry both cover arg[1], so a tainted format
12571
+ // string emits two findings. Unlike the logger case above, neither is
12572
+ // redundant — CWE-134 holds for any writer, while CWE-79 is the actionable
12573
+ // finding when the writer is an http.ResponseWriter (`fmt.Fprintf(w, taint)`
12574
+ // is the standard Go reflected-XSS shape). Collapsing it requires deciding
12575
+ // by the *type of arg[0]*, which sink patterns cannot express today: the
12576
+ // options are a Go-specific gate (mirroring isSafeGoJsonUnmarshalCall) that
12577
+ // emits xss only for a resolved ResponseWriter, or dropping one CWE and
12578
+ // accepting either lost XSS recall or a mis-classified stderr write.
12579
+ // Left as-is pending the Go corpus run — see tasks.md.
12528
12580
  { method: "Fprintf", class: "fmt", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [1], languages: ["go"] },
12529
12581
  // cognium-dev #264 — Go stdlib `log` package format-string entry points.
12530
12582
  // log.Printf / Fatalf / Panicf take the format string at arg[0]; tainted
12531
12583
  // format string reaches the same fmt.Sprintf machinery internally.
12584
+ //
12585
+ // These deliberately coexist with the Go plugin's log_injection (CWE-117)
12586
+ // entries for the same methods, so such a call emits both. The Logger
12587
+ // decision note in the Java log_injection section above does NOT apply
12588
+ // here: it reasons about SLF4J-style APIs where the format string is a
12589
+ // literal with `{}` placeholders, whereas Go's `log.Printf(fmt, ...)` takes
12590
+ // a real format string that an attacker can control. CWE-117 covers the
12591
+ // forged log line, CWE-134 the format string itself. Locked by
12592
+ // tests/analysis/passes/format-string-additions.test.ts.
12532
12593
  { method: "Printf", class: "log", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [0], languages: ["go"] },
12533
12594
  { method: "Fatalf", class: "log", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [0], languages: ["go"] },
12534
12595
  { method: "Panicf", class: "log", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [0], languages: ["go"] },
@@ -12542,6 +12603,14 @@ var DEFAULT_SINKS = [
12542
12603
  { method: "cookie", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1], languages: ["javascript", "typescript"] },
12543
12604
  // Express: res.location(url) and res.redirect(url) — Location header.
12544
12605
  { method: "location", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [0], languages: ["javascript", "typescript"] },
12606
+ // `redirect` is deliberately dual-classified: this crlf (CWE-113) entry
12607
+ // coexists with the open_redirect (CWE-601) entry for js/ts, so a tainted
12608
+ // redirect target emits two flows. That is intentional, not duplication —
12609
+ // #189 Sprint 82 shipped a sink-type-aware flow dedup specifically so both
12610
+ // survive ("res.redirect is both open_redirect AND crlf"), and #132 added a
12611
+ // crlf recall test for the bare `res.redirect(req.query.url)` shape. Locked
12612
+ // by issue-189-sprint82-open-redirect-cluster.test.ts and
12613
+ // crlf-stage8-fp.test.ts — do not collapse without revisiting both tickets.
12545
12614
  { method: "redirect", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [0], languages: ["javascript", "typescript"] },
12546
12615
  // Go net/http: w.Header().Set(k, v) / Add(k, v) — first arg is the value
12547
12616
  // (Header is a map; the actual `value` is arg 1 of the call). We flag the
@@ -13367,18 +13436,28 @@ function analyzeTaint(calls, types, config = getDefaultConfig(), typeHierarchy,
13367
13436
  const sanitizers = findSanitizers(calls, types, config.sanitizers, sourceLines);
13368
13437
  return { sources, sinks, sanitizers };
13369
13438
  }
13439
+ function sinkPatternAppliesTo(pattern, language) {
13440
+ if (language === void 0) return true;
13441
+ if (pattern.languages && pattern.languages.length > 0 && !pattern.languages.includes(language)) {
13442
+ return false;
13443
+ }
13444
+ if (pattern.exclude_languages && pattern.exclude_languages.includes(language)) {
13445
+ return false;
13446
+ }
13447
+ return true;
13448
+ }
13370
13449
  function expandPromisifyAliases(patterns, sourceLines, language) {
13371
13450
  if (!sourceLines || sourceLines.length === 0) return patterns;
13372
13451
  if (language !== "javascript" && language !== "typescript") return patterns;
13373
13452
  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*\)/;
13374
13453
  const innerToPattern = /* @__PURE__ */ new Map();
13375
13454
  for (const p of patterns) {
13376
- if (p.languages && !p.languages.includes(language)) continue;
13455
+ if (!sinkPatternAppliesTo(p, language)) continue;
13377
13456
  if (p.class !== void 0) continue;
13378
13457
  if (!innerToPattern.has(p.method)) innerToPattern.set(p.method, p);
13379
13458
  }
13380
13459
  for (const p of patterns) {
13381
- if (p.languages && !p.languages.includes(language)) continue;
13460
+ if (!sinkPatternAppliesTo(p, language)) continue;
13382
13461
  if (innerToPattern.has(p.method)) continue;
13383
13462
  innerToPattern.set(p.method, p);
13384
13463
  }
@@ -13412,7 +13491,7 @@ function expandIndirectEvalAliases(patterns, sourceLines, language) {
13412
13491
  const INDIRECT_EVAL_RE = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*(?::[^=]+)?=\s*(eval|Function)\b(?!\s*\()/;
13413
13492
  const innerToPattern = /* @__PURE__ */ new Map();
13414
13493
  for (const p of patterns) {
13415
- if (p.languages && !p.languages.includes(language)) continue;
13494
+ if (!sinkPatternAppliesTo(p, language)) continue;
13416
13495
  if (p.class !== void 0) continue;
13417
13496
  if (p.method !== "eval" && p.method !== "Function") continue;
13418
13497
  if (!innerToPattern.has(p.method)) innerToPattern.set(p.method, p);
@@ -14120,9 +14199,7 @@ function isSafeJinjaRenderCall(call, pattern, language, sourceLines) {
14120
14199
  return false;
14121
14200
  }
14122
14201
  function findSinks(calls, patterns, typeHierarchy, language, sourceLines, types) {
14123
- const patternsForLanguage = language === void 0 ? patterns : patterns.filter(
14124
- (p) => !p.languages || p.languages.length === 0 || p.languages.includes(language)
14125
- );
14202
+ const patternsForLanguage = language === void 0 ? patterns : patterns.filter((p) => sinkPatternAppliesTo(p, language));
14126
14203
  const sinkMap = /* @__PURE__ */ new Map();
14127
14204
  for (const call of calls) {
14128
14205
  for (const pattern of patternsForLanguage) {
@@ -14467,10 +14544,8 @@ var SINK_FQN_EXCLUSIONS = {
14467
14544
  ]
14468
14545
  };
14469
14546
  function matchesSinkPattern(call, pattern, typeHierarchy, language) {
14470
- if (pattern.languages && pattern.languages.length > 0 && language !== void 0) {
14471
- if (!pattern.languages.includes(language)) {
14472
- return false;
14473
- }
14547
+ if (!sinkPatternAppliesTo(pattern, language)) {
14548
+ return false;
14474
14549
  }
14475
14550
  const callMethodName = call.method_name;
14476
14551
  const patternMethod = pattern.method;
@@ -1274,6 +1274,11 @@ const GO_KEYWORDS = new Set([
1274
1274
  function buildGoDFG(tree) {
1275
1275
  const defs = [];
1276
1276
  const uses = [];
1277
+ // Explicit chains for multi-line composite-literal assignments (#267) —
1278
+ // `computeChains` only links same-line use→def, so a struct/slice literal
1279
+ // that spans lines (`req := T{ … tainted … }`) would not propagate taint
1280
+ // from the tainted field to the assigned var.
1281
+ const extraChains = [];
1277
1282
  let defIdCounter = 1;
1278
1283
  let useIdCounter = 1;
1279
1284
  const scopeStack = [new Map()];
@@ -1299,7 +1304,7 @@ function buildGoDFG(tree) {
1299
1304
  // Process function body
1300
1305
  const body = func.childForFieldName('body');
1301
1306
  if (body) {
1302
- processGoBlock(body, defs, uses, scopeStack, { defId: defIdCounter, useId: useIdCounter });
1307
+ processGoBlock(body, defs, uses, scopeStack, { defId: defIdCounter, useId: useIdCounter }, extraChains);
1303
1308
  defIdCounter = defs.length + 1;
1304
1309
  useIdCounter = uses.length + 1;
1305
1310
  }
@@ -1315,7 +1320,18 @@ function buildGoDFG(tree) {
1315
1320
  defIdCounter = defs.length + 1;
1316
1321
  }
1317
1322
  }
1323
+ // Merge same-line chains with the explicit multi-line composite-literal
1324
+ // chains (cognium-dev #267), deduplicating on (from_def, to_def, via).
1318
1325
  const chains = computeChains(defs, uses);
1326
+ const seen = new Set(chains.map(c => `${c.from_def}|${c.to_def}|${c.via}`));
1327
+ for (const ch of extraChains) {
1328
+ const key = `${ch.from_def}|${ch.to_def}|${ch.via}`;
1329
+ if (!seen.has(key)) {
1330
+ seen.add(key);
1331
+ chains.push(ch);
1332
+ }
1333
+ }
1334
+ chains.sort((a, b) => a.from_def - b.from_def || a.to_def - b.to_def);
1319
1335
  return { defs, uses, chains };
1320
1336
  }
1321
1337
  /**
@@ -1351,20 +1367,44 @@ function extractGoParamDefs(params, defs, _startId, scopeStack) {
1351
1367
  /**
1352
1368
  * Process a Go block (function body, if body, etc.) for definitions and uses.
1353
1369
  */
1354
- function processGoBlock(node, defs, uses, scopeStack, counters) {
1370
+ function processGoBlock(node, defs, uses, scopeStack, counters, extraChains = []) {
1371
+ // cognium-dev #267 — link resolved RHS uses to LHS local defs when the
1372
+ // RHS spans multiple lines (a struct/slice composite literal in gofmt
1373
+ // style). `computeChains` only links same-line use→def, so without this a
1374
+ // tainted value used deep inside a multi-line literal never reaches the
1375
+ // assigned variable. Single-line RHS is already covered by computeChains.
1376
+ const linkMultiLineRhs = (right, useStart, useEnd, defStart, defEnd) => {
1377
+ if (right.endPosition.row <= right.startPosition.row)
1378
+ return; // single line
1379
+ for (let ui = useStart; ui < useEnd; ui++) {
1380
+ const u = uses[ui];
1381
+ if (!u || u.def_id === null)
1382
+ continue;
1383
+ for (let di = defStart; di < defEnd; di++) {
1384
+ const d = defs[di];
1385
+ if (d && d.kind === 'local' && d.id !== u.def_id) {
1386
+ extraChains.push({ from_def: u.def_id, to_def: d.id, via: u.variable });
1387
+ }
1388
+ }
1389
+ }
1390
+ };
1355
1391
  walkTree(node, (child) => {
1356
1392
  if (child.type === 'short_var_declaration') {
1357
1393
  // x, y := expr
1358
1394
  const left = child.childForFieldName('left');
1359
1395
  const right = child.childForFieldName('right');
1360
1396
  // Extract uses from right side first
1397
+ const usesBefore = uses.length;
1361
1398
  if (right) {
1362
1399
  extractGoUses(right, uses, scopeStack);
1363
1400
  }
1364
1401
  // Then create defs for left side
1402
+ const defsBefore = defs.length;
1365
1403
  if (left) {
1366
1404
  extractGoLhsDefs(left, defs, scopeStack, child.startPosition.row + 1);
1367
1405
  }
1406
+ if (right)
1407
+ linkMultiLineRhs(right, usesBefore, uses.length, defsBefore, defs.length);
1368
1408
  }
1369
1409
  else if (child.type === 'var_declaration') {
1370
1410
  processGoVarDecl(child, defs, scopeStack, counters);
@@ -1373,13 +1413,17 @@ function processGoBlock(node, defs, uses, scopeStack, counters) {
1373
1413
  const left = child.childForFieldName('left');
1374
1414
  const right = child.childForFieldName('right');
1375
1415
  // Uses from right side
1416
+ const usesBefore = uses.length;
1376
1417
  if (right) {
1377
1418
  extractGoUses(right, uses, scopeStack);
1378
1419
  }
1379
1420
  // Defs for left side (reassignment)
1421
+ const defsBefore = defs.length;
1380
1422
  if (left) {
1381
1423
  extractGoLhsDefs(left, defs, scopeStack, child.startPosition.row + 1);
1382
1424
  }
1425
+ if (right)
1426
+ linkMultiLineRhs(right, usesBefore, uses.length, defsBefore, defs.length);
1383
1427
  }
1384
1428
  else if (child.type === 'for_statement') {
1385
1429
  // range clause: for k, v := range expr