cognium-dev 4.11.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 +903 -60
  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;
@@ -9403,6 +9412,31 @@ function processStatement2(stmt, startDefId, startUseId, scopeStack, isJavaScrip
9403
9412
  }
9404
9413
  break;
9405
9414
  }
9415
+ case "switch_expression":
9416
+ case "switch_statement": {
9417
+ const value = stmt.childForFieldName("condition") ?? stmt.childForFieldName("value");
9418
+ if (value) {
9419
+ const valueUses = extractUses(value, useId, scopeStack, isJavaScript);
9420
+ uses.push(...valueUses.uses);
9421
+ useId = valueUses.nextId;
9422
+ }
9423
+ const body2 = stmt.childForFieldName("body");
9424
+ if (body2) {
9425
+ scopeStack.push(new Map);
9426
+ for (let i2 = 0;i2 < body2.childCount; i2++) {
9427
+ const group = body2.child(i2);
9428
+ if (!group)
9429
+ continue;
9430
+ const result = processBlock(group, defId, useId, scopeStack, isJavaScript);
9431
+ defs.push(...result.defs);
9432
+ uses.push(...result.uses);
9433
+ defId = result.nextDefId;
9434
+ useId = result.nextUseId;
9435
+ }
9436
+ scopeStack.pop();
9437
+ }
9438
+ break;
9439
+ }
9406
9440
  case "block":
9407
9441
  case "statement_block": {
9408
9442
  scopeStack.push(new Map);
@@ -11404,6 +11438,7 @@ var DEFAULT_SOURCES = [
11404
11438
  { method: "get", class: "ConcurrentHashMap", type: "plugin_param", severity: "high", return_tainted: true },
11405
11439
  { method: "get", class: "Hashtable", type: "plugin_param", severity: "high", return_tainted: true },
11406
11440
  { method: "get", class: "Properties", type: "config_param", severity: "high", return_tainted: true },
11441
+ { method: "getProperty", class: "Properties", type: "config_param", severity: "high", return_tainted: true },
11407
11442
  { method: "getText", class: "Message", type: "io_input", severity: "high", return_tainted: true },
11408
11443
  { method: "getPayload", type: "io_input", severity: "high", return_tainted: true },
11409
11444
  { method: "getData", type: "io_input", severity: "high", return_tainted: true },
@@ -11986,8 +12021,8 @@ var DEFAULT_SINKS = [
11986
12021
  { method: "write", class: "PrintWriter", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0] },
11987
12022
  { method: "println", class: "PrintWriter", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0] },
11988
12023
  { method: "print", class: "PrintWriter", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0] },
11989
- { method: "format", class: "PrintWriter", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0, 1] },
11990
- { method: "printf", class: "PrintWriter", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0, 1] },
12024
+ { method: "format", class: "PrintWriter", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0, 1, 2, 3, 4, 5, 6, 7] },
12025
+ { method: "printf", class: "PrintWriter", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0, 1, 2, 3, 4, 5, 6, 7] },
11991
12026
  { method: "write", class: "ServletOutputStream", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0] },
11992
12027
  { method: "print", class: "ServletOutputStream", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0] },
11993
12028
  { method: "println", class: "ServletOutputStream", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0] },
@@ -20139,14 +20174,21 @@ class ConstantPropagator {
20139
20174
  break;
20140
20175
  }
20141
20176
  }
20142
- 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");
20143
20185
  if (!switchBlock) {
20144
20186
  for (const child of node.children) {
20145
20187
  this.visit(child);
20146
20188
  }
20147
20189
  return;
20148
20190
  }
20149
- 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");
20150
20192
  if (switchValue && isKnown(switchValue)) {
20151
20193
  let matchingIdx = -1;
20152
20194
  let defaultIdx = -1;
@@ -20163,6 +20205,16 @@ class ConstantPropagator {
20163
20205
  matchingIdx = i2;
20164
20206
  }
20165
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
+ }
20166
20218
  }
20167
20219
  }
20168
20220
  }
@@ -20183,9 +20235,18 @@ class ConstantPropagator {
20183
20235
  }
20184
20236
  }
