cognium-dev 4.12.0 → 4.13.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 (3) hide show
  1. package/README.md +14 -21
  2. package/dist/cli.js +799 -80
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/cognium-dev.svg)](https://www.npmjs.com/package/cognium-dev)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/cogniumhq/cognium-dev/blob/main/LICENSE)
5
- [![OWASP Benchmark](https://img.shields.io/badge/OWASP%20Benchmark-100%25%20TPR%2C%200%25%20FPR-brightgreen)](https://github.com/cogniumhq/cognium-dev#benchmark-results)
5
+ [![OWASP Benchmark](https://img.shields.io/badge/OWASP%20BenchmarkJava-90.0%25%20TPR%2C%203.0%25%20FPR-green)](https://github.com/cogniumhq/cognium-dev#benchmark-results)
6
6
  [![GitHub Action](https://img.shields.io/badge/GitHub%20Action-available-blue?logo=github)](https://github.com/marketplace/actions/cognium-dev-scan)
7
7
  ![Trust Score](https://raw.githubusercontent.com/cogniumhq/cognium-dev/badges/trust-badge.svg)
8
8
  ![Quality Score](https://raw.githubusercontent.com/cogniumhq/cognium-dev/badges/quality-badge.svg)
@@ -467,35 +467,28 @@ For details on LLM integration and benchmark improvements (42.5% → 78.3% on CW
467
467
 
468
468
  ## Benchmark Results
469
469
 
470
- **All scores below are from the static analysis engine** — fully deterministic, no LLM required:
470
+ Measured on cognium-dev **4.12.0**, the CLI as published. Fully deterministic.
471
471
 
472
- | Benchmark | Score | Details |
473
- |-----------|-------|---------|
474
- | OWASP Benchmark | +100% | TPR 100%, FPR 0% (1415 test cases) |
475
- | Juliet Test Suite | +100% | 156/156 test cases, 9 CWEs |
476
- | SecuriBench Micro | 97.7% TPR | 105/108 vulns detected, 6.7% FPR |
477
- | CWE-Bench-Java | 42.5% | 51/120 real-world CVEs |
472
+ | Benchmark | TPR | FPR | Scope and rule |
473
+ |-----------|-----|-----|----------------|
474
+ | OWASP BenchmarkJava 1.2 | 90.0% | 3.0% | All 2,740 cases; scorecard rule (file-level CWE match) |
475
+ | Juliet Test Suite 1.3 | 99.4% | 21.9% | `_01` cases of 9 injection CWEs; `bad()` vs `good*()` methods; `require-entry-path` disabled |
476
+ | SecuriBench Micro | 88.0% | 6.7% | 123 annotated cases; found sink lines vs `vuln_count` |
477
+ | CWE-Bench-Java | 32.5% (39/120) | n/a | 120 real CVEs; a finding of the CVE's CWE inside the fix method |
478
478
 
479
479
  ### Reproducing Benchmarks
480
480
 
481
- The benchmark scores are verifiable and reproducible:
481
+ Each number above comes from a `cognium-dev scan` JSON report and a scoring script in this repository. For example, OWASP:
482
482
 
483
483
  ```bash
484
- # Install cognium
485
- npm install -g cognium
486
-
487
- # Clone benchmark repositories
484
+ npm install -g cognium-dev
488
485
  git clone https://github.com/OWASP-Benchmark/BenchmarkJava
489
- git clone https://github.com/juliet-test-suite/juliet-test-suite-for-java
490
- git clone https://github.com/CWE-Bench/cwe-bench-java
491
-
492
- # Run scans
493
- cognium-dev scan BenchmarkJava/src --format json -o owasp-results.json
494
- cognium-dev scan juliet-test-suite-for-java --format json -o juliet-results.json
495
- cognium-dev scan cwe-bench-java --format json -o cwe-bench-results.json
486
+ cd BenchmarkJava
487
+ cognium-dev scan . --format json --category security -l java > owasp.json
488
+ node <cognium-dev repo>/bench/sast/score-owasp.mjs expectedresults-1.2.csv owasp.json
496
489
  ```
497
490
 
498
- For detailed benchmark methodology and comparison with other tools, see [cognium.dev](https://cognium.dev).
491
+ The scoring rule and command for every benchmark are in [`bench/README.md`](https://github.com/cogniumhq/cognium-dev/blob/main/bench/README.md#sast-accuracy-benchsast).
499
492
 
500
493
  ## Links
501
494
 
package/dist/cli.js CHANGED
@@ -9055,6 +9055,7 @@ function buildCSharpDFG(tree, cache) {
9055
9055
  currentScope(scopeStack).set(name2, def.id);
9056
9056
  }
9057
9057
  }
9058
+ const plainTargets = [];
9058
9059
  for (const asn of findNodes(body2, "assignment_expression")) {
9059
9060
  const left = asn.childForFieldName("left");
9060
9061
  if (left?.type === "identifier") {
@@ -9062,9 +9063,17 @@ function buildCSharpDFG(tree, cache) {
9062
9063
  const def = { id: defIdCounter++, variable: name2, line: asn.startPosition.row + 1, kind: "local" };
9063
9064
  defs.push(def);
9064
9065
  currentScope(scopeStack).set(name2, def.id);
9066
+ if (asn.children.some((c) => c?.type === "=")) {
9067
+ plainTargets.push({ name: name2, line: left.startPosition.row + 1 });
9068
+ }
9065
9069
  }
9066
9070
  }
9067
9071
  const bodyUses = extractUses(body2, useIdCounter, scopeStack, false);
9072
+ for (const target of plainTargets) {
9073
+ const idx = bodyUses.uses.findIndex((u) => u.variable === target.name && u.line === target.line);
9074
+ if (idx !== -1)
9075
+ bodyUses.uses.splice(idx, 1);
9076
+ }
9068
9077
  resolveReassignedUses(defs.slice(methodDefStart), bodyUses.uses);
9069
9078
  uses.push(...bodyUses.uses);
9070
9079
  useIdCounter = bodyUses.nextId;
@@ -20165,14 +20174,21 @@ class ConstantPropagator {
20165
20174
  break;
20166
20175
  }
20167
20176
  }
20168
- const switchBlock = node.children.find((c) => c.type === "switch_block");
20177
+ if (!switchValue) {
20178
+ const valueNode = node.childForFieldName("value");
20179
+ if (valueNode) {
20180
+ const literal = this.literalValue(valueNode);
20181
+ switchValue = literal !== null ? { type: typeof literal === "number" ? "int" : "string", value: literal, sourceLine: getNodeLine(valueNode) } : this.evaluateExpression(valueNode);
20182
+ }
20183
+ }
20184
+ const switchBlock = node.children.find((c) => c.type === "switch_block" || c.type === "switch_body");
20169
20185
  if (!switchBlock) {
20170
20186
  for (const child of node.children) {
20171
20187
  this.visit(child);
20172
20188
  }
20173
20189
  return;
20174
20190
  }
20175
- const caseGroups = switchBlock.children.filter((c) => c.type === "switch_block_statement_group" || c.type === "switch_rule");
20191
+ const caseGroups = switchBlock.children.filter((c) => c.type === "switch_block_statement_group" || c.type === "switch_rule" || c.type === "switch_section");
20176
20192
  if (switchValue && isKnown(switchValue)) {
20177
20193
  let matchingIdx = -1;
20178
20194
  let defaultIdx = -1;
@@ -20189,6 +20205,16 @@ class ConstantPropagator {
20189
20205
  matchingIdx = i2;
20190
20206
  }
20191
20207
  }
20208
+ } else if (caseGroup.type === "switch_section") {
20209
+ if (child.type === "default") {
20210
+ defaultIdx = i2;
20211
+ } else if (child.type === "constant_pattern") {
20212
+ const inner = child.children.find((c) => c.isNamed);
20213
+ const caseValue = inner ? this.literalValue(inner) : null;
20214
+ if (caseValue !== null && caseValue === switchValue.value) {
20215
+ matchingIdx = i2;
20216
+ }
20217
+ }
20192
20218
  }
20193
20219
  }
20194
20220
  }
@@ -20271,6 +20297,17 @@ class ConstantPropagator {
20271
20297
  }
20272
20298
  return false;
20273
20299
  }
20300
+ literalValue(node) {
20301
+ const text = getNodeText2(node, this.source);
20302
+ if (node.type === "integer_literal" || node.type === "decimal_integer_literal") {
20303
+ const n = Number(text.replace(/_/g, "").replace(/[uUlL]+$/, ""));
20304
+ return Number.isInteger(n) ? n : null;
20305
+ }
20306
+ if (node.type === "character_literal" || node.type === "string_literal") {
20307
+ return text.slice(1, -1);
20308
+ }
20309
+ return null;
20310
+ }
20274
20311
  extractCaseValue(labelNode) {
20275
20312
  for (const child of labelNode.children) {
20276
20313
  if (child.type === "decimal_integer_literal") {
@@ -34062,6 +34099,413 @@ class MyBatisAnnotationSqlSinkPass {
34062
34099
  }
34063
34100
  }
34064
34101
 
34102
+ // ../circle-ir/dist/analysis/passes/csharp-sanitizer-credit.js
34103
+ 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*\)/;
34104
+ var CSHARP_ALLOWLIST_FILTER_RE = /\bnew\s+string\s*\(\s*[A-Za-z_]\w*\s*\.\s*Where\s*\(\s*char\s*\.\s*Is(?:LetterOrDigit|Digit|Letter)\s*\)\s*\.\s*ToArray\s*\(\s*\)\s*\)/;
34105
+ var CSHARP_SANITIZER_RES = [
34106
+ { re: /\b(?:HtmlEncode|JavaScriptStringEncode)\s*\(/, type: "xss" },
34107
+ { re: /\bHtmlEncoder\s*\.\s*(?:Default\s*\.\s*)?Encode\s*\(/, type: "xss" },
34108
+ { re: /\bPath\s*\.\s*GetFileName\s*\(/, type: "path_traversal" },
34109
+ { re: /\bSecurityElement\s*\.\s*Escape\s*\(/, type: "xpath_injection" },
34110
+ { re: CSHARP_ALLOWLIST_STRIP_RE, type: "ldap_injection" },
34111
+ { re: CSHARP_ALLOWLIST_STRIP_RE, type: "xpath_injection" },
34112
+ { re: CSHARP_ALLOWLIST_STRIP_RE, type: "command_injection" },
34113
+ { re: CSHARP_ALLOWLIST_STRIP_RE, type: "sql_injection" },
34114
+ { re: CSHARP_ALLOWLIST_STRIP_RE, type: "crlf" },
34115
+ { re: CSHARP_ALLOWLIST_FILTER_RE, type: "ldap_injection" },
34116
+ { re: CSHARP_ALLOWLIST_FILTER_RE, type: "xpath_injection" },
34117
+ { re: CSHARP_ALLOWLIST_FILTER_RE, type: "command_injection" },
34118
+ { re: CSHARP_ALLOWLIST_FILTER_RE, type: "sql_injection" },
34119
+ { re: CSHARP_ALLOWLIST_FILTER_RE, type: "crlf" }
34120
+ ];
34121
+ var INERT_WHEN_ALPHANUMERIC = new Set([
34122
+ "ldap_injection",
34123
+ "xpath_injection",
34124
+ "command_injection",
34125
+ "sql_injection",
34126
+ "crlf",
34127
+ "xss",
34128
+ "path_traversal",
34129
+ "code_injection"
34130
+ ]);
34131
+ var IDENT = "[A-Za-z_]\\w*";
34132
+ var ASSIGN_RE = new RegExp(`^\\s*(?:var\\s+|[A-Za-z_][\\w.<>\\[\\]?]*\\s+)?(${IDENT})\\s*=(?!=)\\s*(.+?);?\\s*$`);
34133
+ var ENDS_STATEMENT_RE = /[;{}]\s*(?:\/\/.*)?$/;
34134
+ function escapeRegex(s) {
34135
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
34136
+ }
34137
+ function mentions(text, name2) {
34138
+ return new RegExp(`(?<![\\w])${escapeRegex(name2)}(?![\\w])`).test(text);
34139
+ }
34140
+ function insideParens(text, open) {
34141
+ let depth = 0;
34142
+ for (let i2 = open;i2 < text.length; i2++) {
34143
+ if (text[i2] === "(")
34144
+ depth++;
34145
+ else if (text[i2] === ")" && --depth === 0)
34146
+ return text.slice(open + 1, i2);
34147
+ }
34148
+ return null;
34149
+ }
34150
+ function leadingIf(text) {
34151
+ const m = /^\s*(?:\}\s*)?(?:else\s+)?if\s*\(/.exec(text);
34152
+ if (!m)
34153
+ return null;
34154
+ const open = m[0].length - 1;
34155
+ const cond = insideParens(text, open);
34156
+ if (cond === null)
34157
+ return null;
34158
+ return { cond: cond.trim(), rest: text.slice(open + cond.length + 2).trim() };
34159
+ }
34160
+ var EXITS_RE = /^(?:\{\s*)?(?:return\b|throw\b|continue\b|break\b)/;
34161
+
34162
+ class CSharpCredit {
34163
+ methods;
34164
+ lines;
34165
+ helperCache = new Map;
34166
+ guardHelperCache = new Map;
34167
+ constructor(code, methods) {
34168
+ this.methods = methods;
34169
+ this.lines = code.split(/\r?\n/);
34170
+ }
34171
+ methodAt(line) {
34172
+ let best = null;
34173
+ for (const m of this.methods) {
34174
+ if (line < m.start_line || line > m.end_line)
34175
+ continue;
34176
+ if (!best || m.end_line - m.start_line < best.end_line - best.start_line)
34177
+ best = m;
34178
+ }
34179
+ return best;
34180
+ }
34181
+ statements(from, to) {
34182
+ const out2 = [];
34183
+ for (let i2 = Math.max(from, 1);i2 < to; i2++) {
34184
+ let text = (this.lines[i2 - 1] ?? "").trimEnd();
34185
+ const first = i2;
34186
+ if (/^\s*(?:[\w.<>[\]?]+\s+)?[A-Za-z_]\w*\s*=(?!=)/.test(text) && !ENDS_STATEMENT_RE.test(text)) {
34187
+ while (i2 < to - 1 && i2 < first + 12) {
34188
+ i2++;
34189
+ text += " " + (this.lines[i2 - 1] ?? "").trim();
34190
+ if (ENDS_STATEMENT_RE.test(this.lines[i2 - 1] ?? ""))
34191
+ break;
34192
+ }
34193
+ }
34194
+ out2.push({ line: first, text });
34195
+ }
34196
+ return out2;
34197
+ }
34198
+ builtinSanitizes(type, text) {
34199
+ for (const { re, type: t } of CSHARP_SANITIZER_RES) {
34200
+ if (t === type && re.test(text))
34201
+ return true;
34202
+ }
34203
+ if (type === "ldap_injection") {
34204
+ if (/\.\s*Replace\s*\(\s*"\("\s*,/.test(text) && /\.\s*Replace\s*\(\s*"\)"\s*,/.test(text))
34205
+ return true;
34206
+ }
34207
+ if (type === "path_traversal") {
34208
+ if (/\b([A-Za-z_]\w*)\s*\.\s*StartsWith\s*\([^)]*\)\s*\?\s*\1\s*:\s*null\b/.test(text))
34209
+ return true;
34210
+ }
34211
+ return false;
34212
+ }
34213
+ helpers(type) {
34214
+ const cached = this.helperCache.get(type);
34215
+ if (cached)
34216
+ return cached;
34217
+ const set = new Set;
34218
+ this.helperCache.set(type, set);
34219
+ for (const m of this.methods) {
34220
+ const returns = this.returnExprs(m);
34221
+ if (returns.length === 0)
34222
+ continue;
34223
+ const ok = returns.every((r) => {
34224
+ if (this.builtinSanitizes(type, r.expr))
34225
+ return true;
34226
+ const v = /^([A-Za-z_]\w*)$/.exec(r.expr.trim());
34227
+ return !!v && this.sanitizedVarsIn(type, m.start_line, r.line, false).has(v[1]);
34228
+ });
34229
+ if (ok)
34230
+ set.add(m.name);
34231
+ }
34232
+ return set;
34233
+ }
34234
+ returnExprs(m) {
34235
+ const out2 = [];
34236
+ for (const st of this.statements(m.start_line, m.end_line + 1)) {
34237
+ for (const r of st.text.matchAll(/\breturn\s+([^;]+);/g))
34238
+ out2.push({ line: st.line, expr: r[1] });
34239
+ if (st.line === m.start_line) {
34240
+ const body2 = /\)\s*=>\s*(.+?);\s*$/.exec(st.text);
34241
+ if (body2)
34242
+ out2.push({ line: st.line, expr: body2[1] });
34243
+ }
34244
+ }
34245
+ return out2;
34246
+ }
34247
+ sanitizes(type, text) {
34248
+ if (this.builtinSanitizes(type, text))
34249
+ return true;
34250
+ for (const name2 of this.helpers(type)) {
34251
+ if (new RegExp(`(?<![\\w.])${escapeRegex(name2)}\\s*\\(`).test(text))
34252
+ return true;
34253
+ }
34254
+ return false;
34255
+ }
34256
+ sanitizedVarsIn(type, from, to, withHelpers) {
34257
+ const sanitized = new Set;
34258
+ for (const st of this.statements(from, to)) {
34259
+ const m = ASSIGN_RE.exec(st.text);
34260
+ if (!m)
34261
+ continue;
34262
+ const [, lhs, rhs] = m;
34263
+ const credited = (withHelpers ? this.sanitizes(type, rhs) : this.builtinSanitizes(type, rhs)) || [...sanitized].some((v) => mentions(rhs, v));
34264
+ if (credited)
34265
+ sanitized.add(lhs);
34266
+ else
34267
+ sanitized.delete(lhs);
34268
+ }
34269
+ return sanitized;
34270
+ }
34271
+ sanitizedVarsAt(type, sinkLine) {
34272
+ return this.sanitizedVarsIn(type, this.methodAt(sinkLine)?.start_line ?? 1, sinkLine, true);
34273
+ }
34274
+ sinkText(line) {
34275
+ const text = this.lines[line - 1] ?? "";
34276
+ const guarded = leadingIf(text);
34277
+ return guarded && guarded.rest !== "" ? guarded.rest : text;
34278
+ }
34279
+ allowlistVars(cond) {
34280
+ const out2 = [];
34281
+ const anchored = new RegExp(`\\bRegex\\s*\\.\\s*IsMatch\\s*\\(\\s*(${IDENT})\\s*,\\s*@?"\\^\\[(?:[A-Za-z0-9_\\- ]|\\\\w|\\\\d)+\\](?:[+*]|\\{\\d+(?:,\\d*)?\\})\\$"`, "g");
34282
+ for (const m of cond.matchAll(anchored))
34283
+ out2.push(m[1]);
34284
+ const methodGroup = new RegExp(`(${IDENT})\\s*\\.\\s*All\\s*\\(\\s*char\\s*\\.\\s*Is(?:LetterOrDigit|Digit|Letter)\\s*\\)`, "g");
34285
+ for (const m of cond.matchAll(methodGroup))
34286
+ out2.push(m[1]);
34287
+ const lambda = new RegExp(`(${IDENT})\\s*\\.\\s*All\\s*\\(\\s*(${IDENT})\\s*=>`, "g");
34288
+ for (const m of cond.matchAll(lambda)) {
34289
+ const open = cond.indexOf("(", m.index + m[1].length);
34290
+ const body2 = insideParens(cond, open)?.replace(new RegExp(`^\\s*${m[2]}\\s*=>`), "") ?? "";
34291
+ const c = escapeRegex(m[2]);
34292
+ const residue = body2.replace(new RegExp(`char\\s*\\.\\s*Is(?:LetterOrDigit|Digit|Letter)\\s*\\(\\s*${c}\\s*\\)`, "g"), "").replace(new RegExp(`${c}\\s*==\\s*'[_\\-]'`, "g"), "").replace(/\|\||\s/g, "");
34293
+ if (residue === "")
34294
+ out2.push(m[1]);
34295
+ }
34296
+ return out2;
34297
+ }
34298
+ numericVars(cond, parsedFrom) {
34299
+ const out2 = [];
34300
+ const tryParse = new RegExp(`\\b(?:int|long|short|byte|uint|ulong|decimal|double|float|Int32|Int64|Guid)\\s*\\.\\s*TryParse\\s*\\(\\s*(${IDENT})\\s*,`, "g");
34301
+ for (const m of cond.matchAll(tryParse))
34302
+ out2.push(m[1]);
34303
+ for (const [parsed, from] of parsedFrom) {
34304
+ if (new RegExp(`(?<![\\w])${escapeRegex(parsed)}\\s*(?:!=\\s*null|\\.\\s*HasValue)\\b`).test(cond))
34305
+ out2.push(from);
34306
+ }
34307
+ return out2;
34308
+ }
34309
+ hostVars(cond, hostOf) {
34310
+ const out2 = [];
34311
+ const compares = (expr) => {
34312
+ const e = escapeRegex(expr).replace(/\\ /g, "\\s*");
34313
+ return new RegExp(`${e}\\s*==\\s*@?"[^"]+"`).test(cond) || new RegExp(`@?"[^"]+"\\s*==\\s*${e}`).test(cond) || new RegExp(`${e}\\s*\\.\\s*Equals\\s*\\(`).test(cond) || new RegExp(`\\.\\s*Contains\\s*\\(\\s*${e}\\s*[),]`).test(cond) || new RegExp(`\\bIndexOf\\s*\\([^()]*,\\s*${e}\\s*\\)\\s*>=\\s*0`).test(cond);
34314
+ };
34315
+ const direct = new RegExp(`new\\s+(?:System\\s*\\.\\s*)?Uri\\s*\\(\\s*(${IDENT})\\s*\\)\\s*\\.\\s*Host\\b`, "g");
34316
+ for (const m of cond.matchAll(direct)) {
34317
+ if (compares(m[0]))
34318
+ out2.push(m[1]);
34319
+ }
34320
+ for (const [hostVar, from] of hostOf) {
34321
+ if (mentions(cond, hostVar) && compares(hostVar))
34322
+ out2.push(from);
34323
+ }
34324
+ return out2;
34325
+ }
34326
+ guardHelpers(kind) {
34327
+ const cached = this.guardHelperCache.get(kind);
34328
+ if (cached)
34329
+ return cached;
34330
+ const map = new Map;
34331
+ this.guardHelperCache.set(kind, map);
34332
+ for (const m of this.methods) {
34333
+ const signature = this.lines[m.start_line - 1] ?? "";
34334
+ const open = signature.indexOf("(", signature.indexOf(m.name));
34335
+ const params = open === -1 ? null : insideParens(signature, open);
34336
+ if (!params)
34337
+ continue;
34338
+ const names = params.split(",").map((p) => p.trim().split(/\s+/).pop() ?? "");
34339
+ const returns = this.returnExprs(m);
34340
+ if (returns.length !== 1)
34341
+ continue;
34342
+ const proven = this.provenBy(kind, returns[0].expr, new Map, new Map, false);
34343
+ const pos = names.findIndex((n) => proven.includes(n));
34344
+ if (pos !== -1)
34345
+ map.set(m.name, pos);
34346
+ }
34347
+ return map;
34348
+ }
34349
+ provenBy(kind, cond, parsedFrom, hostOf, withHelpers) {
34350
+ const out2 = kind === "allowlist" ? this.allowlistVars(cond) : kind === "numeric" ? this.numericVars(cond, parsedFrom) : this.hostVars(cond, hostOf);
34351
+ if (withHelpers) {
34352
+ for (const [name2, pos] of this.guardHelpers(kind)) {
34353
+ for (const call of cond.matchAll(new RegExp(`(?<![\\w.])${escapeRegex(name2)}\\s*\\(`, "g"))) {
34354
+ const args2 = insideParens(cond, call.index + call[0].length - 1)?.split(",") ?? [];
34355
+ const arg = args2[pos]?.trim();
34356
+ if (arg && new RegExp(`^${IDENT}$`).test(arg))
34357
+ out2.push(arg);
34358
+ }
34359
+ }
34360
+ }
34361
+ return out2;
34362
+ }
34363
+ guardedVarsAt(type, sinkLine) {
34364
+ const kinds = [];
34365
+ if (INERT_WHEN_ALPHANUMERIC.has(type))
34366
+ kinds.push("allowlist", "numeric");
34367
+ if (type === "ssrf")
34368
+ kinds.push("host");
34369
+ const proven = new Set;
34370
+ if (kinds.length === 0)
34371
+ return proven;
34372
+ const scopeStart = this.methodAt(sinkLine)?.start_line ?? 1;
34373
+ const before = this.statements(scopeStart, sinkLine);
34374
+ const boolOf = new Map;
34375
+ const parsedFrom = new Map;
34376
+ const hostOf = new Map;
34377
+ const parseAnywhere = new RegExp(`(${IDENT})\\s*=\\s*(?:\\([^)]*\\)\\s*)?(?:int|long|short|byte|uint|ulong|decimal|double|float|Int32|Int64)\\s*\\.\\s*Parse\\s*\\(\\s*(${IDENT})\\s*[,)]`, "g");
34378
+ for (const st of before) {
34379
+ for (const p of st.text.matchAll(parseAnywhere))
34380
+ parsedFrom.set(p[1], p[2]);
34381
+ const m = ASSIGN_RE.exec(st.text);
34382
+ if (!m)
34383
+ continue;
34384
+ const [, lhs, rhs] = m;
34385
+ const parse2 = new RegExp(`^(?:\\([^)]*\\)\\s*)?(?:int|long|short|byte|uint|ulong|decimal|double|float|Int32|Int64)\\s*\\.\\s*Parse\\s*\\(\\s*(${IDENT})\\s*[,)]`).exec(rhs);
34386
+ if (parse2)
34387
+ parsedFrom.set(lhs, parse2[1]);
34388
+ const host = new RegExp(`^new\\s+(?:System\\s*\\.\\s*)?Uri\\s*\\(\\s*(${IDENT})\\s*\\)\\s*\\.\\s*Host$`).exec(rhs.trim());
34389
+ if (host)
34390
+ hostOf.set(lhs, host[1]);
34391
+ boolOf.set(lhs, rhs);
34392
+ }
34393
+ const credit = (cond) => {
34394
+ const resolved = new RegExp(`^${IDENT}$`).test(cond) ? boolOf.get(cond) ?? cond : cond;
34395
+ for (const kind of kinds) {
34396
+ for (const v of this.provenBy(kind, resolved, parsedFrom, hostOf, true))
34397
+ proven.add(v);
34398
+ }
34399
+ };
34400
+ const own = leadingIf(this.lines[sinkLine - 1] ?? "");
34401
+ if (own && !own.cond.startsWith("!"))
34402
+ credit(own.cond);
34403
+ let depth = 0;
34404
+ for (let i2 = sinkLine - 1;i2 >= scopeStart; i2--) {
34405
+ const text = this.lines[i2 - 1] ?? "";
34406
+ for (let c = text.length - 1;c >= 0; c--) {
34407
+ if (text[c] === "}")
34408
+ depth++;
34409
+ else if (text[c] === "{") {
34410
+ if (depth > 0) {
34411
+ depth--;
34412
+ continue;
34413
+ }
34414
+ let header = text.slice(0, c);
34415
+ for (let h = i2 - 1;header.trim() === "" && h >= scopeStart; h--)
34416
+ header = this.lines[h - 1] ?? "";
34417
+ const enclosing = leadingIf(header);
34418
+ if (enclosing && !enclosing.cond.startsWith("!"))
34419
+ credit(enclosing.cond);
34420
+ }
34421
+ }
34422
+ }
34423
+ for (let i2 = scopeStart;i2 < sinkLine; i2++) {
34424
+ const guard = leadingIf(this.lines[i2 - 1] ?? "");
34425
+ if (!guard)
34426
+ continue;
34427
+ let rest = guard.rest;
34428
+ for (let n = i2 + 1;rest === "" && n < sinkLine; n++)
34429
+ rest = (this.lines[n - 1] ?? "").trim();
34430
+ if (!EXITS_RE.test(rest))
34431
+ continue;
34432
+ if (guard.cond.startsWith("!")) {
34433
+ const inner = guard.cond.slice(1).trim();
34434
+ const wrapped = inner.startsWith("(") && insideParens(inner, 0)?.length === inner.length - 2;
34435
+ credit(wrapped ? inner.slice(1, -1) : inner);
34436
+ } else if (/!=/.test(guard.cond) && !/[|&]/.test(guard.cond))
34437
+ credit(guard.cond.replace("!=", "=="));
34438
+ }
34439
+ return proven;
34440
+ }
34441
+ safeBuilders(credited, sinkLine) {
34442
+ const scopeStart = this.methodAt(sinkLine)?.start_line ?? 1;
34443
+ const onlySafe = (expr) => {
34444
+ const idents = expr.replace(/\$?@?"(?:[^"\\]|\\.)*"/g, " ").replace(/'(?:[^'\\]|\\.)'/g, " ").replace(/\.\s*[A-Za-z_]\w*\s*\(\s*\)/g, " ").match(/[A-Za-z_]\w*/g) ?? [];
34445
+ return idents.every((id) => credited.has(id));
34446
+ };
34447
+ const state = new Map;
34448
+ const declRe = new RegExp(`\\b(?:StringBuilder|var)\\s+(${IDENT})\\s*=\\s*new\\s+(?:System\\s*\\.\\s*Text\\s*\\.\\s*)?StringBuilder\\s*\\(`);
34449
+ const writeRe = new RegExp(`^\\s*(${IDENT})\\s*\\.\\s*(?:Append|AppendLine|AppendFormat|Insert)\\s*\\(`);
34450
+ for (const st of this.statements(scopeStart, sinkLine)) {
34451
+ const decl = declRe.exec(st.text);
34452
+ if (decl) {
34453
+ const args2 = insideParens(st.text, decl.index + decl[0].length - 1) ?? "";
34454
+ state.set(decl[1], onlySafe(args2));
34455
+ continue;
34456
+ }
34457
+ const write = writeRe.exec(st.text);
34458
+ if (write && state.has(write[1])) {
34459
+ const args2 = insideParens(st.text, write[0].length - 1);
34460
+ if (args2 === null || !onlySafe(args2))
34461
+ state.set(write[1], false);
34462
+ }
34463
+ }
34464
+ return new Set([...state].filter(([, safe]) => safe).map(([name2]) => name2));
34465
+ }
34466
+ creditedOnLine(line) {
34467
+ const text = this.sinkText(line);
34468
+ const types = new Set(CSHARP_SANITIZER_RES.map((r) => r.type));
34469
+ for (const t of INERT_WHEN_ALPHANUMERIC)
34470
+ types.add(t);
34471
+ types.add("ssrf");
34472
+ for (const type of types) {
34473
+ if (this.sanitizes(type, text))
34474
+ return true;
34475
+ const credited = new Set([...this.sanitizedVarsAt(type, line), ...this.guardedVarsAt(type, line)]);
34476
+ for (const v of this.safeBuilders(credited, line))
34477
+ credited.add(v);
34478
+ for (const v of credited)
34479
+ if (mentions(text, v))
34480
+ return true;
34481
+ }
34482
+ return this.fixedHostRequest(line);
34483
+ }
34484
+ fixedHostRequest(sinkLine) {
34485
+ const fixedLiteral = '\\$?@?"https?:\\/\\/[A-Za-z0-9.\\-]+(?::\\d+)?[\\/?#][^"]*"';
34486
+ const text = this.lines[sinkLine - 1] ?? "";
34487
+ const argStart = `\\(\\s*(?:new\\s+(?:System\\s*\\.\\s*)?Uri\\s*\\(\\s*)?`;
34488
+ if (new RegExp(argStart + fixedLiteral).test(text))
34489
+ return true;
34490
+ const scopeStart = this.methodAt(sinkLine)?.start_line ?? 1;
34491
+ const fixed = new Set;
34492
+ for (const st of this.statements(scopeStart, sinkLine)) {
34493
+ const m = ASSIGN_RE.exec(st.text);
34494
+ if (!m)
34495
+ continue;
34496
+ if (new RegExp("^(?:new\\s+(?:System\\s*\\.\\s*)?Uri\\s*\\(\\s*)?" + fixedLiteral).test(m[2].trim()))
34497
+ fixed.add(m[1]);
34498
+ else
34499
+ fixed.delete(m[1]);
34500
+ }
34501
+ for (const v of fixed) {
34502
+ if (new RegExp(argStart + escapeRegex(v) + "\\s*[,)]").test(text))
34503
+ return true;
34504
+ }
34505
+ return false;
34506
+ }
34507
+ }
34508
+
34065
34509
  // ../circle-ir/dist/analysis/passes/sink-filter-pass.js
34066
34510
  function escapeReSf(s) {
34067
34511
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -34364,7 +34808,6 @@ function jsSsrfHostGuardedLines(code) {
34364
34808
  }
34365
34809
  return covered;
34366
34810
  }
34367
- 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*\)/;
34368
34811
  function isUnwrittenLocalMapKeyRead(code, source) {
34369
34812
  if (source.type !== "plugin_param")
34370
34813
  return false;
@@ -34391,15 +34834,6 @@ function isUnwrittenLocalMapKeyRead(code, source) {
34391
34834
  return false;
34392
34835
  return true;
34393
34836
  }
34394
- var CSHARP_SANITIZER_RES = [
34395
- { re: /\b(?:HtmlEncode|JavaScriptStringEncode)\s*\(/, type: "xss" },
34396
- { re: /\bHtmlEncoder\s*\.\s*Encode\s*\(/, type: "xss" },
34397
- { re: /\bPath\s*\.\s*GetFileName\s*\(/, type: "path_traversal" },
34398
- { re: CSHARP_ALLOWLIST_STRIP_RE, type: "ldap_injection" },
34399
- { re: CSHARP_ALLOWLIST_STRIP_RE, type: "xpath_injection" },
34400
- { re: CSHARP_ALLOWLIST_STRIP_RE, type: "command_injection" },
34401
- { re: CSHARP_ALLOWLIST_STRIP_RE, type: "sql_injection" }
34402
- ];
34403
34837
  function csharpSanitizedVarsByType(code) {
34404
34838
  const lines = code.split(`
34405
34839
  `);
@@ -34432,7 +34866,7 @@ function csharpSanitizedVarsByType(code) {
34432
34866
  for (const [type, set] of byType) {
34433
34867
  if (set.has(lhs))
34434
34868
  continue;
34435
- const refsSanitized = [...set].some((v) => new RegExp(`(?<![\\w])${escapeRegex(v)}(?![\\w])`).test(rhs));
34869
+ const refsSanitized = [...set].some((v) => new RegExp(`(?<![\\w])${escapeRegex2(v)}(?![\\w])`).test(rhs));
34436
34870
  if (refsSanitized) {
34437
34871
  set.add(lhs);
34438
34872
  changed = true;
@@ -35328,22 +35762,34 @@ class SinkFilterPass {
35328
35762
  const sourceLines = ctx.code.split(`
35329
35763
  `);
35330
35764
  const sanitizedByType = csharpSanitizedVarsByType(ctx.code);
35765
+ const csFields = new Set(ctx.graph.ir.types.flatMap((t) => (t.fields ?? []).map((f) => f.name)));
35766
+ const credit = new CSharpCredit(ctx.code, ctx.graph.ir.types.flatMap((t) => t.methods));
35331
35767
  const xxeHardened = /\bXmlResolver\s*=\s*null\b/.test(ctx.code) || /\bDtdProcessing\s*=\s*(?:DtdProcessing\s*\.\s*)?(?:Prohibit|Ignore)\b/.test(ctx.code);
35332
35768
  filtered = filtered.filter((sink) => {
35333
35769
  if (sink.type === "xxe" && xxeHardened)
35334
35770
  return false;
35335
35771
  const sinkLineText = sourceLines[sink.line - 1] ?? "";
35336
- for (const { re, type } of CSHARP_SANITIZER_RES) {
35337
- if (type === sink.type && re.test(sinkLineText))
35772
+ if (credit.sanitizes(sink.type, sinkLineText))
35773
+ return false;
35774
+ if (sink.type === "ssrf" && credit.fixedHostRequest(sink.line))
35775
+ return false;
35776
+ const scoped = credit.sanitizedVarsAt(sink.type, sink.line);
35777
+ const fileWide = sanitizedByType.get(sink.type);
35778
+ const sanitizedVars = new Set(scoped);
35779
+ if (fileWide) {
35780
+ for (const v of fileWide)
35781
+ if (csFields.has(v))
35782
+ sanitizedVars.add(v);
35783
+ }
35784
+ for (const v of credit.guardedVarsAt(sink.type, sink.line))
35785
+ sanitizedVars.add(v);
35786
+ for (const v of credit.safeBuilders(sanitizedVars, sink.line))
35787
+ sanitizedVars.add(v);
35788
+ const received = credit.sinkText(sink.line);
35789
+ for (const v of sanitizedVars) {
35790
+ if (new RegExp(`(?<![\\w])${escapeRegex2(v)}(?![\\w])`).test(received))
35338
35791
  return false;
35339
35792
  }
35340
- const sanitizedVars = sanitizedByType.get(sink.type);
35341
- if (sanitizedVars && sanitizedVars.size > 0) {
35342
- for (const v of sanitizedVars) {
35343
- if (new RegExp(`(?<![\\w])${escapeRegex(v)}(?![\\w])`).test(sinkLineText))
35344
- return false;
35345
- }
35346
- }
35347
35793
  return true;
35348
35794
  });
35349
35795
  }
@@ -35351,7 +35797,7 @@ class SinkFilterPass {
35351
35797
  const sourceLines = ctx.code.split(`
35352
35798
  `);
35353
35799
  const constBaseClients = new Set;
35354
- const declRe = /\b(?:var|HttpClient)\s+([A-Za-z_]\w*)\s*=\s*new\s+HttpClient\s*\{[^}]*\bBaseAddress\s*=\s*new\s+Uri\s*\(\s*(?:@?"[^"]*"|\$?"[^"{}]*")\s*\)/;
35800
+ const declRe = /\b(?:var|HttpClient)\s+([A-Za-z_]\w*)\s*=\s*new\s+HttpClient\s*(?:\([^)]*\)\s*)?\{[^}]*\bBaseAddress\s*=\s*new\s+(?:System\s*\.\s*)?Uri\s*\(\s*(?:@?"[^"]*"|\$?"[^"{}]*")\s*\)/;
35355
35801
  for (const line of sourceLines) {
35356
35802
  const m = declRe.exec(line);
35357
35803
  if (m)
@@ -35365,14 +35811,22 @@ class SinkFilterPass {
35365
35811
  const text = sourceLines[sink.line - 1] ?? "";
35366
35812
  let onConstClient = false;
35367
35813
  for (const v of constBaseClients) {
35368
- if (new RegExp(`(?<![\\w])${escapeRegex(v)}\\s*\\.`).test(text)) {
35814
+ if (new RegExp(`(?<![\\w])${escapeRegex2(v)}\\s*\\.`).test(text)) {
35369
35815
  onConstClient = true;
35370
35816
  break;
35371
35817
  }
35372
35818
  }
35373
35819
  if (!onConstClient)
35374
35820
  return true;
35375
- const m = relativeLiteralArgRe.exec(text);
35821
+ let m = relativeLiteralArgRe.exec(text);
35822
+ if (!m) {
35823
+ const arg = /Async\s*\(\s*([A-Za-z_]\w*)\s*[,)]/.exec(text)?.[1];
35824
+ if (arg) {
35825
+ const assigned = new RegExp(`\\b${escapeRegex2(arg)}\\s*=\\s*(?:@?\\$?)"([^"]*)"`);
35826
+ for (let i2 = sink.line - 2;i2 >= 0 && !m; i2--)
35827
+ m = assigned.exec(sourceLines[i2] ?? "");
35828
+ }
35829
+ }
35376
35830
  if (!m)
35377
35831
  return true;
35378
35832
  const lit = m[1];
@@ -35408,7 +35862,7 @@ class SinkFilterPass {
35408
35862
  const wrappers = findPythonLdapStripWrappers(sourceLines);
35409
35863
  const allLdapSanitizers = [...PY_BUILTIN_LDAP_SANITIZERS, ...wrappers];
35410
35864
  if (allLdapSanitizers.length > 0) {
35411
- const ldapPatterns = allLdapSanitizers.map((name2) => new RegExp(`\\b${escapeRegex(name2)}\\s*\\(`));
35865
+ const ldapPatterns = allLdapSanitizers.map((name2) => new RegExp(`\\b${escapeRegex2(name2)}\\s*\\(`));
35412
35866
  filtered = filtered.filter((sink) => {
35413
35867
  if (sink.type !== "ldap_injection")
35414
35868
  return true;
@@ -35494,7 +35948,7 @@ class SinkFilterPass {
35494
35948
  return { sources, sinks: filtered, sanitizers };
35495
35949
  }
35496
35950
  }
35497
- function escapeRegex(s) {
35951
+ function escapeRegex2(s) {
35498
35952
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
35499
35953
  }
35500
35954
  var RESERVED_KEYWORDS = new Set([
@@ -35576,7 +36030,7 @@ function extractSourceIdentifiers(text) {
35576
36030
  return out2;
35577
36031
  }
35578
36032
  function isAssignedFromSanitizerPattern(sourceLines, sinkLine, varName, sanitizerPatterns, lookback = 30) {
35579
- const escapedVar = escapeRegex(varName);
36033
+ const escapedVar = escapeRegex2(varName);
35580
36034
  const assignRe = new RegExp(`(?:\\b(?:const|let|var|final|String)\\s+)?\\b${escapedVar}\\s*=\\s*(.+)`);
35581
36035
  const fnBoundaryRe = /^\s*(?:def\s+\w+\s*\(|function\s+\w+\s*\(|\w+\s*[:=]\s*(?:async\s+)?\(?[^)]*\)?\s*=>)/;
35582
36036
  const lo = Math.max(0, sinkLine - 1 - lookback);
@@ -37145,7 +37599,7 @@ class LibraryProfileXssGatePass {
37145
37599
 
37146
37600
  // ../circle-ir/dist/analysis/passes/taint-propagation-pass.js
37147
37601
  var CONTAINER_READ_SOURCES = new Set(["plugin_param", "config_param"]);
37148
- function makeConstPropVeto(constProp, graph, sources, code) {
37602
+ function makeConstPropVeto(constProp, graph, sources, code, language) {
37149
37603
  const lines = typeof code === "string" ? code.split(`
37150
37604
  `) : [];
37151
37605
  const selfReassign = new Map;
@@ -37182,6 +37636,8 @@ function makeConstPropVeto(constProp, graph, sources, code) {
37182
37636
  return false;
37183
37637
  if (r.reason !== "variable_not_tainted")
37184
37638
  return true;
37639
+ if (language === "csharp")
37640
+ return false;
37185
37641
  const lo = Math.min(sourceLine, sinkLine);
37186
37642
  let d = defAt(step.variable, step.line);
37187
37643
  let selfHops = 0;
@@ -37220,7 +37676,7 @@ class TaintPropagationPass {
37220
37676
  return { flows: [] };
37221
37677
  }
37222
37678
  const propagationResult = propagateTaint(graph, sources, sinks, sanitizers);
37223
- const isVetoed = makeConstPropVeto(constProp, graph, sources, ctx.code);
37679
+ const isVetoed = makeConstPropVeto(constProp, graph, sources, ctx.code, ctx.language);
37224
37680
  const verifiedFlows = propagationResult.flows.filter((flow) => {
37225
37681
  if (constProp.unreachableLines.has(flow.sink.line))
37226
37682
  return false;
@@ -37790,7 +38246,7 @@ function detectCollectionFlows(calls, sources, sinks, taintedVars, unreachableLi
37790
38246
  if (source.variable && source.variable !== varName && source.in_method && call.in_method && source.in_method !== call.in_method) {
37791
38247
  continue;
37792
38248
  }
37793
- if (typeof code === "string" && isReassignedToLiteralBetween(code, varName, source.line, sink.line)) {
38249
+ if (typeof code === "string" && isReassignedToLiteralBetween(code, varName, source.line, sink.line, unreachableLines)) {
37794
38250
  continue;
37795
38251
  }
37796
38252
  flows.push({
@@ -37830,7 +38286,7 @@ function detectCollectionFlows(calls, sources, sinks, taintedVars, unreachableLi
37830
38286
  if (source.variable && source.variable !== collectionVar && source.in_method && call.in_method && source.in_method !== call.in_method) {
37831
38287
  continue;
37832
38288
  }
37833
- if (typeof code === "string" && isReassignedToLiteralBetween(code, collectionVar, source.line, sink.line)) {
38289
+ if (typeof code === "string" && isReassignedToLiteralBetween(code, collectionVar, source.line, sink.line, unreachableLines)) {
37834
38290
  continue;
37835
38291
  }
37836
38292
  flows.push({
@@ -37949,7 +38405,7 @@ function detectParameterSinkFlows(types, calls, sources, sinks, unreachableLines
37949
38405
  if (paramSource) {
37950
38406
  const exists = flows.some((f) => f.source_line === paramSource.line && f.sink_line === sink.line && f.sink_type === sink.type);
37951
38407
  if (!exists) {
37952
- if (typeof code === "string" && isReassignedToLiteralBetween(code, arg.variable, paramSource.line, sink.line)) {
38408
+ if (typeof code === "string" && isReassignedToLiteralBetween(code, arg.variable, paramSource.line, sink.line, unreachableLines)) {
37953
38409
  continue;
37954
38410
  }
37955
38411
  flows.push({
@@ -37972,6 +38428,96 @@ function detectParameterSinkFlows(types, calls, sources, sinks, unreachableLines
37972
38428
  }
37973
38429
  return flows;
37974
38430
  }
38431
+ function csharpMethodScopedAliases(code, seeds, methods, fieldNames) {
38432
+ const lines = code.split(`
38433
+ `);
38434
+ const methodOf = (line) => {
38435
+ let best = -1;
38436
+ let span = Infinity;
38437
+ methods.forEach((m, i2) => {
38438
+ if (line >= m.start_line && line <= m.end_line && m.end_line - m.start_line < span) {
38439
+ best = i2;
38440
+ span = m.end_line - m.start_line;
38441
+ }
38442
+ });
38443
+ return best;
38444
+ };
38445
+ const declRe = /^\s*(?:public|private|protected|internal|static|readonly|const|\s)*\s*(?:[A-Za-z_][\w.]*(?:\s*<[^>]*>)?(?:\s*\[\s*\])*\??)\s+([A-Za-z_]\w*)\s*=\s*(.+?);\s*$/;
38446
+ const assignRe = /^\s*(?:this\s*\.\s*)?([A-Za-z_]\w*)\s*=\s*(.+?);\s*$/;
38447
+ const returnRe = /^\s*return\s+(.+?);\s*$/;
38448
+ const KEYWORDS = new Set(["return", "throw", "new", "await", "yield", "else", "case", "using", "var"]);
38449
+ const codeOf = (rhs) => rhs.replace(/\$@?"(?:[^"\\]|\\.)*"/g, (lit) => " " + [...lit.matchAll(/\{([^}]*)\}/g)].map((x) => x[1]).join(" ") + " ").replace(/@?"(?:[^"\\]|\\.)*"/g, " ").replace(/'(?:[^'\\]|\\.)'/g, " ");
38450
+ const local = new Map;
38451
+ const taintedFields = new Set;
38452
+ const returnsTainted = new Set;
38453
+ const localsOf = (m) => {
38454
+ let set = local.get(m);
38455
+ if (!set) {
38456
+ set = new Set;
38457
+ local.set(m, set);
38458
+ }
38459
+ return set;
38460
+ };
38461
+ for (const seed of seeds) {
38462
+ const base = seed.variable.replace(/^this\./, "");
38463
+ if (fieldNames.has(base))
38464
+ taintedFields.add(base);
38465
+ else
38466
+ localsOf(methodOf(seed.line)).add(seed.variable);
38467
+ }
38468
+ const out2 = [];
38469
+ let changed = true;
38470
+ for (let guard = 0;changed && guard < lines.length + 2; guard++) {
38471
+ changed = false;
38472
+ for (let i2 = 0;i2 < lines.length; i2++) {
38473
+ let text = lines[i2];
38474
+ const trimmed = text.trimStart();
38475
+ if (trimmed.startsWith("//") || trimmed.startsWith("*"))
38476
+ continue;
38477
+ if (/^\s*(?:[\w.<>\[\]?]+\s+)?[A-Za-z_]\w*\s*=(?!=)/.test(text) && !/;\s*(?:\/\/.*)?$/.test(text)) {
38478
+ for (let j = i2 + 1;j < lines.length && j <= i2 + 12; j++) {
38479
+ text = text.trimEnd() + " " + lines[j].trim();
38480
+ if (/;\s*(?:\/\/.*)?$/.test(lines[j]))
38481
+ break;
38482
+ }
38483
+ }
38484
+ const m = methodOf(i2 + 1);
38485
+ const here = localsOf(m);
38486
+ const ret = returnRe.exec(text);
38487
+ if (ret && m !== -1 && !returnsTainted.has(methods[m].name)) {
38488
+ const idents2 = codeOf(ret[1]).match(/[\p{L}\p{N}_]+/gu) ?? [];
38489
+ if (idents2.some((t) => here.has(t) || taintedFields.has(t))) {
38490
+ returnsTainted.add(methods[m].name);
38491
+ changed = true;
38492
+ }
38493
+ continue;
38494
+ }
38495
+ const d = declRe.exec(text) ?? assignRe.exec(text);
38496
+ if (!d)
38497
+ continue;
38498
+ const [, lhs, rhs] = d;
38499
+ if (KEYWORDS.has(lhs))
38500
+ continue;
38501
+ const isField = fieldNames.has(lhs) && !declRe.test(text);
38502
+ if (isField ? taintedFields.has(lhs) : here.has(lhs))
38503
+ continue;
38504
+ const rhsCode = codeOf(rhs);
38505
+ const idents = rhsCode.match(/[\p{L}\p{N}_]+/gu) ?? [];
38506
+ const viaLocal = idents.some((t) => here.has(t));
38507
+ const viaField = idents.some((t) => taintedFields.has(t) && !here.has(t));
38508
+ const viaCall = [...rhsCode.matchAll(/([\p{L}_][\p{L}\p{N}_]*)\s*\(/gu)].some((c) => returnsTainted.has(c[1]));
38509
+ if (!viaLocal && !viaField && !viaCall)
38510
+ continue;
38511
+ if (isField)
38512
+ taintedFields.add(lhs);
38513
+ else
38514
+ here.add(lhs);
38515
+ out2.push({ variable: lhs, line: i2 + 1, carried: !viaLocal });
38516
+ changed = true;
38517
+ }
38518
+ }
38519
+ return out2;
38520
+ }
37975
38521
  var _reassignSplitCode;
37976
38522
  var _reassignSplitLines;
37977
38523
  function splitLinesCached(code) {
@@ -37982,7 +38528,7 @@ function splitLinesCached(code) {
37982
38528
  `);
37983
38529
  return _reassignSplitLines;
37984
38530
  }
37985
- function isReassignedToLiteralBetween(code, variable, srcLine, sinkLine) {
38531
+ function isReassignedToLiteralBetween(code, variable, srcLine, sinkLine, unreachableLines) {
37986
38532
  if (!variable || sinkLine - srcLine < 2)
37987
38533
  return false;
37988
38534
  if (!/^[A-Za-z_][\w]*$/.test(variable))
@@ -38002,49 +38548,117 @@ function isReassignedToLiteralBetween(code, variable, srcLine, sinkLine) {
38002
38548
  return true;
38003
38549
  if (!reNaked.test(line) && !reSwitchCase.test(line))
38004
38550
  continue;
38005
- if (switchAlsoAssignsNonLiteral(lines, i2, variable, strLit))
38551
+ if (siblingBranchAssignsNonLiteral(lines, i2, variable, strLit, unreachableLines))
38006
38552
  continue;
38007
38553
  return true;
38008
38554
  }
38009
38555
  return false;
38010
38556
  }
38011
- function switchAlsoAssignsNonLiteral(lines, idx, variable, strLit) {
38557
+ function enclosingBrace(lines, line, col) {
38012
38558
  let depth = 0;
38013
- let start2 = -1;
38014
- for (let i2 = idx;i2 >= 0; i2--) {
38015
- const line = lines[i2];
38016
- for (let c = line.length - 1;c >= 0; c--) {
38017
- if (line[c] === "}")
38559
+ for (let i2 = line;i2 >= 0; i2--) {
38560
+ const text = lines[i2];
38561
+ for (let c = i2 === line ? Math.min(col, text.length) - 1 : text.length - 1;c >= 0; c--) {
38562
+ if (text[c] === "}")
38018
38563
  depth++;
38019
- else if (line[c] === "{")
38564
+ else if (text[c] === "{") {
38565
+ if (depth === 0) {
38566
+ let header = text.slice(0, c).trim();
38567
+ let headerLine = i2;
38568
+ for (let h = i2 - 1;header === "" && h >= 0; h--) {
38569
+ header = lines[h].trim();
38570
+ headerLine = h;
38571
+ }
38572
+ return { line: i2, col: c, header, headerLine };
38573
+ }
38020
38574
  depth--;
38575
+ }
38021
38576
  }
38022
- if (depth < 0) {
38023
- if (/\bswitch\b/.test(line))
38024
- start2 = i2;
38025
- break;
38577
+ }
38578
+ return null;
38579
+ }
38580
+ function matchingClose(lines, line, col) {
38581
+ let depth = 0;
38582
+ for (let i2 = line;i2 < lines.length; i2++) {
38583
+ const text = lines[i2];
38584
+ for (let c = i2 === line ? col : 0;c < text.length; c++) {
38585
+ if (text[c] === "{")
38586
+ depth++;
38587
+ else if (text[c] === "}" && --depth === 0)
38588
+ return { line: i2, col: c };
38026
38589
  }
38027
38590
  }
38028
- if (start2 < 0)
38591
+ return null;
38592
+ }
38593
+ function siblingBranchAssignsNonLiteral(lines, idx, variable, strLit, unreachableLines) {
38594
+ let block = enclosingBrace(lines, idx, 0);
38595
+ if (block && /^(?:case\b.*|default\s*):$/.test(block.header)) {
38596
+ block = enclosingBrace(lines, block.line, block.col);
38597
+ }
38598
+ if (!block)
38599
+ return false;
38600
+ let first = block.headerLine;
38601
+ let last;
38602
+ const end = matchingClose(lines, block.line, block.col);
38603
+ if (!end)
38029
38604
  return false;
38605
+ last = end.line;
38606
+ if (/\bswitch\b/.test(block.header)) {} else if (/^(?:\}\s*)?else\b/.test(block.header) || /^(?:\}\s*)?(?:else\s+)?if\s*\(/.test(block.header)) {
38607
+ let cur = block;
38608
+ for (let guard = 0;guard < 64 && /^(?:\}\s*)?else\b/.test(cur.header); guard++) {
38609
+ let closeLine = cur.headerLine;
38610
+ let closeCol = lines[closeLine].indexOf("}");
38611
+ if (closeCol === -1 || closeCol > lines[closeLine].search(/\belse\b/)) {
38612
+ closeLine--;
38613
+ while (closeLine >= 0 && lines[closeLine].trim() === "")
38614
+ closeLine--;
38615
+ if (closeLine < 0 || !lines[closeLine].trimEnd().endsWith("}"))
38616
+ return false;
38617
+ closeCol = lines[closeLine].lastIndexOf("}");
38618
+ }
38619
+ const prev = enclosingBrace(lines, closeLine, closeCol);
38620
+ if (!prev)
38621
+ return false;
38622
+ cur = prev;
38623
+ first = prev.headerLine;
38624
+ }
38625
+ let close = end;
38626
+ for (let guard = 0;guard < 64; guard++) {
38627
+ let l = close.line;
38628
+ let rest = lines[l].slice(close.col + 1).trim();
38629
+ while (rest === "" && l + 1 < lines.length) {
38630
+ l++;
38631
+ rest = lines[l].trim();
38632
+ }
38633
+ if (!/^else\b/.test(rest))
38634
+ break;
38635
+ let openLine = l;
38636
+ let openCol = lines[l].indexOf("{", l === close.line ? close.col + 1 : 0);
38637
+ if (openCol === -1) {
38638
+ openLine = l + 1;
38639
+ while (openLine < lines.length && lines[openLine].trim() === "")
38640
+ openLine++;
38641
+ if (openLine >= lines.length || !lines[openLine].trimStart().startsWith("{"))
38642
+ break;
38643
+ openCol = lines[openLine].indexOf("{");
38644
+ }
38645
+ const next = matchingClose(lines, openLine, openCol);
38646
+ if (!next)
38647
+ break;
38648
+ close = next;
38649
+ last = next.line;
38650
+ }
38651
+ } else {
38652
+ return false;
38653
+ }
38030
38654
  const assign = new RegExp(`(?:^|[^\\w.$])${variable}\\s*(?::?=)(?!=)\\s*(.*?)\\s*;?\\s*(?:break\\s*;?)?\\s*$`);
38031
38655
  const literalOnly = new RegExp(`^${strLit}$`);
38032
- depth = 0;
38033
- for (let i2 = start2;i2 < lines.length; i2++) {
38034
- const line = lines[i2];
38035
- if (i2 > start2 || line.includes("{")) {
38036
- const m = assign.exec(line);
38037
- if (m && m[1] && !literalOnly.test(m[1]))
38038
- return true;
38039
- }
38040
- for (const ch of line) {
38041
- if (ch === "{")
38042
- depth++;
38043
- else if (ch === "}")
38044
- depth--;
38045
- }
38046
- if (i2 > start2 && depth <= 0)
38047
- break;
38656
+ for (let i2 = first;i2 <= last; i2++) {
38657
+ if (i2 === idx || unreachableLines?.has(i2 + 1))
38658
+ continue;
38659
+ const m = assign.exec(lines[i2]);
38660
+ if (m && m[1] && !literalOnly.test(m[1]))
38661
+ return true;
38048
38662
  }
38049
38663
  return false;
38050
38664
  }
@@ -38172,6 +38786,22 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
38172
38786
  const goFnStart = (line) => enclosingStart(goFnRanges, line);
38173
38787
  const csFnRanges = methodRanges(["csharp"]);
38174
38788
  const csFnStart = (line) => enclosingStart(csFnRanges, line);
38789
+ const csFieldNames = new Set;
38790
+ if (language === "csharp") {
38791
+ for (const t of types ?? [])
38792
+ for (const f of t.fields ?? [])
38793
+ csFieldNames.add(f.name);
38794
+ }
38795
+ const csDifferentMethod = (variable, sourceLine, otherLine) => {
38796
+ if (csFnRanges.length === 0)
38797
+ return false;
38798
+ const a = csFnStart(sourceLine);
38799
+ const b = csFnStart(otherLine);
38800
+ if (a === -1 || b === -1 || a === b)
38801
+ return false;
38802
+ const base = variable.replace(/^this\./, "").split(".")[0];
38803
+ return !csFieldNames.has(base);
38804
+ };
38175
38805
  const sourcesWithVar = sources.filter((s) => typeof s.variable === "string" && s.variable.length > 0);
38176
38806
  const aliasSanitizedFor = new Map;
38177
38807
  if (language === "python" && typeof code === "string") {
@@ -38444,7 +39074,29 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
38444
39074
  }
38445
39075
  }
38446
39076
  }
38447
- if ((language === "java" || language === "csharp") && typeof code === "string" && sourcesWithVar.length > 0) {
39077
+ if (language === "csharp" && typeof code === "string" && sourcesWithVar.length > 0 && csFnRanges.length > 0) {
39078
+ const csMethods = (types ?? []).flatMap((t) => t.methods);
39079
+ const seeds = sourcesWithVar.map((sv) => ({ variable: sv.variable, line: sv.line }));
39080
+ const methodStartOf = (line) => csFnStart(line);
39081
+ const aliases = csharpMethodScopedAliases(code, seeds, csMethods, csFieldNames);
39082
+ const fileAnchor = sourcesWithVar.reduce((a, sv) => sv.line < a.line ? sv : a, sourcesWithVar[0]);
39083
+ const have = new Set(sourcesWithVar.map((sv) => `${sv.variable}@${methodStartOf(sv.line)}`));
39084
+ for (const alias of aliases) {
39085
+ const fn = methodStartOf(alias.line);
39086
+ const isField = csFieldNames.has(alias.variable);
39087
+ const key = `${alias.variable}@${isField ? "field" : fn}`;
39088
+ if (have.has(key))
39089
+ continue;
39090
+ have.add(key);
39091
+ const inMethod = sourcesWithVar.filter((sv) => methodStartOf(sv.line) === fn && sv.line <= alias.line);
39092
+ const from = !alias.carried && inMethod.length > 0 ? inMethod.reduce((a, sv) => sv.line < a.line ? sv : a, inMethod[0]) : fileAnchor;
39093
+ sourcesWithVar.push({
39094
+ ...from,
39095
+ variable: alias.variable,
39096
+ ...alias.carried || inMethod.length === 0 ? { line: alias.line } : {}
39097
+ });
39098
+ }
39099
+ } else if ((language === "java" || language === "csharp") && typeof code === "string" && sourcesWithVar.length > 0) {
38448
39100
  const seedVars = new Set(sourcesWithVar.map((s) => s.variable));
38449
39101
  const derived = buildJavaTaintedVars(code, seedVars);
38450
39102
  if (derived.size > 0) {
@@ -38493,23 +39145,26 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
38493
39145
  const lines = code.split(`
38494
39146
  `);
38495
39147
  const ctRe = /(\w+)\s*\.\s*CommandText\s*\+?=\s*(.+)/;
38496
- const anchor = sourcesWithVar.reduce((a, s) => s.line < a.line ? s : a, sourcesWithVar[0]);
39148
+ const seeded = new Set;
38497
39149
  let added = true;
38498
39150
  let guard = 0;
38499
39151
  while (added && guard < lines.length + 2) {
38500
39152
  added = false;
38501
39153
  guard++;
38502
- const known = new Set(sourcesWithVar.map((s) => s.variable));
38503
39154
  for (let i2 = 0;i2 < lines.length; i2++) {
38504
39155
  const m = ctRe.exec(lines[i2]);
38505
39156
  if (!m)
38506
39157
  continue;
38507
39158
  const [, obj, rhs] = m;
38508
- if (known.has(obj))
39159
+ const key = `${obj}@${csFnStart(i2 + 1)}`;
39160
+ if (seeded.has(key))
38509
39161
  continue;
38510
- if (!argsReferenceTaintedVar(rhs, sourcesWithVar))
39162
+ const reachable = sourcesWithVar.filter((sv) => sv.line <= i2 + 1 && !csDifferentMethod(sv.variable, sv.line, i2 + 1));
39163
+ if (reachable.length === 0 || !argsReferenceTaintedVar(rhs, reachable))
38511
39164
  continue;
38512
- sourcesWithVar.push({ ...anchor, variable: obj, line: i2 + 1 });
39165
+ const from = reachable.reduce((a, sv) => sv.line < a.line ? sv : a, reachable[0]);
39166
+ sourcesWithVar.push({ ...from, variable: obj, line: i2 + 1 });
39167
+ seeded.add(key);
38513
39168
  added = true;
38514
39169
  }
38515
39170
  }
@@ -38528,24 +39183,26 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
38528
39183
  if (builders.size > 0) {
38529
39184
  const appendRe = /([\p{L}_][\p{L}\p{N}_]*)\s*\.\s*(?:Append|AppendLine|AppendFormat|Insert)\s*\(([\s\S]*)/u;
38530
39185
  const anchor = sourcesWithVar.reduce((a, s) => s.line < a.line ? s : a, sourcesWithVar[0]);
39186
+ const builderSeeded = new Set;
38531
39187
  let added = true;
38532
39188
  let guard = 0;
38533
39189
  while (added && guard < lines.length + 2) {
38534
39190
  added = false;
38535
39191
  guard++;
38536
- const known = new Set(sourcesWithVar.map((s) => s.variable));
38537
39192
  for (let i2 = 0;i2 < lines.length; i2++) {
38538
39193
  const m = appendRe.exec(lines[i2]);
38539
39194
  if (!m)
38540
39195
  continue;
38541
39196
  const [, obj, args2] = m;
38542
- if (!builders.has(obj) || known.has(obj))
39197
+ const seedKey = `${obj}@${csFnStart(i2 + 1)}`;
39198
+ if (!builders.has(obj) || builderSeeded.has(seedKey))
38543
39199
  continue;
38544
39200
  const appendFn = csFnStart(i2 + 1);
38545
39201
  const reachable = sourcesWithVar.filter((sv) => appendFn === -1 || csFnStart(sv.line) === -1 || csFnStart(sv.line) === appendFn);
38546
39202
  if (reachable.length > 0 && argsReferenceTaintedVar(args2, reachable)) {
38547
39203
  sourcesWithVar.push({ ...anchor, variable: obj, line: i2 + 1 });
38548
39204
  builderSeeds.add(`${obj}@${i2 + 1}`);
39205
+ builderSeeded.add(seedKey);
38549
39206
  added = true;
38550
39207
  }
38551
39208
  }
@@ -38628,6 +39285,8 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
38628
39285
  if (srcFn !== sinkFn)
38629
39286
  continue;
38630
39287
  }
39288
+ if (csDifferentMethod(source.variable, source.line, sink.line))
39289
+ continue;
38631
39290
  if (complex) {
38632
39291
  const re = reCache.get(source.variable);
38633
39292
  if (!re || !re.test(expr))
@@ -38641,7 +39300,7 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
38641
39300
  if (aliasSanitizedFor.get(source.variable)?.has(sink.type)) {
38642
39301
  break;
38643
39302
  }
38644
- if (typeof code === "string" && isReassignedToLiteralBetween(code, source.variable, source.line, sink.line)) {
39303
+ if (typeof code === "string" && isReassignedToLiteralBetween(code, source.variable, source.line, sink.line, unreachableLines)) {
38645
39304
  break;
38646
39305
  }
38647
39306
  flows.push({
@@ -38948,7 +39607,14 @@ var CSHARP_NON_ESCAPE_METHODS = new Set([
38948
39607
  "TryAdd",
38949
39608
  "Insert",
38950
39609
  "Push",
38951
- "Enqueue"
39610
+ "Enqueue",
39611
+ "EscapeDataString",
39612
+ "EscapeUriString",
39613
+ "UrlEncode",
39614
+ "UrlPathEncode",
39615
+ "HtmlEncode",
39616
+ "HtmlAttributeEncode",
39617
+ "JavaScriptStringEncode"
38952
39618
  ]);
38953
39619
  function isCSharpNonEscapeCall(call) {
38954
39620
  const name2 = call.method_name;
@@ -39520,6 +40186,7 @@ class InterproceduralPass {
39520
40186
  }
39521
40187
  }
39522
40188
  const language = graph.ir.meta.language;
40189
+ const csCredit = language === "csharp" && typeof ctx.code === "string" ? new CSharpCredit(ctx.code, graph.ir.types.flatMap((t) => t.methods)) : null;
39523
40190
  if (sinks.length > 0) {
39524
40191
  const interProc = analyzeInterprocedural2(graph, sources, sinks, sanitizers, {
39525
40192
  taintedVariables: constProp.tainted
@@ -39552,6 +40219,9 @@ class InterproceduralPass {
39552
40219
  continue;
39553
40220
  if (sanitizerMethodNames.has(method.name))
39554
40221
  continue;
40222
+ if (language === "csharp" && typeof ctx.code === "string" && !csharpParamReachesSink(ctx.code, method, edge.taintedArgs, sink, csCredit)) {
40223
+ continue;
40224
+ }
39555
40225
  for (const source of sources) {
39556
40226
  if (source.line > edge.callLine)
39557
40227
  continue;
@@ -39693,9 +40363,58 @@ class InterproceduralPass {
39693
40363
  return !sanitizedSinkKeys.has(`${s.line}:${s.type}`);
39694
40364
  });
39695
40365
  }
40366
+ if (csCredit) {
40367
+ const credited = new Set;
40368
+ filteredAdditionalFlows = filteredAdditionalFlows.filter((f) => {
40369
+ if (f.sink_type !== "external_taint_escape" || !csCredit.creditedOnLine(f.sink_line))
40370
+ return true;
40371
+ credited.add(f.sink_line);
40372
+ return false;
40373
+ });
40374
+ filteredAdditionalSinks = filteredAdditionalSinks.filter((s) => !(s.type === "external_taint_escape" && credited.has(s.line)));
40375
+ }
39696
40376
  return { additionalSinks: filteredAdditionalSinks, additionalFlows: filteredAdditionalFlows, interprocedural };
39697
40377
  }
39698
40378
  }
40379
+ function csharpParamReachesSink(code, method, taintedArgs, sink, credit) {
40380
+ const lines = code.split(/\r?\n/);
40381
+ const params = taintedArgs.length > 0 ? method.parameters.filter((p) => taintedArgs.includes(p.position)) : method.parameters;
40382
+ if (params.length === 0)
40383
+ return true;
40384
+ const derived = new Set(params.map((p) => p.name));
40385
+ const codeOf = (text) => text.replace(/\$@?"(?:[^"\\]|\\.)*"/g, (lit) => " " + [...lit.matchAll(/\{([^}]*)\}/g)].map((x) => x[1]).join(" ") + " ").replace(/@?"(?:[^"\\]|\\.)*"/g, " ").replace(/'(?:[^'\\]|\\.)'/g, " ");
40386
+ const mentionsDerived = (text) => {
40387
+ const bare = codeOf(text);
40388
+ return [...derived].filter((v) => new RegExp(`(?<![\\w.])${v}(?![\\w])`).test(bare));
40389
+ };
40390
+ const assignRe = /^\s*(?:var\s+|[A-Za-z_][\w.<>[\]?]*\s+)?([A-Za-z_]\w*)\s*=(?!=)\s*(.+)$/;
40391
+ const objectWriteRe = /^\s*([A-Za-z_]\w*)\s*\.\s*[A-Za-z_]\w*\s*(?:\+?=(?!=)|\()(.*)$/;
40392
+ for (let i2 = method.startLine + 1;i2 < sink.line; i2++) {
40393
+ const text = lines[i2 - 1] ?? "";
40394
+ const assign = assignRe.exec(text);
40395
+ if (assign) {
40396
+ if (mentionsDerived(assign[2]).length > 0)
40397
+ derived.add(assign[1]);
40398
+ else if (!params.some((p) => p.name === assign[1]))
40399
+ derived.delete(assign[1]);
40400
+ continue;
40401
+ }
40402
+ const write = objectWriteRe.exec(text);
40403
+ if (write && mentionsDerived(write[2]).length > 0)
40404
+ derived.add(write[1]);
40405
+ }
40406
+ let statement = lines[sink.line - 1] ?? "";
40407
+ for (let i2 = sink.line;i2 < lines.length && i2 < sink.line + 6 && !/[;{}]\s*$/.test(statement.trimEnd()); i2++) {
40408
+ statement += " " + (lines[i2] ?? "").trim();
40409
+ }
40410
+ const reaching = mentionsDerived(statement);
40411
+ if (reaching.length === 0)
40412
+ return false;
40413
+ if (!credit)
40414
+ return true;
40415
+ const safe = new Set([...credit.sanitizedVarsAt(sink.type, sink.line), ...credit.guardedVarsAt(sink.type, sink.line)]);
40416
+ return reaching.some((v) => !safe.has(v));
40417
+ }
39699
40418
 
39700
40419
  // ../circle-ir/dist/analysis/passes/dead-code-pass.js
39701
40420
  class DeadCodePass {
@@ -40408,7 +41127,7 @@ class NullDerefPass {
40408
41127
  }
40409
41128
 
40410
41129
  // ../circle-ir/dist/analysis/passes/resource-leak-pass.js
40411
- function escapeRegex2(s) {
41130
+ function escapeRegex3(s) {
40412
41131
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
40413
41132
  }
40414
41133
  var FACTORY_METHOD_NAME_RE = /^(?:open|create|new|get|make|build)[A-Z]/;
@@ -40599,7 +41318,7 @@ class ResourceLeakPass {
40599
41318
  return false;
40600
41319
  }
40601
41320
  isReturnedToCaller(lines, variable, fromLine, toLine) {
40602
- const varRe = new RegExp(`\\breturn\\b[^;]*\\b${escapeRegex2(variable)}\\b`);
41321
+ const varRe = new RegExp(`\\breturn\\b[^;]*\\b${escapeRegex3(variable)}\\b`);
40603
41322
  for (let l = fromLine;l <= toLine && l <= lines.length; l++) {
40604
41323
  if (varRe.test(lines[l - 1] ?? ""))
40605
41324
  return true;
@@ -40607,7 +41326,7 @@ class ResourceLeakPass {
40607
41326
  return false;
40608
41327
  }
40609
41328
  fieldStoredName(lines, variable, openLine, toLine) {
40610
- const thisAssignRe = new RegExp(`\\bthis\\s*\\.\\s*(\\w+)\\s*=\\s*${escapeRegex2(variable)}\\b`);
41329
+ const thisAssignRe = new RegExp(`\\bthis\\s*\\.\\s*(\\w+)\\s*=\\s*${escapeRegex3(variable)}\\b`);
40611
41330
  for (let l = openLine;l <= toLine && l <= lines.length; l++) {
40612
41331
  const m = thisAssignRe.exec(lines[l - 1] ?? "");
40613
41332
  if (m)
@@ -40650,7 +41369,7 @@ class ResourceLeakPass {
40650
41369
  if (fieldNames.has(variable)) {
40651
41370
  candidateField = variable;
40652
41371
  } else {
40653
- const thisAssignRe = new RegExp(`(?:\\bthis\\s*\\.\\s*)?(\\w+)\\s*=\\s*${escapeRegex2(variable)}\\b`);
41372
+ const thisAssignRe = new RegExp(`(?:\\bthis\\s*\\.\\s*)?(\\w+)\\s*=\\s*${escapeRegex3(variable)}\\b`);
40654
41373
  for (let l = fromLine;l <= toLine && l <= lines.length; l++) {
40655
41374
  const m = thisAssignRe.exec(lines[l - 1] ?? "");
40656
41375
  if (m && fieldNames.has(m[1])) {
@@ -51135,7 +51854,7 @@ var colors = {
51135
51854
  };
51136
51855
 
51137
51856
  // src/version.ts
51138
- var version = "4.12.0";
51857
+ var version = "4.13.0";
51139
51858
 
51140
51859
  // src/formatters.ts
51141
51860
  var LIBRARY_API_SURFACE_TAG2 = "library-api-surface:caller-responsibility";
@@ -51687,8 +52406,8 @@ function generateSarifResults(results, crossFileData) {
51687
52406
 
51688
52407
  // src/build-info.ts
51689
52408
  var buildInfo = {
51690
- gitSha: "8c284bc",
51691
- builtAt: "2026-10-08T00:42:12.776Z"
52409
+ gitSha: "a45b21b",
52410
+ builtAt: "2026-10-08T13:38:40.827Z"
51692
52411
  };
51693
52412
 
51694
52413
  // src/utils/args.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cognium-dev",
3
- "version": "4.12.0",
3
+ "version": "4.13.0",
4
4
  "description": "Static Application Security Testing CLI for detecting security vulnerabilities via taint tracking",
5
5
  "bin": {
6
6
  "cognium-dev": "dist/cli.js"
@@ -65,7 +65,7 @@
65
65
  },
66
66
  "dependencies": {
67
67
  "@cognium/project-profile-detect": "1.1.2",
68
- "circle-ir": "4.12.0"
68
+ "circle-ir": "4.13.0"
69
69
  },
70
70
  "devDependencies": {
71
71
  "@types/node": "^26.6.4",