cognium-dev 4.12.0 → 4.13.1

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 +919 -84
  3. package/package.json +2 -2
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;
@@ -20030,7 +20039,13 @@ class ConstantPropagator {
20030
20039
  }
20031
20040
  return;
20032
20041
  }
20033
- const condValue = this.evaluateExpression(condition);
20042
+ let condValue = this.evaluateExpression(condition);
20043
+ if (!(isKnown(condValue) && condValue.type === "bool")) {
20044
+ const literal = this.literalCondition(condition);
20045
+ if (literal !== null) {
20046
+ condValue = { type: "bool", value: literal, sourceLine: getNodeLine(condition) };
20047
+ }
20048
+ }
20034
20049
  if (isKnown(condValue) && condValue.type === "bool") {
20035
20050
  if (condValue.value === true) {
20036
20051
  if (alternative) {
@@ -20165,19 +20180,41 @@ class ConstantPropagator {
20165
20180
  break;
20166
20181
  }
20167
20182
  }
20168
- const switchBlock = node.children.find((c) => c.type === "switch_block");
20183
+ if (!switchValue) {
20184
+ const valueNode = node.childForFieldName("value");
20185
+ if (valueNode) {
20186
+ const literal = this.literalValue(valueNode);
20187
+ switchValue = literal !== null ? { type: typeof literal === "number" ? "int" : "string", value: literal, sourceLine: getNodeLine(valueNode) } : this.evaluateExpression(valueNode);
20188
+ }
20189
+ }
20190
+ const switchBlock = node.children.find((c) => c.type === "switch_block" || c.type === "switch_body");
20169
20191
  if (!switchBlock) {
20170
20192
  for (const child of node.children) {
20171
20193
  this.visit(child);
20172
20194
  }
20173
20195
  return;
20174
20196
  }
20175
- const caseGroups = switchBlock.children.filter((c) => c.type === "switch_block_statement_group" || c.type === "switch_rule");
20197
+ const caseGroups = switchBlock.children.filter((c) => c.type === "switch_block_statement_group" || c.type === "switch_rule" || c.type === "switch_section" || c.type === "switch_case" || c.type === "switch_default");
20198
+ if (caseGroups.length === 0) {
20199
+ this.visit(switchBlock);
20200
+ return;
20201
+ }
20176
20202
  if (switchValue && isKnown(switchValue)) {
20177
20203
  let matchingIdx = -1;
20178
20204
  let defaultIdx = -1;
20179
20205
  for (let i2 = 0;i2 < caseGroups.length; i2++) {
20180
20206
  const caseGroup = caseGroups[i2];
20207
+ if (caseGroup.type === "switch_default") {
20208
+ defaultIdx = i2;
20209
+ continue;
20210
+ }
20211
+ if (caseGroup.type === "switch_case") {
20212
+ const label = caseGroup.childForFieldName("value");
20213
+ const caseValue = label ? this.literalValue(label) : null;
20214
+ if (caseValue !== null && caseValue === switchValue.value)
20215
+ matchingIdx = i2;
20216
+ continue;
20217
+ }
20181
20218
  for (const child of caseGroup.children) {
20182
20219
  if (child.type === "switch_label") {
20183
20220
  const labelText = getNodeText2(child, this.source);
@@ -20189,6 +20226,16 @@ class ConstantPropagator {
20189
20226
  matchingIdx = i2;
20190
20227
  }
20191
20228
  }
20229
+ } else if (caseGroup.type === "switch_section") {
20230
+ if (child.type === "default") {
20231
+ defaultIdx = i2;
20232
+ } else if (child.type === "constant_pattern") {
20233
+ const inner = child.children.find((c) => c.isNamed);
20234
+ const caseValue = inner ? this.literalValue(inner) : null;
20235
+ if (caseValue !== null && caseValue === switchValue.value) {
20236
+ matchingIdx = i2;
20237
+ }
20238
+ }
20192
20239
  }
20193
20240
  }
20194
20241
  }
@@ -20271,6 +20318,41 @@ class ConstantPropagator {
20271
20318
  }
20272
20319
  return false;
20273
20320
  }
20321
+ literalCondition(node) {
20322
+ if (node.type === "parenthesized_expression") {
20323
+ const inner = node.children.find((c) => c.isNamed);
20324
+ return inner ? this.literalCondition(inner) : null;
20325
+ }
20326
+ if (node.type === "boolean_literal") {
20327
+ const text = getNodeText2(node, this.source);
20328
+ return text === "true" ? true : text === "false" ? false : null;
20329
+ }
20330
+ if (node.type === "binary_expression") {
20331
+ const left = node.childForFieldName("left");
20332
+ const right = node.childForFieldName("right");
20333
+ const op = node.childForFieldName("operator");
20334
+ const opText = op ? getNodeText2(op, this.source) : "";
20335
+ if (!left || !right || opText !== "==" && opText !== "!=")
20336
+ return null;
20337
+ const l = this.literalValue(left);
20338
+ const r = this.literalValue(right);
20339
+ if (l === null || r === null)
20340
+ return null;
20341
+ return opText === "==" ? l === r : l !== r;
20342
+ }
20343
+ return null;
20344
+ }
20345
+ literalValue(node) {
20346
+ const text = getNodeText2(node, this.source);
20347
+ if (node.type === "integer_literal" || node.type === "decimal_integer_literal") {
20348
+ const n = Number(text.replace(/_/g, "").replace(/[uUlL]+$/, ""));
20349
+ return Number.isInteger(n) ? n : null;
20350
+ }
20351
+ if (node.type === "character_literal" || node.type === "string_literal") {
20352
+ return text.slice(1, -1);
20353
+ }
20354
+ return null;
20355
+ }
20274
20356
  extractCaseValue(labelNode) {
20275
20357
  for (const child of labelNode.children) {
20276
20358
  if (child.type === "decimal_integer_literal") {
@@ -34062,6 +34144,486 @@ class MyBatisAnnotationSqlSinkPass {
34062
34144
  }
34063
34145
  }
34064
34146
 
34147
+ // ../circle-ir/dist/analysis/passes/csharp-sanitizer-credit.js
34148
+ 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*\)/;
34149
+ 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*\)/;
34150
+ var CSHARP_SANITIZER_RES = [
34151
+ { re: /\b(?:HtmlEncode|JavaScriptStringEncode)\s*\(/, type: "xss" },
34152
+ { re: /\bHtmlEncoder\s*\.\s*(?:Default\s*\.\s*)?Encode\s*\(/, type: "xss" },
34153
+ { re: /\bPath\s*\.\s*GetFileName\s*\(/, type: "path_traversal" },
34154
+ { re: /\bSecurityElement\s*\.\s*Escape\s*\(/, type: "xpath_injection" },
34155
+ { re: CSHARP_ALLOWLIST_STRIP_RE, type: "ldap_injection" },
34156
+ { re: CSHARP_ALLOWLIST_STRIP_RE, type: "xpath_injection" },
34157
+ { re: CSHARP_ALLOWLIST_STRIP_RE, type: "command_injection" },
34158
+ { re: CSHARP_ALLOWLIST_STRIP_RE, type: "sql_injection" },
34159
+ { re: CSHARP_ALLOWLIST_STRIP_RE, type: "crlf" },
34160
+ { re: CSHARP_ALLOWLIST_FILTER_RE, type: "ldap_injection" },
34161
+ { re: CSHARP_ALLOWLIST_FILTER_RE, type: "xpath_injection" },
34162
+ { re: CSHARP_ALLOWLIST_FILTER_RE, type: "command_injection" },
34163
+ { re: CSHARP_ALLOWLIST_FILTER_RE, type: "sql_injection" },
34164
+ { re: CSHARP_ALLOWLIST_FILTER_RE, type: "crlf" }
34165
+ ];
34166
+ var INERT_WHEN_ALPHANUMERIC = new Set([
34167
+ "ldap_injection",
34168
+ "xpath_injection",
34169
+ "command_injection",
34170
+ "sql_injection",
34171
+ "crlf",
34172
+ "xss",
34173
+ "path_traversal",
34174
+ "code_injection"
34175
+ ]);
34176
+ var IDENT = "[A-Za-z_]\\w*";
34177
+ var ASSIGN_RE = new RegExp(`^\\s*(?:var\\s+|[A-Za-z_][\\w.<>\\[\\]?]*\\s+)?(${IDENT})\\s*=(?!=)\\s*(.+?);?\\s*$`);
34178
+ var ENDS_STATEMENT_RE = /[;{}]\s*(?:\/\/.*)?$/;
34179
+ function escapeRegex(s) {
34180
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
34181
+ }
34182
+ function withoutStringLiterals(text) {
34183
+ return text.replace(/@"(?:[^"]|"")*"|"(?:[^"\\]|\\.)*"/g, '""');
34184
+ }
34185
+ function mentions(text, name2) {
34186
+ return new RegExp(`(?<![\\w])${escapeRegex(name2)}(?![\\w])`).test(text);
34187
+ }
34188
+ function insideParens(text, open) {
34189
+ let depth = 0;
34190
+ for (let i2 = open;i2 < text.length; i2++) {
34191
+ if (text[i2] === "(")
34192
+ depth++;
34193
+ else if (text[i2] === ")" && --depth === 0)
34194
+ return text.slice(open + 1, i2);
34195
+ }
34196
+ return null;
34197
+ }
34198
+ function lastCallArguments(text) {
34199
+ const close = text.lastIndexOf(")");
34200
+ if (close < 0)
34201
+ return null;
34202
+ let depth = 0;
34203
+ for (let i2 = close;i2 >= 0; i2--) {
34204
+ if (text[i2] === ")")
34205
+ depth++;
34206
+ else if (text[i2] === "(" && --depth === 0)
34207
+ return text.slice(i2 + 1, close);
34208
+ }
34209
+ return null;
34210
+ }
34211
+ function leadingIf(text) {
34212
+ const m = /^\s*(?:\}\s*)?(?:else\s+)?if\s*\(/.exec(text);
34213
+ if (!m)
34214
+ return null;
34215
+ const open = m[0].length - 1;
34216
+ const cond = insideParens(text, open);
34217
+ if (cond === null)
34218
+ return null;
34219
+ return { cond: cond.trim(), rest: text.slice(open + cond.length + 2).trim() };
34220
+ }
34221
+ var EXITS_RE = /^(?:\{\s*)?(?:return\b|throw\b|continue\b|break\b)/;
34222
+
34223
+ class CSharpCredit {
34224
+ methods;
34225
+ lines;
34226
+ helperCache = new Map;
34227
+ guardHelperCache = new Map;
34228
+ constructor(code, methods) {
34229
+ this.methods = methods;
34230
+ this.lines = code.split(/\r?\n/);
34231
+ }
34232
+ methodAt(line) {
34233
+ let best = null;
34234
+ for (const m of this.methods) {
34235
+ if (line < m.start_line || line > m.end_line)
34236
+ continue;
34237
+ if (!best || m.end_line - m.start_line < best.end_line - best.start_line)
34238
+ best = m;
34239
+ }
34240
+ return best;
34241
+ }
34242
+ statements(from, to) {
34243
+ const out2 = [];
34244
+ for (let i2 = Math.max(from, 1);i2 < to; i2++) {
34245
+ let text = (this.lines[i2 - 1] ?? "").trimEnd();
34246
+ const first = i2;
34247
+ if (/^\s*(?:[\w.<>[\]?]+\s+)?[A-Za-z_]\w*\s*=(?!=)/.test(text) && !ENDS_STATEMENT_RE.test(text)) {
34248
+ while (i2 < to - 1 && i2 < first + 12) {
34249
+ i2++;
34250
+ text += " " + (this.lines[i2 - 1] ?? "").trim();
34251
+ if (ENDS_STATEMENT_RE.test(this.lines[i2 - 1] ?? ""))
34252
+ break;
34253
+ }
34254
+ }
34255
+ out2.push({ line: first, text });
34256
+ }
34257
+ return out2;
34258
+ }
34259
+ builtinSanitizes(type, text) {
34260
+ for (const { re, type: t } of CSHARP_SANITIZER_RES) {
34261
+ if (t === type && re.test(text))
34262
+ return true;
34263
+ }
34264
+ if (type === "ldap_injection") {
34265
+ if (/\.\s*Replace\s*\(\s*"\("\s*,/.test(text) && /\.\s*Replace\s*\(\s*"\)"\s*,/.test(text))
34266
+ return true;
34267
+ }
34268
+ if (type === "path_traversal") {
34269
+ if (/\b([A-Za-z_]\w*)\s*\.\s*StartsWith\s*\([^)]*\)\s*\?\s*\1\s*:\s*null\b/.test(text))
34270
+ return true;
34271
+ }
34272
+ return false;
34273
+ }
34274
+ helpers(type) {
34275
+ const cached = this.helperCache.get(type);
34276
+ if (cached)
34277
+ return cached;
34278
+ const set = new Set;
34279
+ this.helperCache.set(type, set);
34280
+ for (const m of this.methods) {
34281
+ const returns = this.returnExprs(m);
34282
+ if (returns.length === 0)
34283
+ continue;
34284
+ const ok = returns.every((r) => {
34285
+ if (this.builtinSanitizes(type, r.expr))
34286
+ return true;
34287
+ const v = /^([A-Za-z_]\w*)$/.exec(r.expr.trim());
34288
+ return !!v && this.sanitizedVarsIn(type, m.start_line, r.line, false).has(v[1]);
34289
+ });
34290
+ if (ok)
34291
+ set.add(m.name);
34292
+ }
34293
+ return set;
34294
+ }
34295
+ returnExprs(m) {
34296
+ const out2 = [];
34297
+ for (const st of this.statements(m.start_line, m.end_line + 1)) {
34298
+ for (const r of st.text.matchAll(/\breturn\s+([^;]+);/g))
34299
+ out2.push({ line: st.line, expr: r[1] });
34300
+ if (st.line === m.start_line) {
34301
+ const body2 = /\)\s*=>\s*(.+?);\s*$/.exec(st.text);
34302
+ if (body2)
34303
+ out2.push({ line: st.line, expr: body2[1] });
34304
+ }
34305
+ }
34306
+ return out2;
34307
+ }
34308
+ sanitizes(type, text) {
34309
+ if (this.builtinSanitizes(type, text))
34310
+ return true;
34311
+ for (const name2 of this.helpers(type)) {
34312
+ if (new RegExp(`(?<![\\w.])${escapeRegex(name2)}\\s*\\(`).test(text))
34313
+ return true;
34314
+ }
34315
+ return false;
34316
+ }
34317
+ sanitizedVarsIn(type, from, to, withHelpers) {
34318
+ const sanitized = new Set;
34319
+ for (const st of this.statements(from, to)) {
34320
+ const m = ASSIGN_RE.exec(st.text);
34321
+ if (!m)
34322
+ continue;
34323
+ const [, lhs, rhs] = m;
34324
+ const credited = (withHelpers ? this.sanitizes(type, rhs) : this.builtinSanitizes(type, rhs)) || [...sanitized].some((v) => mentions(rhs, v));
34325
+ if (credited)
34326
+ sanitized.add(lhs);
34327
+ else
34328
+ sanitized.delete(lhs);
34329
+ }
34330
+ return sanitized;
34331
+ }
34332
+ sanitizedVarsAt(type, sinkLine) {
34333
+ return this.sanitizedVarsIn(type, this.methodAt(sinkLine)?.start_line ?? 1, sinkLine, true);
34334
+ }
34335
+ rawTaintReaches(type, sinkLine, credited, seeds) {
34336
+ if (seeds.length === 0)
34337
+ return false;
34338
+ const method = this.methodAt(sinkLine);
34339
+ const from = method?.start_line ?? 1;
34340
+ const tainted = new Set;
34341
+ const stmts = this.statements(from, sinkLine);
34342
+ for (let i2 = 0;i2 < stmts.length; i2++) {
34343
+ const st = stmts[i2];
34344
+ const end = i2 + 1 < stmts.length ? stmts[i2 + 1].line : sinkLine;
34345
+ const m = ASSIGN_RE.exec(st.text);
34346
+ if (m) {
34347
+ const rhs = withoutStringLiterals(m[2]);
34348
+ if ([...tainted].some((v) => mentions(rhs, v)))
34349
+ tainted.add(m[1]);
34350
+ else
34351
+ tainted.delete(m[1]);
34352
+ }
34353
+ for (const s of seeds)
34354
+ if (s.line >= st.line && s.line < end)
34355
+ tainted.add(s.variable);
34356
+ }
34357
+ if (tainted.size === 0)
34358
+ return false;
34359
+ const call = withoutStringLiterals(this.sinkText(sinkLine));
34360
+ const rest = this.withoutSanitizerCalls(type, lastCallArguments(call) ?? call);
34361
+ if (this.builtinSanitizes(type, rest))
34362
+ return false;
34363
+ for (const v of tainted)
34364
+ if (!credited.has(v) && mentions(rest, v))
34365
+ return true;
34366
+ return false;
34367
+ }
34368
+ withoutSanitizerCalls(type, text) {
34369
+ const res = [];
34370
+ for (const { re, type: t } of CSHARP_SANITIZER_RES)
34371
+ if (t === type)
34372
+ res.push(re);
34373
+ for (const name2 of this.helpers(type))
34374
+ res.push(new RegExp(`(?<![\\w.])${escapeRegex(name2)}\\s*\\(`));
34375
+ let out2 = text;
34376
+ for (const re of res) {
34377
+ for (let guard = 0;guard < 16; guard++) {
34378
+ const m = re.exec(out2);
34379
+ if (!m)
34380
+ break;
34381
+ if (!m[0].endsWith("(")) {
34382
+ out2 = out2.slice(0, m.index) + out2.slice(m.index + m[0].length);
34383
+ continue;
34384
+ }
34385
+ const open = m.index + m[0].length - 1;
34386
+ const inner = insideParens(out2, open);
34387
+ if (inner === null)
34388
+ break;
34389
+ out2 = out2.slice(0, m.index) + out2.slice(open + inner.length + 2);
34390
+ }
34391
+ }
34392
+ return out2;
34393
+ }
34394
+ sinkText(line) {
34395
+ const text = this.lines[line - 1] ?? "";
34396
+ const guarded = leadingIf(text);
34397
+ return guarded && guarded.rest !== "" ? guarded.rest : text;
34398
+ }
34399
+ allowlistVars(cond) {
34400
+ const out2 = [];
34401
+ const anchored = new RegExp(`\\bRegex\\s*\\.\\s*IsMatch\\s*\\(\\s*(${IDENT})\\s*,\\s*@?"\\^\\[(?:[A-Za-z0-9_\\- ]|\\\\w|\\\\d)+\\](?:[+*]|\\{\\d+(?:,\\d*)?\\})\\$"`, "g");
34402
+ for (const m of cond.matchAll(anchored))
34403
+ out2.push(m[1]);
34404
+ const methodGroup = new RegExp(`(${IDENT})\\s*\\.\\s*All\\s*\\(\\s*char\\s*\\.\\s*Is(?:LetterOrDigit|Digit|Letter)\\s*\\)`, "g");
34405
+ for (const m of cond.matchAll(methodGroup))
34406
+ out2.push(m[1]);
34407
+ const lambda = new RegExp(`(${IDENT})\\s*\\.\\s*All\\s*\\(\\s*(${IDENT})\\s*=>`, "g");
34408
+ for (const m of cond.matchAll(lambda)) {
34409
+ const open = cond.indexOf("(", m.index + m[1].length);
34410
+ const body2 = insideParens(cond, open)?.replace(new RegExp(`^\\s*${m[2]}\\s*=>`), "") ?? "";
34411
+ const c = escapeRegex(m[2]);
34412
+ 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, "");
34413
+ if (residue === "")
34414
+ out2.push(m[1]);
34415
+ }
34416
+ return out2;
34417
+ }
34418
+ numericVars(cond, parsedFrom) {
34419
+ const out2 = [];
34420
+ 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");
34421
+ for (const m of cond.matchAll(tryParse))
34422
+ out2.push(m[1]);
34423
+ for (const [parsed, from] of parsedFrom) {
34424
+ if (new RegExp(`(?<![\\w])${escapeRegex(parsed)}\\s*(?:!=\\s*null|\\.\\s*HasValue)\\b`).test(cond))
34425
+ out2.push(from);
34426
+ }
34427
+ return out2;
34428
+ }
34429
+ hostVars(cond, hostOf) {
34430
+ const out2 = [];
34431
+ const compares = (expr) => {
34432
+ const e = escapeRegex(expr).replace(/\\ /g, "\\s*");
34433
+ 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);
34434
+ };
34435
+ const direct = new RegExp(`new\\s+(?:System\\s*\\.\\s*)?Uri\\s*\\(\\s*(${IDENT})\\s*\\)\\s*\\.\\s*Host\\b`, "g");
34436
+ for (const m of cond.matchAll(direct)) {
34437
+ if (compares(m[0]))
34438
+ out2.push(m[1]);
34439
+ }
34440
+ for (const [hostVar, from] of hostOf) {
34441
+ if (mentions(cond, hostVar) && compares(hostVar))
34442
+ out2.push(from);
34443
+ }
34444
+ return out2;
34445
+ }
34446
+ guardHelpers(kind) {
34447
+ const cached = this.guardHelperCache.get(kind);
34448
+ if (cached)
34449
+ return cached;
34450
+ const map = new Map;
34451
+ this.guardHelperCache.set(kind, map);
34452
+ for (const m of this.methods) {
34453
+ const signature = this.lines[m.start_line - 1] ?? "";
34454
+ const open = signature.indexOf("(", signature.indexOf(m.name));
34455
+ const params = open === -1 ? null : insideParens(signature, open);
34456
+ if (!params)
34457
+ continue;
34458
+ const names = params.split(",").map((p) => p.trim().split(/\s+/).pop() ?? "");
34459
+ const returns = this.returnExprs(m);
34460
+ if (returns.length !== 1)
34461
+ continue;
34462
+ const proven = this.provenBy(kind, returns[0].expr, new Map, new Map, false);
34463
+ const pos = names.findIndex((n) => proven.includes(n));
34464
+ if (pos !== -1)
34465
+ map.set(m.name, pos);
34466
+ }
34467
+ return map;
34468
+ }
34469
+ provenBy(kind, cond, parsedFrom, hostOf, withHelpers) {
34470
+ const out2 = kind === "allowlist" ? this.allowlistVars(cond) : kind === "numeric" ? this.numericVars(cond, parsedFrom) : this.hostVars(cond, hostOf);
34471
+ if (withHelpers) {
34472
+ for (const [name2, pos] of this.guardHelpers(kind)) {
34473
+ for (const call of cond.matchAll(new RegExp(`(?<![\\w.])${escapeRegex(name2)}\\s*\\(`, "g"))) {
34474
+ const args2 = insideParens(cond, call.index + call[0].length - 1)?.split(",") ?? [];
34475
+ const arg = args2[pos]?.trim();
34476
+ if (arg && new RegExp(`^${IDENT}$`).test(arg))
34477
+ out2.push(arg);
34478
+ }
34479
+ }
34480
+ }
34481
+ return out2;
34482
+ }
34483
+ guardedVarsAt(type, sinkLine) {
34484
+ const kinds = [];
34485
+ if (INERT_WHEN_ALPHANUMERIC.has(type))
34486
+ kinds.push("allowlist", "numeric");
34487
+ if (type === "ssrf")
34488
+ kinds.push("host");
34489
+ const proven = new Set;
34490
+ if (kinds.length === 0)
34491
+ return proven;
34492
+ const scopeStart = this.methodAt(sinkLine)?.start_line ?? 1;
34493
+ const before = this.statements(scopeStart, sinkLine);
34494
+ const boolOf = new Map;
34495
+ const parsedFrom = new Map;
34496
+ const hostOf = new Map;
34497
+ 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");
34498
+ for (const st of before) {
34499
+ for (const p of st.text.matchAll(parseAnywhere))
34500
+ parsedFrom.set(p[1], p[2]);
34501
+ const m = ASSIGN_RE.exec(st.text);
34502
+ if (!m)
34503
+ continue;
34504
+ const [, lhs, rhs] = m;
34505
+ 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);
34506
+ if (parse2)
34507
+ parsedFrom.set(lhs, parse2[1]);
34508
+ const host = new RegExp(`^new\\s+(?:System\\s*\\.\\s*)?Uri\\s*\\(\\s*(${IDENT})\\s*\\)\\s*\\.\\s*Host$`).exec(rhs.trim());
34509
+ if (host)
34510
+ hostOf.set(lhs, host[1]);
34511
+ boolOf.set(lhs, rhs);
34512
+ }
34513
+ const credit = (cond) => {
34514
+ const resolved = new RegExp(`^${IDENT}$`).test(cond) ? boolOf.get(cond) ?? cond : cond;
34515
+ for (const kind of kinds) {
34516
+ for (const v of this.provenBy(kind, resolved, parsedFrom, hostOf, true))
34517
+ proven.add(v);
34518
+ }
34519
+ };
34520
+ const own = leadingIf(this.lines[sinkLine - 1] ?? "");
34521
+ if (own && !own.cond.startsWith("!"))
34522
+ credit(own.cond);
34523
+ let depth = 0;
34524
+ for (let i2 = sinkLine - 1;i2 >= scopeStart; i2--) {
34525
+ const text = this.lines[i2 - 1] ?? "";
34526
+ for (let c = text.length - 1;c >= 0; c--) {
34527
+ if (text[c] === "}")
34528
+ depth++;
34529
+ else if (text[c] === "{") {
34530
+ if (depth > 0) {
34531
+ depth--;
34532
+ continue;
34533
+ }
34534
+ let header = text.slice(0, c);
34535
+ for (let h = i2 - 1;header.trim() === "" && h >= scopeStart; h--)
34536
+ header = this.lines[h - 1] ?? "";
34537
+ const enclosing = leadingIf(header);
34538
+ if (enclosing && !enclosing.cond.startsWith("!"))
34539
+ credit(enclosing.cond);
34540
+ }
34541
+ }
34542
+ }
34543
+ for (let i2 = scopeStart;i2 < sinkLine; i2++) {
34544
+ const guard = leadingIf(this.lines[i2 - 1] ?? "");
34545
+ if (!guard)
34546
+ continue;
34547
+ let rest = guard.rest;
34548
+ for (let n = i2 + 1;rest === "" && n < sinkLine; n++)
34549
+ rest = (this.lines[n - 1] ?? "").trim();
34550
+ if (!EXITS_RE.test(rest))
34551
+ continue;
34552
+ if (guard.cond.startsWith("!")) {
34553
+ const inner = guard.cond.slice(1).trim();
34554
+ const wrapped = inner.startsWith("(") && insideParens(inner, 0)?.length === inner.length - 2;
34555
+ credit(wrapped ? inner.slice(1, -1) : inner);
34556
+ } else if (/!=/.test(guard.cond) && !/[|&]/.test(guard.cond))
34557
+ credit(guard.cond.replace("!=", "=="));
34558
+ }
34559
+ return proven;
34560
+ }
34561
+ safeBuilders(credited, sinkLine) {
34562
+ const scopeStart = this.methodAt(sinkLine)?.start_line ?? 1;
34563
+ const onlySafe = (expr) => {
34564
+ const idents = expr.replace(/\$?@?"(?:[^"\\]|\\.)*"/g, " ").replace(/'(?:[^'\\]|\\.)'/g, " ").replace(/\.\s*[A-Za-z_]\w*\s*\(\s*\)/g, " ").match(/[A-Za-z_]\w*/g) ?? [];
34565
+ return idents.every((id) => credited.has(id));
34566
+ };
34567
+ const state = new Map;
34568
+ const declRe = new RegExp(`\\b(?:StringBuilder|var)\\s+(${IDENT})\\s*=\\s*new\\s+(?:System\\s*\\.\\s*Text\\s*\\.\\s*)?StringBuilder\\s*\\(`);
34569
+ const writeRe = new RegExp(`^\\s*(${IDENT})\\s*\\.\\s*(?:Append|AppendLine|AppendFormat|Insert)\\s*\\(`);
34570
+ for (const st of this.statements(scopeStart, sinkLine)) {
34571
+ const decl = declRe.exec(st.text);
34572
+ if (decl) {
34573
+ const args2 = insideParens(st.text, decl.index + decl[0].length - 1) ?? "";
34574
+ state.set(decl[1], onlySafe(args2));
34575
+ continue;
34576
+ }
34577
+ const write = writeRe.exec(st.text);
34578
+ if (write && state.has(write[1])) {
34579
+ const args2 = insideParens(st.text, write[0].length - 1);
34580
+ if (args2 === null || !onlySafe(args2))
34581
+ state.set(write[1], false);
34582
+ }
34583
+ }
34584
+ return new Set([...state].filter(([, safe]) => safe).map(([name2]) => name2));
34585
+ }
34586
+ creditedOnLine(line, seeds = []) {
34587
+ const text = this.sinkText(line);
34588
+ const types = new Set(CSHARP_SANITIZER_RES.map((r) => r.type));
34589
+ for (const t of INERT_WHEN_ALPHANUMERIC)
34590
+ types.add(t);
34591
+ types.add("ssrf");
34592
+ for (const type of types) {
34593
+ const credited = new Set([...this.sanitizedVarsAt(type, line), ...this.guardedVarsAt(type, line)]);
34594
+ for (const v of this.safeBuilders(credited, line))
34595
+ credited.add(v);
34596
+ const covered = this.sanitizes(type, text) || [...credited].some((v) => mentions(text, v));
34597
+ if (covered && !this.rawTaintReaches(type, line, credited, seeds))
34598
+ return true;
34599
+ }
34600
+ return this.fixedHostRequest(line);
34601
+ }
34602
+ fixedHostRequest(sinkLine) {
34603
+ const fixedLiteral = '\\$?@?"https?:\\/\\/[A-Za-z0-9.\\-]+(?::\\d+)?[\\/?#][^"]*"';
34604
+ const text = this.lines[sinkLine - 1] ?? "";
34605
+ const argStart = `\\(\\s*(?:new\\s+(?:System\\s*\\.\\s*)?Uri\\s*\\(\\s*)?`;
34606
+ if (new RegExp(argStart + fixedLiteral).test(text))
34607
+ return true;
34608
+ const scopeStart = this.methodAt(sinkLine)?.start_line ?? 1;
34609
+ const fixed = new Set;
34610
+ for (const st of this.statements(scopeStart, sinkLine)) {
34611
+ const m = ASSIGN_RE.exec(st.text);
34612
+ if (!m)
34613
+ continue;
34614
+ if (new RegExp("^(?:new\\s+(?:System\\s*\\.\\s*)?Uri\\s*\\(\\s*)?" + fixedLiteral).test(m[2].trim()))
34615
+ fixed.add(m[1]);
34616
+ else
34617
+ fixed.delete(m[1]);
34618
+ }
34619
+ for (const v of fixed) {
34620
+ if (new RegExp(argStart + escapeRegex(v) + "\\s*[,)]").test(text))
34621
+ return true;
34622
+ }
34623
+ return false;
34624
+ }
34625
+ }
34626
+
34065
34627
  // ../circle-ir/dist/analysis/passes/sink-filter-pass.js
34066
34628
  function escapeReSf(s) {
34067
34629
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -34364,7 +34926,6 @@ function jsSsrfHostGuardedLines(code) {
34364
34926
  }
34365
34927
  return covered;
34366
34928
  }
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
34929
  function isUnwrittenLocalMapKeyRead(code, source) {
34369
34930
  if (source.type !== "plugin_param")
34370
34931
  return false;
@@ -34391,15 +34952,6 @@ function isUnwrittenLocalMapKeyRead(code, source) {
34391
34952
  return false;
34392
34953
  return true;
34393
34954
  }
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
34955
  function csharpSanitizedVarsByType(code) {
34404
34956
  const lines = code.split(`
34405
34957
  `);
@@ -34432,7 +34984,7 @@ function csharpSanitizedVarsByType(code) {
34432
34984
  for (const [type, set] of byType) {
34433
34985
  if (set.has(lhs))
34434
34986
  continue;
34435
- const refsSanitized = [...set].some((v) => new RegExp(`(?<![\\w])${escapeRegex(v)}(?![\\w])`).test(rhs));
34987
+ const refsSanitized = [...set].some((v) => new RegExp(`(?<![\\w])${escapeRegex2(v)}(?![\\w])`).test(rhs));
34436
34988
  if (refsSanitized) {
34437
34989
  set.add(lhs);
34438
34990
  changed = true;
@@ -35328,30 +35880,39 @@ class SinkFilterPass {
35328
35880
  const sourceLines = ctx.code.split(`
35329
35881
  `);
35330
35882
  const sanitizedByType = csharpSanitizedVarsByType(ctx.code);
35883
+ const csFields = new Set(ctx.graph.ir.types.flatMap((t) => (t.fields ?? []).map((f) => f.name)));
35884
+ const credit = new CSharpCredit(ctx.code, ctx.graph.ir.types.flatMap((t) => t.methods));
35885
+ const csSeeds = mergedSources.flatMap((s) => s.variable ? [{ variable: s.variable, line: s.line }] : []);
35331
35886
  const xxeHardened = /\bXmlResolver\s*=\s*null\b/.test(ctx.code) || /\bDtdProcessing\s*=\s*(?:DtdProcessing\s*\.\s*)?(?:Prohibit|Ignore)\b/.test(ctx.code);
35332
35887
  filtered = filtered.filter((sink) => {
35333
35888
  if (sink.type === "xxe" && xxeHardened)
35334
35889
  return false;
35335
35890
  const sinkLineText = sourceLines[sink.line - 1] ?? "";
35336
- for (const { re, type } of CSHARP_SANITIZER_RES) {
35337
- if (type === sink.type && re.test(sinkLineText))
35338
- return false;
35339
- }
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
- return true;
35891
+ const inlineSanitized = credit.sanitizes(sink.type, sinkLineText);
35892
+ if (sink.type === "ssrf" && credit.fixedHostRequest(sink.line))
35893
+ return false;
35894
+ const scoped = credit.sanitizedVarsAt(sink.type, sink.line);
35895
+ const fileWide = sanitizedByType.get(sink.type);
35896
+ const sanitizedVars = new Set(scoped);
35897
+ if (fileWide) {
35898
+ for (const v of fileWide)
35899
+ if (csFields.has(v))
35900
+ sanitizedVars.add(v);
35901
+ }
35902
+ for (const v of credit.guardedVarsAt(sink.type, sink.line))
35903
+ sanitizedVars.add(v);
35904
+ for (const v of credit.safeBuilders(sanitizedVars, sink.line))
35905
+ sanitizedVars.add(v);
35906
+ const received = credit.sinkText(sink.line);
35907
+ const covered = inlineSanitized || [...sanitizedVars].some((v) => new RegExp(`(?<![\\w])${escapeRegex2(v)}(?![\\w])`).test(received));
35908
+ return !covered || credit.rawTaintReaches(sink.type, sink.line, sanitizedVars, csSeeds);
35348
35909
  });
35349
35910
  }
35350
35911
  if (language === "csharp") {
35351
35912
  const sourceLines = ctx.code.split(`
35352
35913
  `);
35353
35914
  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*\)/;
35915
+ 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
35916
  for (const line of sourceLines) {
35356
35917
  const m = declRe.exec(line);
35357
35918
  if (m)
@@ -35365,14 +35926,22 @@ class SinkFilterPass {
35365
35926
  const text = sourceLines[sink.line - 1] ?? "";
35366
35927
  let onConstClient = false;
35367
35928
  for (const v of constBaseClients) {
35368
- if (new RegExp(`(?<![\\w])${escapeRegex(v)}\\s*\\.`).test(text)) {
35929
+ if (new RegExp(`(?<![\\w])${escapeRegex2(v)}\\s*\\.`).test(text)) {
35369
35930
  onConstClient = true;
35370
35931
  break;
35371
35932
  }
35372
35933
  }
35373
35934
  if (!onConstClient)
35374
35935
  return true;
35375
- const m = relativeLiteralArgRe.exec(text);
35936
+ let m = relativeLiteralArgRe.exec(text);
35937
+ if (!m) {
35938
+ const arg = /Async\s*\(\s*([A-Za-z_]\w*)\s*[,)]/.exec(text)?.[1];
35939
+ if (arg) {
35940
+ const assigned = new RegExp(`\\b${escapeRegex2(arg)}\\s*=\\s*(?:@?\\$?)"([^"]*)"`);
35941
+ for (let i2 = sink.line - 2;i2 >= 0 && !m; i2--)
35942
+ m = assigned.exec(sourceLines[i2] ?? "");
35943
+ }
35944
+ }
35376
35945
  if (!m)
35377
35946
  return true;
35378
35947
  const lit = m[1];
@@ -35408,7 +35977,7 @@ class SinkFilterPass {
35408
35977
  const wrappers = findPythonLdapStripWrappers(sourceLines);
35409
35978
  const allLdapSanitizers = [...PY_BUILTIN_LDAP_SANITIZERS, ...wrappers];
35410
35979
  if (allLdapSanitizers.length > 0) {
35411
- const ldapPatterns = allLdapSanitizers.map((name2) => new RegExp(`\\b${escapeRegex(name2)}\\s*\\(`));
35980
+ const ldapPatterns = allLdapSanitizers.map((name2) => new RegExp(`\\b${escapeRegex2(name2)}\\s*\\(`));
35412
35981
  filtered = filtered.filter((sink) => {
35413
35982
  if (sink.type !== "ldap_injection")
35414
35983
  return true;
@@ -35494,7 +36063,7 @@ class SinkFilterPass {
35494
36063
  return { sources, sinks: filtered, sanitizers };
35495
36064
  }
35496
36065
  }
35497
- function escapeRegex(s) {
36066
+ function escapeRegex2(s) {
35498
36067
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
35499
36068
  }
35500
36069
  var RESERVED_KEYWORDS = new Set([
@@ -35576,7 +36145,7 @@ function extractSourceIdentifiers(text) {
35576
36145
  return out2;
35577
36146
  }
35578
36147
  function isAssignedFromSanitizerPattern(sourceLines, sinkLine, varName, sanitizerPatterns, lookback = 30) {
35579
- const escapedVar = escapeRegex(varName);
36148
+ const escapedVar = escapeRegex2(varName);
35580
36149
  const assignRe = new RegExp(`(?:\\b(?:const|let|var|final|String)\\s+)?\\b${escapedVar}\\s*=\\s*(.+)`);
35581
36150
  const fnBoundaryRe = /^\s*(?:def\s+\w+\s*\(|function\s+\w+\s*\(|\w+\s*[:=]\s*(?:async\s+)?\(?[^)]*\)?\s*=>)/;
35582
36151
  const lo = Math.max(0, sinkLine - 1 - lookback);
@@ -37145,7 +37714,7 @@ class LibraryProfileXssGatePass {
37145
37714
 
37146
37715
  // ../circle-ir/dist/analysis/passes/taint-propagation-pass.js
37147
37716
  var CONTAINER_READ_SOURCES = new Set(["plugin_param", "config_param"]);
37148
- function makeConstPropVeto(constProp, graph, sources, code) {
37717
+ function makeConstPropVeto(constProp, graph, sources, code, language) {
37149
37718
  const lines = typeof code === "string" ? code.split(`
37150
37719
  `) : [];
37151
37720
  const selfReassign = new Map;
@@ -37182,6 +37751,8 @@ function makeConstPropVeto(constProp, graph, sources, code) {
37182
37751
  return false;
37183
37752
  if (r.reason !== "variable_not_tainted")
37184
37753
  return true;
37754
+ if (language === "csharp")
37755
+ return false;
37185
37756
  const lo = Math.min(sourceLine, sinkLine);
37186
37757
  let d = defAt(step.variable, step.line);
37187
37758
  let selfHops = 0;
@@ -37220,7 +37791,7 @@ class TaintPropagationPass {
37220
37791
  return { flows: [] };
37221
37792
  }
37222
37793
  const propagationResult = propagateTaint(graph, sources, sinks, sanitizers);
37223
- const isVetoed = makeConstPropVeto(constProp, graph, sources, ctx.code);
37794
+ const isVetoed = makeConstPropVeto(constProp, graph, sources, ctx.code, ctx.language);
37224
37795
  const verifiedFlows = propagationResult.flows.filter((flow) => {
37225
37796
  if (constProp.unreachableLines.has(flow.sink.line))
37226
37797
  return false;
@@ -37790,7 +38361,7 @@ function detectCollectionFlows(calls, sources, sinks, taintedVars, unreachableLi
37790
38361
  if (source.variable && source.variable !== varName && source.in_method && call.in_method && source.in_method !== call.in_method) {
37791
38362
  continue;
37792
38363
  }
37793
- if (typeof code === "string" && isReassignedToLiteralBetween(code, varName, source.line, sink.line)) {
38364
+ if (typeof code === "string" && isReassignedToLiteralBetween(code, varName, source.line, sink.line, unreachableLines)) {
37794
38365
  continue;
37795
38366
  }
37796
38367
  flows.push({
@@ -37830,7 +38401,7 @@ function detectCollectionFlows(calls, sources, sinks, taintedVars, unreachableLi
37830
38401
  if (source.variable && source.variable !== collectionVar && source.in_method && call.in_method && source.in_method !== call.in_method) {
37831
38402
  continue;
37832
38403
  }
37833
- if (typeof code === "string" && isReassignedToLiteralBetween(code, collectionVar, source.line, sink.line)) {
38404
+ if (typeof code === "string" && isReassignedToLiteralBetween(code, collectionVar, source.line, sink.line, unreachableLines)) {
37834
38405
  continue;
37835
38406
  }
37836
38407
  flows.push({
@@ -37949,7 +38520,7 @@ function detectParameterSinkFlows(types, calls, sources, sinks, unreachableLines
37949
38520
  if (paramSource) {
37950
38521
  const exists = flows.some((f) => f.source_line === paramSource.line && f.sink_line === sink.line && f.sink_type === sink.type);
37951
38522
  if (!exists) {
37952
- if (typeof code === "string" && isReassignedToLiteralBetween(code, arg.variable, paramSource.line, sink.line)) {
38523
+ if (typeof code === "string" && isReassignedToLiteralBetween(code, arg.variable, paramSource.line, sink.line, unreachableLines)) {
37953
38524
  continue;
37954
38525
  }
37955
38526
  flows.push({
@@ -37972,6 +38543,96 @@ function detectParameterSinkFlows(types, calls, sources, sinks, unreachableLines
37972
38543
  }
37973
38544
  return flows;
37974
38545
  }
38546
+ function csharpMethodScopedAliases(code, seeds, methods, fieldNames) {
38547
+ const lines = code.split(`
38548
+ `);
38549
+ const methodOf = (line) => {
38550
+ let best = -1;
38551
+ let span = Infinity;
38552
+ methods.forEach((m, i2) => {
38553
+ if (line >= m.start_line && line <= m.end_line && m.end_line - m.start_line < span) {
38554
+ best = i2;
38555
+ span = m.end_line - m.start_line;
38556
+ }
38557
+ });
38558
+ return best;
38559
+ };
38560
+ 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*$/;
38561
+ const assignRe = /^\s*(?:this\s*\.\s*)?([A-Za-z_]\w*)\s*=\s*(.+?);\s*$/;
38562
+ const returnRe = /^\s*return\s+(.+?);\s*$/;
38563
+ const KEYWORDS = new Set(["return", "throw", "new", "await", "yield", "else", "case", "using", "var"]);
38564
+ const codeOf = (rhs) => rhs.replace(/\$@?"(?:[^"\\]|\\.)*"/g, (lit) => " " + [...lit.matchAll(/\{([^}]*)\}/g)].map((x) => x[1]).join(" ") + " ").replace(/@?"(?:[^"\\]|\\.)*"/g, " ").replace(/'(?:[^'\\]|\\.)'/g, " ");
38565
+ const local = new Map;
38566
+ const taintedFields = new Set;
38567
+ const returnsTainted = new Set;
38568
+ const localsOf = (m) => {
38569
+ let set = local.get(m);
38570
+ if (!set) {
38571
+ set = new Set;
38572
+ local.set(m, set);
38573
+ }
38574
+ return set;
38575
+ };
38576
+ for (const seed of seeds) {
38577
+ const base = seed.variable.replace(/^this\./, "");
38578
+ if (fieldNames.has(base))
38579
+ taintedFields.add(base);
38580
+ else
38581
+ localsOf(methodOf(seed.line)).add(seed.variable);
38582
+ }
38583
+ const out2 = [];
38584
+ let changed = true;
38585
+ for (let guard = 0;changed && guard < lines.length + 2; guard++) {
38586
+ changed = false;
38587
+ for (let i2 = 0;i2 < lines.length; i2++) {
38588
+ let text = lines[i2];
38589
+ const trimmed = text.trimStart();
38590
+ if (trimmed.startsWith("//") || trimmed.startsWith("*"))
38591
+ continue;
38592
+ if (/^\s*(?:[\w.<>\[\]?]+\s+)?[A-Za-z_]\w*\s*=(?!=)/.test(text) && !/;\s*(?:\/\/.*)?$/.test(text)) {
38593
+ for (let j = i2 + 1;j < lines.length && j <= i2 + 12; j++) {
38594
+ text = text.trimEnd() + " " + lines[j].trim();
38595
+ if (/;\s*(?:\/\/.*)?$/.test(lines[j]))
38596
+ break;
38597
+ }
38598
+ }
38599
+ const m = methodOf(i2 + 1);
38600
+ const here = localsOf(m);
38601
+ const ret = returnRe.exec(text);
38602
+ if (ret && m !== -1 && !returnsTainted.has(methods[m].name)) {
38603
+ const idents2 = codeOf(ret[1]).match(/[\p{L}\p{N}_]+/gu) ?? [];
38604
+ if (idents2.some((t) => here.has(t) || taintedFields.has(t))) {
38605
+ returnsTainted.add(methods[m].name);
38606
+ changed = true;
38607
+ }
38608
+ continue;
38609
+ }
38610
+ const d = declRe.exec(text) ?? assignRe.exec(text);
38611
+ if (!d)
38612
+ continue;
38613
+ const [, lhs, rhs] = d;
38614
+ if (KEYWORDS.has(lhs))
38615
+ continue;
38616
+ const isField = fieldNames.has(lhs) && !declRe.test(text);
38617
+ if (isField ? taintedFields.has(lhs) : here.has(lhs))
38618
+ continue;
38619
+ const rhsCode = codeOf(rhs);
38620
+ const idents = rhsCode.match(/[\p{L}\p{N}_]+/gu) ?? [];
38621
+ const viaLocal = idents.some((t) => here.has(t));
38622
+ const viaField = idents.some((t) => taintedFields.has(t) && !here.has(t));
38623
+ const viaCall = [...rhsCode.matchAll(/([\p{L}_][\p{L}\p{N}_]*)\s*\(/gu)].some((c) => returnsTainted.has(c[1]));
38624
+ if (!viaLocal && !viaField && !viaCall)
38625
+ continue;
38626
+ if (isField)
38627
+ taintedFields.add(lhs);
38628
+ else
38629
+ here.add(lhs);
38630
+ out2.push({ variable: lhs, line: i2 + 1, carried: !viaLocal });
38631
+ changed = true;
38632
+ }
38633
+ }
38634
+ return out2;
38635
+ }
37975
38636
  var _reassignSplitCode;
37976
38637
  var _reassignSplitLines;
37977
38638
  function splitLinesCached(code) {
@@ -37982,7 +38643,7 @@ function splitLinesCached(code) {
37982
38643
  `);
37983
38644
  return _reassignSplitLines;
37984
38645
  }
37985
- function isReassignedToLiteralBetween(code, variable, srcLine, sinkLine) {
38646
+ function isReassignedToLiteralBetween(code, variable, srcLine, sinkLine, unreachableLines) {
37986
38647
  if (!variable || sinkLine - srcLine < 2)
37987
38648
  return false;
37988
38649
  if (!/^[A-Za-z_][\w]*$/.test(variable))
@@ -38002,49 +38663,117 @@ function isReassignedToLiteralBetween(code, variable, srcLine, sinkLine) {
38002
38663
  return true;
38003
38664
  if (!reNaked.test(line) && !reSwitchCase.test(line))
38004
38665
  continue;
38005
- if (switchAlsoAssignsNonLiteral(lines, i2, variable, strLit))
38666
+ if (siblingBranchAssignsNonLiteral(lines, i2, variable, strLit, unreachableLines))
38006
38667
  continue;
38007
38668
  return true;
38008
38669
  }
38009
38670
  return false;
38010
38671
  }
38011
- function switchAlsoAssignsNonLiteral(lines, idx, variable, strLit) {
38672
+ function enclosingBrace(lines, line, col) {
38012
38673
  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] === "}")
38674
+ for (let i2 = line;i2 >= 0; i2--) {
38675
+ const text = lines[i2];
38676
+ for (let c = i2 === line ? Math.min(col, text.length) - 1 : text.length - 1;c >= 0; c--) {
38677
+ if (text[c] === "}")
38018
38678
  depth++;
38019
- else if (line[c] === "{")
38679
+ else if (text[c] === "{") {
38680
+ if (depth === 0) {
38681
+ let header = text.slice(0, c).trim();
38682
+ let headerLine = i2;
38683
+ for (let h = i2 - 1;header === "" && h >= 0; h--) {
38684
+ header = lines[h].trim();
38685
+ headerLine = h;
38686
+ }
38687
+ return { line: i2, col: c, header, headerLine };
38688
+ }
38020
38689
  depth--;
38690
+ }
38021
38691
  }
38022
- if (depth < 0) {
38023
- if (/\bswitch\b/.test(line))
38024
- start2 = i2;
38025
- break;
38692
+ }
38693
+ return null;
38694
+ }
38695
+ function matchingClose(lines, line, col) {
38696
+ let depth = 0;
38697
+ for (let i2 = line;i2 < lines.length; i2++) {
38698
+ const text = lines[i2];
38699
+ for (let c = i2 === line ? col : 0;c < text.length; c++) {
38700
+ if (text[c] === "{")
38701
+ depth++;
38702
+ else if (text[c] === "}" && --depth === 0)
38703
+ return { line: i2, col: c };
38026
38704
  }
38027
38705
  }
38028
- if (start2 < 0)
38706
+ return null;
38707
+ }
38708
+ function siblingBranchAssignsNonLiteral(lines, idx, variable, strLit, unreachableLines) {
38709
+ let block = enclosingBrace(lines, idx, 0);
38710
+ if (block && /^(?:case\b.*|default\s*):$/.test(block.header)) {
38711
+ block = enclosingBrace(lines, block.line, block.col);
38712
+ }
38713
+ if (!block)
38714
+ return false;
38715
+ let first = block.headerLine;
38716
+ let last;
38717
+ const end = matchingClose(lines, block.line, block.col);
38718
+ if (!end)
38719
+ return false;
38720
+ last = end.line;
38721
+ if (/\bswitch\b/.test(block.header)) {} else if (/^(?:\}\s*)?else\b/.test(block.header) || /^(?:\}\s*)?(?:else\s+)?if\s*\(/.test(block.header)) {
38722
+ let cur = block;
38723
+ for (let guard = 0;guard < 64 && /^(?:\}\s*)?else\b/.test(cur.header); guard++) {
38724
+ let closeLine = cur.headerLine;
38725
+ let closeCol = lines[closeLine].indexOf("}");
38726
+ if (closeCol === -1 || closeCol > lines[closeLine].search(/\belse\b/)) {
38727
+ closeLine--;
38728
+ while (closeLine >= 0 && lines[closeLine].trim() === "")
38729
+ closeLine--;
38730
+ if (closeLine < 0 || !lines[closeLine].trimEnd().endsWith("}"))
38731
+ return false;
38732
+ closeCol = lines[closeLine].lastIndexOf("}");
38733
+ }
38734
+ const prev = enclosingBrace(lines, closeLine, closeCol);
38735
+ if (!prev)
38736
+ return false;
38737
+ cur = prev;
38738
+ first = prev.headerLine;
38739
+ }
38740
+ let close = end;
38741
+ for (let guard = 0;guard < 64; guard++) {
38742
+ let l = close.line;
38743
+ let rest = lines[l].slice(close.col + 1).trim();
38744
+ while (rest === "" && l + 1 < lines.length) {
38745
+ l++;
38746
+ rest = lines[l].trim();
38747
+ }
38748
+ if (!/^else\b/.test(rest))
38749
+ break;
38750
+ let openLine = l;
38751
+ let openCol = lines[l].indexOf("{", l === close.line ? close.col + 1 : 0);
38752
+ if (openCol === -1) {
38753
+ openLine = l + 1;
38754
+ while (openLine < lines.length && lines[openLine].trim() === "")
38755
+ openLine++;
38756
+ if (openLine >= lines.length || !lines[openLine].trimStart().startsWith("{"))
38757
+ break;
38758
+ openCol = lines[openLine].indexOf("{");
38759
+ }
38760
+ const next = matchingClose(lines, openLine, openCol);
38761
+ if (!next)
38762
+ break;
38763
+ close = next;
38764
+ last = next.line;
38765
+ }
38766
+ } else {
38029
38767
  return false;
38768
+ }
38030
38769
  const assign = new RegExp(`(?:^|[^\\w.$])${variable}\\s*(?::?=)(?!=)\\s*(.*?)\\s*;?\\s*(?:break\\s*;?)?\\s*$`);
38031
38770
  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;
38771
+ for (let i2 = first;i2 <= last; i2++) {
38772
+ if (i2 === idx || unreachableLines?.has(i2 + 1))
38773
+ continue;
38774
+ const m = assign.exec(lines[i2]);
38775
+ if (m && m[1] && !literalOnly.test(m[1]))
38776
+ return true;
38048
38777
  }
38049
38778
  return false;
38050
38779
  }
@@ -38172,6 +38901,22 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
38172
38901
  const goFnStart = (line) => enclosingStart(goFnRanges, line);
38173
38902
  const csFnRanges = methodRanges(["csharp"]);
38174
38903
  const csFnStart = (line) => enclosingStart(csFnRanges, line);
38904
+ const csFieldNames = new Set;
38905
+ if (language === "csharp") {
38906
+ for (const t of types ?? [])
38907
+ for (const f of t.fields ?? [])
38908
+ csFieldNames.add(f.name);
38909
+ }
38910
+ const csDifferentMethod = (variable, sourceLine, otherLine) => {
38911
+ if (csFnRanges.length === 0)
38912
+ return false;
38913
+ const a = csFnStart(sourceLine);
38914
+ const b = csFnStart(otherLine);
38915
+ if (a === -1 || b === -1 || a === b)
38916
+ return false;
38917
+ const base = variable.replace(/^this\./, "").split(".")[0];
38918
+ return !csFieldNames.has(base);
38919
+ };
38175
38920
  const sourcesWithVar = sources.filter((s) => typeof s.variable === "string" && s.variable.length > 0);
38176
38921
  const aliasSanitizedFor = new Map;
38177
38922
  if (language === "python" && typeof code === "string") {
@@ -38444,7 +39189,29 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
38444
39189
  }
38445
39190
  }
38446
39191
  }
38447
- if ((language === "java" || language === "csharp") && typeof code === "string" && sourcesWithVar.length > 0) {
39192
+ if (language === "csharp" && typeof code === "string" && sourcesWithVar.length > 0 && csFnRanges.length > 0) {
39193
+ const csMethods = (types ?? []).flatMap((t) => t.methods);
39194
+ const seeds = sourcesWithVar.map((sv) => ({ variable: sv.variable, line: sv.line }));
39195
+ const methodStartOf = (line) => csFnStart(line);
39196
+ const aliases = csharpMethodScopedAliases(code, seeds, csMethods, csFieldNames);
39197
+ const fileAnchor = sourcesWithVar.reduce((a, sv) => sv.line < a.line ? sv : a, sourcesWithVar[0]);
39198
+ const have = new Set(sourcesWithVar.map((sv) => `${sv.variable}@${methodStartOf(sv.line)}`));
39199
+ for (const alias of aliases) {
39200
+ const fn = methodStartOf(alias.line);
39201
+ const isField = csFieldNames.has(alias.variable);
39202
+ const key = `${alias.variable}@${isField ? "field" : fn}`;
39203
+ if (have.has(key))
39204
+ continue;
39205
+ have.add(key);
39206
+ const inMethod = sourcesWithVar.filter((sv) => methodStartOf(sv.line) === fn && sv.line <= alias.line);
39207
+ const from = !alias.carried && inMethod.length > 0 ? inMethod.reduce((a, sv) => sv.line < a.line ? sv : a, inMethod[0]) : fileAnchor;
39208
+ sourcesWithVar.push({
39209
+ ...from,
39210
+ variable: alias.variable,
39211
+ ...alias.carried || inMethod.length === 0 ? { line: alias.line } : {}
39212
+ });
39213
+ }
39214
+ } else if ((language === "java" || language === "csharp") && typeof code === "string" && sourcesWithVar.length > 0) {
38448
39215
  const seedVars = new Set(sourcesWithVar.map((s) => s.variable));
38449
39216
  const derived = buildJavaTaintedVars(code, seedVars);
38450
39217
  if (derived.size > 0) {
@@ -38493,23 +39260,26 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
38493
39260
  const lines = code.split(`
38494
39261
  `);
38495
39262
  const ctRe = /(\w+)\s*\.\s*CommandText\s*\+?=\s*(.+)/;
38496
- const anchor = sourcesWithVar.reduce((a, s) => s.line < a.line ? s : a, sourcesWithVar[0]);
39263
+ const seeded = new Set;
38497
39264
  let added = true;
38498
39265
  let guard = 0;
38499
39266
  while (added && guard < lines.length + 2) {
38500
39267
  added = false;
38501
39268
  guard++;
38502
- const known = new Set(sourcesWithVar.map((s) => s.variable));
38503
39269
  for (let i2 = 0;i2 < lines.length; i2++) {
38504
39270
  const m = ctRe.exec(lines[i2]);
38505
39271
  if (!m)
38506
39272
  continue;
38507
39273
  const [, obj, rhs] = m;
38508
- if (known.has(obj))
39274
+ const key = `${obj}@${csFnStart(i2 + 1)}`;
39275
+ if (seeded.has(key))
38509
39276
  continue;
38510
- if (!argsReferenceTaintedVar(rhs, sourcesWithVar))
39277
+ const reachable = sourcesWithVar.filter((sv) => sv.line <= i2 + 1 && !csDifferentMethod(sv.variable, sv.line, i2 + 1));
39278
+ if (reachable.length === 0 || !argsReferenceTaintedVar(rhs, reachable))
38511
39279
  continue;
38512
- sourcesWithVar.push({ ...anchor, variable: obj, line: i2 + 1 });
39280
+ const from = reachable.reduce((a, sv) => sv.line < a.line ? sv : a, reachable[0]);
39281
+ sourcesWithVar.push({ ...from, variable: obj, line: i2 + 1 });
39282
+ seeded.add(key);
38513
39283
  added = true;
38514
39284
  }
38515
39285
  }
@@ -38528,24 +39298,26 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
38528
39298
  if (builders.size > 0) {
38529
39299
  const appendRe = /([\p{L}_][\p{L}\p{N}_]*)\s*\.\s*(?:Append|AppendLine|AppendFormat|Insert)\s*\(([\s\S]*)/u;
38530
39300
  const anchor = sourcesWithVar.reduce((a, s) => s.line < a.line ? s : a, sourcesWithVar[0]);
39301
+ const builderSeeded = new Set;
38531
39302
  let added = true;
38532
39303
  let guard = 0;
38533
39304
  while (added && guard < lines.length + 2) {
38534
39305
  added = false;
38535
39306
  guard++;
38536
- const known = new Set(sourcesWithVar.map((s) => s.variable));
38537
39307
  for (let i2 = 0;i2 < lines.length; i2++) {
38538
39308
  const m = appendRe.exec(lines[i2]);
38539
39309
  if (!m)
38540
39310
  continue;
38541
39311
  const [, obj, args2] = m;
38542
- if (!builders.has(obj) || known.has(obj))
39312
+ const seedKey = `${obj}@${csFnStart(i2 + 1)}`;
39313
+ if (!builders.has(obj) || builderSeeded.has(seedKey))
38543
39314
  continue;
38544
39315
  const appendFn = csFnStart(i2 + 1);
38545
39316
  const reachable = sourcesWithVar.filter((sv) => appendFn === -1 || csFnStart(sv.line) === -1 || csFnStart(sv.line) === appendFn);
38546
39317
  if (reachable.length > 0 && argsReferenceTaintedVar(args2, reachable)) {
38547
39318
  sourcesWithVar.push({ ...anchor, variable: obj, line: i2 + 1 });
38548
39319
  builderSeeds.add(`${obj}@${i2 + 1}`);
39320
+ builderSeeded.add(seedKey);
38549
39321
  added = true;
38550
39322
  }
38551
39323
  }
@@ -38628,6 +39400,8 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
38628
39400
  if (srcFn !== sinkFn)
38629
39401
  continue;
38630
39402
  }
39403
+ if (csDifferentMethod(source.variable, source.line, sink.line))
39404
+ continue;
38631
39405
  if (complex) {
38632
39406
  const re = reCache.get(source.variable);
38633
39407
  if (!re || !re.test(expr))
@@ -38641,7 +39415,7 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
38641
39415
  if (aliasSanitizedFor.get(source.variable)?.has(sink.type)) {
38642
39416
  break;
38643
39417
  }
38644
- if (typeof code === "string" && isReassignedToLiteralBetween(code, source.variable, source.line, sink.line)) {
39418
+ if (typeof code === "string" && isReassignedToLiteralBetween(code, source.variable, source.line, sink.line, unreachableLines)) {
38645
39419
  break;
38646
39420
  }
38647
39421
  flows.push({
@@ -38948,7 +39722,14 @@ var CSHARP_NON_ESCAPE_METHODS = new Set([
38948
39722
  "TryAdd",
38949
39723
  "Insert",
38950
39724
  "Push",
38951
- "Enqueue"
39725
+ "Enqueue",
39726
+ "EscapeDataString",
39727
+ "EscapeUriString",
39728
+ "UrlEncode",
39729
+ "UrlPathEncode",
39730
+ "HtmlEncode",
39731
+ "HtmlAttributeEncode",
39732
+ "JavaScriptStringEncode"
38952
39733
  ]);
38953
39734
  function isCSharpNonEscapeCall(call) {
38954
39735
  const name2 = call.method_name;
@@ -39520,6 +40301,7 @@ class InterproceduralPass {
39520
40301
  }
39521
40302
  }
39522
40303
  const language = graph.ir.meta.language;
40304
+ const csCredit = language === "csharp" && typeof ctx.code === "string" ? new CSharpCredit(ctx.code, graph.ir.types.flatMap((t) => t.methods)) : null;
39523
40305
  if (sinks.length > 0) {
39524
40306
  const interProc = analyzeInterprocedural2(graph, sources, sinks, sanitizers, {
39525
40307
  taintedVariables: constProp.tainted
@@ -39552,6 +40334,9 @@ class InterproceduralPass {
39552
40334
  continue;
39553
40335
  if (sanitizerMethodNames.has(method.name))
39554
40336
  continue;
40337
+ if (language === "csharp" && typeof ctx.code === "string" && !csharpParamReachesSink(ctx.code, method, edge.taintedArgs, sink, csCredit)) {
40338
+ continue;
40339
+ }
39555
40340
  for (const source of sources) {
39556
40341
  if (source.line > edge.callLine)
39557
40342
  continue;
@@ -39693,9 +40478,59 @@ class InterproceduralPass {
39693
40478
  return !sanitizedSinkKeys.has(`${s.line}:${s.type}`);
39694
40479
  });
39695
40480
  }
40481
+ if (csCredit) {
40482
+ const csSeeds = sources.flatMap((s) => s.variable ? [{ variable: s.variable, line: s.line }] : []);
40483
+ const credited = new Set;
40484
+ filteredAdditionalFlows = filteredAdditionalFlows.filter((f) => {
40485
+ if (f.sink_type !== "external_taint_escape" || !csCredit.creditedOnLine(f.sink_line, csSeeds))
40486
+ return true;
40487
+ credited.add(f.sink_line);
40488
+ return false;
40489
+ });
40490
+ filteredAdditionalSinks = filteredAdditionalSinks.filter((s) => !(s.type === "external_taint_escape" && credited.has(s.line)));
40491
+ }
39696
40492
  return { additionalSinks: filteredAdditionalSinks, additionalFlows: filteredAdditionalFlows, interprocedural };
39697
40493
  }
39698
40494
  }
40495
+ function csharpParamReachesSink(code, method, taintedArgs, sink, credit) {
40496
+ const lines = code.split(/\r?\n/);
40497
+ const params = taintedArgs.length > 0 ? method.parameters.filter((p) => taintedArgs.includes(p.position)) : method.parameters;
40498
+ if (params.length === 0)
40499
+ return true;
40500
+ const derived = new Set(params.map((p) => p.name));
40501
+ const codeOf = (text) => text.replace(/\$@?"(?:[^"\\]|\\.)*"/g, (lit) => " " + [...lit.matchAll(/\{([^}]*)\}/g)].map((x) => x[1]).join(" ") + " ").replace(/@?"(?:[^"\\]|\\.)*"/g, " ").replace(/'(?:[^'\\]|\\.)'/g, " ");
40502
+ const mentionsDerived = (text) => {
40503
+ const bare = codeOf(text);
40504
+ return [...derived].filter((v) => new RegExp(`(?<![\\w.])${v}(?![\\w])`).test(bare));
40505
+ };
40506
+ const assignRe = /^\s*(?:var\s+|[A-Za-z_][\w.<>[\]?]*\s+)?([A-Za-z_]\w*)\s*=(?!=)\s*(.+)$/;
40507
+ const objectWriteRe = /^\s*([A-Za-z_]\w*)\s*\.\s*[A-Za-z_]\w*\s*(?:\+?=(?!=)|\()(.*)$/;
40508
+ for (let i2 = method.startLine + 1;i2 < sink.line; i2++) {
40509
+ const text = lines[i2 - 1] ?? "";
40510
+ const assign = assignRe.exec(text);
40511
+ if (assign) {
40512
+ if (mentionsDerived(assign[2]).length > 0)
40513
+ derived.add(assign[1]);
40514
+ else if (!params.some((p) => p.name === assign[1]))
40515
+ derived.delete(assign[1]);
40516
+ continue;
40517
+ }
40518
+ const write = objectWriteRe.exec(text);
40519
+ if (write && mentionsDerived(write[2]).length > 0)
40520
+ derived.add(write[1]);
40521
+ }
40522
+ let statement = lines[sink.line - 1] ?? "";
40523
+ for (let i2 = sink.line;i2 < lines.length && i2 < sink.line + 6 && !/[;{}]\s*$/.test(statement.trimEnd()); i2++) {
40524
+ statement += " " + (lines[i2] ?? "").trim();
40525
+ }
40526
+ const reaching = mentionsDerived(statement);
40527
+ if (reaching.length === 0)
40528
+ return false;
40529
+ if (!credit)
40530
+ return true;
40531
+ const safe = new Set([...credit.sanitizedVarsAt(sink.type, sink.line), ...credit.guardedVarsAt(sink.type, sink.line)]);
40532
+ return reaching.some((v) => !safe.has(v));
40533
+ }
39699
40534
 
39700
40535
  // ../circle-ir/dist/analysis/passes/dead-code-pass.js
39701
40536
  class DeadCodePass {
@@ -40408,7 +41243,7 @@ class NullDerefPass {
40408
41243
  }
40409
41244
 
40410
41245
  // ../circle-ir/dist/analysis/passes/resource-leak-pass.js
40411
- function escapeRegex2(s) {
41246
+ function escapeRegex3(s) {
40412
41247
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
40413
41248
  }
40414
41249
  var FACTORY_METHOD_NAME_RE = /^(?:open|create|new|get|make|build)[A-Z]/;
@@ -40599,7 +41434,7 @@ class ResourceLeakPass {
40599
41434
  return false;
40600
41435
  }
40601
41436
  isReturnedToCaller(lines, variable, fromLine, toLine) {
40602
- const varRe = new RegExp(`\\breturn\\b[^;]*\\b${escapeRegex2(variable)}\\b`);
41437
+ const varRe = new RegExp(`\\breturn\\b[^;]*\\b${escapeRegex3(variable)}\\b`);
40603
41438
  for (let l = fromLine;l <= toLine && l <= lines.length; l++) {
40604
41439
  if (varRe.test(lines[l - 1] ?? ""))
40605
41440
  return true;
@@ -40607,7 +41442,7 @@ class ResourceLeakPass {
40607
41442
  return false;
40608
41443
  }
40609
41444
  fieldStoredName(lines, variable, openLine, toLine) {
40610
- const thisAssignRe = new RegExp(`\\bthis\\s*\\.\\s*(\\w+)\\s*=\\s*${escapeRegex2(variable)}\\b`);
41445
+ const thisAssignRe = new RegExp(`\\bthis\\s*\\.\\s*(\\w+)\\s*=\\s*${escapeRegex3(variable)}\\b`);
40611
41446
  for (let l = openLine;l <= toLine && l <= lines.length; l++) {
40612
41447
  const m = thisAssignRe.exec(lines[l - 1] ?? "");
40613
41448
  if (m)
@@ -40650,7 +41485,7 @@ class ResourceLeakPass {
40650
41485
  if (fieldNames.has(variable)) {
40651
41486
  candidateField = variable;
40652
41487
  } else {
40653
- const thisAssignRe = new RegExp(`(?:\\bthis\\s*\\.\\s*)?(\\w+)\\s*=\\s*${escapeRegex2(variable)}\\b`);
41488
+ const thisAssignRe = new RegExp(`(?:\\bthis\\s*\\.\\s*)?(\\w+)\\s*=\\s*${escapeRegex3(variable)}\\b`);
40654
41489
  for (let l = fromLine;l <= toLine && l <= lines.length; l++) {
40655
41490
  const m = thisAssignRe.exec(lines[l - 1] ?? "");
40656
41491
  if (m && fieldNames.has(m[1])) {
@@ -51135,7 +51970,7 @@ var colors = {
51135
51970
  };
51136
51971
 
51137
51972
  // src/version.ts
51138
- var version = "4.12.0";
51973
+ var version = "4.13.1";
51139
51974
 
51140
51975
  // src/formatters.ts
51141
51976
  var LIBRARY_API_SURFACE_TAG2 = "library-api-surface:caller-responsibility";
@@ -51687,8 +52522,8 @@ function generateSarifResults(results, crossFileData) {
51687
52522
 
51688
52523
  // src/build-info.ts
51689
52524
  var buildInfo = {
51690
- gitSha: "8c284bc",
51691
- builtAt: "2026-10-08T00:42:12.776Z"
52525
+ gitSha: "51b2da7",
52526
+ builtAt: "2026-10-08T18:47:29.958Z"
51692
52527
  };
51693
52528
 
51694
52529
  // src/utils/args.ts