circle-ir 3.141.0 → 3.144.3

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 (45) hide show
  1. package/configs/sink-semantics.json +53 -0
  2. package/dist/analysis/config-loader.d.ts +24 -2
  3. package/dist/analysis/config-loader.d.ts.map +1 -1
  4. package/dist/analysis/config-loader.js +92 -2
  5. package/dist/analysis/config-loader.js.map +1 -1
  6. package/dist/analysis/findings.d.ts +32 -0
  7. package/dist/analysis/findings.d.ts.map +1 -1
  8. package/dist/analysis/findings.js +52 -2
  9. package/dist/analysis/findings.js.map +1 -1
  10. package/dist/analysis/passes/language-sources-pass.d.ts +19 -0
  11. package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
  12. package/dist/analysis/passes/language-sources-pass.js +133 -3
  13. package/dist/analysis/passes/language-sources-pass.js.map +1 -1
  14. package/dist/analysis/passes/scan-secrets-pass.d.ts.map +1 -1
  15. package/dist/analysis/passes/scan-secrets-pass.js +37 -4
  16. package/dist/analysis/passes/scan-secrets-pass.js.map +1 -1
  17. package/dist/analysis/passes/sink-filter-pass.d.ts.map +1 -1
  18. package/dist/analysis/passes/sink-filter-pass.js +10 -1
  19. package/dist/analysis/passes/sink-filter-pass.js.map +1 -1
  20. package/dist/analysis/passes/sink-semantics-pass.d.ts +52 -0
  21. package/dist/analysis/passes/sink-semantics-pass.d.ts.map +1 -0
  22. package/dist/analysis/passes/sink-semantics-pass.js +100 -0
  23. package/dist/analysis/passes/sink-semantics-pass.js.map +1 -0
  24. package/dist/analysis/passes/source-semantics-pass.d.ts +66 -0
  25. package/dist/analysis/passes/source-semantics-pass.d.ts.map +1 -0
  26. package/dist/analysis/passes/source-semantics-pass.js +165 -0
  27. package/dist/analysis/passes/source-semantics-pass.js.map +1 -0
  28. package/dist/analysis/passes/taint-propagation-pass.js +91 -10
  29. package/dist/analysis/passes/taint-propagation-pass.js.map +1 -1
  30. package/dist/analysis/taint-matcher.d.ts.map +1 -1
  31. package/dist/analysis/taint-matcher.js +55 -3
  32. package/dist/analysis/taint-matcher.js.map +1 -1
  33. package/dist/analyzer.d.ts.map +1 -1
  34. package/dist/analyzer.js +14 -0
  35. package/dist/analyzer.js.map +1 -1
  36. package/dist/browser/circle-ir.js +363 -15
  37. package/dist/core/circle-ir-core.cjs +112 -8
  38. package/dist/core/circle-ir-core.js +112 -8
  39. package/dist/core/extractors/calls.js +14 -0
  40. package/dist/core/extractors/calls.js.map +1 -1
  41. package/dist/types/config.d.ts +47 -0
  42. package/dist/types/config.d.ts.map +1 -1
  43. package/dist/types/index.d.ts +43 -0
  44. package/dist/types/index.d.ts.map +1 -1
  45. package/package.json +1 -1
@@ -6383,6 +6383,12 @@ function resolveReceiverType(receiver, context) {
6383
6383
  return { simpleName: null, fqn: null };
6384
6384
  }
6385
6385
  if (receiver === "super") return { simpleName: null, fqn: null };
