circle-ir 3.47.0 → 3.49.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/dist/analysis/config-loader.d.ts.map +1 -1
- package/dist/analysis/config-loader.js +86 -2
- package/dist/analysis/config-loader.js.map +1 -1
- package/dist/analysis/constant-propagation/index.d.ts.map +1 -1
- package/dist/analysis/constant-propagation/index.js +16 -6
- package/dist/analysis/constant-propagation/index.js.map +1 -1
- package/dist/analysis/constant-propagation/propagator.d.ts +7 -0
- package/dist/analysis/constant-propagation/propagator.d.ts.map +1 -1
- package/dist/analysis/constant-propagation/propagator.js +81 -41
- package/dist/analysis/constant-propagation/propagator.js.map +1 -1
- package/dist/analysis/html/html-attribute-security-pass.js +14 -9
- package/dist/analysis/html/html-attribute-security-pass.js.map +1 -1
- package/dist/analysis/html/html-extractor.d.ts.map +1 -1
- package/dist/analysis/html/html-extractor.js +16 -11
- package/dist/analysis/html/html-extractor.js.map +1 -1
- package/dist/analysis/passes/insecure-cookie-pass.d.ts +53 -0
- package/dist/analysis/passes/insecure-cookie-pass.d.ts.map +1 -0
- package/dist/analysis/passes/insecure-cookie-pass.js +109 -0
- package/dist/analysis/passes/insecure-cookie-pass.js.map +1 -0
- package/dist/analysis/passes/interprocedural-pass.d.ts.map +1 -1
- package/dist/analysis/passes/interprocedural-pass.js +7 -0
- package/dist/analysis/passes/interprocedural-pass.js.map +1 -1
- package/dist/analysis/passes/language-sources-pass.d.ts +14 -0
- package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
- package/dist/analysis/passes/language-sources-pass.js +50 -0
- package/dist/analysis/passes/language-sources-pass.js.map +1 -1
- package/dist/analysis/passes/sink-filter-pass.d.ts.map +1 -1
- package/dist/analysis/passes/sink-filter-pass.js +21 -2
- package/dist/analysis/passes/sink-filter-pass.js.map +1 -1
- package/dist/analysis/passes/taint-propagation-pass.js +94 -3
- package/dist/analysis/passes/taint-propagation-pass.js.map +1 -1
- package/dist/analysis/taint-matcher.d.ts.map +1 -1
- package/dist/analysis/taint-matcher.js +117 -20
- package/dist/analysis/taint-matcher.js.map +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +3 -0
- package/dist/analyzer.js.map +1 -1
- package/dist/browser/circle-ir.js +453 -99
- package/dist/core/circle-ir-core.cjs +251 -64
- package/dist/core/circle-ir-core.js +251 -64
- package/dist/core/extractors/types.js +85 -2
- package/dist/core/extractors/types.js.map +1 -1
- package/dist/core/parser.d.ts +10 -0
- package/dist/core/parser.d.ts.map +1 -1
- package/dist/core/parser.js +20 -5
- package/dist/core/parser.js.map +1 -1
- package/dist/languages/plugins/base.d.ts.map +1 -1
- package/dist/languages/plugins/base.js +15 -11
- package/dist/languages/plugins/base.js.map +1 -1
- package/dist/languages/plugins/java.d.ts.map +1 -1
- package/dist/languages/plugins/java.js +8 -4
- package/dist/languages/plugins/java.js.map +1 -1
- package/package.json +1 -1
|
@@ -4086,11 +4086,13 @@ async function parse(code, language) {
|
|
|
4086
4086
|
return tree;
|
|
4087
4087
|
}
|
|
4088
4088
|
function walkTree(node, visitor) {
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
const
|
|
4092
|
-
|
|
4093
|
-
|
|
4089
|
+
const stack = [node];
|
|
4090
|
+
while (stack.length > 0) {
|
|
4091
|
+
const current = stack.pop();
|
|
4092
|
+
visitor(current);
|
|
4093
|
+
for (let i2 = current.childCount - 1; i2 >= 0; i2--) {
|
|
4094
|
+
const child = current.child(i2);
|
|
4095
|
+
if (child) stack.push(child);
|
|
4094
4096
|
}
|
|
4095
4097
|
}
|
|
4096
4098
|
}
|
|
@@ -4730,14 +4732,46 @@ function extractJSClassInfo(node) {
|
|
|
4730
4732
|
end_line: node.endPosition.row + 1
|
|
4731
4733
|
};
|
|
4732
4734
|
}
|
|
4735
|
+
function extractDecoratorName(node) {
|
|
4736
|
+
const child = node.namedChildCount > 0 ? node.namedChild(0) : null;
|
|
4737
|
+
if (!child) return null;
|
|
4738
|
+
if (child.type === "identifier") return getNodeText(child);
|
|
4739
|
+
if (child.type === "call_expression") {
|
|
4740
|
+
const fn = child.childForFieldName("function");
|
|
4741
|
+
if (fn) {
|
|
4742
|
+
if (fn.type === "identifier") return getNodeText(fn);
|
|
4743
|
+
if (fn.type === "member_expression") {
|
|
4744
|
+
const propNode = fn.childForFieldName("property");
|
|
4745
|
+
if (propNode) return getNodeText(propNode);
|
|
4746
|
+
}
|
|
4747
|
+
}
|
|
4748
|
+
}
|
|
4749
|
+
if (child.type === "member_expression") {
|
|
4750
|
+
const propNode = child.childForFieldName("property");
|
|
4751
|
+
if (propNode) return getNodeText(propNode);
|
|
4752
|
+
}
|
|
4753
|
+
return null;
|
|
4754
|
+
}
|
|
4733
4755
|
function extractJSMethods(body2) {
|
|
4734
4756
|
const methods = [];
|
|
4757
|
+
let pendingDecorators = [];
|
|
4735
4758
|
for (let i2 = 0; i2 < body2.childCount; i2++) {
|
|
4736
4759
|
const child = body2.child(i2);
|
|
4737
4760
|
if (!child) continue;
|
|
4761
|
+
if (child.type === "decorator") {
|
|
4762
|
+
const name2 = extractDecoratorName(child);
|
|
4763
|
+
if (name2) pendingDecorators.push(name2);
|
|
4764
|
+
continue;
|
|
4765
|
+
}
|
|
4766
|
+
if (child.type === "comment") continue;
|
|
4738
4767
|
if (child.type === "method_definition") {
|
|
4739
|
-
|
|
4768
|
+
const m = extractJSMethodInfo(child);
|
|
4769
|
+
if (pendingDecorators.length > 0) {
|
|
4770
|
+
m.annotations = pendingDecorators;
|
|
4771
|
+
}
|
|
4772
|
+
methods.push(m);
|
|
4740
4773
|
}
|
|
4774
|
+
pendingDecorators = [];
|
|
4741
4775
|
}
|
|
4742
4776
|
return methods;
|
|
4743
4777
|
}
|
|
@@ -4899,10 +4933,18 @@ function extractJSParameters(params) {
|
|
|
4899
4933
|
if (typeNode) {
|
|
4900
4934
|
paramType = getNodeText(typeNode).replace(/^:\s*/, "");
|
|
4901
4935
|
}
|
|
4936
|
+
const decorators = [];
|
|
4937
|
+
for (let j = 0; j < child.childCount; j++) {
|
|
4938
|
+
const c = child.child(j);
|
|
4939
|
+
if (c && c.type === "decorator") {
|
|
4940
|
+
const name2 = extractDecoratorName(c);
|
|
4941
|
+
if (name2) decorators.push(name2);
|
|
4942
|
+
}
|
|
4943
|
+
}
|
|
4902
4944
|
parameters.push({
|
|
4903
4945
|
name: paramName,
|
|
4904
4946
|
type: paramType,
|
|
4905
|
-
annotations:
|
|
4947
|
+
annotations: decorators,
|
|
4906
4948
|
line: child.startPosition.row + 1
|
|
4907
4949
|
});
|
|
4908
4950
|
}
|
|
@@ -10949,6 +10991,26 @@ var DEFAULT_SINKS = [
|
|
|
10949
10991
|
{ method: "setQuotedArgumentsEnabled", class: "Shell", type: "command_injection", cwe: "CWE-78", severity: "high", arg_positions: [0] },
|
|
10950
10992
|
// Sandbox/script security
|
|
10951
10993
|
{ method: "onNewInstance", class: "SandboxInterceptor", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
|
|
10994
|
+
// Java Log Injection (slf4j / logback / java.util.logging) — CWE-117
|
|
10995
|
+
// Issue #44: log.info/warn/error/debug emit the message argument and any
|
|
10996
|
+
// {} format arguments to the log stream. Untrusted input forwarded into
|
|
10997
|
+
// these calls allows log forging (newline injection) and downstream log
|
|
10998
|
+
// analyzer pollution. Scoped to `java` so the generic method names don't
|
|
10999
|
+
// collide with JS console / Python logger entries below.
|
|
11000
|
+
{ method: "info", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3], languages: ["java"] },
|
|
11001
|
+
{ method: "warn", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3], languages: ["java"] },
|
|
11002
|
+
{ method: "error", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3], languages: ["java"] },
|
|
11003
|
+
{ method: "debug", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3], languages: ["java"] },
|
|
11004
|
+
{ method: "trace", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3], languages: ["java"] },
|
|
11005
|
+
// java.util.logging.Logger uses the same class name `Logger` — same entries above cover it.
|
|
11006
|
+
// Severity-tagged levels: SEVERE/WARNING/INFO/CONFIG/FINE/FINER/FINEST
|
|
11007
|
+
{ method: "severe", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0], languages: ["java"] },
|
|
11008
|
+
{ method: "warning", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0], languages: ["java"] },
|
|
11009
|
+
{ method: "config", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0], languages: ["java"] },
|
|
11010
|
+
{ method: "fine", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0], languages: ["java"] },
|
|
11011
|
+
{ method: "finer", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0], languages: ["java"] },
|
|
11012
|
+
{ method: "finest", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0], languages: ["java"] },
|
|
11013
|
+
{ method: "log", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [1, 2, 3], languages: ["java"] },
|
|
10952
11014
|
// =========================================================================
|
|
10953
11015
|
// Node.js/Express Sinks
|
|
10954
11016
|
// =========================================================================
|
|
@@ -11001,13 +11063,47 @@ var DEFAULT_SINKS = [
|
|
|
11001
11063
|
{ method: "runInContext", class: "vm", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0] },
|
|
11002
11064
|
{ method: "runInNewContext", class: "vm", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0] },
|
|
11003
11065
|
{ method: "runInThisContext", class: "vm", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0] },
|
|
11004
|
-
// Node.js NoSQL Injection (MongoDB)
|
|
11066
|
+
// Node.js NoSQL Injection (MongoDB native driver + mongoose) — CWE-943
|
|
11067
|
+
// Issue #45: the bare `class: 'Collection'` constraint missed mongoose's
|
|
11068
|
+
// fluent chains (mongoose.connection.db.collection('x').find({...})) and
|
|
11069
|
+
// Model.find calls because the call-site receiver type does not resolve
|
|
11070
|
+
// to `Collection`. Add classless+language-scoped entries for the
|
|
11071
|
+
// MongoDB-specific method names (findOne/aggregate/updateOne/etc.) and
|
|
11072
|
+
// mongoose `Model`/`Query` class entries. Bare `find` stays class-scoped
|
|
11073
|
+
// to avoid colliding with Array.prototype.find.
|
|
11005
11074
|
{ method: "find", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0] },
|
|
11006
11075
|
{ method: "findOne", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0] },
|
|
11007
11076
|
{ method: "updateOne", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0] },
|
|
11008
11077
|
{ method: "updateMany", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0] },
|
|
11009
11078
|
{ method: "deleteOne", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0] },
|
|
11010
11079
|
{ method: "deleteMany", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0] },
|
|
11080
|
+
// Mongoose Model/Query class entries — Model.find/findOne/etc.
|
|
11081
|
+
{ method: "find", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
|
|
11082
|
+
{ method: "findOne", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
|
|
11083
|
+
{ method: "findById", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
|
|
11084
|
+
{ method: "findOneAndUpdate", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0, 1], languages: ["javascript", "typescript"] },
|
|
11085
|
+
{ method: "findOneAndDelete", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
|
|
11086
|
+
{ method: "findOneAndReplace", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0, 1], languages: ["javascript", "typescript"] },
|
|
11087
|
+
{ method: "updateOne", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0, 1], languages: ["javascript", "typescript"] },
|
|
11088
|
+
{ method: "updateMany", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0, 1], languages: ["javascript", "typescript"] },
|
|
11089
|
+
{ method: "deleteOne", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
|
|
11090
|
+
{ method: "deleteMany", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
|
|
11091
|
+
{ method: "countDocuments", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
|
|
11092
|
+
{ method: "aggregate", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
|
|
11093
|
+
// Mongoose Query class entries — chain methods returning Query
|
|
11094
|
+
{ method: "where", class: "Query", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
|
|
11095
|
+
{ method: "equals", class: "Query", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
|
|
11096
|
+
// Classless MongoDB-specific method names (rare outside MongoDB APIs) —
|
|
11097
|
+
// language-scoped to JS/TS. Excludes plain `find` (Array.prototype.find FP).
|
|
11098
|
+
{ method: "findOne", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
|
|
11099
|
+
{ method: "findOneAndUpdate", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0, 1], languages: ["javascript", "typescript"] },
|
|
11100
|
+
{ method: "findOneAndDelete", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
|
|
11101
|
+
{ method: "findOneAndReplace", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0, 1], languages: ["javascript", "typescript"] },
|
|
11102
|
+
{ method: "updateOne", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0, 1], languages: ["javascript", "typescript"] },
|
|
11103
|
+
{ method: "updateMany", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0, 1], languages: ["javascript", "typescript"] },
|
|
11104
|
+
{ method: "deleteOne", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
|
|
11105
|
+
{ method: "deleteMany", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
|
|
11106
|
+
{ method: "aggregate", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
|
|
11011
11107
|
// Node.js SSRF (HTTP clients)
|
|
11012
11108
|
{ method: "get", class: "axios", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0] },
|
|
11013
11109
|
{ method: "post", class: "axios", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0] },
|
|
@@ -11029,6 +11125,24 @@ var DEFAULT_SINKS = [
|
|
|
11029
11125
|
{ method: "post", class: "superagent", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0] },
|
|
11030
11126
|
// node-fetch
|
|
11031
11127
|
{ method: "default", class: "node-fetch", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0] },
|
|
11128
|
+
// Node.js / JavaScript Log Injection (console.*) — CWE-117
|
|
11129
|
+
// Issue #44: console.log/warn/error/info with tainted template literals
|
|
11130
|
+
// allow log forging (newline-injection) and downstream log analyzer
|
|
11131
|
+
// pollution. Scoped to JS/TS so the bare class `console` doesn't collide
|
|
11132
|
+
// with Python `console` module or Java identifiers.
|
|
11133
|
+
{ method: "log", class: "console", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3], languages: ["javascript", "typescript"] },
|
|
11134
|
+
{ method: "warn", class: "console", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3], languages: ["javascript", "typescript"] },
|
|
11135
|
+
{ method: "error", class: "console", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3], languages: ["javascript", "typescript"] },
|
|
11136
|
+
{ method: "info", class: "console", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3], languages: ["javascript", "typescript"] },
|
|
11137
|
+
{ method: "debug", class: "console", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3], languages: ["javascript", "typescript"] },
|
|
11138
|
+
{ method: "trace", class: "console", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3], languages: ["javascript", "typescript"] },
|
|
11139
|
+
// Node.js / Express Open Redirect — CWE-601
|
|
11140
|
+
// Issue #46: `res.redirect(req.query.next)` did not fire because the
|
|
11141
|
+
// legacy `class: 'Response'` constraint depended on receiver type
|
|
11142
|
+
// resolution of the Express `res` parameter. Mirror Python's classless
|
|
11143
|
+
// pattern with a language-scoped classless entry. The method name
|
|
11144
|
+
// `redirect` is rare outside HTTP frameworks so the FP risk is low.
|
|
11145
|
+
{ method: "redirect", type: "open_redirect", cwe: "CWE-601", severity: "medium", arg_positions: [0], languages: ["javascript", "typescript"] },
|
|
11032
11146
|
// =========================================================================
|
|
11033
11147
|
// Python Sinks
|
|
11034
11148
|
// =========================================================================
|
|
@@ -11070,7 +11184,12 @@ var DEFAULT_SINKS = [
|
|
|
11070
11184
|
{ method: "rmtree", class: "shutil", type: "path_traversal", cwe: "CWE-22", severity: "critical", arg_positions: [0] },
|
|
11071
11185
|
{ method: "send_file", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["python"] },
|
|
11072
11186
|
// Python XSS / SSTI
|
|
11073
|
-
|
|
11187
|
+
// Issue #54: Flask's `render_template_string(template_str)` with an
|
|
11188
|
+
// attacker-controlled template string is Server-Side Template Injection
|
|
11189
|
+
// (Jinja2 SSTI → RCE), not reflected XSS. Classify as code_injection
|
|
11190
|
+
// (CWE-94) with critical severity to match `jinja2.Template().render()`
|
|
11191
|
+
// and `Template.from_string()` entries above.
|
|
11192
|
+
{ method: "render_template_string", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0], languages: ["python"] },
|
|
11074
11193
|
{ method: "Markup", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["python"] },
|
|
11075
11194
|
{ method: "mark_safe", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["python"] },
|
|
11076
11195
|
// Python SSRF
|
|
@@ -11433,6 +11552,13 @@ var DEFAULT_SANITIZERS = [
|
|
|
11433
11552
|
{ method: "secure_filename", class: "werkzeug.utils", removes: ["path_traversal"] },
|
|
11434
11553
|
{ method: "basename", class: "os.path", removes: ["path_traversal"] },
|
|
11435
11554
|
{ method: "normpath", class: "os.path", removes: ["path_traversal"] },
|
|
11555
|
+
// Issue #48 part 2: realpath/abspath are canonical Python path-canonicalization
|
|
11556
|
+
// functions (analogous to Java File.getCanonicalPath). Register on both
|
|
11557
|
+
// `os.path` and the bare `path` receiver to cover `import os.path as path`.
|
|
11558
|
+
{ method: "realpath", class: "os.path", removes: ["path_traversal"] },
|
|
11559
|
+
{ method: "abspath", class: "os.path", removes: ["path_traversal"] },
|
|
11560
|
+
{ method: "realpath", class: "path", removes: ["path_traversal"] },
|
|
11561
|
+
{ method: "abspath", class: "path", removes: ["path_traversal"] },
|
|
11436
11562
|
// Python Type coercion
|
|
11437
11563
|
{ method: "int", removes: ["sql_injection", "command_injection", "xss"] },
|
|
11438
11564
|
{ method: "float", removes: ["sql_injection", "command_injection"] },
|
|
@@ -11494,7 +11620,7 @@ var PYTHON_TAINTED_PATTERNS = [
|
|
|
11494
11620
|
];
|
|
11495
11621
|
function analyzeTaint(calls, types, config = getDefaultConfig(), typeHierarchy, language, code) {
|
|
11496
11622
|
const sourceLines = code !== void 0 ? code.split("\n") : void 0;
|
|
11497
|
-
const sources = findSources(calls, types, config.sources, sourceLines);
|
|
11623
|
+
const sources = findSources(calls, types, config.sources, sourceLines, language);
|
|
11498
11624
|
const sinks = findSinks(calls, config.sinks, typeHierarchy, language, sourceLines);
|
|
11499
11625
|
const sanitizers = findSanitizers(calls, types, config.sanitizers);
|
|
11500
11626
|
return { sources, sinks, sanitizers };
|
|
@@ -11512,7 +11638,7 @@ function attachSourceLineCode(sources, sinks, code) {
|
|
|
11512
11638
|
}
|
|
11513
11639
|
}
|
|
11514
11640
|
}
|
|
11515
|
-
function findSources(calls, types, patterns, sourceLines) {
|
|
11641
|
+
function findSources(calls, types, patterns, sourceLines, language) {
|
|
11516
11642
|
const sources = [];
|
|
11517
11643
|
for (const call of calls) {
|
|
11518
11644
|
for (const pattern of patterns) {
|
|
@@ -11565,23 +11691,29 @@ function findSources(calls, types, patterns, sourceLines) {
|
|
|
11565
11691
|
}
|
|
11566
11692
|
}
|
|
11567
11693
|
}
|
|
11568
|
-
const
|
|
11694
|
+
const RUST_EXTRACTOR_KIND = /(?:^|::)(Json|Form|Query|Path|Extension|Multipart|Body|Bytes)(?:<|$)/;
|
|
11569
11695
|
for (const type of types) {
|
|
11570
11696
|
for (const method of type.methods) {
|
|
11571
11697
|
for (const param of method.parameters) {
|
|
11572
|
-
if (param.type
|
|
11573
|
-
|
|
11574
|
-
|
|
11575
|
-
|
|
11576
|
-
|
|
11577
|
-
|
|
11578
|
-
|
|
11579
|
-
|
|
11580
|
-
|
|
11581
|
-
|
|
11582
|
-
|
|
11583
|
-
|
|
11584
|
-
|
|
11698
|
+
if (!param.type) continue;
|
|
11699
|
+
const kindMatch = RUST_EXTRACTOR_KIND.exec(param.type);
|
|
11700
|
+
if (!kindMatch) continue;
|
|
11701
|
+
const kind = kindMatch[1];
|
|
11702
|
+
if (kind === "Extension") continue;
|
|
11703
|
+
const sourceType = kind === "Form" || kind === "Query" || kind === "Path" ? "http_param" : "http_body";
|
|
11704
|
+
const paramLine = param.line ?? method.start_line;
|
|
11705
|
+
const alreadyExists = sources.some(
|
|
11706
|
+
(s) => s.line === paramLine && s.variable === param.name
|
|
11707
|
+
);
|
|
11708
|
+
if (alreadyExists) continue;
|
|
11709
|
+
sources.push({
|
|
11710
|
+
type: sourceType,
|
|
11711
|
+
location: `${param.type} ${param.name} in ${method.name}`,
|
|
11712
|
+
severity: "high",
|
|
11713
|
+
line: paramLine,
|
|
11714
|
+
confidence: 1,
|
|
11715
|
+
variable: param.name
|
|
11716
|
+
});
|
|
11585
11717
|
}
|
|
11586
11718
|
}
|
|
11587
11719
|
}
|
|
@@ -11663,6 +11795,15 @@ function findSources(calls, types, patterns, sourceLines) {
|
|
|
11663
11795
|
s.code = sourceLines[s.line - 1]?.trim();
|
|
11664
11796
|
}
|
|
11665
11797
|
}
|
|
11798
|
+
if (language === "rust" && sourceLines) {
|
|
11799
|
+
const LET_BINDING = /^\s*let\s+(?:mut\s+)?([A-Za-z_]\w*)\s*(?::\s*[^=]+)?=/;
|
|
11800
|
+
for (const s of result) {
|
|
11801
|
+
if (s.variable && s.variable.length > 0) continue;
|
|
11802
|
+
const lineText = sourceLines[s.line - 1] ?? "";
|
|
11803
|
+
const m = LET_BINDING.exec(lineText);
|
|
11804
|
+
if (m) s.variable = m[1];
|
|
11805
|
+
}
|
|
11806
|
+
}
|
|
11666
11807
|
return result;
|
|
11667
11808
|
}
|
|
11668
11809
|
function isInterproceduralTaintableType(typeName) {
|
|
@@ -11768,6 +11909,20 @@ function isParameterizedQueryCall(call, pattern) {
|
|
|
11768
11909
|
}
|
|
11769
11910
|
return false;
|
|
11770
11911
|
}
|
|
11912
|
+
function isSafePythonSubprocessCall(call, pattern, language) {
|
|
11913
|
+
if (language !== "python") return false;
|
|
11914
|
+
if (pattern.type !== "command_injection") return false;
|
|
11915
|
+
if (pattern.class !== "subprocess") return false;
|
|
11916
|
+
const arg0 = call.arguments.find((a) => a.position === 0);
|
|
11917
|
+
if (!arg0) return false;
|
|
11918
|
+
const expr0 = (arg0.literal ?? arg0.expression ?? "").trim();
|
|
11919
|
+
if (!expr0.startsWith("[")) return false;
|
|
11920
|
+
for (const a of call.arguments) {
|
|
11921
|
+
const e = (a.expression ?? "").trim();
|
|
11922
|
+
if (/^shell\s*=\s*True\b/.test(e)) return false;
|
|
11923
|
+
}
|
|
11924
|
+
return true;
|
|
11925
|
+
}
|
|
11771
11926
|
var CLASS_LITERAL_RE = /^(?:[A-Za-z_][\w]*\.)*[A-Z][\w]*(?:\[\])*\.class$/;
|
|
11772
11927
|
function argIsClassLiteral(call, position) {
|
|
11773
11928
|
const arg = call.arguments.find((a) => a.position === position);
|
|
@@ -11784,6 +11939,9 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines) {
|
|
|
11784
11939
|
if (isParameterizedQueryCall(call, pattern)) {
|
|
11785
11940
|
continue;
|
|
11786
11941
|
}
|
|
11942
|
+
if (isSafePythonSubprocessCall(call, pattern, language)) {
|
|
11943
|
+
continue;
|
|
11944
|
+
}
|
|
11787
11945
|
if (pattern.safe_if_class_literal_at !== void 0 && argIsClassLiteral(call, pattern.safe_if_class_literal_at)) {
|
|
11788
11946
|
continue;
|
|
11789
11947
|
}
|
|
@@ -12186,7 +12344,12 @@ function receiverMightBeClass(receiver, className) {
|
|
|
12186
12344
|
"controller",
|
|
12187
12345
|
"task",
|
|
12188
12346
|
"thread",
|
|
12189
|
-
"job"
|
|
12347
|
+
"job",
|
|
12348
|
+
// Short Python DB abbreviation; would otherwise prefix-match obscure XSS
|
|
12349
|
+
// sink classes like XWiki's `CurrentTimePlugin` ('current'.startsWith('cur'))
|
|
12350
|
+
// via the CamelCase word prefix heuristic and produce an xss FP on every
|
|
12351
|
+
// `cur.execute(...)`. Resolved via commonMappings → ['Cursor']. See #65 / #48 pt3.
|
|
12352
|
+
"cur"
|
|
12190
12353
|
]);
|
|
12191
12354
|
const isAmbiguous = ambiguousIdentifiers.has(lowerReceiver);
|
|
12192
12355
|
if (!isAmbiguous && lowerReceiver.length >= 3 && lowerClass.includes(lowerReceiver)) {
|
|
@@ -12196,7 +12359,9 @@ function receiverMightBeClass(receiver, className) {
|
|
|
12196
12359
|
}
|
|
12197
12360
|
if (!isAmbiguous && lowerReceiver.length >= 2) {
|
|
12198
12361
|
if (lowerClass.startsWith(lowerReceiver) || lowerClass.endsWith(lowerReceiver)) {
|
|
12199
|
-
|
|
12362
|
+
if (lowerReceiver.length / lowerClass.length >= 0.4) {
|
|
12363
|
+
return true;
|
|
12364
|
+
}
|
|
12200
12365
|
}
|
|
12201
12366
|
}
|
|
12202
12367
|
if (!isAmbiguous && lowerReceiver.length >= 3) {
|
|
@@ -12219,6 +12384,9 @@ function receiverMightBeClass(receiver, className) {
|
|
|
12219
12384
|
ps: ["PreparedStatement"],
|
|
12220
12385
|
rs: ["ResultSet"],
|
|
12221
12386
|
template: ["JdbcTemplate"],
|
|
12387
|
+
cur: ["Cursor"],
|
|
12388
|
+
// Python DB-API cursor — see ambiguousIdentifiers note
|
|
12389
|
+
cursor: ["Cursor"],
|
|
12222
12390
|
// I/O
|
|
12223
12391
|
writer: ["PrintWriter"],
|
|
12224
12392
|
out: ["PrintWriter", "OutputStream"],
|
|
@@ -13906,8 +14074,10 @@ var ConstantPropagator = class _ConstantPropagator {
|
|
|
13906
14074
|
* These are variables declared directly in the class body, not inside methods.
|
|
13907
14075
|
*/
|
|
13908
14076
|
collectClassFields(root) {
|
|
13909
|
-
const
|
|
13910
|
-
|
|
14077
|
+
const stack = [root];
|
|
14078
|
+
while (stack.length > 0) {
|
|
14079
|
+
const n = stack.pop();
|
|
14080
|
+
if (!n) continue;
|
|
13911
14081
|
if (n.type === "class_body") {
|
|
13912
14082
|
for (const child of n.children) {
|
|
13913
14083
|
if (child.type === "field_declaration") {
|
|
@@ -13921,32 +14091,28 @@ var ConstantPropagator = class _ConstantPropagator {
|
|
|
13921
14091
|
}
|
|
13922
14092
|
}
|
|
13923
14093
|
}
|
|
13924
|
-
|
|
13925
|
-
traverse(child, true, true);
|
|
13926
|
-
} else {
|
|
13927
|
-
traverse(child, true, false);
|
|
13928
|
-
}
|
|
14094
|
+
stack.push(child);
|
|
13929
14095
|
}
|
|
13930
|
-
|
|
14096
|
+
continue;
|
|
13931
14097
|
}
|
|
13932
14098
|
for (const child of n.children) {
|
|
13933
|
-
|
|
14099
|
+
stack.push(child);
|
|
13934
14100
|
}
|
|
13935
|
-
}
|
|
13936
|
-
traverse(root, false, false);
|
|
14101
|
+
}
|
|
13937
14102
|
}
|
|
13938
14103
|
findAllMethods(node) {
|
|
13939
14104
|
const methods = [];
|
|
13940
|
-
const
|
|
13941
|
-
|
|
14105
|
+
const stack = [node];
|
|
14106
|
+
while (stack.length > 0) {
|
|
14107
|
+
const n = stack.pop();
|
|
14108
|
+
if (!n) continue;
|
|
13942
14109
|
if (n.type === "method_declaration" || n.type === "function_declaration") {
|
|
13943
14110
|
methods.push(n);
|
|
13944
14111
|
}
|
|
13945
14112
|
for (const child of n.children) {
|
|
13946
|
-
if (child)
|
|
14113
|
+
if (child) stack.push(child);
|
|
13947
14114
|
}
|
|
13948
|
-
}
|
|
13949
|
-
traverse(node);
|
|
14115
|
+
}
|
|
13950
14116
|
return methods;
|
|
13951
14117
|
}
|
|
13952
14118
|
getMethodName(method) {
|
|
@@ -13997,9 +14163,24 @@ var ConstantPropagator = class _ConstantPropagator {
|
|
|
13997
14163
|
// AST Visitor
|
|
13998
14164
|
// ===========================================================================
|
|
13999
14165
|
visit(node) {
|
|
14166
|
+
const stack = [node];
|
|
14167
|
+
while (stack.length > 0) {
|
|
14168
|
+
const current = stack.pop();
|
|
14169
|
+
if (this.visitOne(current)) continue;
|
|
14170
|
+
for (let i2 = current.children.length - 1; i2 >= 0; i2--) {
|
|
14171
|
+
stack.push(current.children[i2]);
|
|
14172
|
+
}
|
|
14173
|
+
}
|
|
14174
|
+
}
|
|
14175
|
+
/**
|
|
14176
|
+
* Visit a single node. Returns true if the handler already descended into
|
|
14177
|
+
* children (and the caller should NOT push them), false to fall through to
|
|
14178
|
+
* the default pre-order descent.
|
|
14179
|
+
*/
|
|
14180
|
+
visitOne(node) {
|
|
14000
14181
|
const line = getNodeLine(node);
|
|
14001
14182
|
if (this.unreachableLines.has(line)) {
|
|
14002
|
-
return;
|
|
14183
|
+
return true;
|
|
14003
14184
|
}
|
|
14004
14185
|
if (this.conditionStack.length > 0 && !this.lineConditions.has(line)) {
|
|
14005
14186
|
this.lineConditions.set(line, this.conditionStack[this.conditionStack.length - 1]);
|
|
@@ -14008,43 +14189,41 @@ var ConstantPropagator = class _ConstantPropagator {
|
|
|
14008
14189
|
case "method_declaration":
|
|
14009
14190
|
case "constructor_declaration":
|
|
14010
14191
|
this.handleMethodDeclaration(node);
|
|
14011
|
-
return;
|
|
14192
|
+
return true;
|
|
14012
14193
|
// Don't visit children directly, handleMethodDeclaration does it
|
|
14013
14194
|
case "local_variable_declaration":
|
|
14014
14195
|
this.handleVariableDeclaration(node);
|
|
14015
|
-
|
|
14196
|
+
return false;
|
|
14016
14197
|
case "assignment_expression":
|
|
14017
14198
|
this.handleAssignment(node);
|
|
14018
|
-
|
|
14199
|
+
return false;
|
|
14019
14200
|
case "update_expression":
|
|
14020
14201
|
this.handleUpdateExpression(node);
|
|
14021
|
-
|
|
14202
|
+
return false;
|
|
14022
14203
|
case "if_statement":
|
|
14023
14204
|
this.handleIfStatement(node);
|
|
14024
|
-
return;
|
|
14205
|
+
return true;
|
|
14025
14206
|
case "switch_expression":
|
|
14026
14207
|
case "switch_statement":
|
|
14027
14208
|
this.handleSwitch(node);
|
|
14028
|
-
return;
|
|
14209
|
+
return true;
|
|
14029
14210
|
case "ternary_expression":
|
|
14030
14211
|
this.handleTernary(node);
|
|
14031
|
-
|
|
14212
|
+
return false;
|
|
14032
14213
|
case "expression_statement":
|
|
14033
14214
|
this.handleExpressionStatement(node);
|
|
14034
|
-
|
|
14215
|
+
return false;
|
|
14035
14216
|
case "for_statement":
|
|
14036
14217
|
case "enhanced_for_statement":
|
|
14037
14218
|
case "while_statement":
|
|
14038
14219
|
case "do_statement":
|
|
14039
14220
|
this.handleLoopStatement(node);
|
|
14040
|
-
return;
|
|
14221
|
+
return true;
|
|
14041
14222
|
case "synchronized_statement":
|
|
14042
14223
|
this.handleSynchronizedStatement(node);
|
|
14043
|
-
return;
|
|
14224
|
+
return true;
|
|
14044
14225
|
default:
|
|
14045
|
-
|
|
14046
|
-
this.visit(child);
|
|
14047
|
-
}
|
|
14226
|
+
return false;
|
|
14048
14227
|
}
|
|
14049
14228
|
}
|
|
14050
14229
|
/**
|
|
@@ -14812,6 +14991,19 @@ var ConstantPropagator = class _ConstantPropagator {
|
|
|
14812
14991
|
return null;
|
|
14813
14992
|
}
|
|
14814
14993
|
isTaintedExpression(node) {
|
|
14994
|
+
const stack = [node];
|
|
14995
|
+
while (stack.length > 0) {
|
|
14996
|
+
const current = stack.pop();
|
|
14997
|
+
const result = this.isTaintedExpressionStep(current);
|
|
14998
|
+
if (result === true) return true;
|
|
14999
|
+
if (result === false) continue;
|
|
15000
|
+
for (let i2 = current.children.length - 1; i2 >= 0; i2--) {
|
|
15001
|
+
stack.push(current.children[i2]);
|
|
15002
|
+
}
|
|
15003
|
+
}
|
|
15004
|
+
return false;
|
|
15005
|
+
}
|
|
15006
|
+
isTaintedExpressionStep(node) {
|
|
14815
15007
|
const text = getNodeText2(node, this.source);
|
|
14816
15008
|
if (node.type === "method_invocation") {
|
|
14817
15009
|
const nameNode = node.childForFieldName("name");
|
|
@@ -15071,12 +15263,7 @@ var ConstantPropagator = class _ConstantPropagator {
|
|
|
15071
15263
|
}
|
|
15072
15264
|
return isTainted;
|
|
15073
15265
|
}
|
|
15074
|
-
|
|
15075
|
-
if (this.isTaintedExpression(child)) {
|
|
15076
|
-
return true;
|
|
15077
|
-
}
|
|
15078
|
-
}
|
|
15079
|
-
return false;
|
|
15266
|
+
return void 0;
|
|
15080
15267
|
}
|
|
15081
15268
|
checkCollectionTaint(node) {
|
|
15082
15269
|
const objectNode = node.childForFieldName("object");
|
|
@@ -15247,7 +15434,7 @@ function isFalsePositive(result, sinkLine, taintedVar) {
|
|
|
15247
15434
|
if (varValue && varValue.type !== "unknown" && !result.tainted.has(taintedVar)) {
|
|
15248
15435
|
return { isFalsePositive: true, reason: `variable_is_constant: ${varValue.value}` };
|
|
15249
15436
|
}
|
|
15250
|
-
if (result.symbols.
|
|
15437
|
+
if (result.symbols.has(taintedVar) && !result.tainted.has(taintedVar)) {
|
|
15251
15438
|
return { isFalsePositive: true, reason: "variable_not_tainted" };
|
|
15252
15439
|
}
|
|
15253
15440
|
return { isFalsePositive: false, reason: null };
|
|
@@ -560,18 +560,90 @@ function extractJSClassInfo(node) {
|
|
|
560
560
|
end_line: node.endPosition.row + 1,
|
|
561
561
|
};
|
|
562
562
|
}
|
|
563
|
+
/**
|
|
564
|
+
* Extract the name of a `decorator` node (TypeScript / NestJS / Angular etc).
|
|
565
|
+
*
|
|
566
|
+
* The grammar permits three shapes:
|
|
567
|
+
* @Foo → decorator > identifier
|
|
568
|
+
* @Foo('bar') → decorator > call_expression > identifier
|
|
569
|
+
* @ns.Foo → decorator > member_expression (use .property)
|
|
570
|
+
* @ns.Foo('bar') → decorator > call_expression > member_expression
|
|
571
|
+
*/
|
|
572
|
+
function extractDecoratorName(node) {
|
|
573
|
+
// tree-sitter-typescript stores the decorator expression as namedChild(0)
|
|
574
|
+
const child = node.namedChildCount > 0 ? node.namedChild(0) : null;
|
|
575
|
+
if (!child)
|
|
576
|
+
return null;
|
|
577
|
+
if (child.type === 'identifier')
|
|
578
|
+
return getNodeText(child);
|
|
579
|
+
if (child.type === 'call_expression') {
|
|
580
|
+
const fn = child.childForFieldName('function');
|
|
581
|
+
if (fn) {
|
|
582
|
+
if (fn.type === 'identifier')
|
|
583
|
+
return getNodeText(fn);
|
|
584
|
+
if (fn.type === 'member_expression') {
|
|
585
|
+
const propNode = fn.childForFieldName('property');
|
|
586
|
+
if (propNode)
|
|
587
|
+
return getNodeText(propNode);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
if (child.type === 'member_expression') {
|
|
592
|
+
const propNode = child.childForFieldName('property');
|
|
593
|
+
if (propNode)
|
|
594
|
+
return getNodeText(propNode);
|
|
595
|
+
}
|
|
596
|
+
return null;
|
|
597
|
+
}
|
|
563
598
|
/**
|
|
564
599
|
* Extract JavaScript methods from a class body.
|
|
600
|
+
*
|
|
601
|
+
* Method-level decorators (TS / NestJS / Angular) appear as preceding
|
|
602
|
+
* `decorator` siblings of `method_definition` inside `class_body`. We accumulate
|
|
603
|
+
* them as we walk children and attach to the very next `method_definition`.
|
|
604
|
+
*
|
|
605
|
+
* IMPORTANT: `pendingDecorators` is reset on ANY non-decorator class member
|
|
606
|
+
* (field, accessor, abstract signature, …), not just method_definition. A
|
|
607
|
+
* decorator preceding a field like
|
|
608
|
+
*
|
|
609
|
+
* @Inject('USER_REPO') private repo: Repository<User>;
|
|
610
|
+
* @Get('search') async search(...) { ... }
|
|
611
|
+
*
|
|
612
|
+
* belongs to the field, not the method below. Failing to reset after the
|
|
613
|
+
* field_definition would silently transfer `Inject` onto `search.annotations`,
|
|
614
|
+
* polluting the IR consumed by taint-matcher.ts (`matchesAnnotation` against
|
|
615
|
+
* `pattern.method_annotation`).
|
|
565
616
|
*/
|
|
566
617
|
function extractJSMethods(body) {
|
|
567
618
|
const methods = [];
|
|
619
|
+
let pendingDecorators = [];
|
|
568
620
|
for (let i = 0; i < body.childCount; i++) {
|
|
569
621
|
const child = body.child(i);
|
|
570
622
|
if (!child)
|
|
571
623
|
continue;
|
|
624
|
+
if (child.type === 'decorator') {
|
|
625
|
+
const name = extractDecoratorName(child);
|
|
626
|
+
if (name)
|
|
627
|
+
pendingDecorators.push(name);
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
// Tree-sitter emits comments as anonymous children of `class_body`.
|
|
631
|
+
// A `// note` line between a decorator and its method must NOT clear
|
|
632
|
+
// the pending decorator list — skip without resetting.
|
|
633
|
+
if (child.type === 'comment')
|
|
634
|
+
continue;
|
|
572
635
|
if (child.type === 'method_definition') {
|
|
573
|
-
|
|
636
|
+
const m = extractJSMethodInfo(child);
|
|
637
|
+
if (pendingDecorators.length > 0) {
|
|
638
|
+
m.annotations = pendingDecorators;
|
|
639
|
+
}
|
|
640
|
+
methods.push(m);
|
|
574
641
|
}
|
|
642
|
+
// Reset pending decorators on ANY non-decorator, non-comment child
|
|
643
|
+
// (method, field, accessor, abstract_method_signature,
|
|
644
|
+
// public_field_definition, …). Decorators only ever apply to the
|
|
645
|
+
// immediately-following member.
|
|
646
|
+
pendingDecorators = [];
|
|
575
647
|
}
|
|
576
648
|
return methods;
|
|
577
649
|
}
|
|
@@ -776,10 +848,21 @@ function extractJSParameters(params) {
|
|
|
776
848
|
// type_annotation includes the leading ':'; strip it for storage parity with other languages
|
|
777
849
|
paramType = getNodeText(typeNode).replace(/^:\s*/, '');
|
|
778
850
|
}
|
|
851
|
+
// Parameter decorators (NestJS @Query/@Param/@Body, Angular @Inject, etc.)
|
|
852
|
+
// appear as `decorator` children of the required_parameter node itself.
|
|
853
|
+
const decorators = [];
|
|
854
|
+
for (let j = 0; j < child.childCount; j++) {
|
|
855
|
+
const c = child.child(j);
|
|
856
|
+
if (c && c.type === 'decorator') {
|
|
857
|
+
const name = extractDecoratorName(c);
|
|
858
|
+
if (name)
|
|
859
|
+
decorators.push(name);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
779
862
|
parameters.push({
|
|
780
863
|
name: paramName,
|
|
781
864
|
type: paramType,
|
|
782
|
-
annotations:
|
|
865
|
+
annotations: decorators,
|
|
783
866
|
line: child.startPosition.row + 1,
|
|
784
867
|
});
|
|
785
868
|
}
|