circle-ir 3.194.0 → 3.197.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 +624 -2
  7. package/dist/analysis/passes/language-sources-pass.js.map +1 -1
  8. package/dist/analysis/passes/sink-filter-pass.d.ts.map +1 -1
  9. package/dist/analysis/passes/sink-filter-pass.js +68 -1
  10. package/dist/analysis/passes/sink-filter-pass.js.map +1 -1
  11. package/dist/analysis/passes/taint-matcher-pass.d.ts.map +1 -1
  12. package/dist/analysis/passes/taint-matcher-pass.js.map +1 -1
  13. package/dist/analysis/rules.d.ts.map +1 -1
  14. package/dist/analysis/rules.js +42 -0
  15. package/dist/analysis/rules.js.map +1 -1
  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/browser/circle-ir.js +582 -604
  20. package/dist/core/circle-ir-core.cjs +59 -13
  21. package/dist/core/circle-ir-core.js +59 -13
  22. package/dist/languages/plugins/bash.d.ts.map +1 -1
  23. package/dist/languages/plugins/bash.js +12 -16
  24. package/dist/languages/plugins/bash.js.map +1 -1
  25. package/dist/languages/plugins/go.d.ts +4 -0
  26. package/dist/languages/plugins/go.d.ts.map +1 -1
  27. package/dist/languages/plugins/go.js +16 -32
  28. package/dist/languages/plugins/go.js.map +1 -1
  29. package/dist/languages/plugins/java.d.ts +9 -1
  30. package/dist/languages/plugins/java.d.ts.map +1 -1
  31. package/dist/languages/plugins/java.js +16 -127
  32. package/dist/languages/plugins/java.js.map +1 -1
  33. package/dist/languages/plugins/javascript.d.ts +7 -0
  34. package/dist/languages/plugins/javascript.d.ts.map +1 -1
  35. package/dist/languages/plugins/javascript.js +10 -117
  36. package/dist/languages/plugins/javascript.js.map +1 -1
  37. package/dist/languages/plugins/python.d.ts +7 -0
  38. package/dist/languages/plugins/python.d.ts.map +1 -1
  39. package/dist/languages/plugins/python.js +24 -177
  40. package/dist/languages/plugins/python.js.map +1 -1
  41. package/dist/languages/plugins/rust.d.ts +9 -0
  42. package/dist/languages/plugins/rust.d.ts.map +1 -1
  43. package/dist/languages/plugins/rust.js +22 -110
  44. package/dist/languages/plugins/rust.js.map +1 -1
  45. package/dist/languages/types.d.ts +3 -3
  46. package/dist/languages/types.d.ts.map +1 -1
  47. package/dist/types/config.d.ts +15 -0
  48. package/dist/types/config.d.ts.map +1 -1
  49. package/dist/types/index.d.ts +13 -2
  50. package/dist/types/index.d.ts.map +1 -1
  51. package/dist/types/index.js +37 -1
  52. package/dist/types/index.js.map +1 -1
  53. package/package.json +3 -2