6386
+ const ctorMatch = receiver.match(/^new\s+([A-Za-z_$][\w$.]*)\s*[<(]/);
6387
+ if (ctorMatch) {
6388
+ const ctorClass = ctorMatch[1];
6389
+ const simple = ctorClass.includes(".") ? ctorClass.substring(ctorClass.lastIndexOf(".") + 1) : ctorClass;
6390
+ return resolveFqn(simple, context);
6391
+ }
6386
6392
  const declaredType = context.localVarTypes.get(receiver) ?? context.paramTypes.get(receiver) ?? context.fieldTypes.get(receiver);
6387
6393
  if (declaredType) {
6388
6394
  return resolveFqn(stripGenerics(declaredType), context);
@@ -10109,12 +10115,25 @@ function loadSinkConfigs(configs) {
10109
10115
  }
10110
10116
  return { sinks, sanitizers };
10111
10117
  }
10112
- function createTaintConfig(sourceContents, sinkContents) {
10118
+ function loadSinkSemanticsConfigs(configs) {
10119
+ const entries = [];
10120
+ for (const config of configs) {
10121
+ if (config.sinks) {
10122
+ entries.push(...config.sinks);
10123
+ }
10124
+ }
10125
+ return entries;
10126
+ }
10127
+ function createTaintConfig(sourceContents, sinkContents, sinkSemanticsContents = []) {
10113
10128
  const sourceConfigs = sourceContents.map((c) => parseConfig(c));
10114
10129
  const sinkConfigs = sinkContents.map((c) => parseConfig(c));
10130
+ const sinkSemanticsConfigs = sinkSemanticsContents.map(
10131
+ (c) => parseConfig(c)
10132
+ );
10115
10133
  const sources = loadSourceConfigs(sourceConfigs);
10116
10134
  const { sinks, sanitizers } = loadSinkConfigs(sinkConfigs);
10117
- return { sources, sinks, sanitizers };
10135
+ const sinkSemantics = loadSinkSemanticsConfigs(sinkSemanticsConfigs);
10136
+ return { sources, sinks, sanitizers, sinkSemantics };
10118
10137
  }
10119
10138
  var DEFAULT_SOURCES = [
10120
10139
  // HTTP Sources (Servlet API)
@@ -10833,6 +10852,14 @@ var DEFAULT_SINKS = [
10833
10852
  // reflection remains a downstream concern of body-writing sinks.
10834
10853
  { method: "setHeader", class: "HttpServletResponse", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1] },
10835
10854
  { method: "addHeader", class: "HttpServletResponse", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1] },
10855
+ // Cookie constructor + addCookie — HTTP response splitting via cookie
10856
+ // name/value (CWE-113). Reflects the #189 Sprint 92 V02SetCookie fixture
10857
+ // where `res.addCookie(new Cookie(name, req.getParameter("v")))` allows
10858
+ // CRLF injection into the Set-Cookie header. Both the ctor arg[1] (value)
10859
+ // and the addCookie arg[0] (Cookie handle) are flagged so intermediate-var
10860
+ // and inline-ctor shapes both surface.
10861
+ { method: "Cookie", class: "constructor", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [0, 1] },
10862
+ { method: "addCookie", class: "HttpServletResponse", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [0] },
10836
10863
  // Note: `sendRedirect` is primarily classified as `ssrf` / open-redirect
10837
10864
  // (CWE-601) further down — see entry near line 1195. CRLF via Location
10838
10865
  // header is a secondary concern; keeping the canonical SSRF entry avoids
@@ -12272,11 +12299,62 @@ var DEFAULT_SANITIZERS = [
12272
12299
  { method: "UUID", class: "uuid", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] },
12273
12300
  { method: "Decimal", class: "decimal", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] }
12274
12301
  ];
