circle-ir 3.193.0 → 3.195.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.
- package/configs/README.md +39 -0
- package/dist/analysis/config-loader.d.ts.map +1 -1
- package/dist/analysis/config-loader.js +43 -3
- package/dist/analysis/config-loader.js.map +1 -1
- package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
- package/dist/analysis/passes/language-sources-pass.js +101 -0
- package/dist/analysis/passes/language-sources-pass.js.map +1 -1
- package/dist/analysis/passes/prompt-injection-safety-gate-pass.d.ts +75 -0
- package/dist/analysis/passes/prompt-injection-safety-gate-pass.d.ts.map +1 -0
- package/dist/analysis/passes/prompt-injection-safety-gate-pass.js +353 -0
- package/dist/analysis/passes/prompt-injection-safety-gate-pass.js.map +1 -0
- package/dist/analysis/passes/speculative-prompt-param-source-pass.d.ts +67 -0
- package/dist/analysis/passes/speculative-prompt-param-source-pass.d.ts.map +1 -0
- package/dist/analysis/passes/speculative-prompt-param-source-pass.js +193 -0
- package/dist/analysis/passes/speculative-prompt-param-source-pass.js.map +1 -0
- package/dist/analysis/taint-matcher.d.ts.map +1 -1
- package/dist/analysis/taint-matcher.js +24 -8
- package/dist/analysis/taint-matcher.js.map +1 -1
- package/dist/analyzer.d.ts +19 -0
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +30 -0
- package/dist/analyzer.js.map +1 -1
- package/dist/browser/circle-ir.js +587 -597
- package/dist/core/circle-ir-core.cjs +90 -15
- package/dist/core/circle-ir-core.js +90 -15
- package/dist/core/extractors/dfg.js +46 -2
- package/dist/core/extractors/dfg.js.map +1 -1
- package/dist/languages/plugins/bash.d.ts.map +1 -1
- package/dist/languages/plugins/bash.js +12 -16
- package/dist/languages/plugins/bash.js.map +1 -1
- package/dist/languages/plugins/go.d.ts +4 -0
- package/dist/languages/plugins/go.d.ts.map +1 -1
- package/dist/languages/plugins/go.js +16 -32
- package/dist/languages/plugins/go.js.map +1 -1
- package/dist/languages/plugins/java.d.ts +9 -1
- package/dist/languages/plugins/java.d.ts.map +1 -1
- package/dist/languages/plugins/java.js +16 -127
- package/dist/languages/plugins/java.js.map +1 -1
- package/dist/languages/plugins/javascript.d.ts +7 -0
- package/dist/languages/plugins/javascript.d.ts.map +1 -1
- package/dist/languages/plugins/javascript.js +10 -117
- package/dist/languages/plugins/javascript.js.map +1 -1
- package/dist/languages/plugins/python.d.ts +7 -0
- package/dist/languages/plugins/python.d.ts.map +1 -1
- package/dist/languages/plugins/python.js +24 -177
- package/dist/languages/plugins/python.js.map +1 -1
- package/dist/languages/plugins/rust.d.ts +9 -0
- package/dist/languages/plugins/rust.d.ts.map +1 -1
- package/dist/languages/plugins/rust.js +22 -110
- package/dist/languages/plugins/rust.js.map +1 -1
- package/dist/types/config.d.ts +15 -0
- package/dist/types/config.d.ts.map +1 -1
- package/package.json +3 -2
|
@@ -9981,6 +9981,7 @@ var GO_KEYWORDS = /* @__PURE__ */ new Set([
|
|
|
9981
9981
|
function buildGoDFG(tree) {
|
|
9982
9982
|
const defs = [];
|
|
9983
9983
|
const uses = [];
|
|
9984
|
+
const extraChains = [];
|
|
9984
9985
|
let defIdCounter = 1;
|
|
9985
9986
|
let useIdCounter = 1;
|
|
9986
9987
|
const scopeStack = [/* @__PURE__ */ new Map()];
|
|
@@ -10002,7 +10003,7 @@ function buildGoDFG(tree) {
|
|
|
10002
10003
|
}
|
|
10003
10004
|
const body2 = func2.childForFieldName("body");
|
|
10004
10005
|
if (body2) {
|
|
10005
|
-
processGoBlock(body2, defs, uses, scopeStack, { defId: defIdCounter, useId: useIdCounter });
|
|
10006
|
+
processGoBlock(body2, defs, uses, scopeStack, { defId: defIdCounter, useId: useIdCounter }, extraChains);
|
|
10006
10007
|
defIdCounter = defs.length + 1;
|
|
10007
10008
|
useIdCounter = uses.length + 1;
|
|
10008
10009
|
}
|
|
@@ -10017,6 +10018,15 @@ function buildGoDFG(tree) {
|
|
|
10017
10018
|
}
|
|
10018
10019
|
}
|
|
10019
10020
|
const chains = computeChains(defs, uses);
|
|
10021
|
+
const seen = new Set(chains.map((c) => `${c.from_def}|${c.to_def}|${c.via}`));
|
|
10022
|
+
for (const ch of extraChains) {
|
|
10023
|
+
const key = `${ch.from_def}|${ch.to_def}|${ch.via}`;
|
|
10024
|
+
if (!seen.has(key)) {
|
|
10025
|
+
seen.add(key);
|
|
10026
|
+
chains.push(ch);
|
|
10027
|
+
}
|
|
10028
|
+
}
|
|
10029
|
+
chains.sort((a, b) => a.from_def - b.from_def || a.to_def - b.to_def);
|
|
10020
10030
|
return { defs, uses, chains };
|
|
10021
10031
|
}
|
|
10022
10032
|
function extractGoParamDefs(params, defs, _startId, scopeStack) {
|
|
@@ -10041,28 +10051,47 @@ function extractGoParamDefs(params, defs, _startId, scopeStack) {
|
|
|
10041
10051
|
}
|
|
10042
10052
|
}
|
|
10043
10053
|
}
|
|
10044
|
-
function processGoBlock(node, defs, uses, scopeStack, counters) {
|
|
10054
|
+
function processGoBlock(node, defs, uses, scopeStack, counters, extraChains = []) {
|
|
10055
|
+
const linkMultiLineRhs = (right, useStart, useEnd, defStart, defEnd) => {
|
|
10056
|
+
if (right.endPosition.row <= right.startPosition.row) return;
|
|
10057
|
+
for (let ui = useStart; ui < useEnd; ui++) {
|
|
10058
|
+
const u = uses[ui];
|
|
10059
|
+
if (!u || u.def_id === null) continue;
|
|
10060
|
+
for (let di = defStart; di < defEnd; di++) {
|
|
10061
|
+
const d = defs[di];
|
|
10062
|
+
if (d && d.kind === "local" && d.id !== u.def_id) {
|
|
10063
|
+
extraChains.push({ from_def: u.def_id, to_def: d.id, via: u.variable });
|
|
10064
|
+
}
|
|
10065
|
+
}
|
|
10066
|
+
}
|
|
10067
|
+
};
|
|
10045
10068
|
walkTree(node, (child) => {
|
|
10046
10069
|
if (child.type === "short_var_declaration") {
|
|
10047
10070
|
const left = child.childForFieldName("left");
|
|
10048
10071
|
const right = child.childForFieldName("right");
|
|
10072
|
+
const usesBefore = uses.length;
|
|
10049
10073
|
if (right) {
|
|
10050
10074
|
extractGoUses(right, uses, scopeStack);
|
|
10051
10075
|
}
|
|
10076
|
+
const defsBefore = defs.length;
|
|
10052
10077
|
if (left) {
|
|
10053
10078
|
extractGoLhsDefs(left, defs, scopeStack, child.startPosition.row + 1);
|
|
10054
10079
|
}
|
|
10080
|
+
if (right) linkMultiLineRhs(right, usesBefore, uses.length, defsBefore, defs.length);
|
|
10055
10081
|
} else if (child.type === "var_declaration") {
|
|
10056
10082
|
processGoVarDecl(child, defs, scopeStack, counters);
|
|
10057
10083
|
} else if (child.type === "assignment_statement") {
|
|
10058
10084
|
const left = child.childForFieldName("left");
|
|
10059
10085
|
const right = child.childForFieldName("right");
|
|
10086
|
+
const usesBefore = uses.length;
|
|
10060
10087
|
if (right) {
|
|
10061
10088
|
extractGoUses(right, uses, scopeStack);
|
|
10062
10089
|
}
|
|
10090
|
+
const defsBefore = defs.length;
|
|
10063
10091
|
if (left) {
|
|
10064
10092
|
extractGoLhsDefs(left, defs, scopeStack, child.startPosition.row + 1);
|
|
10065
10093
|
}
|
|
10094
|
+
if (right) linkMultiLineRhs(right, usesBefore, uses.length, defsBefore, defs.length);
|
|
10066
10095
|
} else if (child.type === "for_statement") {
|
|
10067
10096
|
const rangeClause = findChildByTypeGo(child, "range_clause");
|
|
10068
10097
|
if (rangeClause) {
|
|
@@ -10726,7 +10755,11 @@ var DEFAULT_SOURCES = [
|
|
|
10726
10755
|
{ method: "Json", type: "http_body", severity: "high", return_tainted: true, languages: ["rust"] },
|
|
10727
10756
|
{ method: "Query", type: "http_param", severity: "high", return_tainted: true, languages: ["rust"] },
|
|
10728
10757
|
{ method: "Path", type: "http_path", severity: "high", return_tainted: true, languages: ["rust"] },
|
|
10729
|
-
|
|
10758
|
+
// `Form<T>` decodes the urlencoded request *body* (`Query<T>` above is the
|
|
10759
|
+
// query-string extractor), so it is http_body — retyped from http_param
|
|
10760
|
+
// when the RustPlugin's http_body duplicate was removed, so the two
|
|
10761
|
+
// surfaces no longer disagree. Matches Gin's `Context.PostForm`.
|
|
10762
|
+
{ method: "Form", type: "http_body", severity: "high", return_tainted: true, languages: ["rust"] },
|
|
10730
10763
|
// Rust std library
|
|
10731
10764
|
{ method: "var", class: "env", type: "env_input", severity: "medium", return_tainted: true },
|
|
10732
10765
|
{ method: "var_os", class: "env", type: "env_input", severity: "medium", return_tainted: true },
|
|
@@ -12004,7 +12037,11 @@ var DEFAULT_SINKS = [
|
|
|
12004
12037
|
// Also match without receiver (destructured imports: const { exec } = require('child_process'))
|
|
12005
12038
|
// `exec` is intentionally classless: catches Node.js child_process.exec AND
|
|
12006
12039
|
// Java Runtime.exec (via `r.exec()` where heuristic can't resolve r → Runtime).
|
|
12007
|
-
|
|
12040
|
+
// Excluded from Python: there, bare `exec(code)` is the code-execution
|
|
12041
|
+
// builtin, registered below as code_injection / CWE-94. Before the exclusion
|
|
12042
|
+
// a Python `exec(user_code)` matched both patterns and emitted two findings
|
|
12043
|
+
// for one call — CWE-78 (wrong: no shell is involved) and CWE-94 (right).
|
|
12044
|
+
{ method: "exec", type: "command_injection", cwe: "CWE-78", severity: "high", arg_positions: [0], exclude_languages: ["python"] },
|
|
12008
12045
|
// `execSync`/`spawn`/`spawnSync`/`execFile` are Node-specific — language-scope them.
|
|
12009
12046
|
{ method: "execSync", type: "command_injection", cwe: "CWE-78", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
|
|
12010
12047
|
{ method: "spawn", type: "command_injection", cwe: "CWE-78", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
|
|
@@ -12032,7 +12069,11 @@ var DEFAULT_SINKS = [
|
|
|
12032
12069
|
// Note: classless { method: 'query' } removed — too many FPs (UriComponentsBuilder.query(), etc.)
|
|
12033
12070
|
// SQL query calls are covered by class-specific patterns above (Connection, Pool, Client, JdbcTemplate)
|
|
12034
12071
|
// Note: `raw` is shared with Python (Django ORM) — scoped to JS+TS to avoid leaking.
|
|
12035
|
-
|
|
12072
|
+
// Severity `critical` (not `high`) preserves what the JS plugin's duplicate
|
|
12073
|
+
// builtin emitted before the #4 follow-up consolidation removed it: the
|
|
12074
|
+
// plugin copy carried `critical`, which won the `findSinks` dedup slot via
|
|
12075
|
+
// the +0.1 critical-severity confidence boost.
|
|
12076
|
+
{ method: "raw", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["javascript", "typescript"] },
|
|
12036
12077
|
// sqlite3 (npm) — Database/Statement methods. The JS plugin resolves
|
|
12037
12078
|
// `const db = new sqlite3.Database(...); db.all(sql)` to the resolution
|
|
12038
12079
|
// target `Connection.all`, so class-scoped patterns matching `Connection`
|
|
@@ -12591,10 +12632,30 @@ var DEFAULT_SINKS = [
|
|
|
12591
12632
|
{ method: "Sprintf", class: "fmt", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [0], languages: ["go"] },
|
|
12592
12633
|
{ method: "Printf", class: "fmt", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [0], languages: ["go"] },
|
|
12593
12634
|
{ method: "Errorf", class: "fmt", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [0], languages: ["go"] },
|
|
12635
|
+
// `fmt.Fprintf` is deliberately dual-classified: this CWE-134 entry and the
|
|
12636
|
+
// Go plugin's xss (CWE-79) entry both cover arg[1], so a tainted format
|
|
12637
|
+
// string emits two findings. Unlike the logger case above, neither is
|
|
12638
|
+
// redundant — CWE-134 holds for any writer, while CWE-79 is the actionable
|
|
12639
|
+
// finding when the writer is an http.ResponseWriter (`fmt.Fprintf(w, taint)`
|
|
12640
|
+
// is the standard Go reflected-XSS shape). Collapsing it requires deciding
|
|
12641
|
+
// by the *type of arg[0]*, which sink patterns cannot express today: the
|
|
12642
|
+
// options are a Go-specific gate (mirroring isSafeGoJsonUnmarshalCall) that
|
|
12643
|
+
// emits xss only for a resolved ResponseWriter, or dropping one CWE and
|
|
12644
|
+
// accepting either lost XSS recall or a mis-classified stderr write.
|
|
12645
|
+
// Left as-is pending the Go corpus run — see tasks.md.
|
|
12594
12646
|
{ method: "Fprintf", class: "fmt", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [1], languages: ["go"] },
|
|
12595
12647
|
// cognium-dev #264 — Go stdlib `log` package format-string entry points.
|
|
12596
12648
|
// log.Printf / Fatalf / Panicf take the format string at arg[0]; tainted
|
|
12597
12649
|
// format string reaches the same fmt.Sprintf machinery internally.
|
|
12650
|
+
//
|
|
12651
|
+
// These deliberately coexist with the Go plugin's log_injection (CWE-117)
|
|
12652
|
+
// entries for the same methods, so such a call emits both. The Logger
|
|
12653
|
+
// decision note in the Java log_injection section above does NOT apply
|
|
12654
|
+
// here: it reasons about SLF4J-style APIs where the format string is a
|
|
12655
|
+
// literal with `{}` placeholders, whereas Go's `log.Printf(fmt, ...)` takes
|
|
12656
|
+
// a real format string that an attacker can control. CWE-117 covers the
|
|
12657
|
+
// forged log line, CWE-134 the format string itself. Locked by
|
|
12658
|
+
// tests/analysis/passes/format-string-additions.test.ts.
|
|
12598
12659
|
{ method: "Printf", class: "log", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [0], languages: ["go"] },
|
|
12599
12660
|
{ method: "Fatalf", class: "log", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [0], languages: ["go"] },
|
|
12600
12661
|
{ method: "Panicf", class: "log", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [0], languages: ["go"] },
|
|
@@ -12608,6 +12669,14 @@ var DEFAULT_SINKS = [
|
|
|
12608
12669
|
{ method: "cookie", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1], languages: ["javascript", "typescript"] },
|
|
12609
12670
|
// Express: res.location(url) and res.redirect(url) — Location header.
|
|
12610
12671
|
{ method: "location", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [0], languages: ["javascript", "typescript"] },
|
|
12672
|
+
// `redirect` is deliberately dual-classified: this crlf (CWE-113) entry
|
|
12673
|
+
// coexists with the open_redirect (CWE-601) entry for js/ts, so a tainted
|
|
12674
|
+
// redirect target emits two flows. That is intentional, not duplication —
|
|
12675
|
+
// #189 Sprint 82 shipped a sink-type-aware flow dedup specifically so both
|
|
12676
|
+
// survive ("res.redirect is both open_redirect AND crlf"), and #132 added a
|
|
12677
|
+
// crlf recall test for the bare `res.redirect(req.query.url)` shape. Locked
|
|
12678
|
+
// by issue-189-sprint82-open-redirect-cluster.test.ts and
|
|
12679
|
+
// crlf-stage8-fp.test.ts — do not collapse without revisiting both tickets.
|
|
12611
12680
|
{ method: "redirect", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [0], languages: ["javascript", "typescript"] },
|
|
12612
12681
|
// Go net/http: w.Header().Set(k, v) / Add(k, v) — first arg is the value
|
|
12613
12682
|
// (Header is a map; the actual `value` is arg 1 of the call). We flag the
|
|
@@ -13433,18 +13502,28 @@ function analyzeTaint(calls, types, config = getDefaultConfig(), typeHierarchy,
|
|
|
13433
13502
|
const sanitizers = findSanitizers(calls, types, config.sanitizers, sourceLines);
|
|
13434
13503
|
return { sources, sinks, sanitizers };
|
|
13435
13504
|
}
|
|
13505
|
+
function sinkPatternAppliesTo(pattern, language) {
|
|
13506
|
+
if (language === void 0) return true;
|
|
13507
|
+
if (pattern.languages && pattern.languages.length > 0 && !pattern.languages.includes(language)) {
|
|
13508
|
+
return false;
|
|
13509
|
+
}
|
|
13510
|
+
if (pattern.exclude_languages && pattern.exclude_languages.includes(language)) {
|
|
13511
|
+
return false;
|
|
13512
|
+
}
|
|
13513
|
+
return true;
|
|
13514
|
+
}
|
|
13436
13515
|
function expandPromisifyAliases(patterns, sourceLines, language) {
|
|
13437
13516
|
if (!sourceLines || sourceLines.length === 0) return patterns;
|
|
13438
13517
|
if (language !== "javascript" && language !== "typescript") return patterns;
|
|
13439
13518
|
const PROMISIFY_RE = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:util\s*\.\s*)?promisify\s*\(\s*([A-Za-z_$][\w$]*)\s*\)/;
|
|
13440
13519
|
const innerToPattern = /* @__PURE__ */ new Map();
|
|
13441
13520
|
for (const p of patterns) {
|
|
13442
|
-
if (p
|
|
13521
|
+
if (!sinkPatternAppliesTo(p, language)) continue;
|
|
13443
13522
|
if (p.class !== void 0) continue;
|
|
13444
13523
|
if (!innerToPattern.has(p.method)) innerToPattern.set(p.method, p);
|
|
13445
13524
|
}
|
|
13446
13525
|
for (const p of patterns) {
|
|
13447
|
-
if (p
|
|
13526
|
+
if (!sinkPatternAppliesTo(p, language)) continue;
|
|
13448
13527
|
if (innerToPattern.has(p.method)) continue;
|
|
13449
13528
|
innerToPattern.set(p.method, p);
|
|
13450
13529
|
}
|
|
@@ -13478,7 +13557,7 @@ function expandIndirectEvalAliases(patterns, sourceLines, language) {
|
|
|
13478
13557
|
const INDIRECT_EVAL_RE = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*(?::[^=]+)?=\s*(eval|Function)\b(?!\s*\()/;
|
|
13479
13558
|
const innerToPattern = /* @__PURE__ */ new Map();
|
|
13480
13559
|
for (const p of patterns) {
|
|
13481
|
-
if (p
|
|
13560
|
+
if (!sinkPatternAppliesTo(p, language)) continue;
|
|
13482
13561
|
if (p.class !== void 0) continue;
|
|
13483
13562
|
if (p.method !== "eval" && p.method !== "Function") continue;
|
|
13484
13563
|
if (!innerToPattern.has(p.method)) innerToPattern.set(p.method, p);
|
|
@@ -14186,9 +14265,7 @@ function isSafeJinjaRenderCall(call, pattern, language, sourceLines) {
|
|
|
14186
14265
|
return false;
|
|
14187
14266
|
}
|
|
14188
14267
|
function findSinks(calls, patterns, typeHierarchy, language, sourceLines, types) {
|
|
14189
|
-
const patternsForLanguage = language === void 0 ? patterns : patterns.filter(
|
|
14190
|
-
(p) => !p.languages || p.languages.length === 0 || p.languages.includes(language)
|
|
14191
|
-
);
|
|
14268
|
+
const patternsForLanguage = language === void 0 ? patterns : patterns.filter((p) => sinkPatternAppliesTo(p, language));
|
|
14192
14269
|
const sinkMap = /* @__PURE__ */ new Map();
|
|
14193
14270
|
for (const call of calls) {
|
|
14194
14271
|
for (const pattern of patternsForLanguage) {
|
|
@@ -14533,10 +14610,8 @@ var SINK_FQN_EXCLUSIONS = {
|
|
|
14533
14610
|
]
|
|
14534
14611
|
};
|
|
14535
14612
|
function matchesSinkPattern(call, pattern, typeHierarchy, language) {
|
|
14536
|
-
if (pattern
|
|
14537
|
-
|
|
14538
|
-
return false;
|
|
14539
|
-
}
|
|
14613
|
+
if (!sinkPatternAppliesTo(pattern, language)) {
|
|
14614
|
+
return false;
|
|
14540
14615
|
}
|
|
14541
14616
|
const callMethodName = call.method_name;
|
|
14542
14617
|
const patternMethod = pattern.method;
|
|
@@ -9915,6 +9915,7 @@ var GO_KEYWORDS = /* @__PURE__ */ new Set([
|
|
|
9915
9915
|
function buildGoDFG(tree) {
|
|
9916
9916
|
const defs = [];
|
|
9917
9917
|
const uses = [];
|
|
9918
|
+
const extraChains = [];
|
|
9918
9919
|
let defIdCounter = 1;
|
|
9919
9920
|
let useIdCounter = 1;
|
|
9920
9921
|
const scopeStack = [/* @__PURE__ */ new Map()];
|
|
@@ -9936,7 +9937,7 @@ function buildGoDFG(tree) {
|
|
|
9936
9937
|
}
|
|
9937
9938
|
const body2 = func2.childForFieldName("body");
|
|
9938
9939
|
if (body2) {
|
|
9939
|
-
processGoBlock(body2, defs, uses, scopeStack, { defId: defIdCounter, useId: useIdCounter });
|
|
9940
|
+
processGoBlock(body2, defs, uses, scopeStack, { defId: defIdCounter, useId: useIdCounter }, extraChains);
|
|
9940
9941
|
defIdCounter = defs.length + 1;
|
|
9941
9942
|
useIdCounter = uses.length + 1;
|
|
9942
9943
|
}
|
|
@@ -9951,6 +9952,15 @@ function buildGoDFG(tree) {
|
|
|
9951
9952
|
}
|
|
9952
9953
|
}
|
|
9953
9954
|
const chains = computeChains(defs, uses);
|
|
9955
|
+
const seen = new Set(chains.map((c) => `${c.from_def}|${c.to_def}|${c.via}`));
|
|
9956
|
+
for (const ch of extraChains) {
|
|
9957
|
+
const key = `${ch.from_def}|${ch.to_def}|${ch.via}`;
|
|
9958
|
+
if (!seen.has(key)) {
|
|
9959
|
+
seen.add(key);
|
|
9960
|
+
chains.push(ch);
|
|
9961
|
+
}
|
|
9962
|
+
}
|
|
9963
|
+
chains.sort((a, b) => a.from_def - b.from_def || a.to_def - b.to_def);
|
|
9954
9964
|
return { defs, uses, chains };
|
|
9955
9965
|
}
|
|
9956
9966
|
function extractGoParamDefs(params, defs, _startId, scopeStack) {
|
|
@@ -9975,28 +9985,47 @@ function extractGoParamDefs(params, defs, _startId, scopeStack) {
|
|
|
9975
9985
|
}
|
|
9976
9986
|
}
|
|
9977
9987
|
}
|
|
9978
|
-
function processGoBlock(node, defs, uses, scopeStack, counters) {
|
|
9988
|
+
function processGoBlock(node, defs, uses, scopeStack, counters, extraChains = []) {
|
|
9989
|
+
const linkMultiLineRhs = (right, useStart, useEnd, defStart, defEnd) => {
|
|
9990
|
+
if (right.endPosition.row <= right.startPosition.row) return;
|
|
9991
|
+
for (let ui = useStart; ui < useEnd; ui++) {
|
|
9992
|
+
const u = uses[ui];
|
|
9993
|
+
if (!u || u.def_id === null) continue;
|
|
9994
|
+
for (let di = defStart; di < defEnd; di++) {
|
|
9995
|
+
const d = defs[di];
|
|
9996
|
+
if (d && d.kind === "local" && d.id !== u.def_id) {
|
|
9997
|
+
extraChains.push({ from_def: u.def_id, to_def: d.id, via: u.variable });
|
|
9998
|
+
}
|
|
9999
|
+
}
|
|
10000
|
+
}
|
|
10001
|
+
};
|
|
9979
10002
|
walkTree(node, (child) => {
|
|
9980
10003
|
if (child.type === "short_var_declaration") {
|
|
9981
10004
|
const left = child.childForFieldName("left");
|
|
9982
10005
|
const right = child.childForFieldName("right");
|
|
10006
|
+
const usesBefore = uses.length;
|
|
9983
10007
|
if (right) {
|
|
9984
10008
|
extractGoUses(right, uses, scopeStack);
|
|
9985
10009
|
}
|
|
10010
|
+
const defsBefore = defs.length;
|
|
9986
10011
|
if (left) {
|
|
9987
10012
|
extractGoLhsDefs(left, defs, scopeStack, child.startPosition.row + 1);
|
|
9988
10013
|
}
|
|
10014
|
+
if (right) linkMultiLineRhs(right, usesBefore, uses.length, defsBefore, defs.length);
|
|
9989
10015
|
} else if (child.type === "var_declaration") {
|
|
9990
10016
|
processGoVarDecl(child, defs, scopeStack, counters);
|
|
9991
10017
|
} else if (child.type === "assignment_statement") {
|
|
9992
10018
|
const left = child.childForFieldName("left");
|
|
9993
10019
|
const right = child.childForFieldName("right");
|
|
10020
|
+
const usesBefore = uses.length;
|
|
9994
10021
|
if (right) {
|
|
9995
10022
|
extractGoUses(right, uses, scopeStack);
|
|
9996
10023
|
}
|
|
10024
|
+
const defsBefore = defs.length;
|
|
9997
10025
|
if (left) {
|
|
9998
10026
|
extractGoLhsDefs(left, defs, scopeStack, child.startPosition.row + 1);
|
|
9999
10027
|
}
|
|
10028
|
+
if (right) linkMultiLineRhs(right, usesBefore, uses.length, defsBefore, defs.length);
|
|
10000
10029
|
} else if (child.type === "for_statement") {
|
|
10001
10030
|
const rangeClause = findChildByTypeGo(child, "range_clause");
|
|
10002
10031
|
if (rangeClause) {
|
|
@@ -10660,7 +10689,11 @@ var DEFAULT_SOURCES = [
|
|
|
10660
10689
|
{ method: "Json", type: "http_body", severity: "high", return_tainted: true, languages: ["rust"] },
|
|
10661
10690
|
{ method: "Query", type: "http_param", severity: "high", return_tainted: true, languages: ["rust"] },
|
|
10662
10691
|
{ method: "Path", type: "http_path", severity: "high", return_tainted: true, languages: ["rust"] },
|
|
10663
|
-
|
|
10692
|
+
// `Form<T>` decodes the urlencoded request *body* (`Query<T>` above is the
|
|
10693
|
+
// query-string extractor), so it is http_body — retyped from http_param
|
|
10694
|
+
// when the RustPlugin's http_body duplicate was removed, so the two
|
|
10695
|
+
// surfaces no longer disagree. Matches Gin's `Context.PostForm`.
|
|
10696
|
+
{ method: "Form", type: "http_body", severity: "high", return_tainted: true, languages: ["rust"] },
|
|
10664
10697
|
// Rust std library
|
|
10665
10698
|
{ method: "var", class: "env", type: "env_input", severity: "medium", return_tainted: true },
|
|
10666
10699
|
{ method: "var_os", class: "env", type: "env_input", severity: "medium", return_tainted: true },
|
|
@@ -11938,7 +11971,11 @@ var DEFAULT_SINKS = [
|
|
|
11938
11971
|
// Also match without receiver (destructured imports: const { exec } = require('child_process'))
|
|
11939
11972
|
// `exec` is intentionally classless: catches Node.js child_process.exec AND
|
|
11940
11973
|
// Java Runtime.exec (via `r.exec()` where heuristic can't resolve r → Runtime).
|
|
11941
|
-
|
|
11974
|
+
// Excluded from Python: there, bare `exec(code)` is the code-execution
|
|
11975
|
+
// builtin, registered below as code_injection / CWE-94. Before the exclusion
|
|
11976
|
+
// a Python `exec(user_code)` matched both patterns and emitted two findings
|
|
11977
|
+
// for one call — CWE-78 (wrong: no shell is involved) and CWE-94 (right).
|
|
11978
|
+
{ method: "exec", type: "command_injection", cwe: "CWE-78", severity: "high", arg_positions: [0], exclude_languages: ["python"] },
|
|
11942
11979
|
// `execSync`/`spawn`/`spawnSync`/`execFile` are Node-specific — language-scope them.
|
|
11943
11980
|
{ method: "execSync", type: "command_injection", cwe: "CWE-78", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
|
|
11944
11981
|
{ method: "spawn", type: "command_injection", cwe: "CWE-78", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
|
|
@@ -11966,7 +12003,11 @@ var DEFAULT_SINKS = [
|
|
|
11966
12003
|
// Note: classless { method: 'query' } removed — too many FPs (UriComponentsBuilder.query(), etc.)
|
|
11967
12004
|
// SQL query calls are covered by class-specific patterns above (Connection, Pool, Client, JdbcTemplate)
|
|
11968
12005
|
// Note: `raw` is shared with Python (Django ORM) — scoped to JS+TS to avoid leaking.
|
|
11969
|
-
|
|
12006
|
+
// Severity `critical` (not `high`) preserves what the JS plugin's duplicate
|
|
12007
|
+
// builtin emitted before the #4 follow-up consolidation removed it: the
|
|
12008
|
+
// plugin copy carried `critical`, which won the `findSinks` dedup slot via
|
|
12009
|
+
// the +0.1 critical-severity confidence boost.
|
|
12010
|
+
{ method: "raw", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["javascript", "typescript"] },
|
|
11970
12011
|
// sqlite3 (npm) — Database/Statement methods. The JS plugin resolves
|
|
11971
12012
|
// `const db = new sqlite3.Database(...); db.all(sql)` to the resolution
|
|
11972
12013
|
// target `Connection.all`, so class-scoped patterns matching `Connection`
|
|
@@ -12525,10 +12566,30 @@ var DEFAULT_SINKS = [
|
|
|
12525
12566
|
{ method: "Sprintf", class: "fmt", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [0], languages: ["go"] },
|
|
12526
12567
|
{ method: "Printf", class: "fmt", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [0], languages: ["go"] },
|
|
12527
12568
|
{ method: "Errorf", class: "fmt", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [0], languages: ["go"] },
|
|
12569
|
+
// `fmt.Fprintf` is deliberately dual-classified: this CWE-134 entry and the
|
|
12570
|
+
// Go plugin's xss (CWE-79) entry both cover arg[1], so a tainted format
|
|
12571
|
+
// string emits two findings. Unlike the logger case above, neither is
|
|
12572
|
+
// redundant — CWE-134 holds for any writer, while CWE-79 is the actionable
|
|
12573
|
+
// finding when the writer is an http.ResponseWriter (`fmt.Fprintf(w, taint)`
|
|
12574
|
+
// is the standard Go reflected-XSS shape). Collapsing it requires deciding
|
|
12575
|
+
// by the *type of arg[0]*, which sink patterns cannot express today: the
|
|
12576
|
+
// options are a Go-specific gate (mirroring isSafeGoJsonUnmarshalCall) that
|
|
12577
|
+
// emits xss only for a resolved ResponseWriter, or dropping one CWE and
|
|
12578
|
+
// accepting either lost XSS recall or a mis-classified stderr write.
|
|
12579
|
+
// Left as-is pending the Go corpus run — see tasks.md.
|
|
12528
12580
|
{ method: "Fprintf", class: "fmt", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [1], languages: ["go"] },
|
|
12529
12581
|
// cognium-dev #264 — Go stdlib `log` package format-string entry points.
|
|
12530
12582
|
// log.Printf / Fatalf / Panicf take the format string at arg[0]; tainted
|
|
12531
12583
|
// format string reaches the same fmt.Sprintf machinery internally.
|
|
12584
|
+
//
|
|
12585
|
+
// These deliberately coexist with the Go plugin's log_injection (CWE-117)
|
|
12586
|
+
// entries for the same methods, so such a call emits both. The Logger
|
|
12587
|
+
// decision note in the Java log_injection section above does NOT apply
|
|
12588
|
+
// here: it reasons about SLF4J-style APIs where the format string is a
|
|
12589
|
+
// literal with `{}` placeholders, whereas Go's `log.Printf(fmt, ...)` takes
|
|
12590
|
+
// a real format string that an attacker can control. CWE-117 covers the
|
|
12591
|
+
// forged log line, CWE-134 the format string itself. Locked by
|
|
12592
|
+
// tests/analysis/passes/format-string-additions.test.ts.
|
|
12532
12593
|
{ method: "Printf", class: "log", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [0], languages: ["go"] },
|
|
12533
12594
|
{ method: "Fatalf", class: "log", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [0], languages: ["go"] },
|
|
12534
12595
|
{ method: "Panicf", class: "log", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [0], languages: ["go"] },
|
|
@@ -12542,6 +12603,14 @@ var DEFAULT_SINKS = [
|
|
|
12542
12603
|
{ method: "cookie", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1], languages: ["javascript", "typescript"] },
|
|
12543
12604
|
// Express: res.location(url) and res.redirect(url) — Location header.
|
|
12544
12605
|
{ method: "location", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [0], languages: ["javascript", "typescript"] },
|
|
12606
|
+
// `redirect` is deliberately dual-classified: this crlf (CWE-113) entry
|
|
12607
|
+
// coexists with the open_redirect (CWE-601) entry for js/ts, so a tainted
|
|
12608
|
+
// redirect target emits two flows. That is intentional, not duplication —
|
|
12609
|
+
// #189 Sprint 82 shipped a sink-type-aware flow dedup specifically so both
|
|
12610
|
+
// survive ("res.redirect is both open_redirect AND crlf"), and #132 added a
|
|
12611
|
+
// crlf recall test for the bare `res.redirect(req.query.url)` shape. Locked
|
|
12612
|
+
// by issue-189-sprint82-open-redirect-cluster.test.ts and
|
|
12613
|
+
// crlf-stage8-fp.test.ts — do not collapse without revisiting both tickets.
|
|
12545
12614
|
{ method: "redirect", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [0], languages: ["javascript", "typescript"] },
|
|
12546
12615
|
// Go net/http: w.Header().Set(k, v) / Add(k, v) — first arg is the value
|
|
12547
12616
|
// (Header is a map; the actual `value` is arg 1 of the call). We flag the
|
|
@@ -13367,18 +13436,28 @@ function analyzeTaint(calls, types, config = getDefaultConfig(), typeHierarchy,
|
|
|
13367
13436
|
const sanitizers = findSanitizers(calls, types, config.sanitizers, sourceLines);
|
|
13368
13437
|
return { sources, sinks, sanitizers };
|
|
13369
13438
|
}
|
|
13439
|
+
function sinkPatternAppliesTo(pattern, language) {
|
|
13440
|
+
if (language === void 0) return true;
|
|
13441
|
+
if (pattern.languages && pattern.languages.length > 0 && !pattern.languages.includes(language)) {
|
|
13442
|
+
return false;
|
|
13443
|
+
}
|
|
13444
|
+
if (pattern.exclude_languages && pattern.exclude_languages.includes(language)) {
|
|
13445
|
+
return false;
|
|
13446
|
+
}
|
|
13447
|
+
return true;
|
|
13448
|
+
}
|
|
13370
13449
|
function expandPromisifyAliases(patterns, sourceLines, language) {
|
|
13371
13450
|
if (!sourceLines || sourceLines.length === 0) return patterns;
|
|
13372
13451
|
if (language !== "javascript" && language !== "typescript") return patterns;
|
|
13373
13452
|
const PROMISIFY_RE = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:util\s*\.\s*)?promisify\s*\(\s*([A-Za-z_$][\w$]*)\s*\)/;
|
|
13374
13453
|
const innerToPattern = /* @__PURE__ */ new Map();
|
|
13375
13454
|
for (const p of patterns) {
|
|
13376
|
-
if (p
|
|
13455
|
+
if (!sinkPatternAppliesTo(p, language)) continue;
|
|
13377
13456
|
if (p.class !== void 0) continue;
|
|
13378
13457
|
if (!innerToPattern.has(p.method)) innerToPattern.set(p.method, p);
|
|
13379
13458
|
}
|
|
13380
13459
|
for (const p of patterns) {
|
|
13381
|
-
if (p
|
|
13460
|
+
if (!sinkPatternAppliesTo(p, language)) continue;
|
|
13382
13461
|
if (innerToPattern.has(p.method)) continue;
|
|
13383
13462
|
innerToPattern.set(p.method, p);
|
|
13384
13463
|
}
|
|
@@ -13412,7 +13491,7 @@ function expandIndirectEvalAliases(patterns, sourceLines, language) {
|
|
|
13412
13491
|
const INDIRECT_EVAL_RE = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*(?::[^=]+)?=\s*(eval|Function)\b(?!\s*\()/;
|
|
13413
13492
|
const innerToPattern = /* @__PURE__ */ new Map();
|
|
13414
13493
|
for (const p of patterns) {
|
|
13415
|
-
if (p
|
|
13494
|
+
if (!sinkPatternAppliesTo(p, language)) continue;
|
|
13416
13495
|
if (p.class !== void 0) continue;
|
|
13417
13496
|
if (p.method !== "eval" && p.method !== "Function") continue;
|
|
13418
13497
|
if (!innerToPattern.has(p.method)) innerToPattern.set(p.method, p);
|
|
@@ -14120,9 +14199,7 @@ function isSafeJinjaRenderCall(call, pattern, language, sourceLines) {
|
|
|
14120
14199
|
return false;
|
|
14121
14200
|
}
|
|
14122
14201
|
function findSinks(calls, patterns, typeHierarchy, language, sourceLines, types) {
|
|
14123
|
-
const patternsForLanguage = language === void 0 ? patterns : patterns.filter(
|
|
14124
|
-
(p) => !p.languages || p.languages.length === 0 || p.languages.includes(language)
|
|
14125
|
-
);
|
|
14202
|
+
const patternsForLanguage = language === void 0 ? patterns : patterns.filter((p) => sinkPatternAppliesTo(p, language));
|
|
14126
14203
|
const sinkMap = /* @__PURE__ */ new Map();
|
|
14127
14204
|
for (const call of calls) {
|
|
14128
14205
|
for (const pattern of patternsForLanguage) {
|
|
@@ -14467,10 +14544,8 @@ var SINK_FQN_EXCLUSIONS = {
|
|
|
14467
14544
|
]
|
|
14468
14545
|
};
|
|
14469
14546
|
function matchesSinkPattern(call, pattern, typeHierarchy, language) {
|
|
14470
|
-
if (pattern
|
|
14471
|
-
|
|
14472
|
-
return false;
|
|
14473
|
-
}
|
|
14547
|
+
if (!sinkPatternAppliesTo(pattern, language)) {
|
|
14548
|
+
return false;
|
|
14474
14549
|
}
|
|
14475
14550
|
const callMethodName = call.method_name;
|
|
14476
14551
|
const patternMethod = pattern.method;
|
|
@@ -1274,6 +1274,11 @@ const GO_KEYWORDS = new Set([
|
|
|
1274
1274
|
function buildGoDFG(tree) {
|
|
1275
1275
|
const defs = [];
|
|
1276
1276
|
const uses = [];
|
|
1277
|
+
// Explicit chains for multi-line composite-literal assignments (#267) —
|
|
1278
|
+
// `computeChains` only links same-line use→def, so a struct/slice literal
|
|
1279
|
+
// that spans lines (`req := T{ … tainted … }`) would not propagate taint
|
|
1280
|
+
// from the tainted field to the assigned var.
|
|
1281
|
+
const extraChains = [];
|
|
1277
1282
|
let defIdCounter = 1;
|
|
1278
1283
|
let useIdCounter = 1;
|
|
1279
1284
|
const scopeStack = [new Map()];
|
|
@@ -1299,7 +1304,7 @@ function buildGoDFG(tree) {
|
|
|
1299
1304
|
// Process function body
|
|
1300
1305
|
const body = func.childForFieldName('body');
|
|
1301
1306
|
if (body) {
|
|
1302
|
-
processGoBlock(body, defs, uses, scopeStack, { defId: defIdCounter, useId: useIdCounter });
|
|
1307
|
+
processGoBlock(body, defs, uses, scopeStack, { defId: defIdCounter, useId: useIdCounter }, extraChains);
|
|
1303
1308
|
defIdCounter = defs.length + 1;
|
|
1304
1309
|
useIdCounter = uses.length + 1;
|
|
1305
1310
|
}
|
|
@@ -1315,7 +1320,18 @@ function buildGoDFG(tree) {
|
|
|
1315
1320
|
defIdCounter = defs.length + 1;
|
|
1316
1321
|
}
|
|
1317
1322
|
}
|
|
1323
|
+
// Merge same-line chains with the explicit multi-line composite-literal
|
|
1324
|
+
// chains (cognium-dev #267), deduplicating on (from_def, to_def, via).
|
|
1318
1325
|
const chains = computeChains(defs, uses);
|
|
1326
|
+
const seen = new Set(chains.map(c => `${c.from_def}|${c.to_def}|${c.via}`));
|
|
1327
|
+
for (const ch of extraChains) {
|
|
1328
|
+
const key = `${ch.from_def}|${ch.to_def}|${ch.via}`;
|
|
1329
|
+
if (!seen.has(key)) {
|
|
1330
|
+
seen.add(key);
|
|
1331
|
+
chains.push(ch);
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
chains.sort((a, b) => a.from_def - b.from_def || a.to_def - b.to_def);
|
|
1319
1335
|
return { defs, uses, chains };
|
|
1320
1336
|
}
|
|
1321
1337
|
/**
|
|
@@ -1351,20 +1367,44 @@ function extractGoParamDefs(params, defs, _startId, scopeStack) {
|
|
|
1351
1367
|
/**
|
|
1352
1368
|
* Process a Go block (function body, if body, etc.) for definitions and uses.
|
|
1353
1369
|
*/
|
|
1354
|
-
function processGoBlock(node, defs, uses, scopeStack, counters) {
|
|
1370
|
+
function processGoBlock(node, defs, uses, scopeStack, counters, extraChains = []) {
|
|
1371
|
+
// cognium-dev #267 — link resolved RHS uses to LHS local defs when the
|
|
1372
|
+
// RHS spans multiple lines (a struct/slice composite literal in gofmt
|
|
1373
|
+
// style). `computeChains` only links same-line use→def, so without this a
|
|
1374
|
+
// tainted value used deep inside a multi-line literal never reaches the
|
|
1375
|
+
// assigned variable. Single-line RHS is already covered by computeChains.
|
|
1376
|
+
const linkMultiLineRhs = (right, useStart, useEnd, defStart, defEnd) => {
|
|
1377
|
+
if (right.endPosition.row <= right.startPosition.row)
|
|
1378
|
+
return; // single line
|
|
1379
|
+
for (let ui = useStart; ui < useEnd; ui++) {
|
|
1380
|
+
const u = uses[ui];
|
|
1381
|
+
if (!u || u.def_id === null)
|
|
1382
|
+
continue;
|
|
1383
|
+
for (let di = defStart; di < defEnd; di++) {
|
|
1384
|
+
const d = defs[di];
|
|
1385
|
+
if (d && d.kind === 'local' && d.id !== u.def_id) {
|
|
1386
|
+
extraChains.push({ from_def: u.def_id, to_def: d.id, via: u.variable });
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
};
|
|
1355
1391
|
walkTree(node, (child) => {
|
|
1356
1392
|
if (child.type === 'short_var_declaration') {
|
|
1357
1393
|
// x, y := expr
|
|
1358
1394
|
const left = child.childForFieldName('left');
|
|
1359
1395
|
const right = child.childForFieldName('right');
|
|
1360
1396
|
// Extract uses from right side first
|
|
1397
|
+
const usesBefore = uses.length;
|
|
1361
1398
|
if (right) {
|
|
1362
1399
|
extractGoUses(right, uses, scopeStack);
|
|
1363
1400
|
}
|
|
1364
1401
|
// Then create defs for left side
|
|
1402
|
+
const defsBefore = defs.length;
|
|
1365
1403
|
if (left) {
|
|
1366
1404
|
extractGoLhsDefs(left, defs, scopeStack, child.startPosition.row + 1);
|
|
1367
1405
|
}
|
|
1406
|
+
if (right)
|
|
1407
|
+
linkMultiLineRhs(right, usesBefore, uses.length, defsBefore, defs.length);
|
|
1368
1408
|
}
|
|
1369
1409
|
else if (child.type === 'var_declaration') {
|
|
1370
1410
|
processGoVarDecl(child, defs, scopeStack, counters);
|
|
@@ -1373,13 +1413,17 @@ function processGoBlock(node, defs, uses, scopeStack, counters) {
|
|
|
1373
1413
|
const left = child.childForFieldName('left');
|
|
1374
1414
|
const right = child.childForFieldName('right');
|
|
1375
1415
|
// Uses from right side
|
|
1416
|
+
const usesBefore = uses.length;
|
|
1376
1417
|
if (right) {
|
|
1377
1418
|
extractGoUses(right, uses, scopeStack);
|
|
1378
1419
|
}
|
|
1379
1420
|
// Defs for left side (reassignment)
|
|
1421
|
+
const defsBefore = defs.length;
|
|
1380
1422
|
if (left) {
|
|
1381
1423
|
extractGoLhsDefs(left, defs, scopeStack, child.startPosition.row + 1);
|
|
1382
1424
|
}
|
|
1425
|
+
if (right)
|
|
1426
|
+
linkMultiLineRhs(right, usesBefore, uses.length, defsBefore, defs.length);
|
|
1383
1427
|
}
|
|
1384
1428
|
else if (child.type === 'for_statement') {
|
|
1385
1429
|
// range clause: for k, v := range expr
|