circle-ir 4.9.13 → 4.9.14

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.
@@ -6700,6 +6700,14 @@ function buildResolutionContext(tree, cache) {
6700
6700
  }
6701
6701
  }
6702
6702
  }
6703
+ const resourceDecls = getNodesFromCache(tree.rootNode, "resource", cache);
6704
+ for (const res of resourceDecls) {
6705
+ const typeNode = res.childForFieldName("type");
6706
+ const nameNode = res.childForFieldName("name");
6707
+ if (typeNode && nameNode) {
6708
+ context.localVarTypes.set(getNodeText(nameNode), getNodeText(typeNode));
6709
+ }
6710
+ }
6703
6711
  const imports = getNodesFromCache(tree.rootNode, "import_declaration", cache);
6704
6712
  for (const imp of imports) {
6705
6713
  const text = getNodeText(imp);
@@ -12372,9 +12380,27 @@ var DEFAULT_SINKS = [
12372
12380
  // Command Injection (CWE-78)
12373
12381
  { method: "exec", class: "Runtime", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0, 1] },
12374
12382
  { method: "start", class: "ProcessBuilder", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [] },
12375
- // ProcessBuilder constructor
12376
- { method: "ProcessBuilder", class: "constructor", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
12377
- { method: "command", class: "ProcessBuilder", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
12383
+ // ProcessBuilder constructor + command(): cognium-dev#351 — BOTH take
12384
+ // `String...` varargs (or a single List<String>), and EVERY element is a
12385
+ // command token, so no position is safe. Pinning these to `[0]` missed the
12386
+ // canonical shell-wrapper shape, where the payload is the last argument:
12387
+ //
12388
+ // new ProcessBuilder("bash", "-c", tainted) // payload at position 2
12389
+ // pb.command("bash", "-c", tainted)
12390
+ //
12391
+ // `Runtime.exec(String[])` with the identical array already fired, so the
12392
+ // taint was available and only the sink model was wrong.
12393
+ //
12394
+ // The positions are enumerated rather than left `[]` because the two layers
12395
+ // that read this field disagree about what an empty array means:
12396
+ // `isInDangerousPosition` (taint-matcher) does `arg_positions.includes(pos)`,
12397
+ // so `[]` matches NOTHING, while the sink-reachability loop in
12398
+ // taint-propagation treats `length === 0` as "any argument".
12399
+ // `ProcessBuilder.start` gets away with `[]` only because it takes no
12400
+ // arguments at all. Enumerating is unambiguous in both, and matches how the
12401
+ // logging sinks in this file already spell it.
12402
+ { method: "ProcessBuilder", class: "constructor", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0, 1, 2, 3, 4, 5, 6, 7] },
12403
+ { method: "command", class: "ProcessBuilder", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0, 1, 2, 3, 4, 5, 6, 7] },
12378
12404
  // Commons Exec
12379
12405
  // Note: bare class 'Executor' removed — it collided with java.util.concurrent.Executor
12380
12406
  // (Executor.execute(Runnable) is not command injection). Apache Commons Exec users
@@ -13652,8 +13678,10 @@ var DEFAULT_SINKS = [
13652
13678
  // =========================================================================
13653
13679
  // Java CWE-Bench Enhancement Patterns (Collection/Builder)
13654
13680
  // =========================================================================
13655
- // Collection-based command injection (ProcessBuilder with List)
13656
- { method: "command", class: "ProcessBuilder", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
13681
+ // Collection-based command injection (ProcessBuilder with List) — the
13682
+ // `ProcessBuilder.command` entry lives with the other ProcessBuilder sinks
13683
+ // above; a second copy here only risked the two drifting apart, which is
13684
+ // what happened (cognium-dev#351).
13657
13685
  // ProcessBuilder.inheritIO removed in 3.83.0 (#124): no args, no command
13658
13686
  // string flows into it. See note above next to the Process-related cluster.
13659
13687
  // Jenkins DSL patterns
@@ -15583,7 +15611,7 @@ function isSafeGoExecCommandCall(call, pattern, language) {
15583
15611
  const stripped = expr.slice(1, -1);
15584
15612
  program = stripped.split("/").pop() ?? stripped;
15585
15613
  }
15586
- const SHELL_PROGRAMS = /* @__PURE__ */ new Set([
15614
+ const SHELL_PROGRAMS2 = /* @__PURE__ */ new Set([
15587
15615
  "sh",
15588
15616
  "bash",
15589
15617
  "zsh",
@@ -15597,7 +15625,7 @@ function isSafeGoExecCommandCall(call, pattern, language) {
15597
15625
  "powershell.exe",
15598
15626
  "pwsh.exe"
15599
15627
  ]);
15600
- if (SHELL_PROGRAMS.has(program)) return false;
15628
+ if (SHELL_PROGRAMS2.has(program)) return false;
15601
15629
  const PACKAGE_MANAGER_EXEC_SUBCOMMANDS = {
15602
15630
  go: /* @__PURE__ */ new Set(["install", "run", "get"]),
15603
15631
  npm: /* @__PURE__ */ new Set(["install", "i", "exec"]),
@@ -15648,7 +15676,7 @@ function isSafeJSChildProcessCall(call, pattern, language) {
15648
15676
  const stripped = expr.slice(1, -1);
15649
15677
  program = stripped.split("/").pop() ?? stripped;
15650
15678
  }
15651
- const SHELL_PROGRAMS = /* @__PURE__ */ new Set([
15679
+ const SHELL_PROGRAMS2 = /* @__PURE__ */ new Set([
15652
15680
  "sh",
15653
15681
  "bash",
15654
15682
  "zsh",
@@ -15662,7 +15690,7 @@ function isSafeJSChildProcessCall(call, pattern, language) {
15662
15690
  "powershell.exe",
15663
15691
  "pwsh.exe"
15664
15692
  ]);
15665
- if (SHELL_PROGRAMS.has(program)) return false;
15693
+ if (SHELL_PROGRAMS2.has(program)) return false;
15666
15694
  return true;
15667
15695
  }
15668
15696
  var CSHARP_SHELL_PROGRAMS = /* @__PURE__ */ new Set([
@@ -15895,7 +15923,7 @@ function isSafeRustCommandCall(call, pattern, language) {
15895
15923
  if (language !== "rust") return false;
15896
15924
  if (pattern.type !== "command_injection") return false;
15897
15925
  if (pattern.class !== void 0 && pattern.class !== "Command") return false;
15898
- const SHELL_PROGRAMS = /* @__PURE__ */ new Set([
15926
+ const SHELL_PROGRAMS2 = /* @__PURE__ */ new Set([
15899
15927
  "sh",
15900
15928
  "bash",
15901
15929
  "zsh",
@@ -15930,13 +15958,13 @@ function isSafeRustCommandCall(call, pattern, language) {
15930
15958
  const stripped = expr.slice(1, -1);
15931
15959
  program = stripped.split("/").pop() ?? stripped;
15932
15960
  }
15933
- return !SHELL_PROGRAMS.has(program);
15961
+ return !SHELL_PROGRAMS2.has(program);
15934
15962
  }
15935
15963
  if (pattern.method === "arg" || pattern.method === "args" || pattern.method === "spawn" || pattern.method === "output") {
15936
15964
  const receiverText = call.receiver ?? "";
15937
15965
  const program = extractProgram(receiverText);
15938
15966
  if (program === null) return false;
15939
- return !SHELL_PROGRAMS.has(program);
15967
+ return !SHELL_PROGRAMS2.has(program);
15940
15968
  }
15941
15969
  return false;
15942
15970
  }
@@ -33881,6 +33909,29 @@ var PROTOCOL_CLIENT_PACKAGES = [
33881
33909
  ];
33882
33910
  var OS_EXEC_RECEIVER_RE = /\b(?:Runtime|ProcessBuilder|DefaultExecutor|Executor|Exec|Launcher|ProcStarter|ProcessExecutor|RuntimeUtil)\s*[.(]/;
33883
33911
  var PROCESS_BUILDER_ARGV_FORM_RE = /\bnew\s+ProcessBuilder\s*\(\s*(?:Arrays\.asList\b|List\.of\b|Collections\.singletonList\b|new\s+ArrayList\b|new\s+String\s*\[\s*\]\s*\{|"[^"]*"\s*,)/;
33912
+ var PROCESS_BUILDER_COMMAND_ARGV_FORM_RE = /\.\s*command\s*\(\s*(?:Arrays\.asList\b|List\.of\b|Collections\.singletonList\b|new\s+ArrayList\b|new\s+String\s*\[\s*\]\s*\{|"[^"]*"\s*,)/;
33913
+ var SHELL_PROGRAMS = /* @__PURE__ */ new Set([
33914
+ "sh",
33915
+ "bash",
33916
+ "zsh",
33917
+ "ksh",
33918
+ "dash",
33919
+ "ash",
33920
+ "csh",
33921
+ "tcsh",
33922
+ "fish",
33923
+ "cmd",
33924
+ "command",
33925
+ "powershell",
33926
+ "pwsh"
33927
+ ]);
33928
+ var ARGV_FIRST_LITERAL_RE = /(?:\bnew\s+ProcessBuilder\s*\(|\.\s*command\s*\()\s*(?:Arrays\.asList\s*\(|List\.of\s*\(|Collections\.singletonList\s*\(|new\s+ArrayList\s*(?:<[^>]*>)?\s*\(\s*(?:Arrays\.asList\s*\()?|new\s+String\s*\[\s*\]\s*\{)?\s*"([^"]*)"/;
33929
+ function argvFirstLiteralIsShell(lineText) {
33930
+ const m = ARGV_FIRST_LITERAL_RE.exec(lineText);
33931
+ if (!m) return false;
33932
+ const base = m[1].split(/[\\/]/).pop().replace(/\.exe$/i, "").toLowerCase();
33933
+ return SHELL_PROGRAMS.has(base);
33934
+ }
33884
33935
  var JAVA_THROW_STATEMENT_RE = /^\s*throw\s+new\s+\w+(?:Exception|Error)\b[^;]*;\s*(?:\/\/.*|\/\*.*)?$/;
33885
33936
  var JEXL_ENGINE_TYPES = /* @__PURE__ */ new Set(["JexlEngine", "Jexl", "JxltEngine"]);
33886
33937
  var JEXL_EXPRESSION_TYPE_RE = /(?:Jexl)?(?:Expression|Script|Template)(?:Script)?$/;
@@ -34028,6 +34079,27 @@ function jsSsrfHostGuardedLines(code) {
34028
34079
  return covered;
34029
34080
  }
34030
34081
  var CSHARP_ALLOWLIST_STRIP_RE = /\bRegex\s*\.\s*Replace\s*\([^,]*,\s*@?"\[\^(?:[A-Za-z0-9_\- ]|\\w|\\d|\\s)+\]"\s*,\s*@?"[A-Za-z0-9_]?"\s*\)/;
34082
+ function isUnwrittenLocalMapKeyRead(code, source) {
34083
+ if (source.type !== "plugin_param") return false;
34084
+ const lines = code.split("\n");
34085
+ const lineText = lines[source.line - 1] ?? "";
34086
+ const read = /\b([A-Za-z_]\w*)\s*\.\s*get\s*\(\s*"([^"]*)"\s*\)/.exec(lineText);
34087
+ if (!read) return false;
34088
+ const [, recv, key] = read;
34089
+ const ctorRe = new RegExp(
34090
+ `\\b${recv}\\s*=\\s*new\\s+(?:Hash|Tree|LinkedHash|ConcurrentHash|Concurrent|Identity|Weak)?Map\\s*(?:<[^>]*>)?\\s*\\(`
34091
+ );
34092
+ if (!lines.some((l) => ctorRe.test(l))) return false;
34093
+ const escapeRe2 = new RegExp(`\\w\\s*\\([^)]*(?<![\\w.])${recv}(?![\\w.])[^)]*\\)`);
34094
+ for (const l of lines) {
34095
+ if (!escapeRe2.test(l)) continue;
34096
+ const stripped = l.replace(new RegExp(`\\b${recv}\\s*\\.\\s*\\w+\\s*\\(`, "g"), "X(");
34097
+ if (escapeRe2.test(stripped)) return false;
34098
+ }
34099
+ const putRe = new RegExp(`\\b${recv}\\s*\\.\\s*put\\s*\\(\\s*"${key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}"`);
34100
+ if (lines.some((l) => putRe.test(l))) return false;
34101
+ return true;
34102
+ }
34031
34103
  var CSHARP_SANITIZER_RES = [
34032
34104
  { re: /\b(?:HtmlEncode|JavaScriptStringEncode)\s*\(/, type: "xss" },
34033
34105
  { re: /\bHtmlEncoder\s*\.\s*Encode\s*\(/, type: "xss" },
@@ -34280,9 +34352,10 @@ var SinkFilterPass = class {
34280
34352
  const constProp = ctx.getResult("constant-propagation");
34281
34353
  const langSources = ctx.getResult("language-sources");
34282
34354
  const mergedSources = [...taintMatcher.sources, ...langSources.additionalSources];
34283
- const sources = ctx.code && ctx.language ? mergedSources.filter(
34355
+ const executableSources = ctx.code && ctx.language ? mergedSources.filter(
34284
34356
  (s) => !isNonExecutableSourceLine(ctx.code, s.line, ctx.language)
34285
34357
  ) : mergedSources;
34358
+ const sources = ctx.code ? executableSources.filter((s) => !isUnwrittenLocalMapKeyRead(ctx.code, s)) : executableSources;
34286
34359
  const sinks = [...taintMatcher.sinks];
34287
34360
  for (const s of langSources.additionalSinks) {
34288
34361
  if (!sinks.some((x) => x.line === s.line && x.cwe === s.cwe && x.type === s.type)) {
@@ -34550,10 +34623,19 @@ var SinkFilterPass = class {
34550
34623
  const irTypes = ctx.graph.ir.types;
34551
34624
  filtered = filtered.filter((sink) => {
34552
34625
  if (sink.type !== "command_injection") return true;
34553
- if (sink.method !== "ProcessBuilder" && sink.method !== "start") return true;
34626
+ if (sink.method !== "ProcessBuilder" && sink.method !== "start" && sink.method !== "command") {
34627
+ return true;
34628
+ }
34554
34629
  const sinkLineText = sourceLines[sink.line - 1] ?? "";
34555
- if (!/\bnew\s+ProcessBuilder\s*\(/.test(sinkLineText)) return true;
34556
- if (PROCESS_BUILDER_ARGV_FORM_RE.test(sinkLineText)) return false;
34630
+ const isCtorLine = /\bnew\s+ProcessBuilder\s*\(/.test(sinkLineText);
34631
+ const isCommandLine = /\.\s*command\s*\(/.test(sinkLineText);
34632
+ if (!isCtorLine && !isCommandLine) return true;
34633
+ if (argvFirstLiteralIsShell(sinkLineText)) return true;
34634
+ if (isCtorLine && PROCESS_BUILDER_ARGV_FORM_RE.test(sinkLineText)) return false;
34635
+ if (isCommandLine && PROCESS_BUILDER_COMMAND_ARGV_FORM_RE.test(sinkLineText)) {
34636
+ return false;
34637
+ }
34638
+ if (!isCtorLine) return true;
34557
34639
  const ctorCall = irCalls.find(
34558
34640
  (c) => c.method_name === "ProcessBuilder" && c.location.line === sink.line && (c.receiver === null || c.receiver === void 0)
34559
34641
  );
@@ -37124,6 +37206,103 @@ function isReassignedToLiteralBetween(code, variable, srcLine, sinkLine) {
37124
37206
  }
37125
37207
  return false;
37126
37208
  }
37209
+ var MASKABLE_STRING_LANGUAGES = /* @__PURE__ */ new Set(["python", "javascript", "typescript"]);
37210
+ function maskStringLiteralsForTokenScan(expr) {
37211
+ let out2 = "";
37212
+ let i2 = 0;
37213
+ const braceExprs = (body2) => {
37214
+ let acc = "";
37215
+ for (let j = 0; j < body2.length; ) {
37216
+ if (body2[j] === "{" && body2[j + 1] === "{") {
37217
+ j += 2;
37218
+ continue;
37219
+ }
37220
+ if (body2[j] === "}" && body2[j + 1] === "}") {
37221
+ j += 2;
37222
+ continue;
37223
+ }
37224
+ if (body2[j] === "{") {
37225
+ j++;
37226
+ let depth = 1;
37227
+ let inner = "";
37228
+ while (j < body2.length && depth > 0) {
37229
+ if (body2[j] === "{") depth++;
37230
+ else if (body2[j] === "}") {
37231
+ depth--;
37232
+ if (depth === 0) break;
37233
+ }
37234
+ inner += body2[j];
37235
+ j++;
37236
+ }
37237
+ j++;
37238
+ acc += " " + maskStringLiteralsForTokenScan(inner) + " ";
37239
+ continue;
37240
+ }
37241
+ j++;
37242
+ }
37243
+ return acc;
37244
+ };
37245
+ while (i2 < expr.length) {
37246
+ const ch = expr[i2];
37247
+ if (ch === "`") {
37248
+ i2++;
37249
+ let acc = "";
37250
+ while (i2 < expr.length && expr[i2] !== "`") {
37251
+ if (expr[i2] === "\\") {
37252
+ i2 += 2;
37253
+ continue;
37254
+ }
37255
+ if (expr[i2] === "$" && expr[i2 + 1] === "{") {
37256
+ i2 += 2;
37257
+ let depth = 1;
37258
+ let inner = "";
37259
+ while (i2 < expr.length && depth > 0) {
37260
+ if (expr[i2] === "{") depth++;
37261
+ else if (expr[i2] === "}") {
37262
+ depth--;
37263
+ if (depth === 0) break;
37264
+ }
37265
+ inner += expr[i2];
37266
+ i2++;
37267
+ }
37268
+ i2++;
37269
+ acc += " " + maskStringLiteralsForTokenScan(inner) + " ";
37270
+ continue;
37271
+ }
37272
+ i2++;
37273
+ }
37274
+ i2++;
37275
+ out2 += " " + acc + " ";
37276
+ continue;
37277
+ }
37278
+ if (ch === '"' || ch === "'") {
37279
+ const pm = /[A-Za-z_][A-Za-z0-9_]*$/.exec(out2);
37280
+ const prefix = pm ? pm[0] : "";
37281
+ const isPrefix = prefix.length > 0 && /^[rRbBuUfF]+$/.test(prefix);
37282
+ const isF = isPrefix && /[fF]/.test(prefix);
37283
+ if (isPrefix) out2 = out2.slice(0, out2.length - prefix.length) + " ";
37284
+ const triple = expr.startsWith(ch.repeat(3), i2);
37285
+ const delim = triple ? ch.repeat(3) : ch;
37286
+ i2 += delim.length;
37287
+ let body2 = "";
37288
+ while (i2 < expr.length && !expr.startsWith(delim, i2)) {
37289
+ if (expr[i2] === "\\") {
37290
+ body2 += " ";
37291
+ i2 += 2;
37292
+ continue;
37293
+ }
37294
+ body2 += expr[i2];
37295
+ i2++;
37296
+ }
37297
+ i2 += delim.length;
37298
+ out2 += isF ? " " + braceExprs(body2) + " " : " ";
37299
+ continue;
37300
+ }
37301
+ out2 += ch;
37302
+ i2++;
37303
+ }
37304
+ return out2;
37305
+ }
37127
37306
  function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachableLines, tainted, code, language, types) {
37128
37307
  const flows = [];
37129
37308
  const sourcesWithVar = sources.filter(
@@ -37477,7 +37656,8 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
37477
37656
  }
37478
37657
  const expr = arg.expression;
37479
37658
  if (!expr) continue;
37480
- const exprIdentMatches = expr.match(/[\p{L}\p{N}_]+/gu);
37659
+ const scannable = language !== void 0 && MASKABLE_STRING_LANGUAGES.has(language) ? maskStringLiteralsForTokenScan(expr) : expr;
37660
+ const exprIdentMatches = scannable.match(/[\p{L}\p{N}_]+/gu);
37481
37661
  let candidates = complexSources.length > 0 ? complexSources.slice() : null;
37482
37662
  if (exprIdentMatches) {
37483
37663
  for (const id of new Set(exprIdentMatches)) {
@@ -6746,6 +6746,14 @@ function buildResolutionContext(tree, cache) {
6746
6746
  }
6747
6747
  }
6748
6748
  }
6749
+ const resourceDecls = getNodesFromCache(tree.rootNode, "resource", cache);
6750
+ for (const res of resourceDecls) {
6751
+ const typeNode = res.childForFieldName("type");
6752
+ const nameNode = res.childForFieldName("name");
6753
+ if (typeNode && nameNode) {
6754
+ context.localVarTypes.set(getNodeText(nameNode), getNodeText(typeNode));
6755
+ }
6756
+ }
6749
6757
  const imports = getNodesFromCache(tree.rootNode, "import_declaration", cache);
6750
6758
  for (const imp of imports) {
6751
6759
  const text = getNodeText(imp);
@@ -11767,9 +11775,27 @@ var DEFAULT_SINKS = [
11767
11775
  // Command Injection (CWE-78)
11768
11776
  { method: "exec", class: "Runtime", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0, 1] },
11769
11777
  { method: "start", class: "ProcessBuilder", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [] },
11770
- // ProcessBuilder constructor
11771
- { method: "ProcessBuilder", class: "constructor", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
11772
- { method: "command", class: "ProcessBuilder", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
11778
+ // ProcessBuilder constructor + command(): cognium-dev#351 — BOTH take
11779
+ // `String...` varargs (or a single List<String>), and EVERY element is a
11780
+ // command token, so no position is safe. Pinning these to `[0]` missed the
11781
+ // canonical shell-wrapper shape, where the payload is the last argument:
11782
+ //
11783
+ // new ProcessBuilder("bash", "-c", tainted) // payload at position 2
11784
+ // pb.command("bash", "-c", tainted)
11785
+ //
11786
+ // `Runtime.exec(String[])` with the identical array already fired, so the
11787
+ // taint was available and only the sink model was wrong.
11788
+ //
11789
+ // The positions are enumerated rather than left `[]` because the two layers
11790
+ // that read this field disagree about what an empty array means:
11791
+ // `isInDangerousPosition` (taint-matcher) does `arg_positions.includes(pos)`,
11792
+ // so `[]` matches NOTHING, while the sink-reachability loop in
11793
+ // taint-propagation treats `length === 0` as "any argument".
11794
+ // `ProcessBuilder.start` gets away with `[]` only because it takes no
11795
+ // arguments at all. Enumerating is unambiguous in both, and matches how the
11796
+ // logging sinks in this file already spell it.
11797
+ { method: "ProcessBuilder", class: "constructor", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0, 1, 2, 3, 4, 5, 6, 7] },
11798
+ { method: "command", class: "ProcessBuilder", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0, 1, 2, 3, 4, 5, 6, 7] },
11773
11799
  // Commons Exec
11774
11800
  // Note: bare class 'Executor' removed — it collided with java.util.concurrent.Executor
11775
11801
  // (Executor.execute(Runnable) is not command injection). Apache Commons Exec users
@@ -13047,8 +13073,10 @@ var DEFAULT_SINKS = [
13047
13073
  // =========================================================================
13048
13074
  // Java CWE-Bench Enhancement Patterns (Collection/Builder)
13049
13075
  // =========================================================================
13050
- // Collection-based command injection (ProcessBuilder with List)
13051
- { method: "command", class: "ProcessBuilder", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
13076
+ // Collection-based command injection (ProcessBuilder with List) — the
13077
+ // `ProcessBuilder.command` entry lives with the other ProcessBuilder sinks
13078
+ // above; a second copy here only risked the two drifting apart, which is
13079
+ // what happened (cognium-dev#351).
13052
13080
  // ProcessBuilder.inheritIO removed in 3.83.0 (#124): no args, no command
13053
13081
  // string flows into it. See note above next to the Process-related cluster.
13054
13082
  // Jenkins DSL patterns
@@ -6680,6 +6680,14 @@ function buildResolutionContext(tree, cache) {
6680
6680
  }
6681
6681
  }
6682
6682
  }
6683
+ const resourceDecls = getNodesFromCache(tree.rootNode, "resource", cache);
6684
+ for (const res of resourceDecls) {
6685
+ const typeNode = res.childForFieldName("type");
6686
+ const nameNode = res.childForFieldName("name");
6687
+ if (typeNode && nameNode) {
6688
+ context.localVarTypes.set(getNodeText(nameNode), getNodeText(typeNode));
6689
+ }
6690
+ }
6683
6691
  const imports = getNodesFromCache(tree.rootNode, "import_declaration", cache);
6684
6692
  for (const imp of imports) {
6685
6693
  const text = getNodeText(imp);
@@ -11701,9 +11709,27 @@ var DEFAULT_SINKS = [
11701
11709
  // Command Injection (CWE-78)
11702
11710
  { method: "exec", class: "Runtime", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0, 1] },
11703
11711
  { method: "start", class: "ProcessBuilder", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [] },
11704
- // ProcessBuilder constructor
11705
- { method: "ProcessBuilder", class: "constructor", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
11706
- { method: "command", class: "ProcessBuilder", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
11712
+ // ProcessBuilder constructor + command(): cognium-dev#351 — BOTH take
11713
+ // `String...` varargs (or a single List<String>), and EVERY element is a
11714
+ // command token, so no position is safe. Pinning these to `[0]` missed the
11715
+ // canonical shell-wrapper shape, where the payload is the last argument:
11716
+ //
11717
+ // new ProcessBuilder("bash", "-c", tainted) // payload at position 2
11718
+ // pb.command("bash", "-c", tainted)
11719
+ //
11720
+ // `Runtime.exec(String[])` with the identical array already fired, so the
11721
+ // taint was available and only the sink model was wrong.
11722
+ //
11723
+ // The positions are enumerated rather than left `[]` because the two layers
11724
+ // that read this field disagree about what an empty array means:
11725
+ // `isInDangerousPosition` (taint-matcher) does `arg_positions.includes(pos)`,
11726
+ // so `[]` matches NOTHING, while the sink-reachability loop in
11727
+ // taint-propagation treats `length === 0` as "any argument".
11728
+ // `ProcessBuilder.start` gets away with `[]` only because it takes no
11729
+ // arguments at all. Enumerating is unambiguous in both, and matches how the
11730
+ // logging sinks in this file already spell it.
11731
+ { method: "ProcessBuilder", class: "constructor", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0, 1, 2, 3, 4, 5, 6, 7] },
11732
+ { method: "command", class: "ProcessBuilder", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0, 1, 2, 3, 4, 5, 6, 7] },
11707
11733
  // Commons Exec
11708
11734
  // Note: bare class 'Executor' removed — it collided with java.util.concurrent.Executor
11709
11735
  // (Executor.execute(Runnable) is not command injection). Apache Commons Exec users
@@ -12981,8 +13007,10 @@ var DEFAULT_SINKS = [
12981
13007
  // =========================================================================
12982
13008
  // Java CWE-Bench Enhancement Patterns (Collection/Builder)
12983
13009
  // =========================================================================
12984
- // Collection-based command injection (ProcessBuilder with List)
12985
- { method: "command", class: "ProcessBuilder", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
13010
+ // Collection-based command injection (ProcessBuilder with List) — the
13011
+ // `ProcessBuilder.command` entry lives with the other ProcessBuilder sinks
13012
+ // above; a second copy here only risked the two drifting apart, which is
13013
+ // what happened (cognium-dev#351).
12986
13014
  // ProcessBuilder.inheritIO removed in 3.83.0 (#124): no args, no command
12987
13015
  // string flows into it. See note above next to the Process-related cluster.
12988
13016
  // Jenkins DSL patterns
@@ -1049,6 +1049,25 @@ function buildResolutionContext(tree, cache) {
1049
1049
  }
1050
1050
  }
1051
1051
  }
1052
+ // cognium-dev#350 — try-with-resources declarations are ALSO local variables.
1053
+ //
1054
+ // try (Connection c = …; Statement s = c.createStatement()) { s.executeQuery(q); }
1055
+ //
1056
+ // A resource is a `resource` node under `resource_specification`, and it holds
1057
+ // its type and name directly — there is no `variable_declarator` child — so
1058
+ // the loop above never sees it. Without an entry here the receiver type stays
1059
+ // `null`, the class-scoped `Statement.executeQuery` sink cannot match, and the
1060
+ // call degrades to the generic `external_taint_escape` fallback: the sink is
1061
+ // still reported, but as CWE-668 instead of CWE-89. That is the idiomatic JDBC
1062
+ // form and the shape most real Spring code uses.
1063
+ const resourceDecls = getNodesFromCache(tree.rootNode, 'resource', cache);
1064
+ for (const res of resourceDecls) {
1065
+ const typeNode = res.childForFieldName('type');
1066
+ const nameNode = res.childForFieldName('name');
1067
+ if (typeNode && nameNode) {
1068
+ context.localVarTypes.set(getNodeText(nameNode), getNodeText(typeNode));
1069
+ }
1070
+ }
1052
1071
  // Collect imports — map simple class name to FQN
1053
1072
  const imports = getNodesFromCache(tree.rootNode, 'import_declaration', cache);
1054
1073
  for (const imp of imports) {