12302
+ var DEFAULT_SINK_SEMANTICS = [
12303
+ {
12304
+ signature: "Jedis#executeCommand",
12305
+ real_class: "db_protocol",
12306
+ overrides: ["command_injection", "code_injection"],
12307
+ note: "Redis wire-protocol serialization, not OS exec"
12308
+ },
12309
+ {
12310
+ signature: "Connection#executeCommand",
12311
+ real_class: "db_protocol",
12312
+ overrides: ["command_injection", "code_injection"],
12313
+ note: "Jedis abstract Connection base"
12314
+ },
12315
+ {
12316
+ signature: "JedisCluster#executeCommand",
12317
+ real_class: "db_protocol",
12318
+ overrides: ["command_injection", "code_injection"],
12319
+ note: "Jedis cluster client"
12320
+ },
12321
+ {
12322
+ signature: "Func1#exec",
12323
+ real_class: "functional_dispatch",
12324
+ overrides: ["command_injection", "code_injection"],
12325
+ note: "RxJava functional dispatch, not OS exec"
12326
+ },
12327
+ {
12328
+ signature: "Action0#call",
12329
+ real_class: "functional_dispatch",
12330
+ overrides: ["command_injection"],
12331
+ note: "RxJava Action0 dispatch"
12332
+ },
12333
+ {
12334
+ signature: "Action1#call",
12335
+ real_class: "functional_dispatch",
12336
+ overrides: ["command_injection"],
12337
+ note: "RxJava Action1 dispatch"
12338
+ },
12339
+ {
12340
+ signature: "Unsafe#defineAnonymousClass",
12341
+ real_class: "jdk_internal",
12342
+ overrides: ["code_injection"],
12343
+ note: "sun.misc.Unsafe JDK-internal reflective bridge"
12344
+ },
12345
+ {
12346
+ signature: "MethodHandle#invokeExact",
12347
+ real_class: "jdk_internal",
12348
+ overrides: ["code_injection"],
12349
+ note: "java.lang.invoke.MethodHandle \u2014 JDK-internal"
12350
+ }
12351
+ ];
12275
12352
  function getDefaultConfig() {
12276
12353
  return {
12277
12354
  sources: DEFAULT_SOURCES,
12278
12355
  sinks: DEFAULT_SINKS,
12279
- sanitizers: DEFAULT_SANITIZERS
12356
+ sanitizers: DEFAULT_SANITIZERS,
12357
+ sinkSemantics: DEFAULT_SINK_SEMANTICS
12280
12358
  };
12281
12359
  }
12282
12360
 
@@ -12499,7 +12577,20 @@ function findSources(calls, types, patterns, sourceLines, language) {
12499
12577
  line: paramLine,
12500
12578
  confidence: param.type ? 0.7 : 0.5,
12501
12579
  // Lower confidence for untyped params
12502
- in_method: method.name
12580
+ in_method: method.name,
12581
+ // cognium-dev #220 — expose the parameter name to variable-scan flow
12582
+ // detection so uses of the param (including through concat-derived
12583
+ // aliases via `buildJavaTaintedVars`) can be bridged to sinks.
12584
+ //
12585
+ // Java-only: the Python/Rust alias-expansion branches in
12586
+ // taint-propagation-pass.ts use the earliest sourcesWithVar entry
12587
+ // as the anchor for synthetic derived sources. Adding
12588
+ // interprocedural_param to sourcesWithVar in those languages
12589
+ // would cause the anchor to become the low-confidence
12590
+ // interprocedural_param at the method decl line instead of the
12591
+ // real HTTP source, breaking downstream sink-type filters
12592
+ // (regressed #78, #92.1, #105 FP-31, #215 recall).
12593
+ ...language === "java" ? { variable: param.name } : {}
12503
12594
  });
12504
12595
  }
12505
12596
  }
@@ -12963,7 +13054,16 @@ function isSafeGoJsonUnmarshalCall(call, pattern, language, sourceLines) {
12963
13054
  return false;
12964
13055
  }