@@ -11360,7 +11360,11 @@ var DEFAULT_SOURCES = [
11360
11360
  { method: "Json", type: "http_body", severity: "high", return_tainted: true, languages: ["rust"] },
11361
11361
  { method: "Query", type: "http_param", severity: "high", return_tainted: true, languages: ["rust"] },
11362
11362
  { method: "Path", type: "http_path", severity: "high", return_tainted: true, languages: ["rust"] },
11363
- { 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"] },
11364
11368
  // Rust std library
11365
11369
  { method: "var", class: "env", type: "env_input", severity: "medium", return_tainted: true },
11366
11370
  { method: "var_os", class: "env", type: "env_input", severity: "medium", return_tainted: true },
@@ -12638,7 +12642,11 @@ var DEFAULT_SINKS = [
12638
12642
  // Also match without receiver (destructured imports: const { exec } = require('child_process'))
12639
12643
  // `exec` is intentionally classless: catches Node.js child_process.exec AND
12640
12644
  // Java Runtime.exec (via `r.exec()` where heuristic can't resolve r → Runtime).
12641
- { 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"] },
12642
12650
  // `execSync`/`spawn`/`spawnSync`/`execFile` are Node-specific — language-scope them.
12643
12651
  { method: "execSync", type: "command_injection", cwe: "CWE-78", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
12644
12652
  { method: "spawn", type: "command_injection", cwe: "CWE-78", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
@@ -12666,7 +12674,11 @@ var DEFAULT_SINKS = [
12666
12674
  // Note: classless { method: 'query' } removed — too many FPs (UriComponentsBuilder.query(), etc.)
12667
12675
  // SQL query calls are covered by class-specific patterns above (Connection, Pool, Client, JdbcTemplate)
12668
12676
  // Note: `raw` is shared with Python (Django ORM) — scoped to JS+TS to avoid leaking.
12669
- { 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"] },
12670
12682
  // sqlite3 (npm) — Database/Statement methods. The JS plugin resolves
12671
12683
  // `const db = new sqlite3.Database(...); db.all(sql)` to the resolution
12672
12684
  // target `Connection.all`, so class-scoped patterns matching `Connection`
@@ -13225,10 +13237,30 @@ var DEFAULT_SINKS = [
13225
13237
  { method: "Sprintf", class: "fmt", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [0], languages: ["go"] },
13226
13238
  { method: "Printf", class: "fmt", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [0], languages: ["go"] },
13227
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.
13228
13251
  { method: "Fprintf", class: "fmt", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [1], languages: ["go"] },
13229
13252
  // cognium-dev #264 — Go stdlib `log` package format-string entry points.
13230
13253
  // log.Printf / Fatalf / Panicf take the format string at arg[0]; tainted
13231
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.
13232
13264
  { method: "Printf", class: "log", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [0], languages: ["go"] },
13233
13265
  { method: "Fatalf", class: "log", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [0], languages: ["go"] },
13234
13266
  { method: "Panicf", class: "log", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [0], languages: ["go"] },
@@ -13242,6 +13274,14 @@ var DEFAULT_SINKS = [
13242
13274
  { method: "cookie", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1], languages: ["javascript", "typescript"] },
13243
13275
  // Express: res.location(url) and res.redirect(url) — Location header.
13244
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.
13245
13285
  { method: "redirect", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [0], languages: ["javascript", "typescript"] },
13246
13286
  // Go net/http: w.Header().Set(k, v) / Add(k, v) — first arg is the value
13247
13287
  // (Header is a map; the actual `value` is arg 1 of the call). We flag the
@@ -14162,18 +14202,28 @@ function analyzeTaint(calls, types, config = getDefaultConfig(), typeHierarchy,
14162
14202
  const sanitizers = findSanitizers(calls, types, config.sanitizers, sourceLines);
14163
14203
  return { sources, sinks, sanitizers };
14164
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
+ }
14165
14215
  function expandPromisifyAliases(patterns, sourceLines, language) {
14166
14216
  if (!sourceLines || sourceLines.length === 0) return patterns;
14167
14217
  if (language !== "javascript" && language !== "typescript") return patterns;
14168
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*\)/;
14169
14219
  const innerToPattern = /* @__PURE__ */ new Map();
14170
14220
  for (const p of patterns) {
14171
- if (p.languages && !p.languages.includes(language)) continue;
14221
+ if (!sinkPatternAppliesTo(p, language)) continue;
14172
14222
  if (p.class !== void 0) continue;
14173
14223
  if (!innerToPattern.has(p.method)) innerToPattern.set(p.method, p);
14174
14224
  }
14175
14225
  for (const p of patterns) {
14176
- if (p.languages && !p.languages.includes(language)) continue;
14226
+ if (!sinkPatternAppliesTo(p, language)) continue;
14177
14227
  if (innerToPattern.has(p.method)) continue;
14178
14228
  innerToPattern.set(p.method, p);
14179
14229
  }
@@ -14207,7 +14257,7 @@ function expandIndirectEvalAliases(patterns, sourceLines, language) {
14207
14257
  const INDIRECT_EVAL_RE = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*(?::[^=]+)?=\s*(eval|Function)\b(?!\s*\()/;
14208
14258
  const innerToPattern = /* @__PURE__ */ new Map();
14209
14259
  for (const p of patterns) {
14210
- if (p.languages && !p.languages.includes(language)) continue;
14260
+ if (!sinkPatternAppliesTo(p, language)) continue;
14211
14261
  if (p.class !== void 0) continue;
14212
14262
  if (p.method !== "eval" && p.method !== "Function") continue;
14213
14263
  if (!innerToPattern.has(p.method)) innerToPattern.set(p.method, p);
@@ -14915,9 +14965,7 @@ function isSafeJinjaRenderCall(call, pattern, language, sourceLines) {
14915
14965
  return false;
14916
14966
  }
14917
14967
  function findSinks(calls, patterns, typeHierarchy, language, sourceLines, types) {
14918
- const patternsForLanguage = language === void 0 ? patterns : patterns.filter(
14919
- (p) => !p.languages || p.languages.length === 0 || p.languages.includes(language)
14920
- );
14968
+ const patternsForLanguage = language === void 0 ? patterns : patterns.filter((p) => sinkPatternAppliesTo(p, language));
14921
14969
  const sinkMap = /* @__PURE__ */ new Map();
14922
14970
  for (const call of calls) {
14923
14971
  for (const pattern of patternsForLanguage) {
@@ -15262,10 +15310,8 @@ var SINK_FQN_EXCLUSIONS = {
15262
15310
  ]
15263
15311
  };
15264
15312
  function matchesSinkPattern(call, pattern, typeHierarchy, language) {
15265
- if (pattern.languages && pattern.languages.length > 0 && language !== void 0) {
15266
- if (!pattern.languages.includes(language)) {
15267
- return false;
15268
- }
15313
+ if (!sinkPatternAppliesTo(pattern, language)) {
15314
+ return false;
15269
15315
  }
15270
15316
  const callMethodName = call.method_name;
15271
15317
  const patternMethod = pattern.method;
@@ -22146,107 +22192,30 @@ var JavaPlugin = class extends BaseLanguagePlugin {
22146
22192
  severity: "high",
22147
22193
  confidence: 0.9,
22148
22194
  returnTainted: true
22149
- },
22150
- // Servlet API
22151
- {
22152
- method: "getParameter",
22153
- class: "HttpServletRequest",
22154
- type: "http_param",
22155
- severity: "high",
22156
- confidence: 0.95,
22157
- returnTainted: true
22158
- },
22159
- {
22160
- method: "getParameterValues",
22161
- class: "HttpServletRequest",
22162
- type: "http_param",
22163
- severity: "high",
22164
- confidence: 0.95,
22165
- returnTainted: true
22166
- },
22167
- {
22168
- method: "getHeader",
22169
- class: "HttpServletRequest",
22170
- type: "http_header",
22171
- severity: "high",
22172
- confidence: 0.9,
22173
- returnTainted: true
22174
- },
22175
- {
22176
- method: "getCookies",
22177
- class: "HttpServletRequest",
22178
- type: "http_cookie",
22179
- severity: "high",
22180
- confidence: 0.9,
22181
- returnTainted: true
22182
- },
22183
- {
22184
- method: "getInputStream",
22185
- class: "HttpServletRequest",
22186
- type: "http_body",
22187
- severity: "high",
22188
- confidence: 0.85,
22189
- returnTainted: true
22190
- },
22191
- {
22192
- method: "getReader",
22193
- class: "HttpServletRequest",
22194
- type: "http_body",
22195
- severity: "high",
22196
- confidence: 0.85,
22197
- returnTainted: true
22198
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).
22199
22203
  ];
22200
22204
  }
22201
22205
  /**
22202
22206
  * Java-specific taint sink patterns.
22203
- * 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`).
22204
22216
  */
22205
22217
  getBuiltinSinks() {
22206
22218
  return [
22207
- // SQL Injection
22208
- {
22209
- method: "executeQuery",
22210
- class: "Statement",
22211
- type: "sql_injection",
22212
- cwe: "CWE-89",
22213
- severity: "critical",
22214
- argPositions: [0]
22215
- },
22216
- {
22217
- method: "executeUpdate",
22218
- class: "Statement",
22219
- type: "sql_injection",
22220
- cwe: "CWE-89",
22221
- severity: "critical",
22222
- argPositions: [0]
22223
- },
22224
- {
22225
- method: "execute",
22226
- class: "Statement",
22227
- type: "sql_injection",
22228
- cwe: "CWE-89",
22229
- severity: "critical",
22230
- argPositions: [0]
22231
- },
22232
- // Command Injection
22233
- {
22234
- method: "exec",
22235
- class: "Runtime",
22236
- type: "command_injection",
22237
- cwe: "CWE-78",
22238
- severity: "critical",
22239
- argPositions: [0]
22240
- },
22241
- {
22242
- method: "start",
22243
- class: "ProcessBuilder",
22244
- type: "command_injection",
22245
- cwe: "CWE-78",
22246
- severity: "critical",
22247
- argPositions: []
22248
- // Constructor args are dangerous
22249
- },
22250
22219
  // Path Traversal
22251
22220
  {
22252
22221
  method: "FileInputStream",
@@ -22262,41 +22231,6 @@ var JavaPlugin = class extends BaseLanguagePlugin {
22262
22231
  severity: "high",
22263
22232
  argPositions: [0]
22264
22233
  },
22265
- // XSS
22266
- {
22267
- method: "write",
22268
- class: "PrintWriter",
22269
- type: "xss",
22270
- cwe: "CWE-79",
22271
- severity: "high",
22272
- argPositions: [0]
22273
- },
22274
- {
22275
- method: "println",
22276
- class: "PrintWriter",
22277
- type: "xss",
22278
- cwe: "CWE-79",
22279
- severity: "high",
22280
- argPositions: [0]
22281
- },
22282
- // LDAP Injection
22283
- {
22284
- method: "search",
22285
- class: "DirContext",
22286
- type: "ldap_injection",
22287
- cwe: "CWE-90",
22288
- severity: "high",
22289
- argPositions: [0, 1]
22290
- },
22291
- // XPath Injection
22292
- {
22293
- method: "evaluate",
22294
- class: "XPath",
22295
- type: "xpath_injection",
22296
- cwe: "CWE-643",
22297
- severity: "high",
22298
- argPositions: [0]
22299
- },
22300
22234
  // Deserialization
22301
22235
  {
22302
22236
  method: "readObject",
@@ -22855,42 +22789,17 @@ var JavaScriptPlugin = class extends BaseLanguagePlugin {
22855
22789
  }
22856
22790
  /**
22857
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.
22858
22799
  */
22859
22800
  getBuiltinSinks() {
22860
22801
  return [
22861
- // Command Injection
22862
- {
22863
- method: "exec",
22864
- class: "child_process",
22865
- type: "command_injection",
22866
- cwe: "CWE-78",
22867
- severity: "critical",
22868
- argPositions: [0]
22869
- },
22870
- {
22871
- method: "execSync",
22872
- class: "child_process",
22873
- type: "command_injection",
22874
- cwe: "CWE-78",
22875
- severity: "critical",
22876
- argPositions: [0]
22877
- },
22878
- {
22879
- method: "spawn",
22880
- class: "child_process",
22881
- type: "command_injection",
22882
- cwe: "CWE-78",
22883
- severity: "critical",
22884
- argPositions: [0, 1]
22885
- },
22886
22802
  // Code Injection
22887
- {
22888
- method: "eval",
22889
- type: "code_injection",
22890
- cwe: "CWE-94",
22891
- severity: "critical",
22892
- argPositions: [0]
22893
- },
22894
22803
  {
22895
22804
  method: "Function",
22896
22805
  type: "code_injection",
@@ -22914,46 +22823,7 @@ var JavaScriptPlugin = class extends BaseLanguagePlugin {
22914
22823
  argPositions: [0]
22915
22824
  // When first arg is string
22916
22825
  },
22917
- // Path Traversal
22918
- {
22919
- method: "readFileSync",
22920
- class: "fs",
22921
- type: "path_traversal",
22922
- cwe: "CWE-22",
22923
- severity: "high",
22924
- argPositions: [0]
22925
- },
22926
- {
22927
- method: "writeFileSync",
22928
- class: "fs",
22929
- type: "path_traversal",
22930
- cwe: "CWE-22",
22931
- severity: "high",
22932
- argPositions: [0]
22933
- },
22934
- {
22935
- method: "createReadStream",
22936
- class: "fs",
22937
- type: "path_traversal",
22938
- cwe: "CWE-22",
22939
- severity: "high",
22940
- argPositions: [0]
22941
- },
22942
22826
  // XSS (DOM)
22943
- {
22944
- method: "innerHTML",
22945
- type: "xss",
22946
- cwe: "CWE-79",
22947
- severity: "high",
22948
- argPositions: [0]
22949
- },
22950
- {
22951
- method: "outerHTML",
22952
- type: "xss",
22953
- cwe: "CWE-79",
22954
- severity: "high",
22955
- argPositions: [0]
22956
- },
22957
22827
  {
22958
22828
  method: "document.write",
22959
22829
  type: "xss",
@@ -23005,13 +22875,6 @@ var JavaScriptPlugin = class extends BaseLanguagePlugin {
23005
22875
  severity: "critical",
23006
22876
  argPositions: [0]
23007
22877
  },
23008
- {
23009
- method: "raw",
23010
- type: "sql_injection",
23011
- cwe: "CWE-89",
23012
- severity: "critical",
23013
- argPositions: [0]
23014
- },
23015
22878
  // Prisma ORM - unsafe raw query methods ($executeRaw/$queryRaw with template literals are safe/parameterized)
23016
22879
  {
23017
22880
  method: "$executeRawUnsafe",
@@ -23027,30 +22890,6 @@ var JavaScriptPlugin = class extends BaseLanguagePlugin {
23027
22890
  severity: "critical",
23028
22891
  argPositions: [0]
23029
22892
  },
23030
- // SSRF
23031
- {
23032
- method: "fetch",
23033
- type: "ssrf",
23034
- cwe: "CWE-918",
23035
- severity: "high",
23036
- argPositions: [0]
23037
- },
23038
- {
23039
- method: "get",
23040
- class: "axios",
23041
- type: "ssrf",
23042
- cwe: "CWE-918",
23043
- severity: "high",
23044
- argPositions: [0]
23045
- },
23046
- {
23047
- method: "request",
23048
- class: "http",
23049
- type: "ssrf",
23050
- cwe: "CWE-918",
23051
- severity: "high",
23052
- argPositions: [0]
23053
- },
23054
22893
  // NoSQL Injection
23055
22894
  {
23056
22895
  method: "find",
@@ -23059,13 +22898,6 @@ var JavaScriptPlugin = class extends BaseLanguagePlugin {
23059
22898
  severity: "high",
23060
22899
  argPositions: [0]
23061
22900
  },
23062
- {
23063
- method: "findOne",
23064
- type: "nosql_injection",
23065
- cwe: "CWE-943",
23066
- severity: "high",
23067
- argPositions: [0]
23068
- },
23069
22901
  // Prototype Pollution (JS-specific)
23070
22902
  {
23071
22903
  method: "merge",
@@ -23159,14 +22991,9 @@ var JavaScriptPlugin = class extends BaseLanguagePlugin {
23159
22991
  // =========================================================
23160
22992
  // Next.js Sinks
23161
22993
  // =========================================================
23162
- {
23163
- // Server-side redirect
23164
- method: "redirect",
23165
- type: "open_redirect",
23166
- cwe: "CWE-601",
23167
- severity: "high",
23168
- argPositions: [0]
23169
- },
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).
23170
22997
  {
23171
22998
  // Router push with user-controlled URL
23172
22999
  method: "push",
@@ -23448,13 +23275,15 @@ var PythonPlugin = class extends BaseLanguagePlugin {
23448
23275
  returnTainted: true
23449
23276
  },
23450
23277
  // Standard library sources
23451
- {
23452
- method: "input",
23453
- type: "user_input",
23454
- severity: "high",
23455
- confidence: 0.95,
23456
- returnTainted: true
23457
- },
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).
23458
23287
  {
23459
23288
  method: "argv",
23460
23289
  class: "sys",
@@ -23470,153 +23299,37 @@ var PythonPlugin = class extends BaseLanguagePlugin {
23470
23299
  severity: "medium",
23471
23300
  confidence: 0.85,
23472
23301
  returnTainted: true
23473
- },
23474
- {
23475
- method: "getenv",
23476
- class: "os",
23477
- type: "env_var",
23478
- severity: "medium",
23479
- confidence: 0.85,
23480
- returnTainted: true
23481
- },
23482
- // File reading
23483
- {
23484
- method: "read",
23485
- type: "file_input",
23486
- severity: "medium",
23487
- confidence: 0.8,
23488
- returnTainted: true
23489
- },
23490
- {
23491
- method: "readline",
23492
- type: "file_input",
23493
- severity: "medium",
23494
- confidence: 0.8,
23495
- returnTainted: true
23496
- },
23497
- {
23498
- method: "readlines",
23499
- type: "file_input",
23500
- severity: "medium",
23501
- confidence: 0.8,
23502
- returnTainted: true
23503
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).
23504
23309
  ];
23505
23310
  }
23506
23311
  /**
23507
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.
23508
23320
  */
23509
23321
  getBuiltinSinks() {
23510
23322
  return [
23511
- // Command Injection
23323
+ // Path Traversal
23512
23324
  {
23513
- method: "system",
23514
- class: "os",
23515
- type: "command_injection",
23516
- cwe: "CWE-78",
23517
- severity: "critical",
23518
- argPositions: [0]
23519
- },
23520
- {
23521
- method: "popen",
23522
- class: "os",
23523
- type: "command_injection",
23524
- cwe: "CWE-78",
23525
- severity: "critical",
23526
- argPositions: [0]
23527
- },
23528
- {
23529
- method: "run",
23530
- class: "subprocess",
23531
- type: "command_injection",
23532
- cwe: "CWE-78",
23533
- severity: "critical",
23534
- argPositions: [0]
23535
- },
23536
- {
23537
- method: "call",
23538
- class: "subprocess",
23539
- type: "command_injection",
23540
- cwe: "CWE-78",
23541
- severity: "critical",
23542
- argPositions: [0]
23543
- },
23544
- {
23545
- method: "Popen",
23546
- class: "subprocess",
23547
- type: "command_injection",
23548
- cwe: "CWE-78",
23549
- severity: "critical",
23550
- argPositions: [0]
23551
- },
23552
- // Code Injection
23553
- {
23554
- method: "eval",
23555
- type: "code_injection",
23556
- cwe: "CWE-94",
23557
- severity: "critical",
23558
- argPositions: [0]
23559
- },
23560
- {
23561
- method: "exec",
23562
- type: "code_injection",
23563
- cwe: "CWE-94",
23564
- severity: "critical",
23565
- argPositions: [0]
23566
- },
23567
- {
23568
- method: "compile",
23569
- type: "code_injection",
23570
- cwe: "CWE-94",
23571
- severity: "high",
23572
- argPositions: [0]
23573
- },
23574
- // SQL Injection
23575
- {
23576
- method: "execute",
23577
- type: "sql_injection",
23578
- cwe: "CWE-89",
23579
- severity: "critical",
23580
- argPositions: [0]
23581
- },
23582
- {
23583
- method: "executemany",
23584
- type: "sql_injection",
23585
- cwe: "CWE-89",
23586
- severity: "critical",
23587
- argPositions: [0]
23588
- },
23589
- {
23590
- method: "raw",
23591
- type: "sql_injection",
23592
- cwe: "CWE-89",
23593
- severity: "critical",
23594
- argPositions: [0]
23595
- },
23596
- // Path Traversal
23597
- {
23598
- method: "open",
23599
- type: "path_traversal",
23600
- cwe: "CWE-22",
23601
- severity: "high",
23602
- argPositions: [0]
23603
- },
23604
- {
23605
- method: "read",
23606
- class: "pathlib",
23607
- type: "path_traversal",
23608
- cwe: "CWE-22",
23609
- severity: "high",
23610
- argPositions: [0]
23611
- },
23612
- // XSS (template injection)
23613
- {
23614
- method: "Markup",
23615
- type: "xss",
23616
- cwe: "CWE-79",
23617
- severity: "high",
23325
+ method: "read",
23326
+ class: "pathlib",
23327
+ type: "path_traversal",
23328
+ cwe: "CWE-22",
23329
+ severity: "high",
23618
23330
  argPositions: [0]
23619
23331
  },
23332
+ // XSS (template injection)
23620
23333
  {
23621
23334
  method: "safe",
23622
23335
  type: "xss",
@@ -23625,22 +23338,6 @@ var PythonPlugin = class extends BaseLanguagePlugin {
23625
23338
  argPositions: [0]
23626
23339
  },
23627
23340
  // SSRF
23628
- {
23629
- method: "get",
23630
- class: "requests",
23631
- type: "ssrf",
23632
- cwe: "CWE-918",
23633
- severity: "high",
23634
- argPositions: [0]
23635
- },
23636
- {
23637
- method: "post",
23638
- class: "requests",
23639
- type: "ssrf",
23640
- cwe: "CWE-918",
23641
- severity: "high",
23642
- argPositions: [0]
23643
- },
23644
23341
  {
23645
23342
  method: "urlopen",
23646
23343
  class: "urllib",
@@ -23668,34 +23365,11 @@ var PythonPlugin = class extends BaseLanguagePlugin {
23668
23365
  argPositions: [1]
23669
23366
  },
23670
23367
  // Deserialization
23671
- {
23672
- method: "loads",
23673
- class: "pickle",
23674
- type: "deserialization",
23675
- cwe: "CWE-502",
23676
- severity: "critical",
23677
- argPositions: [0]
23678
- },
23679
- {
23680
- method: "load",
23681
- class: "pickle",
23682
- type: "deserialization",
23683
- cwe: "CWE-502",
23684
- severity: "critical",
23685
- argPositions: [0]
23686
- },
23687
23368
  // NOTE: yaml.safe_load / yaml.SafeLoader are intentionally NOT sinks.
23688
23369
  // safe_load constructs only standard scalar/list/dict types and cannot
23689
23370
  // instantiate arbitrary Python objects, so it is not a CWE-502 sink
23690
- // (unlike yaml.load / yaml.unsafe_load / yaml.full_load below).
23691
- {
23692
- method: "load",
23693
- class: "yaml",
23694
- type: "deserialization",
23695
- cwe: "CWE-502",
23696
- severity: "critical",
23697
- argPositions: [0]
23698
- },
23371
+ // (unlike yaml.load — registered in DEFAULT_SINKS — and
23372
+ // yaml.unsafe_load / yaml.full_load below).
23699
23373
  {
23700
23374
  method: "unsafe_load",
23701
23375
  class: "yaml",
@@ -23854,34 +23528,17 @@ var RustPlugin = class extends BaseLanguagePlugin {
23854
23528
  getBuiltinSources() {
23855
23529
  return [
23856
23530
  // Actix-web request extractors
23857
- {
23858
- method: "Query",
23859
- type: "http_param",
23860
- severity: "high",
23861
- confidence: 0.95,
23862
- returnTainted: true
23863
- },
23864
- {
23865
- method: "Json",
23866
- type: "http_body",
23867
- severity: "high",
23868
- confidence: 0.95,
23869
- returnTainted: true
23870
- },
23871
- {
23872
- method: "Path",
23873
- type: "http_path",
23874
- severity: "high",
23875
- confidence: 0.95,
23876
- returnTainted: true
23877
- },
23878
- {
23879
- method: "Form",
23880
- type: "http_body",
23881
- severity: "high",
23882
- confidence: 0.95,
23883
- returnTainted: true
23884
- },
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.
23885
23542
  // Standard library sources
23886
23543
  {
23887
23544
  method: "args",
@@ -23931,20 +23588,22 @@ var RustPlugin = class extends BaseLanguagePlugin {
23931
23588
  severity: "medium",
23932
23589
  confidence: 0.8,
23933
23590
  returnTainted: true
23934
- },
23935
- // Network input
23936
- {
23937
- method: "read",
23938
- class: "TcpStream",
23939
- type: "network_input",
23940
- severity: "high",
23941
- confidence: 0.85,
23942
- returnTainted: true
23943
23591
  }
23592
+ // NOTE: `TcpStream.read` (network_input) is registered identically in
23593
+ // DEFAULT_SOURCES — duplicate removed in the #4 follow-up consolidation.
23944
23594
  ];
23945
23595
  }
23946
23596
  /**
23947
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`).
23948
23607
  */
23949
23608
  getBuiltinSinks() {
23950
23609
  return [
@@ -23957,30 +23616,7 @@ var RustPlugin = class extends BaseLanguagePlugin {
23957
23616
  severity: "critical",
23958
23617
  argPositions: [0]
23959
23618
  },
23960
- {
23961
- method: "arg",
23962
- class: "Command",
23963
- type: "command_injection",
23964
- cwe: "CWE-78",
23965
- severity: "critical",
23966
- argPositions: [0]
23967
- },
23968
- {
23969
- method: "args",
23970
- class: "Command",
23971
- type: "command_injection",
23972
- cwe: "CWE-78",
23973
- severity: "critical",
23974
- argPositions: [0]
23975
- },
23976
23619
  // SQL Injection
23977
- {
23978
- method: "execute",
23979
- type: "sql_injection",
23980
- cwe: "CWE-89",
23981
- severity: "critical",
23982
- argPositions: [0]
23983
- },
23984
23620
  {
23985
23621
  method: "query",
23986
23622
  type: "sql_injection",
@@ -23996,22 +23632,6 @@ var RustPlugin = class extends BaseLanguagePlugin {
23996
23632
  argPositions: [0]
23997
23633
  },
23998
23634
  // Path Traversal
23999
- {
24000
- method: "open",
24001
- class: "File",
24002
- type: "path_traversal",
24003
- cwe: "CWE-22",
24004
- severity: "high",
24005
- argPositions: [0]
24006
- },
24007
- {
24008
- method: "create",
24009
- class: "File",
24010
- type: "path_traversal",
24011
- cwe: "CWE-22",
24012
- severity: "high",
24013
- argPositions: [0]
24014
- },
24015
23635
  {
24016
23636
  method: "read_to_string",
24017
23637
  class: "std::fs",
@@ -24051,40 +23671,6 @@ var RustPlugin = class extends BaseLanguagePlugin {
24051
23671
  severity: "critical",
24052
23672
  argPositions: [0]
24053
23673
  },
24054
- // Deserialization (with serde)
24055
- {
24056
- method: "from_str",
24057
- class: "serde_json",
24058
- type: "deserialization",
24059
- cwe: "CWE-502",
24060
- severity: "medium",
24061
- argPositions: [0]
24062
- },
24063
- {
24064
- method: "from_slice",
24065
- class: "serde_json",
24066
- type: "deserialization",
24067
- cwe: "CWE-502",
24068
- severity: "medium",
24069
- argPositions: [0]
24070
- },
24071
- // SSRF
24072
- {
24073
- method: "get",
24074
- class: "reqwest",
24075
- type: "ssrf",
24076
- cwe: "CWE-918",
24077
- severity: "high",
24078
- argPositions: [0]
24079
- },
24080
- {
24081
- method: "post",
24082
- class: "reqwest",
24083
- type: "ssrf",
24084
- cwe: "CWE-918",
24085
- severity: "high",
24086
- argPositions: [0]
24087
- },
24088
23674
  // Regex DoS
24089
23675
  {
24090
23676
  method: "new",
@@ -24199,14 +23785,13 @@ var BashPlugin = class extends BaseLanguagePlugin {
24199
23785
  */
24200
23786
  getBuiltinSources() {
24201
23787
  return [
24202
- // read built-in reads user input from stdin.
24203
- {
24204
- method: "read",
24205
- type: "io_input",
24206
- severity: "high",
24207
- confidence: 0.9,
24208
- returnTainted: true
24209
- },
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.
24210
23795
  // curl/wget output as taint sources for supply-chain attack patterns
24211
23796
  // (e.g., curl ... | sh, $(curl ...) piped to eval/bash).
24212
23797
  {
@@ -24232,14 +23817,11 @@ var BashPlugin = class extends BaseLanguagePlugin {
24232
23817
  */
24233
23818
  getBuiltinSinks() {
24234
23819
  return [
24235
- // Code / command injection via eval
24236
- {
24237
- method: "eval",
24238
- type: "code_injection",
24239
- cwe: "CWE-94",
24240
- severity: "critical",
24241
- argPositions: [0]
24242
- },
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).
24243
23825
  // Command injection: spawning a sub-shell with -c flag
24244
23826
  {
24245
23827
  method: "bash",
@@ -24681,30 +24263,15 @@ var GoPlugin = class extends BaseLanguagePlugin {
24681
24263
  returnTainted: true
24682
24264
  },
24683
24265
  // Gin framework
24684
- {
24685
- method: "Query",
24686
- class: "Context",
24687
- type: "http_param",
24688
- severity: "high",
24689
- confidence: 0.95,
24690
- returnTainted: true
24691
- },
24692
- {
24693
- method: "Param",
24694
- class: "Context",
24695
- type: "http_path",
24696
- severity: "high",
24697
- confidence: 0.95,
24698
- returnTainted: true
24699
- },
24700
- {
24701
- method: "PostForm",
24702
- class: "Context",
24703
- type: "http_param",
24704
- severity: "high",
24705
- confidence: 0.95,
24706
- returnTainted: true
24707
- },
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.
24708
24275
  {
24709
24276
  method: "GetRawData",
24710
24277
  class: "Context",
@@ -24750,6 +24317,10 @@ var GoPlugin = class extends BaseLanguagePlugin {
24750
24317
  }
24751
24318
  /**
24752
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.
24753
24324
  */
24754
24325
  getBuiltinSinks() {
24755
24326
  return [
@@ -24926,14 +24497,9 @@ var GoPlugin = class extends BaseLanguagePlugin {
24926
24497
  severity: "medium",
24927
24498
  argPositions: [0]
24928
24499
  },
24929
- {
24930
- method: "Decode",
24931
- class: "Decoder",
24932
- type: "deserialization",
24933
- cwe: "CWE-502",
24934
- severity: "medium",
24935
- argPositions: [0]
24936
- },
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.
24937
24503
  // Log Injection — `log.Printf("event=%s", userInput)` allows
24938
24504
  // CRLF/log forging when the format string interpolates user data.
24939
24505
  // CWE-117. (cognium-dev #107)
@@ -26283,6 +25849,10 @@ var LanguageSourcesPass = class {
26283
25849
  additionalSanitizers.push(...findPythonRangeCheckGuardSanitizers(code));
26284
25850
  additionalSanitizers.push(...findPythonRegexAllowlistWrapperSanitizers(code));
26285
25851
  additionalSanitizers.push(...findPythonSetMembershipXssGuardSanitizers(code));
25852
+ additionalSanitizers.push(...findPythonTraversalRejectGuardSanitizers(code));
25853
+ additionalSanitizers.push(...findPythonStringLiteralGuardSanitizers(code));
25854
+ additionalSanitizers.push(...findPythonDefaultSaxParserXxeSanitizers(code));
25855
+ additionalSanitizers.push(...findPythonUrlAllowlistRedirectSanitizers(code));
26286
25856
  additionalSanitizers.push(...findPythonDefusedXmlSanitizers(code));
26287
25857
  additionalSanitizers.push(...findPythonJinjaAutoescapeSanitizers(code));
26288
25858
  const pyMisconfigFindings = findPythonPatternFindings(code, graph.ir.meta.file);
@@ -26897,13 +26467,138 @@ function findPythonAssignmentSources(sourceCode, language) {
26897
26467
  }
26898
26468
  return sources;
26899
26469
  }
26470
+ function evaluatePythonConstExpression(expr, consts) {
26471
+ const COMPARISON = /(<=|>=|==|!=|<|>)/;
26472
+ const parts2 = expr.split(COMPARISON);
26473
+ if (parts2.length === 3) {
26474
+ const left = evaluatePythonConstExpression(parts2[0], consts);
26475
+ const right = evaluatePythonConstExpression(parts2[2], consts);
26476
+ if (typeof left !== "number" || typeof right !== "number") return void 0;
26477
+ switch (parts2[1]) {
26478
+ case "<":
26479
+ return left < right;
26480
+ case ">":
26481
+ return left > right;
26482
+ case "<=":
26483
+ return left <= right;
26484
+ case ">=":
26485
+ return left >= right;
26486
+ case "==":
26487
+ return left === right;
26488
+ case "!=":
26489
+ return left !== right;
26490
+ default:
26491
+ return void 0;
26492
+ }
26493
+ }
26494
+ if (parts2.length !== 1) return void 0;
26495
+ const matched = expr.match(/\d+|[A-Za-z_][A-Za-z0-9_]*|\/\/|[-+*/%()]|\S/g);
26496
+ if (!matched) return void 0;
26497
+ const tokens = matched;
26498
+ let pos = 0;
26499
+ const peek = () => tokens[pos];
26500
+ const parseFactor = () => {
26501
+ const tok = tokens[pos++];
26502
+ if (tok === void 0) return void 0;
26503
+ if (tok === "(") {
26504
+ const inner = parseSum();
26505
+ if (tokens[pos++] !== ")") return void 0;
26506
+ return inner;
26507
+ }
26508
+ if (tok === "-") {
26509
+ const operand = parseFactor();
26510
+ return operand === void 0 ? void 0 : -operand;
26511
+ }
26512
+ if (/^\d+$/.test(tok)) return Number(tok);
26513
+ if (/^[A-Za-z_]/.test(tok)) return consts.get(tok);
26514
+ return void 0;
26515
+ };
26516
+ const parseTerm = () => {
26517
+ let value = parseFactor();
26518
+ if (value === void 0) return void 0;
26519
+ while (peek() === "*" || peek() === "/" || peek() === "//" || peek() === "%") {
26520
+ const op = tokens[pos++];
26521
+ const rhs = parseFactor();
26522
+ if (rhs === void 0) return void 0;
26523
+ if ((op === "/" || op === "//" || op === "%") && rhs === 0) return void 0;
26524
+ if (op === "*") value *= rhs;
26525
+ else if (op === "/") value /= rhs;
26526
+ else if (op === "//") value = Math.floor(value / rhs);
26527
+ else value = (value % rhs + rhs) % rhs;
26528
+ }
26529
+ return value;
26530
+ };
26531
+ function parseSum() {
26532
+ let value = parseTerm();
26533
+ if (value === void 0) return void 0;
26534
+ while (peek() === "+" || peek() === "-") {
26535
+ const op = tokens[pos++];
26536
+ const rhs = parseTerm();
26537
+ if (rhs === void 0) return void 0;
26538
+ value = op === "+" ? value + rhs : value - rhs;
26539
+ }
26540
+ return value;
26541
+ }
26542
+ const result = parseSum();
26543
+ return pos === tokens.length ? result : void 0;
26544
+ }
26545
+ function normalizeListIndex(index, length) {
26546
+ const resolved = index < 0 ? length + index : index;
26547
+ return resolved >= 0 && resolved < length ? resolved : void 0;
26548
+ }
26549
+ function computeDeadPythonBranchLines(lines) {
26550
+ const dead = /* @__PURE__ */ new Set();
26551
+ const consts = /* @__PURE__ */ new Map();
26552
+ const indentOf = (line) => line.length - line.trimStart().length;
26553
+ for (let i2 = 0; i2 < lines.length; i2++) {
26554
+ const line = lines[i2];
26555
+ const assign = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=(?!=)\s*(.+)$/);
26556
+ if (assign) {
26557
+ const value = evaluatePythonConstExpression(assign[2].trim(), consts);
26558
+ if (typeof value === "number") consts.set(assign[1], value);
26559
+ else consts.delete(assign[1]);
26560
+ }
26561
+ const ifMatch = line.match(/^(\s*)if\s+(.+?)\s*:\s*$/);
26562
+ if (!ifMatch) continue;
26563
+ const indent = ifMatch[1].length;
26564
+ const decided = evaluatePythonConstExpression(ifMatch[2].trim(), consts);
26565
+ if (decided !== true && decided !== false) continue;
26566
+ const thenStart = i2 + 1;
26567
+ let cursor = thenStart;
26568
+ while (cursor < lines.length && (lines[cursor].trim() === "" || indentOf(lines[cursor]) > indent)) {
26569
+ cursor++;
26570
+ }
26571
+ const thenEnd = cursor - 1;
26572
+ let elseStart = -1;
26573
+ let elseEnd = -1;
26574
+ if (cursor < lines.length && indentOf(lines[cursor]) === indent) {
26575
+ if (/^\s*elif\b/.test(lines[cursor])) continue;
26576
+ if (/^\s*else\s*:\s*$/.test(lines[cursor])) {
26577
+ elseStart = cursor + 1;
26578
+ let k = elseStart;
26579
+ while (k < lines.length && (lines[k].trim() === "" || indentOf(lines[k]) > indent)) k++;
26580
+ elseEnd = k - 1;
26581
+ }
26582
+ }
26583
+ if (decided === true) {
26584
+ for (let l = elseStart; l >= 0 && l <= elseEnd; l++) dead.add(l);
26585
+ } else {
26586
+ for (let l = thenStart; l <= thenEnd; l++) dead.add(l);
26587
+ }
26588
+ }
26589
+ return dead;
26590
+ }
26900
26591
  function buildPythonTaintedVars(sourceCode) {
26901
26592
  const tainted = /* @__PURE__ */ new Map();
26902
26593
  const containerTainted = /* @__PURE__ */ new Map();
26903
26594
  const lines = sourceCode.split("\n");
26595
+ const deadBranchLines = computeDeadPythonBranchLines(lines);
26596
+ const listElems = /* @__PURE__ */ new Map();
26597
+ const intConsts = /* @__PURE__ */ new Map();
26904
26598
  for (let i2 = 0; i2 < lines.length; i2++) {
26905
26599
  const line = lines[i2];
26906
26600
  if (line.trimStart().startsWith("#")) continue;
26601
+ if (deadBranchLines.has(i2)) continue;
26907
26602
  const subscriptAssign = line.match(/^\s*([\p{L}\p{N}_]+)\[(['"])([^'"]+)\2\]\s*=\s*(.+)$/u);
26908
26603
  if (subscriptAssign) {
26909
26604
  const [, container, , key, rhs2] = subscriptAssign;
@@ -26920,10 +26615,48 @@ function buildPythonTaintedVars(sourceCode) {
26920
26615
  }
26921
26616
  const containerAppendMatch = line.match(/^\s*([\p{L}\p{N}_]+)\.(append|extend|insert|add|push|put|appendleft)\s*\(\s*(.+?)\s*\)\s*$/u);
26922
26617
  if (containerAppendMatch) {
26923
- const [, receiver, , argExpr] = containerAppendMatch;
26618
+ const [, receiver, method, argExpr] = containerAppendMatch;
26924
26619
  const argIsTainted = [...tainted.keys()].some((v) => new RegExp(`(?<![\\p{L}\\p{N}_])${v}(?![\\p{L}\\p{N}_])`, "u").test(argExpr));
26925
26620
  const argIsDirectSource = PYTHON_TAINTED_PATTERNS2.some((p) => p.pattern.test(argExpr));
26926
26621
  if (argIsTainted || argIsDirectSource) tainted.set(receiver, tainted.get(receiver) ?? i2 + 1);
26622
+ const elems = listElems.get(receiver);
26623
+ if (elems) {
26624
+ if (method === "append") {
26625
+ elems.push(argIsTainted || argIsDirectSource);
26626
+ } else if (method === "insert") {
26627
+ const insertAt = argExpr.match(/^(-?\d+)\s*,\s*(.+)$/);
26628
+ if (insertAt) {
26629
+ const idx = normalizeListIndex(Number(insertAt[1]), elems.length);
26630
+ const valueExpr = insertAt[2];
26631
+ const valueTainted = [...tainted.keys()].some((v) => new RegExp(`(?<![\\p{L}\\p{N}_])${v}(?![\\p{L}\\p{N}_])`, "u").test(valueExpr)) || PYTHON_TAINTED_PATTERNS2.some((p) => p.pattern.test(valueExpr));
26632
+ if (idx !== void 0) elems.splice(idx, 0, valueTainted);
26633
+ else listElems.delete(receiver);
26634
+ } else {
26635
+ listElems.delete(receiver);
26636
+ }
26637
+ } else {
26638
+ listElems.delete(receiver);
26639
+ }
26640
+ }
26641
+ continue;
26642
+ }
26643
+ const popMatch = line.match(/^\s*(?:([\p{L}\p{N}_]+)\s*=\s*)?([\p{L}\p{N}_]+)\.pop\s*\(\s*(-?\d+)?\s*\)\s*$/u);
26644
+ const delMatch = line.match(/^\s*del\s+([\p{L}\p{N}_]+)\s*\[\s*(-?\d+)\s*\]\s*$/u);
26645
+ if (popMatch || delMatch) {
26646
+ const receiver = popMatch ? popMatch[2] : delMatch[1];
26647
+ const rawIndex = popMatch ? popMatch[3] !== void 0 ? Number(popMatch[3]) : -1 : Number(delMatch[2]);
26648
+ const elems = listElems.get(receiver);
26649
+ if (elems) {
26650
+ const idx = normalizeListIndex(rawIndex, elems.length);
26651
+ if (idx === void 0) listElems.delete(receiver);
26652
+ else {
26653
+ const [removed] = elems.splice(idx, 1);
26654
+ if (popMatch && popMatch[1]) {
26655
+ if (removed) tainted.set(popMatch[1], i2 + 1);
26656
+ else tainted.delete(popMatch[1]);
26657
+ }
26658
+ }
26659
+ }
26927
26660
  continue;
26928
26661
  }
26929
26662
  const augAssign = line.match(/^\s*([\p{L}\p{N}_]+)\s*\+=\s*(.+)$/u);
@@ -26943,7 +26676,48 @@ function buildPythonTaintedVars(sourceCode) {
26943
26676
  }
26944
26677
  const assignMatch = line.match(/^\s*([\p{L}\p{N}_]+)\s*=\s*(.+)$/u);
26945
26678
  if (!assignMatch) continue;
26946
- const [, lhs, rhs] = assignMatch;
26679
+ const [, lhs, rhsRaw] = assignMatch;
26680
+ const constValue = evaluatePythonConstExpression(rhsRaw.trim(), intConsts);
26681
+ if (typeof constValue === "number") intConsts.set(lhs, constValue);
26682
+ else intConsts.delete(lhs);
26683
+ let rhs = rhsRaw;
26684
+ const ternary = rhsRaw.match(/^(.+?)\s+if\s+(.+?)\s+else\s+(.+)$/);
26685
+ if (ternary) {
26686
+ const decided = evaluatePythonConstExpression(ternary[2].trim(), intConsts);
26687
+ if (decided === true) rhs = ternary[1].trim();
26688
+ else if (decided === false) rhs = ternary[3].trim();
26689
+ }
26690
+ const listLiteral = rhs.trim().match(/^\[\s*(.*?)\s*\]$/);
26691
+ if (listLiteral) {
26692
+ const inner = listLiteral[1];
26693
+ if (inner === "") {
26694
+ listElems.set(lhs, []);
26695
+ } else if (!inner.includes("[") && !inner.includes("(")) {
26696
+ listElems.set(
26697
+ lhs,
26698
+ inner.split(",").map((part) => {
26699
+ const expr = part.trim();
26700
+ return [...tainted.keys()].some((v) => new RegExp(`(?<![\\p{L}\\p{N}_])${v}(?![\\p{L}\\p{N}_])`, "u").test(expr)) || PYTHON_TAINTED_PATTERNS2.some((p) => p.pattern.test(expr));
26701
+ })
26702
+ );
26703
+ } else {
26704
+ listElems.delete(lhs);
26705
+ }
26706
+ } else {
26707
+ listElems.delete(lhs);
26708
+ }
26709
+ const indexRead = rhs.trim().match(/^([\p{L}\p{N}_]+)\s*\[\s*(-?\d+)\s*\]$/u);
26710
+ if (indexRead) {
26711
+ const elems = listElems.get(indexRead[1]);
26712
+ if (elems) {
26713
+ const idx = normalizeListIndex(Number(indexRead[2]), elems.length);
26714
+ if (idx !== void 0) {
26715
+ if (elems[idx]) tainted.set(lhs, i2 + 1);
26716
+ else tainted.delete(lhs);
26717
+ continue;
26718
+ }
26719
+ }
26720
+ }
26947
26721
  const isDirectSource = PYTHON_TAINTED_PATTERNS2.some((p) => p.pattern.test(rhs));
26948
26722
  let propagatedFrom;
26949
26723
  const dictAccessMatch = rhs.trim().match(/^([\p{L}\p{N}_]+)\[(['"])([^'"]+)\2\]$/u);
@@ -28067,6 +27841,185 @@ function findPythonSetMembershipXssGuardSanitizers(code) {
28067
27841
  }
28068
27842
  return sanitizers;
28069
27843
  }
27844
+ function findPythonTraversalRejectGuardSanitizers(code) {
27845
+ const sanitizers = [];
27846
+ const lines = code.split("\n");
27847
+ const guardOpen = /^(\s*)if\s+(['"])(\.\.[/\\]?)\2\s+in\s+([A-Za-z_][A-Za-z0-9_]*)\s*:\s*$/;
27848
+ const terminator = /\b(return|raise|abort\s*\(|sys\.exit\s*\()/;
27849
+ for (let i2 = 0; i2 < lines.length; i2++) {
27850
+ const m = guardOpen.exec(lines[i2]);
27851
+ if (!m) continue;
27852
+ const guardIndent = m[1].length;
27853
+ const guardedVar = m[4];
27854
+ let bodyHasTerminator = false;
27855
+ let blockEnd = -1;
27856
+ const maxScan = Math.min(lines.length, i2 + 26);
27857
+ for (let j = i2 + 1; j < maxScan; j++) {
27858
+ const line = lines[j];
27859
+ if (line.trim() === "") continue;
27860
+ const indent = line.length - line.trimStart().length;
27861
+ if (indent <= guardIndent) {
27862
+ blockEnd = j - 1;
27863
+ break;
27864
+ }
27865
+ if (terminator.test(line)) bodyHasTerminator = true;
27866
+ }
27867
+ if (blockEnd === -1) blockEnd = Math.min(lines.length - 1, i2 + 25);
27868
+ if (!bodyHasTerminator) continue;
27869
+ const guarded = /* @__PURE__ */ new Set([guardedVar]);
27870
+ const assignRe = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=(?!=)\s*(.*)$/;
27871
+ const mentionsGuarded = (text) => {
27872
+ for (const name2 of guarded) {
27873
+ if (new RegExp(`\\b${name2}\\b`).test(text)) return true;
27874
+ }
27875
+ return false;
27876
+ };
27877
+ for (let l = blockEnd + 2; l <= lines.length; l++) {
27878
+ const lineText = lines[l - 1];
27879
+ const assign = assignRe.exec(lineText);
27880
+ if (assign) {
27881
+ const target = assign[1];
27882
+ const rhs = assign[2];
27883
+ if (mentionsGuarded(rhs)) guarded.add(target);
27884
+ else if (target !== guardedVar) guarded.delete(target);
27885
+ }
27886
+ if (!mentionsGuarded(lineText)) continue;
27887
+ sanitizers.push({
27888
+ type: "python_traversal_reject_guard",
27889
+ method: "if",
27890
+ line: l,
27891
+ sanitizes: ["path_traversal"]
27892
+ });
27893
+ }
27894
+ }
27895
+ return sanitizers;
27896
+ }
27897
+ function findPythonStringLiteralGuardSanitizers(code) {
27898
+ const sanitizers = [];
27899
+ const lines = code.split("\n");
27900
+ const terminator = /\b(return|raise|abort\s*\(|sys\.exit\s*\()/;
27901
+ const Q = `(?:'(?:\\\\')'|"'"|'"'|"(?:\\\\")")`;
27902
+ for (let i2 = 0; i2 < lines.length; i2++) {
27903
+ const guard = lines[i2].match(
27904
+ new RegExp(
27905
+ `^(\\s*)if\\s+not\\s+([A-Za-z_][A-Za-z0-9_]*)\\.startswith\\(\\s*${Q}\\s*\\)\\s+or\\s+not\\s+\\2\\.endswith\\(\\s*${Q}\\s*\\)\\s+or\\s+${Q}\\s+in\\s+\\2\\[1:-1\\]\\s*:\\s*$`
27906
+ )
27907
+ );
27908
+ if (!guard) continue;
27909
+ const guardIndent = guard[1].length;
27910
+ const guardedVar = guard[2];
27911
+ let bodyHasTerminator = false;
27912
+ let blockEnd = -1;
27913
+ const maxScan = Math.min(lines.length, i2 + 26);
27914
+ for (let j = i2 + 1; j < maxScan; j++) {
27915
+ const line = lines[j];
27916
+ if (line.trim() === "") continue;
27917
+ const indent = line.length - line.trimStart().length;
27918
+ if (indent <= guardIndent) {
27919
+ blockEnd = j - 1;
27920
+ break;
27921
+ }
27922
+ if (terminator.test(line)) bodyHasTerminator = true;
27923
+ }
27924
+ if (blockEnd === -1) blockEnd = Math.min(lines.length - 1, i2 + 25);
27925
+ if (!bodyHasTerminator) continue;
27926
+ const varRe = new RegExp(`\\b${guardedVar}\\b`);
27927
+ for (let l = blockEnd + 2; l <= lines.length; l++) {
27928
+ if (!varRe.test(lines[l - 1])) continue;
27929
+ sanitizers.push({
27930
+ type: "python_string_literal_guard",
27931
+ method: "if",
27932
+ line: l,
27933
+ sanitizes: ["code_injection"]
27934
+ });
27935
+ }
27936
+ }
27937
+ return sanitizers;
27938
+ }
27939
+ function findPythonDefaultSaxParserXxeSanitizers(code) {
27940
+ const sanitizers = [];
27941
+ const lines = code.split("\n");
27942
+ const parserVars = /* @__PURE__ */ new Set();
27943
+ for (const line of lines) {
27944
+ const made = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(?:xml\.)?sax\.make_parser\s*\(/);
27945
+ if (made) parserVars.add(made[1]);
27946
+ }
27947
+ if (parserVars.size === 0) return sanitizers;
27948
+ for (const line of lines) {
27949
+ for (const parser of [...parserVars]) {
27950
+ const enabled = new RegExp(
27951
+ `\\b${parser}\\.setFeature\\s*\\([^,]*external_(?:ges|pes)[^,]*,\\s*True\\s*\\)`
27952
+ );
27953
+ const resolver = new RegExp(`\\b${parser}\\.setEntityResolver\\s*\\(`);
27954
+ const rebound = new RegExp(`^\\s*${parser}\\s*=(?!=)`);
27955
+ if (enabled.test(line) || resolver.test(line)) parserVars.delete(parser);
27956
+ else if (rebound.test(line) && !/make_parser\s*\(/.test(line)) parserVars.delete(parser);
27957
+ }
27958
+ }
27959
+ if (parserVars.size === 0) return sanitizers;
27960
+ for (let i2 = 0; i2 < lines.length; i2++) {
27961
+ const line = lines[i2];
27962
+ const parseCall = /\b(?:minidom\.)?parseString\s*\(|\b(?:minidom\.)?parse\s*\(|\bsax\.parse(?:String)?\s*\(/;
27963
+ if (!parseCall.test(line)) continue;
27964
+ const usesDefaultParser = [...parserVars].some((p) => new RegExp(`\\b${p}\\b`).test(line));
27965
+ if (!usesDefaultParser) continue;
27966
+ sanitizers.push({
27967
+ type: "python_default_sax_parser",
27968
+ method: "make_parser",
27969
+ line: i2 + 1,
27970
+ sanitizes: ["xxe"]
27971
+ });
27972
+ }
27973
+ return sanitizers;
27974
+ }
27975
+ function findPythonUrlAllowlistRedirectSanitizers(code) {
27976
+ const sanitizers = [];
27977
+ const lines = code.split("\n");
27978
+ const parsedFrom = /* @__PURE__ */ new Map();
27979
+ for (const line of lines) {
27980
+ const m = line.match(
27981
+ /^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(?:urllib\.parse\.)?urlparse\s*\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*\)/
27982
+ );
27983
+ if (m) parsedFrom.set(m[1], m[2]);
27984
+ }
27985
+ if (parsedFrom.size === 0) return sanitizers;
27986
+ const terminator = /\b(return|raise|abort\s*\(|sys\.exit\s*\()/;
27987
+ for (let i2 = 0; i2 < lines.length; i2++) {
27988
+ const guard = lines[i2].match(
27989
+ /^(\s*)if\s+([A-Za-z_][A-Za-z0-9_]*)\.netloc\s+not\s+in\s+[[({]/
27990
+ );
27991
+ if (!guard) continue;
27992
+ const guardIndent = guard[1].length;
27993
+ const rawVar = parsedFrom.get(guard[2]);
27994
+ if (!rawVar) continue;
27995
+ let bodyHasTerminator = false;
27996
+ let blockEnd = -1;
27997
+ const maxScan = Math.min(lines.length, i2 + 26);
27998
+ for (let j = i2 + 1; j < maxScan; j++) {
27999
+ const line = lines[j];
28000
+ if (line.trim() === "") continue;
28001
+ const indent = line.length - line.trimStart().length;
28002
+ if (indent <= guardIndent) {
28003
+ blockEnd = j - 1;
28004
+ break;
28005
+ }
28006
+ if (terminator.test(line)) bodyHasTerminator = true;
28007
+ }
28008
+ if (blockEnd === -1) blockEnd = Math.min(lines.length - 1, i2 + 25);
28009
+ if (!bodyHasTerminator) continue;
28010
+ const varRe = new RegExp(`\\b${rawVar}\\b`);
28011
+ for (let l = blockEnd + 2; l <= lines.length; l++) {
28012
+ if (!varRe.test(lines[l - 1])) continue;
28013
+ sanitizers.push({
28014
+ type: "python_url_allowlist_guard",
28015
+ method: "if",
28016
+ line: l,
28017
+ sanitizes: ["open_redirect"]
28018
+ });
28019
+ }
28020
+ }
28021
+ return sanitizers;
28022
+ }
28070
28023
  function findPythonDefusedXmlSanitizers(code) {
28071
28024
  const sanitizers = [];
28072
28025
  const lines = code.split("\n");
@@ -32588,15 +32541,19 @@ var SinkFilterPass = class {
32588
32541
  filtered = filtered.filter((sink) => {
32589
32542
  if (sink.type !== "xpath_injection") return true;
32590
32543
  const sinkLineText = sourceLines[sink.line - 1] ?? "";
32591
- const taintedVarOnLine = [...pyTaintedVars.keys()].find(
32592
- (v) => new RegExp(`\\b${v}\\b`).test(sinkLineText)
32544
+ const taintedValueVars = [...pyTaintedVars.keys()].filter(
32545
+ (v) => new RegExp(`(?<![\\w.])${v}\\b(?!\\s*=(?!=))`).test(sinkLineText)
32593
32546
  );
32547
+ const taintedVarOnLine = taintedValueVars[0];
32594
32548
  const oopVarOnLine = [...oopFieldVars].find(
32595
32549
  (v) => sinkLineText.includes(v)
32596
32550
  );
32597
32551
  if (oopVarOnLine) return true;
32598
32552
  if (!taintedVarOnLine) return false;
32553
+ const neutralised = (v) => pySanitizedVars.has(v) || new RegExp(`\\.xpath\\s*\\([^)]*\\b\\w+\\s*=\\s*\\b${v}\\b`).test(sinkLineText) || isXPathQuoteEscapedOnLine(sinkLineText, v);
32554
+ if (taintedValueVars.every(neutralised)) return false;
32599
32555
  if (pySanitizedVars.has(taintedVarOnLine)) return false;
32556
+ if (isXPathQuoteEscapedOnLine(sinkLineText, taintedVarOnLine)) return false;
32600
32557
  if (new RegExp(`\\.xpath\\s*\\([^)]*\\b\\w+\\s*=\\s*\\b${taintedVarOnLine}\\b`).test(sinkLineText)) return false;
32601
32558
  return true;
32602
32559
  });
@@ -33572,6 +33529,27 @@ function filterSanitizedSinks(sinks, sanitizers, calls) {
33572
33529
  return true;
33573
33530
  });
33574
33531
  }
33532
+ function isXPathQuoteEscapedOnLine(lineText, varName) {
33533
+ const STRING_LITERAL = `'(?:[^'\\\\]|\\\\.)*'|"(?:[^"\\\\]|\\\\.)*"`;
33534
+ const replaceCall = new RegExp(
33535
+ `^${varName}\\.replace\\s*\\(\\s*(${STRING_LITERAL})\\s*,\\s*(${STRING_LITERAL})\\s*\\)`
33536
+ );
33537
+ const decode = (literal) => literal.slice(1, -1).replace(/\\(.)/g, "$1");
33538
+ const occurrence = new RegExp(`(?<![\\w.])${varName}\\b`, "g");
33539
+ let match;
33540
+ let seen = 0;
33541
+ while ((match = occurrence.exec(lineText)) !== null) {
33542
+ seen++;
33543
+ const rest = lineText.slice(match.index);
33544
+ const call = replaceCall.exec(rest);
33545
+ if (!call) return false;
33546
+ const searched = decode(call[1]);
33547
+ const replacement = decode(call[2]);
33548
+ if (searched !== "'" && searched !== '"') return false;
33549
+ if (replacement.includes("'") || replacement.includes('"')) return false;
33550
+ }
33551
+ return seen > 0;
33552
+ }
33575
33553
 
33576
33554
  // src/analysis/passes/sink-semantics-pass.ts
33577
33555
  function buildRegistry(entries) {