20185
20237
  } else {
20238
+ const taintedBefore = new Set(this.tainted);
20239
+ const taintedAfter = new Set(taintedBefore);
20240
+ const wasInConditional = this.inConditionalBranch;
20241
+ this.inConditionalBranch = true;
20186
20242
  for (const caseGroup of caseGroups) {
20243
+ this.tainted = new Set(taintedBefore);
20187
20244
  this.visit(caseGroup);
20245
+ for (const v of this.tainted)
20246
+ taintedAfter.add(v);
20188
20247
  }
20248
+ this.inConditionalBranch = wasInConditional;
20249
+ this.tainted = taintedAfter;
20189
20250
  }
20190
20251
  }
20191
20252
  handleTernary(node) {
@@ -20236,6 +20297,17 @@ class ConstantPropagator {
20236
20297
  }
20237
20298
  return false;
20238
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
+ }
20239
20311
  extractCaseValue(labelNode) {
20240
20312
  for (const child of labelNode.children) {
20241
20313
  if (child.type === "decimal_integer_literal") {
@@ -21260,7 +21332,11 @@ function annotationsInclude(annotations, targets) {
21260
21332
  return false;
21261
21333
  }
21262
21334
  function simpleTypeName(ref) {
21263
- return ref.replace(/<.*$/, "").trim();
21335
+ const name2 = ref.replace(/<.*$/, "").trim();
21336
+ return name2.substring(name2.lastIndexOf(".") + 1);
21337
+ }
21338
+ function isSupertypeLifecycleMethod(supertype, methodName) {
21339
+ return TIER_1_BY_SUPERTYPE.get(simpleTypeName(supertype))?.has(methodName) ?? false;
21264
21340
  }
21265
21341
  function looksLikeMainMethod(method) {
21266
21342
  if (method.name !== "main")
@@ -21822,7 +21898,7 @@ function applyRequireEntryPath(fileAnalyses, options = {}) {
21822
21898
  if (graph.methodsByKey.size === 0)
21823
21899
  return;
21824
21900
  const fileContext = buildFileContext(fileAnalyses);
21825
- const entryPointKeys = collectEntryPointKeys(graph, fileContext);
21901
+ const entryPointKeys = collectEntryPointKeys(graph, fileContext, indexTypesByName(fileAnalyses));
21826
21902
  const entryPointKeysByLanguage = countEntryPointKeysByLanguage(graph, entryPointKeys);
21827
21903
  const profileResolver = makeProfileResolver(options.projectProfile);
21828
21904
  for (const fa of fileAnalyses) {
@@ -21984,7 +22060,7 @@ function resolveCalleeKeys(call, methodsByKey, methodsByName) {
21984
22060
  }
21985
22061
  return candidates;
21986
22062
  }
21987
- function collectEntryPointKeys(graph, fileContext) {
22063
+ function collectEntryPointKeys(graph, fileContext, typesByName) {
21988
22064
  const entryPoints = new Set;
21989
22065
  for (const rec of graph.methodsByKey.values()) {
21990
22066
  const fileCtx = fileContext.get(rec.file);
@@ -21995,11 +22071,48 @@ function collectEntryPointKeys(graph, fileContext) {
21995
22071
  calls: fileCtx?.calls ?? null,
21996
22072
  runtimeRegistrations: fileCtx?.runtimeRegistrations ?? null
21997
22073
  });
21998
- if (tier === "TIER_1_ENTRY_POINT")
22074
+ if (tier === "TIER_1_ENTRY_POINT" || inheritsLifecycleEntry(rec.method.name, rec.enclosingType, typesByName)) {
21999
22075
  entryPoints.add(rec.key);
22076
+ }
22000
22077
  }
22001
22078
  return entryPoints;
22002
22079
  }
22080
+ function indexTypesByName(fileAnalyses) {
22081
+ const byName = new Map;
22082
+ for (const fa of fileAnalyses) {
22083
+ for (const t of fa.analysis.types ?? []) {
22084
+ const list = byName.get(t.name) ?? [];
22085
+ list.push(t);
22086
+ byName.set(t.name, list);
22087
+ }
22088
+ }
22089
+ return byName;
22090
+ }
22091
+ var simpleName = (ref) => {
22092
+ const name2 = ref.replace(/<.*$/, "").trim();
22093
+ return name2.substring(name2.lastIndexOf(".") + 1);
22094
+ };
22095
+ function inheritsLifecycleEntry(methodName, enclosingType, typesByName) {
22096
+ if (!methodName || !enclosingType?.extends)
22097
+ return false;
22098
+ const seen = new Set([enclosingType]);
22099
+ let frontier = typesByName.get(simpleName(enclosingType.extends)) ?? [];
22100
+ for (let depth = 0;depth < 8 && frontier.length > 0; depth++) {
22101
+ const next = [];
22102
+ for (const ancestor of frontier) {
22103
+ if (seen.has(ancestor))
22104
+ continue;
22105
+ seen.add(ancestor);
22106
+ const supers = [ancestor.extends, ...ancestor.implements ?? []].filter((x) => !!x);
22107
+ if (supers.some((sup) => isSupertypeLifecycleMethod(sup, methodName)))
22108
+ return true;
22109
+ if (ancestor.extends)
22110
+ next.push(...typesByName.get(simpleName(ancestor.extends)) ?? []);
22111
+ }
22112
+ frontier = next;
22113
+ }
22114
+ return false;
22115
+ }
22003
22116
  function buildFileContext(fileAnalyses) {
22004
22117
  const out2 = new Map;
22005
22118
  for (const fa of fileAnalyses) {
@@ -26307,7 +26420,8 @@ class LanguageSourcesPass {
26307
26420
  for (const finding of findJavaPatternFindings(code, graph.ir.meta.file)) {
26308
26421
  ctx.addFinding(finding);
26309
26422
  }
26310
- for (const finding of findJavaResponseWriterXssFindings(code, graph.ir.meta.file)) {
26423
+ const xssSinkLines = new Set(ctx.getResult("taint-matcher").sinks.filter((s) => s.type === "xss").map((s) => s.line));
26424
+ for (const finding of findJavaResponseWriterXssFindings(code, graph.ir.meta.file, xssSinkLines)) {
26311
26425
  ctx.addFinding(finding);
26312
26426
  }
26313
26427
  for (const finding of findJavaMongoNosqlInjectionFindings(code, graph.ir.meta.file)) {
@@ -30861,7 +30975,7 @@ function findGoXssFindings(code, file) {
30861
30975
  }
30862
30976
  return out2;
30863
30977
  }
30864
- function findJavaResponseWriterXssFindings(code, file) {
30978
+ function findJavaResponseWriterXssFindings(code, file, taintSinkLines = new Set) {
30865
30979
  const out2 = [];
30866
30980
  const lines = code.split(`
30867
30981
  `);
@@ -30897,6 +31011,8 @@ function findJavaResponseWriterXssFindings(code, file) {
30897
31011
  const m = trimmed.match(chainRe);
30898
31012
  if (!m)
30899
31013
  continue;
31014
+ if (taintSinkLines.has(i2 + 1))
31015
+ continue;
30900
31016
  const recv = m[1];
30901
31017
  if (!respNames.has(recv))
30902
31018
  continue;
@@ -33983,6 +34099,413 @@ class MyBatisAnnotationSqlSinkPass {
33983
34099
  }
33984
34100
  }
33985
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
+
33986
34509
  // ../circle-ir/dist/analysis/passes/sink-filter-pass.js
33987
34510
  function escapeReSf(s) {
33988
34511
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -34285,7 +34808,6 @@ function jsSsrfHostGuardedLines(code) {
34285
34808
  }
34286
34809
  return covered;
34287
34810
  }
34288
- 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*\)/;
34289
34811
  function isUnwrittenLocalMapKeyRead(code, source) {
34290
34812
  if (source.type !== "plugin_param")
34291
34813
  return false;
@@ -34312,15 +34834,6 @@ function isUnwrittenLocalMapKeyRead(code, source) {
34312
34834
  return false;
34313
34835
  return true;
34314
34836
  }
34315
- var CSHARP_SANITIZER_RES = [
34316
- { re: /\b(?:HtmlEncode|JavaScriptStringEncode)\s*\(/, type: "xss" },
34317
- { re: /\bHtmlEncoder\s*\.\s*Encode\s*\(/, type: "xss" },
34318
- { re: /\bPath\s*\.\s*GetFileName\s*\(/, type: "path_traversal" },
34319
- { re: CSHARP_ALLOWLIST_STRIP_RE, type: "ldap_injection" },
34320
- { re: CSHARP_ALLOWLIST_STRIP_RE, type: "xpath_injection" },
34321
- { re: CSHARP_ALLOWLIST_STRIP_RE, type: "command_injection" },
34322
- { re: CSHARP_ALLOWLIST_STRIP_RE, type: "sql_injection" }
34323
- ];
34324
34837
  function csharpSanitizedVarsByType(code) {
34325
34838
  const lines = code.split(`
34326
34839
  `);
@@ -34353,7 +34866,7 @@ function csharpSanitizedVarsByType(code) {
34353
34866
  for (const [type, set] of byType) {
34354
34867
  if (set.has(lhs))
34355
34868
  continue;
34356
- 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));
34357
34870
  if (refsSanitized) {
34358
34871
  set.add(lhs);
34359
34872
  changed = true;
@@ -35249,22 +35762,34 @@ class SinkFilterPass {
35249
35762
  const sourceLines = ctx.code.split(`
35250
35763
  `);
35251
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));
35252
35767
  const xxeHardened = /\bXmlResolver\s*=\s*null\b/.test(ctx.code) || /\bDtdProcessing\s*=\s*(?:DtdProcessing\s*\.\s*)?(?:Prohibit|Ignore)\b/.test(ctx.code);
35253
35768
  filtered = filtered.filter((sink) => {
35254
35769
  if (sink.type === "xxe" && xxeHardened)
35255
35770
  return false;
35256
35771
  const sinkLineText = sourceLines[sink.line - 1] ?? "";
35257
- for (const { re, type } of CSHARP_SANITIZER_RES) {
35258
- 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))
35259
35791
  return false;
35260
35792
  }
35261
- const sanitizedVars = sanitizedByType.get(sink.type);
35262
- if (sanitizedVars && sanitizedVars.size > 0) {
35263
- for (const v of sanitizedVars) {
35264
- if (new RegExp(`(?<![\\w])${escapeRegex(v)}(?![\\w])`).test(sinkLineText))
35265
- return false;
35266
- }
35267
- }
35268
35793
  return true;
35269
35794
  });
35270
35795
  }
@@ -35272,7 +35797,7 @@ class SinkFilterPass {
35272
35797
  const sourceLines = ctx.code.split(`
35273
35798
  `);
35274
35799
  const constBaseClients = new Set;
35275
- 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*\)/;
35276
35801
  for (const line of sourceLines) {
35277
35802
  const m = declRe.exec(line);
35278
35803
  if (m)
@@ -35286,14 +35811,22 @@ class SinkFilterPass {
35286
35811
  const text = sourceLines[sink.line - 1] ?? "";
35287
35812
  let onConstClient = false;
35288
35813
  for (const v of constBaseClients) {
35289
- if (new RegExp(`(?<![\\w])${escapeRegex(v)}\\s*\\.`).test(text)) {
35814
+ if (new RegExp(`(?<![\\w])${escapeRegex2(v)}\\s*\\.`).test(text)) {
35290
35815
  onConstClient = true;
35291
35816
  break;
35292
35817
  }
35293
35818
  }
35294
35819
  if (!onConstClient)
35295
35820
  return true;
35296
- 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
+ }
35297
35830
  if (!m)
35298
35831
  return true;
35299
35832
  const lit = m[1];
@@ -35329,7 +35862,7 @@ class SinkFilterPass {
35329
35862
  const wrappers = findPythonLdapStripWrappers(sourceLines);
35330
35863
  const allLdapSanitizers = [...PY_BUILTIN_LDAP_SANITIZERS, ...wrappers];
35331
35864
  if (allLdapSanitizers.length > 0) {
35332
- const ldapPatterns = allLdapSanitizers.map((name2) => new RegExp(`\\b${escapeRegex(name2)}\\s*\\(`));
35865
+ const ldapPatterns = allLdapSanitizers.map((name2) => new RegExp(`\\b${escapeRegex2(name2)}\\s*\\(`));
35333
35866
  filtered = filtered.filter((sink) => {
35334
35867
  if (sink.type !== "ldap_injection")
35335
35868
  return true;
@@ -35415,7 +35948,7 @@ class SinkFilterPass {
35415
35948
  return { sources, sinks: filtered, sanitizers };
35416
35949
  }
35417
35950
  }
35418
- function escapeRegex(s) {
35951
+ function escapeRegex2(s) {
35419
35952
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
35420
35953
  }
35421
35954
  var RESERVED_KEYWORDS = new Set([
@@ -35497,7 +36030,7 @@ function extractSourceIdentifiers(text) {
35497
36030
  return out2;
35498
36031
  }
35499
36032
  function isAssignedFromSanitizerPattern(sourceLines, sinkLine, varName, sanitizerPatterns, lookback = 30) {
35500
- const escapedVar = escapeRegex(varName);
36033
+ const escapedVar = escapeRegex2(varName);
35501
36034
  const assignRe = new RegExp(`(?:\\b(?:const|let|var|final|String)\\s+)?\\b${escapedVar}\\s*=\\s*(.+)`);
35502
36035
  const fnBoundaryRe = /^\s*(?:def\s+\w+\s*\(|function\s+\w+\s*\(|\w+\s*[:=]\s*(?:async\s+)?\(?[^)]*\)?\s*=>)/;
35503
36036
  const lo = Math.max(0, sinkLine - 1 - lookback);
@@ -37066,7 +37599,7 @@ class LibraryProfileXssGatePass {
37066
37599
 
37067
37600
  // ../circle-ir/dist/analysis/passes/taint-propagation-pass.js
37068
37601
  var CONTAINER_READ_SOURCES = new Set(["plugin_param", "config_param"]);
37069
- function makeConstPropVeto(constProp, graph, sources, code) {
37602
+ function makeConstPropVeto(constProp, graph, sources, code, language) {
37070
37603
  const lines = typeof code === "string" ? code.split(`
37071
37604
  `) : [];
37072
37605
  const selfReassign = new Map;
@@ -37103,6 +37636,8 @@ function makeConstPropVeto(constProp, graph, sources, code) {
37103
37636
  return false;
37104
37637
  if (r.reason !== "variable_not_tainted")
37105
37638
  return true;
37639
+ if (language === "csharp")
37640
+ return false;
37106
37641
  const lo = Math.min(sourceLine, sinkLine);
37107
37642
  let d = defAt(step.variable, step.line);
37108
37643
  let selfHops = 0;
@@ -37141,7 +37676,7 @@ class TaintPropagationPass {
37141
37676
  return { flows: [] };
37142
37677
  }
37143
37678
  const propagationResult = propagateTaint(graph, sources, sinks, sanitizers);
37144
- const isVetoed = makeConstPropVeto(constProp, graph, sources, ctx.code);
37679
+ const isVetoed = makeConstPropVeto(constProp, graph, sources, ctx.code, ctx.language);
37145
37680
  const verifiedFlows = propagationResult.flows.filter((flow) => {
37146
37681
  if (constProp.unreachableLines.has(flow.sink.line))
37147
37682
  return false;
@@ -37711,7 +38246,7 @@ function detectCollectionFlows(calls, sources, sinks, taintedVars, unreachableLi
37711
38246
  if (source.variable && source.variable !== varName && source.in_method && call.in_method && source.in_method !== call.in_method) {
37712
38247
  continue;
37713
38248
  }
37714
- if (typeof code === "string" && isReassignedToLiteralBetween(code, varName, source.line, sink.line)) {
38249
+ if (typeof code === "string" && isReassignedToLiteralBetween(code, varName, source.line, sink.line, unreachableLines)) {
37715
38250
  continue;
37716
38251
  }
37717
38252
  flows.push({
@@ -37751,7 +38286,7 @@ function detectCollectionFlows(calls, sources, sinks, taintedVars, unreachableLi
37751
38286
  if (source.variable && source.variable !== collectionVar && source.in_method && call.in_method && source.in_method !== call.in_method) {
37752
38287
  continue;
37753
38288
  }
37754
- if (typeof code === "string" && isReassignedToLiteralBetween(code, collectionVar, source.line, sink.line)) {
38289
+ if (typeof code === "string" && isReassignedToLiteralBetween(code, collectionVar, source.line, sink.line, unreachableLines)) {
37755
38290
  continue;
37756
38291
  }
37757
38292
  flows.push({
@@ -37870,7 +38405,7 @@ function detectParameterSinkFlows(types, calls, sources, sinks, unreachableLines
37870
38405
  if (paramSource) {
37871
38406
  const exists = flows.some((f) => f.source_line === paramSource.line && f.sink_line === sink.line && f.sink_type === sink.type);
37872
38407
  if (!exists) {
37873
- 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)) {
37874
38409
  continue;
37875
38410
  }
37876
38411
  flows.push({
@@ -37893,6 +38428,96 @@ function detectParameterSinkFlows(types, calls, sources, sinks, unreachableLines
37893
38428
  }
37894
38429
  return flows;
37895
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
+ }
37896
38521
  var _reassignSplitCode;
37897
38522
  var _reassignSplitLines;
37898
38523
  function splitLinesCached(code) {
@@ -37903,7 +38528,7 @@ function splitLinesCached(code) {
37903
38528
  `);
37904
38529
  return _reassignSplitLines;
37905
38530
  }
37906
- function isReassignedToLiteralBetween(code, variable, srcLine, sinkLine) {
38531
+ function isReassignedToLiteralBetween(code, variable, srcLine, sinkLine, unreachableLines) {
37907
38532
  if (!variable || sinkLine - srcLine < 2)
37908
38533
  return false;
37909
38534
  if (!/^[A-Za-z_][\w]*$/.test(variable))
@@ -37919,7 +38544,120 @@ function isReassignedToLiteralBetween(code, variable, srcLine, sinkLine) {
37919
38544
  const line = lines[i2];
37920
38545
  if (!line)
37921
38546
  continue;
37922
- if (reNaked.test(line) || reGuarded.test(line) || reSwitchCase.test(line))
38547
+ if (reGuarded.test(line))
38548
+ return true;
38549
+ if (!reNaked.test(line) && !reSwitchCase.test(line))
38550
+ continue;
38551
+ if (siblingBranchAssignsNonLiteral(lines, i2, variable, strLit, unreachableLines))
38552
+ continue;
38553
+ return true;
38554
+ }
38555
+ return false;
38556
+ }
38557
+ function enclosingBrace(lines, line, col) {
38558
+ let depth = 0;
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] === "}")
38563
+ depth++;
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
+ }
38574
+ depth--;
38575
+ }
38576
+ }
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 };
38589
+ }
38590
+ }
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)
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
+ }
38654
+ const assign = new RegExp(`(?:^|[^\\w.$])${variable}\\s*(?::?=)(?!=)\\s*(.*?)\\s*;?\\s*(?:break\\s*;?)?\\s*$`);
38655
+ const literalOnly = new RegExp(`^${strLit}$`);
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]))
37923
38661
  return true;
37924
38662
  }
37925
38663
  return false;
@@ -38048,6 +38786,22 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
38048
38786
  const goFnStart = (line) => enclosingStart(goFnRanges, line);
38049
38787
  const csFnRanges = methodRanges(["csharp"]);
38050
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
+ };
38051
38805
  const sourcesWithVar = sources.filter((s) => typeof s.variable === "string" && s.variable.length > 0);
38052
38806
  const aliasSanitizedFor = new Map;
38053
38807
  if (language === "python" && typeof code === "string") {
@@ -38320,7 +39074,29 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
38320
39074
  }
38321
39075
  }
38322
39076
  }
38323
- 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) {
38324
39100
  const seedVars = new Set(sourcesWithVar.map((s) => s.variable));
38325
39101
  const derived = buildJavaTaintedVars(code, seedVars);
38326
39102
  if (derived.size > 0) {
@@ -38369,23 +39145,26 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
38369
39145
  const lines = code.split(`
38370
39146
  `);
38371
39147
  const ctRe = /(\w+)\s*\.\s*CommandText\s*\+?=\s*(.+)/;
38372
- const anchor = sourcesWithVar.reduce((a, s) => s.line < a.line ? s : a, sourcesWithVar[0]);
39148
+ const seeded = new Set;
38373
39149
  let added = true;
38374
39150
  let guard = 0;
38375
39151
  while (added && guard < lines.length + 2) {
38376
39152
  added = false;
38377
39153
  guard++;
38378
- const known = new Set(sourcesWithVar.map((s) => s.variable));
38379
39154
  for (let i2 = 0;i2 < lines.length; i2++) {
38380
39155
  const m = ctRe.exec(lines[i2]);
38381
39156
  if (!m)
38382
39157
  continue;
38383
39158
  const [, obj, rhs] = m;
38384
- if (known.has(obj))
39159
+ const key = `${obj}@${csFnStart(i2 + 1)}`;
39160
+ if (seeded.has(key))
38385
39161
  continue;
38386
- 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))
38387
39164
  continue;
38388
- 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);
38389
39168
  added = true;
38390
39169
  }
38391
39170
  }
@@ -38404,24 +39183,26 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
38404
39183
  if (builders.size > 0) {
38405
39184
  const appendRe = /([\p{L}_][\p{L}\p{N}_]*)\s*\.\s*(?:Append|AppendLine|AppendFormat|Insert)\s*\(([\s\S]*)/u;
38406
39185
  const anchor = sourcesWithVar.reduce((a, s) => s.line < a.line ? s : a, sourcesWithVar[0]);
39186
+ const builderSeeded = new Set;
38407
39187
  let added = true;
38408
39188
  let guard = 0;
38409
39189
  while (added && guard < lines.length + 2) {
38410
39190
  added = false;
38411
39191
  guard++;
38412
- const known = new Set(sourcesWithVar.map((s) => s.variable));
38413
39192
  for (let i2 = 0;i2 < lines.length; i2++) {
38414
39193
  const m = appendRe.exec(lines[i2]);
38415
39194
  if (!m)
38416
39195
  continue;
38417
39196
  const [, obj, args2] = m;
38418
- if (!builders.has(obj) || known.has(obj))
39197
+ const seedKey = `${obj}@${csFnStart(i2 + 1)}`;
39198
+ if (!builders.has(obj) || builderSeeded.has(seedKey))
38419
39199
  continue;
38420
39200
  const appendFn = csFnStart(i2 + 1);
38421
39201
  const reachable = sourcesWithVar.filter((sv) => appendFn === -1 || csFnStart(sv.line) === -1 || csFnStart(sv.line) === appendFn);
38422
39202
  if (reachable.length > 0 && argsReferenceTaintedVar(args2, reachable)) {
38423
39203
  sourcesWithVar.push({ ...anchor, variable: obj, line: i2 + 1 });
38424
39204
  builderSeeds.add(`${obj}@${i2 + 1}`);
39205
+ builderSeeded.add(seedKey);
38425
39206
  added = true;
38426
39207
  }
38427
39208
  }
@@ -38504,6 +39285,8 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
38504
39285
  if (srcFn !== sinkFn)
38505
39286
  continue;
38506
39287
  }
39288
+ if (csDifferentMethod(source.variable, source.line, sink.line))
39289
+ continue;
38507
39290
  if (complex) {
38508
39291
  const re = reCache.get(source.variable);
38509
39292
  if (!re || !re.test(expr))
@@ -38517,7 +39300,7 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
38517
39300
  if (aliasSanitizedFor.get(source.variable)?.has(sink.type)) {
38518
39301
  break;
38519
39302
  }
38520
- 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)) {
38521
39304
  break;
38522
39305
  }
38523
39306
  flows.push({
@@ -38824,7 +39607,14 @@ var CSHARP_NON_ESCAPE_METHODS = new Set([
38824
39607
  "TryAdd",
38825
39608
  "Insert",
38826
39609
  "Push",
38827
- "Enqueue"
39610
+ "Enqueue",
39611
+ "EscapeDataString",
39612
+ "EscapeUriString",
39613
+ "UrlEncode",
39614
+ "UrlPathEncode",
39615
+ "HtmlEncode",
39616
+ "HtmlAttributeEncode",
39617
+ "JavaScriptStringEncode"
38828
39618
  ]);
38829
39619
  function isCSharpNonEscapeCall(call) {
38830
39620
  const name2 = call.method_name;
@@ -39396,6 +40186,7 @@ class InterproceduralPass {
39396
40186
  }
39397
40187
  }
39398
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;
39399
40190
  if (sinks.length > 0) {
39400
40191
  const interProc = analyzeInterprocedural2(graph, sources, sinks, sanitizers, {
39401
40192
  taintedVariables: constProp.tainted
@@ -39428,6 +40219,9 @@ class InterproceduralPass {
39428
40219
  continue;
39429
40220
  if (sanitizerMethodNames.has(method.name))
39430
40221
  continue;
40222
+ if (language === "csharp" && typeof ctx.code === "string" && !csharpParamReachesSink(ctx.code, method, edge.taintedArgs, sink, csCredit)) {
40223
+ continue;
40224
+ }
39431
40225
  for (const source of sources) {
39432
40226
  if (source.line > edge.callLine)
39433
40227
  continue;
@@ -39569,9 +40363,58 @@ class InterproceduralPass {
39569
40363
  return !sanitizedSinkKeys.has(`${s.line}:${s.type}`);
39570
40364
  });
39571
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
+ }
39572
40376
  return { additionalSinks: filteredAdditionalSinks, additionalFlows: filteredAdditionalFlows, interprocedural };
39573
40377
  }
39574
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
+ }
39575
40418
 
39576
40419
  // ../circle-ir/dist/analysis/passes/dead-code-pass.js
39577
40420
  class DeadCodePass {
@@ -40284,7 +41127,7 @@ class NullDerefPass {
40284
41127
  }
40285
41128
 
40286
41129
  // ../circle-ir/dist/analysis/passes/resource-leak-pass.js
40287
- function escapeRegex2(s) {
41130
+ function escapeRegex3(s) {
40288
41131
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
40289
41132
  }
40290
41133
  var FACTORY_METHOD_NAME_RE = /^(?:open|create|new|get|make|build)[A-Z]/;
@@ -40475,7 +41318,7 @@ class ResourceLeakPass {
40475
41318
  return false;
40476
41319
  }
40477
41320
  isReturnedToCaller(lines, variable, fromLine, toLine) {
40478
- const varRe = new RegExp(`\\breturn\\b[^;]*\\b${escapeRegex2(variable)}\\b`);
41321
+ const varRe = new RegExp(`\\breturn\\b[^;]*\\b${escapeRegex3(variable)}\\b`);
40479
41322
  for (let l = fromLine;l <= toLine && l <= lines.length; l++) {
40480
41323
  if (varRe.test(lines[l - 1] ?? ""))
40481
41324
  return true;
@@ -40483,7 +41326,7 @@ class ResourceLeakPass {
40483
41326
  return false;
40484
41327
  }
40485
41328
  fieldStoredName(lines, variable, openLine, toLine) {
40486
- 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`);
40487
41330
  for (let l = openLine;l <= toLine && l <= lines.length; l++) {
40488
41331
  const m = thisAssignRe.exec(lines[l - 1] ?? "");
40489
41332
  if (m)
@@ -40526,7 +41369,7 @@ class ResourceLeakPass {
40526
41369
  if (fieldNames.has(variable)) {
40527
41370
  candidateField = variable;
40528
41371
  } else {
40529
- 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`);
40530
41373
  for (let l = fromLine;l <= toLine && l <= lines.length; l++) {
40531
41374
  const m = thisAssignRe.exec(lines[l - 1] ?? "");
40532
41375
  if (m && fieldNames.has(m[1])) {
@@ -51011,7 +51854,7 @@ var colors = {
51011
51854
  };
51012
51855
 
51013
51856
  // src/version.ts
51014
- var version = "4.11.0";
51857
+ var version = "4.13.0";
51015
51858
 
51016
51859
  // src/formatters.ts
51017
51860
  var LIBRARY_API_SURFACE_TAG2 = "library-api-surface:caller-responsibility";
@@ -51563,8 +52406,8 @@ function generateSarifResults(results, crossFileData) {
51563
52406
 
51564
52407
  // src/build-info.ts
51565
52408
  var buildInfo = {
51566
- gitSha: "165e82d",
51567
- builtAt: "2026-10-07T23:49:13.604Z"
52409
+ gitSha: "a45b21b",
52410
+ builtAt: "2026-10-08T13:38:40.827Z"
51568
52411
  };
51569
52412
 
51570
52413
  // src/utils/args.ts