12965
13056
  var TEMPLATE_LITERAL_RECEIVER_RE = /^Template\(\s*(?:"[^"\\]*"|'[^'\\]*')\s*\)$/;
12966
- function isSafeJinjaRenderCall(call, pattern, language) {
13057
+ var JINJA_AUTOESCAPE_TRUE_RE = /\bEnvironment\s*\([^)]*\bautoescape\s*=\s*True\b/;
13058
+ var JINJA_SELECT_AUTOESCAPE_RE = /\bEnvironment\s*\([^)]*\bautoescape\s*=\s*select_autoescape\s*\(/;
13059
+ var JINJA_AUTOESCAPE_FALSE_RE = /\bEnvironment\s*\([^)]*\bautoescape\s*=\s*False\b/;
13060
+ function fileHasSafeJinjaEnvironment(sourceLines) {
13061
+ if (!sourceLines || sourceLines.length === 0) return false;
13062
+ const text = sourceLines.join("\n");
13063
+ if (JINJA_AUTOESCAPE_FALSE_RE.test(text)) return false;
13064
+ return JINJA_AUTOESCAPE_TRUE_RE.test(text) || JINJA_SELECT_AUTOESCAPE_RE.test(text);
13065
+ }
13066
+ function isSafeJinjaRenderCall(call, pattern, language, sourceLines) {
12967
13067
  if (language !== "python") return false;
12968
13068
  if (pattern.type !== "xss" && pattern.type !== "code_injection") return false;
12969
13069
  const method = call.method_name;
@@ -12977,7 +13077,8 @@ function isSafeJinjaRenderCall(call, pattern, language) {
12977
13077
  }
12978
13078
  if (method === "render") {
12979
13079
  const receiver = (call.receiver ?? "").trim();
12980
- return TEMPLATE_LITERAL_RECEIVER_RE.test(receiver);
13080
+ if (TEMPLATE_LITERAL_RECEIVER_RE.test(receiver)) return true;
13081
+ if (fileHasSafeJinjaEnvironment(sourceLines)) return true;
12981
13082
  }
12982
13083
  return false;
12983
13084
  }
@@ -13025,7 +13126,7 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines) {
13025
13126
  if (isSafeGoJsonUnmarshalCall(call, pattern, language, sourceLines)) {
13026
13127
  continue;
13027
13128
  }
13028
- if (isSafeJinjaRenderCall(call, pattern, language)) {
13129
+ if (isSafeJinjaRenderCall(call, pattern, language, sourceLines)) {
13029
13130
  continue;
13030
13131
  }
13031
13132
  const location = formatCallLocation(call);
@@ -13033,6 +13134,8 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines) {
13033
13134
  const confidence = calculateSinkConfidence(call, pattern);
13034
13135
  const existing = sinkMap.get(key);
13035
13136
  if (!existing || confidence > existing.confidence) {
13137
+ const receiverType = call.receiver_type;
13138
+ const simpleClass = receiverType ? receiverType.split(".").pop() || void 0 : void 0;
13036
13139
  sinkMap.set(key, {
13037
13140
  type: pattern.type,
13038
13141
  cwe: pattern.cwe,
@@ -13040,7 +13143,8 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines) {
13040
13143
  line: call.location.line,
13041
13144
  confidence,
13042
13145
  method: call.method_name,
13043
- argPositions: pattern.arg_positions
13146
+ argPositions: pattern.arg_positions,
13147
+ class: simpleClass
13044
13148
  });
13045
13149
  }
13046
13150
  }
@@ -6317,6 +6317,12 @@ function resolveReceiverType(receiver, context) {
6317
6317
  return { simpleName: null, fqn: null };
6318
6318
  }
6319
6319
  if (receiver === "super") return { simpleName: null, fqn: null };
6320
+ const ctorMatch = receiver.match(/^new\s+([A-Za-z_$][\w$.]*)\s*[<(]/);
6321
+ if (ctorMatch) {
6322
+ const ctorClass = ctorMatch[1];
6323
+ const simple = ctorClass.includes(".") ? ctorClass.substring(ctorClass.lastIndexOf(".") + 1) : ctorClass;
6324
+ return resolveFqn(simple, context);
6325
+ }
6320
6326
  const declaredType = context.localVarTypes.get(receiver) ?? context.paramTypes.get(receiver) ?? context.fieldTypes.get(receiver);
6321
6327
  if (declaredType) {
6322
6328
  return resolveFqn(stripGenerics(declaredType), context);
@@ -10043,12 +10049,25 @@ function loadSinkConfigs(configs) {
10043
10049
  }
10044
10050
  return { sinks, sanitizers };
10045
10051
  }
10046
- function createTaintConfig(sourceContents, sinkContents) {
10052
+ function loadSinkSemanticsConfigs(configs) {
10053
+ const entries = [];
10054
+ for (const config of configs) {
10055
+ if (config.sinks) {
10056
+ entries.push(...config.sinks);
10057
+ }
10058
+ }
10059
+ return entries;
10060
+ }
10061
+ function createTaintConfig(sourceContents, sinkContents, sinkSemanticsContents = []) {
10047
10062
  const sourceConfigs = sourceContents.map((c) => parseConfig(c));
10048
10063
  const sinkConfigs = sinkContents.map((c) => parseConfig(c));
10064
+ const sinkSemanticsConfigs = sinkSemanticsContents.map(
10065
+ (c) => parseConfig(c)
10066
+ );
10049
10067
  const sources = loadSourceConfigs(sourceConfigs);
10050
10068
  const { sinks, sanitizers } = loadSinkConfigs(sinkConfigs);
10051
- return { sources, sinks, sanitizers };
10069
+ const sinkSemantics = loadSinkSemanticsConfigs(sinkSemanticsConfigs);
10070
+ return { sources, sinks, sanitizers, sinkSemantics };
10052
10071
  }
10053
10072
  var DEFAULT_SOURCES = [
10054
10073
  // HTTP Sources (Servlet API)
@@ -10767,6 +10786,14 @@ var DEFAULT_SINKS = [
10767
10786
  // reflection remains a downstream concern of body-writing sinks.
10768
10787
  { method: "setHeader", class: "HttpServletResponse", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1] },
10769
10788
  { method: "addHeader", class: "HttpServletResponse", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1] },
10789
+ // Cookie constructor + addCookie — HTTP response splitting via cookie
10790
+ // name/value (CWE-113). Reflects the #189 Sprint 92 V02SetCookie fixture
10791
+ // where `res.addCookie(new Cookie(name, req.getParameter("v")))` allows
10792
+ // CRLF injection into the Set-Cookie header. Both the ctor arg[1] (value)
10793
+ // and the addCookie arg[0] (Cookie handle) are flagged so intermediate-var
10794
+ // and inline-ctor shapes both surface.
10795
+ { method: "Cookie", class: "constructor", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [0, 1] },
10796
+ { method: "addCookie", class: "HttpServletResponse", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [0] },
10770
10797
  // Note: `sendRedirect` is primarily classified as `ssrf` / open-redirect
10771
10798
  // (CWE-601) further down — see entry near line 1195. CRLF via Location
10772
10799
  // header is a secondary concern; keeping the canonical SSRF entry avoids
@@ -12206,11 +12233,62 @@ var DEFAULT_SANITIZERS = [
12206
12233
  { method: "UUID", class: "uuid", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] },
12207
12234
  { method: "Decimal", class: "decimal", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] }
12208
12235
  ];
12236
+ var DEFAULT_SINK_SEMANTICS = [
12237
+ {
12238
+ signature: "Jedis#executeCommand",
12239
+ real_class: "db_protocol",
12240
+ overrides: ["command_injection", "code_injection"],
12241
+ note: "Redis wire-protocol serialization, not OS exec"
12242
+ },
12243
+ {
12244
+ signature: "Connection#executeCommand",
12245
+ real_class: "db_protocol",
12246
+ overrides: ["command_injection", "code_injection"],
12247
+ note: "Jedis abstract Connection base"
12248
+ },
12249
+ {
12250
+ signature: "JedisCluster#executeCommand",
12251
+ real_class: "db_protocol",
12252
+ overrides: ["command_injection", "code_injection"],
12253
+ note: "Jedis cluster client"
12254
+ },
12255
+ {
12256
+ signature: "Func1#exec",
12257
+ real_class: "functional_dispatch",
12258
+ overrides: ["command_injection", "code_injection"],
12259
+ note: "RxJava functional dispatch, not OS exec"
12260
+ },
12261
+ {
12262
+ signature: "Action0#call",
12263
+ real_class: "functional_dispatch",
12264
+ overrides: ["command_injection"],
12265
+ note: "RxJava Action0 dispatch"
12266
+ },
12267
+ {
12268
+ signature: "Action1#call",
12269
+ real_class: "functional_dispatch",
12270
+ overrides: ["command_injection"],
12271
+ note: "RxJava Action1 dispatch"
12272
+ },
12273
+ {
12274
+ signature: "Unsafe#defineAnonymousClass",
12275
+ real_class: "jdk_internal",
12276
+ overrides: ["code_injection"],
12277
+ note: "sun.misc.Unsafe JDK-internal reflective bridge"
12278
+ },
12279
+ {
12280
+ signature: "MethodHandle#invokeExact",
12281
+ real_class: "jdk_internal",
12282
+ overrides: ["code_injection"],
12283
+ note: "java.lang.invoke.MethodHandle \u2014 JDK-internal"
12284
+ }
12285
+ ];
12209
12286
  function getDefaultConfig() {
12210
12287
  return {
12211
12288
  sources: DEFAULT_SOURCES,
12212
12289
  sinks: DEFAULT_SINKS,
12213
- sanitizers: DEFAULT_SANITIZERS
12290
+ sanitizers: DEFAULT_SANITIZERS,
12291
+ sinkSemantics: DEFAULT_SINK_SEMANTICS
12214
12292
  };
12215
12293
  }
12216
12294
 
@@ -12433,7 +12511,20 @@ function findSources(calls, types, patterns, sourceLines, language) {
12433
12511
  line: paramLine,
12434
12512
  confidence: param.type ? 0.7 : 0.5,
12435
12513
  // Lower confidence for untyped params
12436
- in_method: method.name
12514
+ in_method: method.name,
12515
+ // cognium-dev #220 — expose the parameter name to variable-scan flow
12516
+ // detection so uses of the param (including through concat-derived
12517
+ // aliases via `buildJavaTaintedVars`) can be bridged to sinks.
12518
+ //
12519
+ // Java-only: the Python/Rust alias-expansion branches in
12520
+ // taint-propagation-pass.ts use the earliest sourcesWithVar entry
12521
+ // as the anchor for synthetic derived sources. Adding
12522
+ // interprocedural_param to sourcesWithVar in those languages
12523
+ // would cause the anchor to become the low-confidence
12524
+ // interprocedural_param at the method decl line instead of the
12525
+ // real HTTP source, breaking downstream sink-type filters
12526
+ // (regressed #78, #92.1, #105 FP-31, #215 recall).
12527
+ ...language === "java" ? { variable: param.name } : {}
12437
12528
  });
12438
12529
  }
12439
12530
  }
