circle-ir 3.181.0 → 3.185.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.
@@ -10020,6 +10020,10 @@ function processGoBlock(node, defs, uses, scopeStack, counters) {
10020
10020
  } else if (child.type === "for_statement") {
10021
10021
  const rangeClause = findChildByTypeGo(child, "range_clause");
10022
10022
  if (rangeClause) {
10023
+ const right = rangeClause.childForFieldName("right");
10024
+ if (right) {
10025
+ extractGoUses(right, uses, scopeStack);
10026
+ }
10023
10027
  const left = rangeClause.childForFieldName("left");
10024
10028
  if (left) {
10025
10029
  extractGoLhsDefs(left, defs, scopeStack, child.startPosition.row + 1);
@@ -10027,6 +10031,7 @@ function processGoBlock(node, defs, uses, scopeStack, counters) {
10027
10031
  }
10028
10032
  } else if (child.type === "call_expression") {
10029
10033
  extractGoUses(child, uses, scopeStack);
10034
+ recordGoOpaqueCodecDestDef(child, defs, scopeStack);
10030
10035
  } else if (child.type === "return_statement") {
10031
10036
  for (let i2 = 0; i2 < child.childCount; i2++) {
10032
10037
  const expr = child.child(i2);
@@ -10119,6 +10124,63 @@ function findChildByTypeGo(node, type) {
10119
10124
  }
10120
10125
  return null;
10121
10126
  }
10127
+ var GO_OPAQUE_CODEC_METHODS = /* @__PURE__ */ new Set([
10128
+ "Unmarshal",
10129
+ // json/xml/yaml/toml/gob
10130
+ "Decode",
10131
+ // json.NewDecoder(r).Decode(&dest), gob.Decoder.Decode
10132
+ "NewDecoder",
10133
+ // wrapper; handled via chained Decode above
10134
+ "UnmarshalYAML",
10135
+ "UnmarshalJSON",
10136
+ "UnmarshalText",
10137
+ "UnmarshalBinary"
10138
+ ]);
10139
+ function recordGoOpaqueCodecDestDef(call, defs, scopeStack) {
10140
+ const fn = call.childForFieldName("function");
10141
+ if (!fn || fn.type !== "selector_expression") return;
10142
+ const fieldNode = fn.childForFieldName("field");
10143
+ if (!fieldNode) return;
10144
+ const method = getNodeText(fieldNode);
10145
+ if (!GO_OPAQUE_CODEC_METHODS.has(method)) return;
10146
+ const argsNode = call.childForFieldName("arguments");
10147
+ if (!argsNode) return;
10148
+ const args2 = [];
10149
+ for (let i2 = 0; i2 < argsNode.childCount; i2++) {
10150
+ const c = argsNode.child(i2);
10151
+ if (!c) continue;
10152
+ if (c.type === "," || c.type === "(" || c.type === ")") continue;
10153
+ args2.push(c);
10154
+ }
10155
+ if (args2.length === 0) return;
10156
+ const destArg = args2.length >= 2 ? args2[1] : args2[0];
10157
+ const destName = extractGoAddressableVarName(destArg);
10158
+ if (!destName || destName === "_") return;
10159
+ const line = call.startPosition.row + 1;
10160
+ const def = {
10161
+ id: defs.length + 1,
10162
+ variable: destName,
10163
+ kind: "local",
10164
+ line
10165
+ };
10166
+ defs.push(def);
10167
+ currentScope(scopeStack).set(destName, def.id);
10168
+ }
10169
+ function extractGoAddressableVarName(node) {
10170
+ if (node.type === "unary_expression") {
10171
+ const operand = node.childForFieldName("operand") ?? node.child(1) ?? null;
10172
+ if (operand) return extractGoAddressableVarName(operand);
10173
+ return null;
10174
+ }
10175
+ if (node.type === "identifier") {
10176
+ return getNodeText(node);
10177
+ }
10178
+ if (node.type === "selector_expression") {
10179
+ const field = node.childForFieldName("field");
10180
+ if (field) return getNodeText(field);
10181
+ }
10182
+ return null;
10183
+ }
10122
10184
 
10123
10185
  // src/core/extractors/runtime-registrations.ts
10124
10186
  var HTTP_VERB_METHODS = /* @__PURE__ */ new Set([
@@ -11264,6 +11326,49 @@ var DEFAULT_SOURCES = [
11264
11326
  // Server-side interceptor receives `Metadata headers`; `headers.get(KEY)`
11265
11327
  // returns caller-supplied header values.
11266
11328
  { method: "get", class: "Metadata", type: "http_header", severity: "high", return_tainted: true, languages: ["java"] },
11329
+ // --- WebSocket transport channels (cognium-dev #213 second slice) ---
11330
+ //
11331
+ // Server-side WebSocket handlers receive attacker-authored frames on
11332
+ // every call to a receive-shaped method. Untrusted the moment they
11333
+ // return, regardless of any application-level auth on the socket.
11334
+ //
11335
+ // Python — FastAPI / Starlette (`from fastapi import WebSocket`):
11336
+ // data = await websocket.receive_text()
11337
+ // data = await websocket.receive_bytes()
11338
+ // data = await websocket.receive_json() # parsed dict/list
11339
+ // data = await websocket.receive() # {'type', 'text'|'bytes'}
11340
+ { method: "receive_text", class: "WebSocket", type: "network_input", severity: "high", return_tainted: true, languages: ["python"] },
11341
+ { method: "receive_bytes", class: "WebSocket", type: "network_input", severity: "high", return_tainted: true, languages: ["python"] },
11342
+ { method: "receive_json", class: "WebSocket", type: "http_body", severity: "high", return_tainted: true, languages: ["python"] },
11343
+ //
11344
+ // `receive` intentionally class-scoped to WebSocket (Starlette pattern).
11345
+ // Broadening to unqualified would collide with queue/signal `.receive()`.
11346
+ { method: "receive", class: "WebSocket", type: "network_input", severity: "high", return_tainted: true, languages: ["python"] },
11347
+ // Django Channels `AsyncJsonWebsocketConsumer` / `WebsocketConsumer`
11348
+ // expose the same receive_* names on `self`; the class filter matches
11349
+ // `WebSocket` only, so add unqualified fallbacks for the receive_json /
11350
+ // receive_text convention (broad — no class filter possible without
11351
+ // hardcoding Django's consumer names).
11352
+ { method: "receive_json", type: "http_body", severity: "high", return_tainted: true, languages: ["python"] },
11353
+ { method: "receive_text", type: "network_input", severity: "high", return_tainted: true, languages: ["python"] },
11354
+ { method: "receive_bytes", type: "network_input", severity: "high", return_tainted: true, languages: ["python"] },
11355
+ // Go — gorilla/websocket (`github.com/gorilla/websocket`):
11356
+ // messageType, message, err := conn.ReadMessage()
11357
+ // _, r, err := conn.NextReader()
11358
+ { method: "ReadMessage", class: "Conn", type: "network_input", severity: "high", return_tainted: true, languages: ["go"] },
11359
+ { method: "NextReader", class: "Conn", type: "network_input", severity: "high", return_tainted: true, languages: ["go"] },
11360
+ // nhooyr.io/websocket exposes a `Read` method on `*websocket.Conn`;
11361
+ // signature is `func (c *Conn) Read(ctx) (MessageType, []byte, error)`.
11362
+ { method: "Read", class: "Conn", type: "network_input", severity: "high", return_tainted: true, languages: ["go"] },
11363
+ // Java — Jakarta / Java WebSocket API (`javax.websocket` / `jakarta.websocket`):
11364
+ // session.getBasicRemote().sendText(input); // OUT (not a source)
11365
+ // @OnMessage public void onMessage(String msg) — `msg` is a callback
11366
+ // param, not a return; handled via param_tainted when annotation is set.
11367
+ { annotation: "OnMessage", type: "network_input", severity: "high", param_tainted: true, languages: ["java"] },
11368
+ // Spring `@MessageMapping` STOMP-over-WebSocket handlers receive
11369
+ // untrusted payloads on every dispatched frame.
11370
+ { annotation: "MessageMapping", type: "http_body", severity: "high", param_tainted: true, languages: ["java"] },
11371
+ { annotation: "SubscribeMapping", type: "http_body", severity: "high", param_tainted: true, languages: ["java"] },
11267
11372
  // --- Cache reads (second-order taint — Redis / Memcached / Django cache) ---
11268
11373
  // The cache round-trip is a canonical second-order sink: whatever was
11269
11374
  // written previously (potentially attacker-controlled) resurfaces on read.
@@ -13336,6 +13441,60 @@ var DEFAULT_SANITIZERS = [
13336
13441
  // Python Type coercion
13337
13442
  { method: "int", removes: ["sql_injection", "command_injection", "xss"] },
13338
13443
  { method: "float", removes: ["sql_injection", "command_injection"] },
13444
+ // Python URL encoding (cognium-dev #213 fifth slice).
13445
+ //
13446
+ // urllib.parse.quote(x) — RFC-3986 percent-encode; safe for URL path
13447
+ // urllib.parse.quote_plus(x) — same + space→+; safe for query string
13448
+ // urllib.parse.urlencode(d) — encode a dict of pairs
13449
+ //
13450
+ // Bounded to URL-context sinks. Class-scoped to the specific
13451
+ // `urllib.parse` receiver to avoid colliding with unrelated bare
13452
+ // `quote(...)` calls in other libraries.
13453
+ { method: "quote", class: "urllib.parse", removes: ["ssrf", "open_redirect", "xss", "path_traversal"] },
13454
+ { method: "quote_plus", class: "urllib.parse", removes: ["ssrf", "open_redirect", "xss", "path_traversal"] },
13455
+ { method: "urlencode", class: "urllib.parse", removes: ["ssrf", "open_redirect", "xss"] },
13456
+ // Bare aliases — `from urllib.parse import quote` then unqualified.
13457
+ { method: "quote_plus", removes: ["ssrf", "open_redirect", "xss", "path_traversal"] },
13458
+ { method: "urlencode", removes: ["ssrf", "open_redirect", "xss"] },
13459
+ // `quote` bare is intentionally NOT registered — it collides with
13460
+ // shlex.quote (bare-imported) which is a command_injection sanitizer,
13461
+ // not a URL sanitizer. The class-scoped variant above catches the
13462
+ // qualified `urllib.parse.quote(...)` shape; unqualified callers who
13463
+ // want URL-context credit should use `quote_plus` (which does not
13464
+ // collide with any command_injection sanitizer).
13465
+ // Python XSS — additional common sanitizers.
13466
+ //
13467
+ // bleach.clean(...) — already covered above (line 2898)
13468
+ // bleach.linkify(...) — turns URLs into anchor tags; sanitizes as well
13469
+ // django.utils.html.escape / strip_tags — Django's XSS escape helpers
13470
+ // jinja2.escape — Jinja2's Markup escape (aliased from markupsafe)
13471
+ // flask.escape — Flask re-export of markupsafe.escape
13472
+ // saxutils.escape — stdlib xml.sax.saxutils.escape for XML docs
13473
+ { method: "linkify", class: "bleach", removes: ["xss"] },
13474
+ // Bare `linkify(...)` alias — `from bleach import linkify`.
13475
+ { method: "linkify", removes: ["xss"] },
13476
+ { method: "escape", class: "django.utils.html", removes: ["xss"] },
13477
+ { method: "strip_tags", class: "django.utils.html", removes: ["xss"] },
13478
+ { method: "escape", class: "jinja2", removes: ["xss"] },
13479
+ { method: "escape", class: "flask", removes: ["xss"] },
13480
+ { method: "escape", class: "saxutils", removes: ["xss"] },
13481
+ { method: "escape", class: "xml.sax.saxutils", removes: ["xss"] },
13482
+ { method: "quoteattr", class: "saxutils", removes: ["xss"] },
13483
+ { method: "quoteattr", class: "xml.sax.saxutils", removes: ["xss"] },
13484
+ // Python ReDoS — `re.escape(user)` when building a regex from user input
13485
+ // strips regex metacharacters. Downstream `re.compile / re.match` cannot
13486
+ // interpret user-supplied alternations or quantifiers. Also covers the
13487
+ // `code_injection` categorization that `re.compile` currently emits
13488
+ // (a re.escape-wrapped pattern cannot execute anything, so both are safe).
13489
+ { method: "escape", class: "re", removes: ["redos", "code_injection"] },
13490
+ // Python SQLAlchemy — `text(...).bindparams(...)` binds params safely.
13491
+ // The `bindparams` call is the sanitizer; the parent `text` wraps the
13492
+ // template. Also add `expression.literal` for explicit SQL literals.
13493
+ { method: "bindparams", removes: ["sql_injection"] },
13494
+ // psycopg2 sql-composition helpers
13495
+ { method: "Identifier", class: "sql", removes: ["sql_injection"] },
13496
+ { method: "Literal", class: "sql", removes: ["sql_injection"] },
13497
+ { method: "Placeholder", class: "sql", removes: ["sql_injection"] },
13339
13498
  // =========================================================================
13340
13499
  // Rust Sanitizers
13341
13500
  // =========================================================================
@@ -15259,7 +15418,14 @@ function matchesSanitizerPattern(call, pattern) {
15259
15418
  return false;
15260
15419
  }
15261
15420
  if (pattern.class) {
15262
- if (!call.receiver || !receiverMightBeClass(call.receiver, pattern.class)) {
15421
+ if (!call.receiver) {
15422
+ const target = call.resolution?.target;
15423
+ const expectedTail = `${pattern.class}.${pattern.method}`;
15424
+ if (target && (target === expectedTail || target.endsWith("." + expectedTail))) {
15425
+ } else {
15426
+ return false;
15427
+ }
15428
+ } else if (!receiverMightBeClass(call.receiver, pattern.class)) {
15263
15429
  return false;
15264
15430
  }
15265
15431
  }
@@ -17002,15 +17168,21 @@ function propagateTaint(graphOrDfg, callsOrSources, sourcesOrSinks, sinksOrSanit
17002
17168
  const callsAtSink = callsByLine.get(sink.line) ?? [];
17003
17169
  for (const call of callsAtSink) {
17004
17170
  for (const arg of call.arguments) {
17005
- if (arg.variable) {
17006
- if (sink.argPositions && sink.argPositions.length > 0) {
17007
- if (!sink.argPositions.includes(arg.position)) {
17008
- continue;
17009
- }
17171
+ if (sink.argPositions && sink.argPositions.length > 0) {
17172
+ if (!sink.argPositions.includes(arg.position)) {
17173
+ continue;
17010
17174
  }
17011
- for (const use of usesAtSink) {
17012
- if (use.variable === arg.variable && use.def_id !== null) {
17175
+ }
17176
+ const candidateUses = arg.variable ? usesAtSink.filter((u) => u.variable === arg.variable) : usesAtSink;
17177
+ {
17178
+ for (const use of candidateUses) {
17179
+ if (use.def_id !== null) {
17013
17180
  if (allTaintedDefIds.has(use.def_id)) {
17181
+ if (!arg.variable) {
17182
+ if (typeof arg.expression !== "string" || arg.expression.length === 0) continue;
17183
+ const re = new RegExp(`(?:^|[^A-Za-z0-9_$])${use.variable.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:[^A-Za-z0-9_$]|$)`);
17184
+ if (!re.test(arg.expression)) continue;
17185
+ }
17014
17186
  const taintInfo = taintByDefId.get(use.def_id);
17015
17187
  if (taintInfo) {
17016
17188
  const isSanitized = checkSanitized(
@@ -17048,7 +17220,15 @@ function propagateTaint(graphOrDfg, callsOrSources, sourcesOrSinks, sinksOrSanit
17048
17220
  }
17049
17221
  }
17050
17222
  }
17051
- return { taintedVars, flows, reachableSinks };
17223
+ const seen = /* @__PURE__ */ new Set();
17224
+ const deduped = [];
17225
+ for (const f of flows) {
17226
+ const key = `${f.source.line}|${f.sink.line}|${f.sink.type}|${f.path.map((p) => p.variable).join(">")}`;
17227
+ if (seen.has(key)) continue;
17228
+ seen.add(key);
17229
+ deduped.push(f);
17230
+ }
17231
+ return { taintedVars, flows: deduped, reachableSinks };
17052
17232
  }
17053
17233
  function findInitialTaint(sources, callsByLine, defsByLine) {
17054
17234
  const tainted = [];
@@ -25571,7 +25751,22 @@ var PYTHON_TAINTED_PATTERNS2 = [
25571
25751
  // configs/sources/python.json but was not in the forward-taint regex
25572
25752
  // registry, so `name = input()` was not added to pyTaintedVars. Closes
25573
25753
  // the deferred `getattr(obj, input())()` reflection-invocation shape.
25574
- { pattern: /\binput\s*\(/, type: "io_input" }
25754
+ { pattern: /\binput\s*\(/, type: "io_input" },
25755
+ // WebSocket transport channels (cognium-dev #213 second slice).
25756
+ // FastAPI / Starlette (`from fastapi import WebSocket`) and Django
25757
+ // Channels consumers both expose `receive_*` methods that return
25758
+ // untrusted frame payloads on every call. Registered in
25759
+ // config-loader.ts as return-tainted; the forward-taint regex here
25760
+ // enables `data = await websocket.receive_text()` → `data` tainted,
25761
+ // which the DFG-less Python path needs so downstream sinks that
25762
+ // consume `data` are flagged.
25763
+ //
25764
+ // Deliberately not adding a bare `.receive\s*\(` here — that would
25765
+ // match too many unrelated APIs (queue receive, signal receive, etc.)
25766
+ // and produce spurious sources on every `x = q.receive()` in the wild.
25767
+ { pattern: /\.receive_text\s*\(/, type: "network_input" },
25768
+ { pattern: /\.receive_bytes\s*\(/, type: "network_input" },
25769
+ { pattern: /\.receive_json\s*\(/, type: "http_body" }
25575
25770
  ];
25576
25771
  var LanguageSourcesPass = class {
25577
25772
  name = "language-sources";
@@ -26263,6 +26458,31 @@ function findJavaScriptAssignmentSources(sourceCode, language) {
26263
26458
  }
26264
26459
  }
26265
26460
  }
26461
+ sources.push(...findJavaScriptCallbackParamSources(sourceCode));
26462
+ return sources;
26463
+ }
26464
+ function findJavaScriptCallbackParamSources(sourceCode) {
26465
+ const sources = [];
26466
+ const CB_EVENT_RE = /\.on\s*\(\s*['"](?:message|text|binary)['"]\s*,\s*(?:async\s+)?(?:function\s*\*?\s*(?:\w+\s*)?\(\s*(\w+)|\(\s*(\w+)\s*(?:[,):]|$)|(\w+)\s*=>)/;
26467
+ const lines = sourceCode.split("\n");
26468
+ for (let i2 = 0; i2 < lines.length; i2++) {
26469
+ const line = lines[i2];
26470
+ const trimmed = line.trimStart();
26471
+ if (trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
26472
+ const m = line.match(CB_EVENT_RE);
26473
+ if (!m) continue;
26474
+ const paramName = m[1] ?? m[2] ?? m[3];
26475
+ if (!paramName) continue;
26476
+ if (paramName === "err" || paramName === "error") continue;
26477
+ sources.push({
26478
+ type: "network_input",
26479
+ location: `WebSocket .on(...) callback param '${paramName}' at line ${i2 + 1}`,
26480
+ severity: "high",
26481
+ line: i2 + 1,
26482
+ confidence: 0.95,
26483
+ variable: paramName
26484
+ });
26485
+ }
26266
26486
  return sources;
26267
26487
  }
26268
26488
  function findPythonAssignmentSources(sourceCode, language) {
@@ -26545,6 +26765,14 @@ function buildJavaScriptTaintedVars(sourceCode, language) {
26545
26765
  if (!["javascript", "typescript"].includes(language)) return /* @__PURE__ */ new Map();
26546
26766
  const tainted = /* @__PURE__ */ new Map();
26547
26767
  const lines = sourceCode.split("\n");
26768
+ const CB_EVENT_RE = /\.on\s*\(\s*['"](?:message|text|binary)['"]\s*,\s*(?:async\s+)?(?:function\s*\*?\s*(?:\w+\s*)?\(\s*(\w+)|\(\s*(\w+)\s*(?:[,):]|$)|(\w+)\s*=>)/;
26769
+ for (let i2 = 0; i2 < lines.length; i2++) {
26770
+ const m = lines[i2].match(CB_EVENT_RE);
26771
+ if (!m) continue;
26772
+ const paramName = m[1] ?? m[2] ?? m[3];
26773
+ if (!paramName || paramName === "err" || paramName === "error") continue;
26774
+ tainted.set(paramName, i2 + 1);
26775
+ }
26548
26776
  for (let i2 = 0; i2 < lines.length; i2++) {
26549
26777
  const line = lines[i2];
26550
26778
  const trimmed = line.trimStart();
@@ -26741,6 +26969,79 @@ function findBashTaintSources(sourceCode, dfg) {
26741
26969
  });
26742
26970
  }
26743
26971
  }
26972
+ const readMatch = /^read\s*\(/.test(trimmed) ? null : trimmed.match(/^read\b([^#(]*)$/);
26973
+ if (readMatch) {
26974
+ const argStr = readMatch[1].trim();
26975
+ const noRedirect = argStr.replace(/\s*<[^<].*$/, "").trim();
26976
+ const tokens = noRedirect.split(/\s+/);
26977
+ const varNames = [];
26978
+ for (let ti = 0; ti < tokens.length; ti++) {
26979
+ const tok = tokens[ti];
26980
+ if (!tok) continue;
26981
+ if (tok.startsWith("-")) {
26982
+ if (/^-[antpidu]$|^-N$/.test(tok)) ti++;
26983
+ continue;
26984
+ }
26985
+ if (/^[A-Za-z_][\w]*$/.test(tok)) varNames.push(tok);
26986
+ }
26987
+ if (varNames.length === 0) varNames.push("REPLY");
26988
+ for (const v of varNames) {
26989
+ const already = sources.some((s) => s.line === lineNumber && s.variable === v);
26990
+ if (already) continue;
26991
+ sources.push({
26992
+ type: "io_input",
26993
+ location: `read \u2192 $${v} (stdin)`,
26994
+ severity: "high",
26995
+ line: lineNumber,
26996
+ confidence: 0.9,
26997
+ variable: v
26998
+ });
26999
+ }
27000
+ }
27001
+ const mapfileMatch = /^(?:mapfile|readarray)\s*\(/.test(trimmed) ? null : trimmed.match(/^(?:mapfile|readarray)\b([^#(]*)$/);
27002
+ if (mapfileMatch) {
27003
+ const argStr = mapfileMatch[1].trim();
27004
+ const tokens = argStr.split(/\s+/);
27005
+ const varNames = [];
27006
+ for (let ti = 0; ti < tokens.length; ti++) {
27007
+ const tok = tokens[ti];
27008
+ if (!tok) continue;
27009
+ if (tok.startsWith("-")) {
27010
+ if (/^-[cCnOsud]$/.test(tok)) ti++;
27011
+ continue;
27012
+ }
27013
+ if (/^[A-Za-z_][\w]*$/.test(tok)) varNames.push(tok);
27014
+ }
27015
+ if (varNames.length === 0) varNames.push("MAPFILE");
27016
+ for (const v of varNames) {
27017
+ const already = sources.some((s) => s.line === lineNumber && s.variable === v);
27018
+ if (already) continue;
27019
+ sources.push({
27020
+ type: "io_input",
27021
+ location: `mapfile \u2192 $${v} (stdin array)`,
27022
+ severity: "high",
27023
+ line: lineNumber,
27024
+ confidence: 0.9,
27025
+ variable: v
27026
+ });
27027
+ }
27028
+ }
27029
+ const getoptsMatch = trimmed.match(/\bgetopts\s+["'][^"']+["']\s+(\w+)/);
27030
+ if (getoptsMatch) {
27031
+ const flagVar = getoptsMatch[1];
27032
+ for (const v of [flagVar, "OPTARG"]) {
27033
+ const already = sources.some((s) => s.line === lineNumber && s.variable === v);
27034
+ if (already) continue;
27035
+ sources.push({
27036
+ type: "io_input",
27037
+ location: `getopts \u2192 $${v} (CLI arg)`,
27038
+ severity: "high",
27039
+ line: lineNumber,
27040
+ confidence: 0.9,
27041
+ variable: v
27042
+ });
27043
+ }
27044
+ }
26744
27045
  const envRe = /\$([A-Z][A-Z0-9_]{2,})|\$\{([A-Z][A-Z0-9_]{2,})\}/g;
26745
27046
  let em;
26746
27047
  while ((em = envRe.exec(line)) !== null) {
@@ -33641,6 +33942,18 @@ var TaintPropagationPass = class {
33641
33942
  if (isFP) continue;
33642
33943
  pushIfNew(f);
33643
33944
  }
33945
+ if (ctx.language === "go" && typeof ctx.code === "string") {
33946
+ const goGlobalFlows = detectGoPackageGlobalFlows(
33947
+ ctx.code,
33948
+ calls,
33949
+ sources,
33950
+ sinks,
33951
+ constProp.unreachableLines
33952
+ ) ?? [];
33953
+ for (const f of goGlobalFlows) {
33954
+ pushIfNew(f);
33955
+ }
33956
+ }
33644
33957
  const sanitizedNames = constProp.sanitizedVars;
33645
33958
  let finalFlows = sanitizedNames.size === 0 ? flows : flows.filter((f) => {
33646
33959
  if (f.path.length === 0) return true;
@@ -33855,6 +34168,96 @@ function isInJavaSanitizedMethod(code, types, sinkLine, sinkType) {
33855
34168
  }
33856
34169
  return false;
33857
34170
  }
34171
+ function detectGoPackageGlobalFlows(code, _calls, sources, sinks, unreachableLines) {
34172
+ const flows = [];
34173
+ if (!code || sinks.length === 0) return flows;
34174
+ const lines = code.split("\n");
34175
+ const packageVars = /* @__PURE__ */ new Set();
34176
+ let inVarBlock = false;
34177
+ for (const line of lines) {
34178
+ const stripped = line.replace(/\s+$/, "");
34179
+ if (!stripped) continue;
34180
+ if (/^[ \t]/.test(line)) {
34181
+ if (inVarBlock) {
34182
+ const m = stripped.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s+/);
34183
+ if (m) packageVars.add(m[1]);
34184
+ if (/^\s*\)/.test(stripped)) inVarBlock = false;
34185
+ }
34186
+ continue;
34187
+ }
34188
+ if (/^var\s*\(/.test(stripped)) {
34189
+ inVarBlock = true;
34190
+ continue;
34191
+ }
34192
+ if (inVarBlock && /^\)/.test(stripped)) {
34193
+ inVarBlock = false;
34194
+ continue;
34195
+ }
34196
+ const varOne = stripped.match(/^var\s+([A-Za-z_][A-Za-z0-9_]*)\b/);
34197
+ if (varOne) packageVars.add(varOne[1]);
34198
+ }
34199
+ if (packageVars.size === 0) return flows;
34200
+ const sourceVarNames = /* @__PURE__ */ new Set();
34201
+ for (const s of sources) {
34202
+ if (typeof s.variable === "string" && s.variable.length > 0) {
34203
+ sourceVarNames.add(s.variable);
34204
+ }
34205
+ }
34206
+ const GO_SOURCE_RE = /\b(r\.(?:URL\.Query|Form|PostForm|Header|Cookie|Body)|mux\.Vars|c\.(?:Query|Param|PostForm|GetHeader|Cookie)|ctx\.(?:Query|Param|PostForm|GetHeader|Cookie))\b/;
34207
+ const writes = [];
34208
+ for (let i2 = 0; i2 < lines.length; i2++) {
34209
+ const line = lines[i2];
34210
+ if (unreachableLines.has(i2 + 1)) continue;
34211
+ if (/:=/.test(line)) continue;
34212
+ const m = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.+)$/);
34213
+ if (!m) continue;
34214
+ const [, lhs, rhs] = m;
34215
+ if (!packageVars.has(lhs)) continue;
34216
+ let taintedBy = null;
34217
+ if (GO_SOURCE_RE.test(rhs)) {
34218
+ const src = sources.find((s) => s.line === i2 + 1);
34219
+ taintedBy = src ? { line: src.line, type: src.type } : { line: i2 + 1, type: "http_param" };
34220
+ } else {
34221
+ for (const v of sourceVarNames) {
34222
+ const re = new RegExp(`(?<![A-Za-z0-9_$])${v.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![A-Za-z0-9_$])`);
34223
+ if (re.test(rhs)) {
34224
+ const src = sources.find((s) => s.variable === v);
34225
+ if (src) {
34226
+ taintedBy = { line: src.line, type: src.type };
34227
+ break;
34228
+ }
34229
+ }
34230
+ }
34231
+ }
34232
+ if (!taintedBy) continue;
34233
+ writes.push({ varName: lhs, line: i2 + 1, sourceLine: taintedBy.line, sourceType: taintedBy.type });
34234
+ }
34235
+ if (writes.length === 0) return flows;
34236
+ for (const sink of sinks) {
34237
+ if (unreachableLines.has(sink.line)) continue;
34238
+ const sinkLineText = lines[sink.line - 1] ?? "";
34239
+ if (!sinkLineText) continue;
34240
+ for (const w of writes) {
34241
+ if (w.line >= sink.line) continue;
34242
+ const re = new RegExp(`(?<![A-Za-z0-9_$])${w.varName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![A-Za-z0-9_$])`);
34243
+ if (!re.test(sinkLineText)) continue;
34244
+ flows.push({
34245
+ source_line: w.sourceLine,
34246
+ sink_line: sink.line,
34247
+ source_type: w.sourceType,
34248
+ sink_type: sink.type,
34249
+ path: [
34250
+ { variable: w.varName, line: w.sourceLine, type: "source" },
34251
+ { variable: w.varName, line: w.line, type: "assignment" },
34252
+ { variable: w.varName, line: sink.line, type: "sink" }
34253
+ ],
34254
+ confidence: 0.9,
34255
+ sanitized: false
34256
+ });
34257
+ }
34258
+ }
34259
+ return flows;
34260
+ }
33858
34261
  function detectCollectionFlows(calls, sources, sinks, taintedVars, unreachableLines, code, types) {
33859
34262
  const flows = [];
33860
34263
  const callsByLine = /* @__PURE__ */ new Map();