@@ -12897,7 +12988,16 @@ function isSafeGoJsonUnmarshalCall(call, pattern, language, sourceLines) {
12897
12988
  return false;
12898
12989
  }
12899
12990
  var TEMPLATE_LITERAL_RECEIVER_RE = /^Template\(\s*(?:"[^"\\]*"|'[^'\\]*')\s*\)$/;
12900
- function isSafeJinjaRenderCall(call, pattern, language) {
12991
+ var JINJA_AUTOESCAPE_TRUE_RE = /\bEnvironment\s*\([^)]*\bautoescape\s*=\s*True\b/;
12992
+ var JINJA_SELECT_AUTOESCAPE_RE = /\bEnvironment\s*\([^)]*\bautoescape\s*=\s*select_autoescape\s*\(/;
12993
+ var JINJA_AUTOESCAPE_FALSE_RE = /\bEnvironment\s*\([^)]*\bautoescape\s*=\s*False\b/;
12994
+ function fileHasSafeJinjaEnvironment(sourceLines) {
12995
+ if (!sourceLines || sourceLines.length === 0) return false;
12996
+ const text = sourceLines.join("\n");
12997
+ if (JINJA_AUTOESCAPE_FALSE_RE.test(text)) return false;
12998
+ return JINJA_AUTOESCAPE_TRUE_RE.test(text) || JINJA_SELECT_AUTOESCAPE_RE.test(text);
12999
+ }
13000
+ function isSafeJinjaRenderCall(call, pattern, language, sourceLines) {
12901
13001
  if (language !== "python") return false;
12902
13002
  if (pattern.type !== "xss" && pattern.type !== "code_injection") return false;
12903
13003
  const method = call.method_name;
@@ -12911,7 +13011,8 @@ function isSafeJinjaRenderCall(call, pattern, language) {
12911
13011
  }
12912
13012
  if (method === "render") {
12913
13013
  const receiver = (call.receiver ?? "").trim();
12914
- return TEMPLATE_LITERAL_RECEIVER_RE.test(receiver);
13014
+ if (TEMPLATE_LITERAL_RECEIVER_RE.test(receiver)) return true;
13015
+ if (fileHasSafeJinjaEnvironment(sourceLines)) return true;
12915
13016
  }
12916
13017
  return false;
12917
13018
  }
@@ -12959,7 +13060,7 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines) {
12959
13060
  if (isSafeGoJsonUnmarshalCall(call, pattern, language, sourceLines)) {
12960
13061
  continue;
12961
13062
  }
12962
- if (isSafeJinjaRenderCall(call, pattern, language)) {
13063
+ if (isSafeJinjaRenderCall(call, pattern, language, sourceLines)) {
12963
13064
  continue;
12964
13065
  }
12965
13066
  const location = formatCallLocation(call);
@@ -12967,6 +13068,8 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines) {
12967
13068
  const confidence = calculateSinkConfidence(call, pattern);
12968
13069
  const existing = sinkMap.get(key);
12969
13070
  if (!existing || confidence > existing.confidence) {
13071
+ const receiverType = call.receiver_type;
13072
+ const simpleClass = receiverType ? receiverType.split(".").pop() || void 0 : void 0;
12970
13073
  sinkMap.set(key, {
12971
13074
  type: pattern.type,
12972
13075
  cwe: pattern.cwe,
@@ -12974,7 +13077,8 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines) {
12974
13077
  line: call.location.line,
12975
13078
  confidence,
12976
13079
  method: call.method_name,
12977
- argPositions: pattern.arg_positions
13080
+ argPositions: pattern.arg_positions,
13081
+ class: simpleClass
12978
13082
  });
12979
13083
  }
12980
13084
  }
@@ -828,6 +828,20 @@ function resolveReceiverType(receiver, context) {
828
828
  // `super` — defer; cannot determine parent class without hierarchy
829
829
  if (receiver === 'super')
830
830
  return { simpleName: null, fqn: null };
831
+ // Constructor receiver: `new X(...)` or `new pkg.X(...)` — resolves to the
832
+ // constructed class. Enables sink matching on inline receivers built via
833
+ // `new Yaml().load(taint)` (#189 Sprint 92 SnakeYAML) or
834
+ // `new ObjectMapper().readValue(taint)` (#189 Sprint 92 Jackson). Nested
835
+ // constructor arguments are ignored — only the outer class name matters
836
+ // for method-receiver resolution.
837
+ const ctorMatch = receiver.match(/^new\s+([A-Za-z_$][\w$.]*)\s*[<(]/);
838
+ if (ctorMatch) {
839
+ const ctorClass = ctorMatch[1];
840
+ const simple = ctorClass.includes('.')
841
+ ? ctorClass.substring(ctorClass.lastIndexOf('.') + 1)
842
+ : ctorClass;
843
+ return resolveFqn(simple, context);
844
+ }
831
845
  // Local variable, parameter, or field declared in this file
832
846
  const declaredType = context.localVarTypes.get(receiver) ??
833
847
  context.paramTypes.get(receiver) ??