codesift-mcp 0.4.0 → 0.5.3
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/README.md +94 -25
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +8 -6
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/platform.d.ts.map +1 -1
- package/dist/cli/platform.js +12 -14
- package/dist/cli/platform.js.map +1 -1
- package/dist/cli/setup.d.ts +1 -1
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +30 -6
- package/dist/cli/setup.js.map +1 -1
- package/dist/formatters.d.ts +2 -2
- package/dist/formatters.d.ts.map +1 -1
- package/dist/formatters.js +23 -0
- package/dist/formatters.js.map +1 -1
- package/dist/instructions.d.ts +1 -1
- package/dist/instructions.d.ts.map +1 -1
- package/dist/instructions.js +21 -21
- package/dist/parser/extractors/php.d.ts.map +1 -1
- package/dist/parser/extractors/php.js +37 -29
- package/dist/parser/extractors/php.js.map +1 -1
- package/dist/parser/extractors/typescript.d.ts.map +1 -1
- package/dist/parser/extractors/typescript.js +43 -0
- package/dist/parser/extractors/typescript.js.map +1 -1
- package/dist/parser/parse-cache.d.ts +39 -0
- package/dist/parser/parse-cache.d.ts.map +1 -0
- package/dist/parser/parse-cache.js +87 -0
- package/dist/parser/parse-cache.js.map +1 -0
- package/dist/parser/parser-manager.d.ts +1 -1
- package/dist/parser/parser-manager.d.ts.map +1 -1
- package/dist/parser/parser-manager.js +14 -5
- package/dist/parser/parser-manager.js.map +1 -1
- package/dist/parser/stub-languages.d.ts +2 -0
- package/dist/parser/stub-languages.d.ts.map +1 -0
- package/dist/parser/stub-languages.js +5 -0
- package/dist/parser/stub-languages.js.map +1 -0
- package/dist/register-tool-loaders.d.ts +130 -0
- package/dist/register-tool-loaders.d.ts.map +1 -0
- package/dist/register-tool-loaders.js +212 -0
- package/dist/register-tool-loaders.js.map +1 -0
- package/dist/register-tools.d.ts +2 -2
- package/dist/register-tools.d.ts.map +1 -1
- package/dist/register-tools.js +355 -634
- package/dist/register-tools.js.map +1 -1
- package/dist/search/tool-ranker.d.ts +90 -0
- package/dist/search/tool-ranker.d.ts.map +1 -0
- package/dist/search/tool-ranker.js +420 -0
- package/dist/search/tool-ranker.js.map +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +23 -14
- package/dist/server.js.map +1 -1
- package/dist/storage/usage-tracker.d.ts.map +1 -1
- package/dist/storage/usage-tracker.js +4 -1
- package/dist/storage/usage-tracker.js.map +1 -1
- package/dist/tools/astro-actions.d.ts +54 -0
- package/dist/tools/astro-actions.d.ts.map +1 -0
- package/dist/tools/astro-actions.js +561 -0
- package/dist/tools/astro-actions.js.map +1 -0
- package/dist/tools/astro-audit.d.ts +87 -0
- package/dist/tools/astro-audit.d.ts.map +1 -0
- package/dist/tools/astro-audit.js +345 -0
- package/dist/tools/astro-audit.js.map +1 -0
- package/dist/tools/astro-content-collections.d.ts +44 -0
- package/dist/tools/astro-content-collections.d.ts.map +1 -0
- package/dist/tools/astro-content-collections.js +630 -0
- package/dist/tools/astro-content-collections.js.map +1 -0
- package/dist/tools/astro-islands.d.ts +3 -1
- package/dist/tools/astro-islands.d.ts.map +1 -1
- package/dist/tools/astro-islands.js +19 -4
- package/dist/tools/astro-islands.js.map +1 -1
- package/dist/tools/astro-migration.d.ts +31 -0
- package/dist/tools/astro-migration.d.ts.map +1 -0
- package/dist/tools/astro-migration.js +378 -0
- package/dist/tools/astro-migration.js.map +1 -0
- package/dist/tools/async-correctness.d.ts +26 -0
- package/dist/tools/async-correctness.d.ts.map +1 -0
- package/dist/tools/async-correctness.js +166 -0
- package/dist/tools/async-correctness.js.map +1 -0
- package/dist/tools/django-view-security-tools.d.ts +32 -0
- package/dist/tools/django-view-security-tools.d.ts.map +1 -0
- package/dist/tools/django-view-security-tools.js +184 -0
- package/dist/tools/django-view-security-tools.js.map +1 -0
- package/dist/tools/fastapi-depends.d.ts +63 -0
- package/dist/tools/fastapi-depends.d.ts.map +1 -0
- package/dist/tools/fastapi-depends.js +191 -0
- package/dist/tools/fastapi-depends.js.map +1 -0
- package/dist/tools/hono-analyze-app.js +1 -9
- package/dist/tools/hono-analyze-app.js.map +1 -1
- package/dist/tools/hono-api-contract.d.ts.map +1 -1
- package/dist/tools/hono-api-contract.js +41 -9
- package/dist/tools/hono-api-contract.js.map +1 -1
- package/dist/tools/hono-context-flow.js +1 -9
- package/dist/tools/hono-context-flow.js.map +1 -1
- package/dist/tools/hono-dead-routes.d.ts.map +1 -1
- package/dist/tools/hono-dead-routes.js +2 -9
- package/dist/tools/hono-dead-routes.js.map +1 -1
- package/dist/tools/hono-entry-resolver.d.ts +27 -0
- package/dist/tools/hono-entry-resolver.d.ts.map +1 -0
- package/dist/tools/hono-entry-resolver.js +31 -0
- package/dist/tools/hono-entry-resolver.js.map +1 -0
- package/dist/tools/hono-inline-analyze.js +1 -9
- package/dist/tools/hono-inline-analyze.js.map +1 -1
- package/dist/tools/hono-middleware-chain.d.ts +24 -6
- package/dist/tools/hono-middleware-chain.d.ts.map +1 -1
- package/dist/tools/hono-middleware-chain.js +77 -40
- package/dist/tools/hono-middleware-chain.js.map +1 -1
- package/dist/tools/hono-modules.js +1 -9
- package/dist/tools/hono-modules.js.map +1 -1
- package/dist/tools/hono-response-types.js +1 -9
- package/dist/tools/hono-response-types.js.map +1 -1
- package/dist/tools/hono-rpc-types.js +1 -9
- package/dist/tools/hono-rpc-types.js.map +1 -1
- package/dist/tools/hono-security.d.ts +14 -4
- package/dist/tools/hono-security.d.ts.map +1 -1
- package/dist/tools/hono-security.js +185 -14
- package/dist/tools/hono-security.js.map +1 -1
- package/dist/tools/hono-visualize.js +1 -9
- package/dist/tools/hono-visualize.js.map +1 -1
- package/dist/tools/nest-ext-tools.d.ts +115 -0
- package/dist/tools/nest-ext-tools.d.ts.map +1 -1
- package/dist/tools/nest-ext-tools.js +393 -0
- package/dist/tools/nest-ext-tools.js.map +1 -1
- package/dist/tools/nest-tools.d.ts +27 -0
- package/dist/tools/nest-tools.d.ts.map +1 -1
- package/dist/tools/nest-tools.js +137 -37
- package/dist/tools/nest-tools.js.map +1 -1
- package/dist/tools/nextjs-component-readers.d.ts +101 -0
- package/dist/tools/nextjs-component-readers.d.ts.map +1 -0
- package/dist/tools/nextjs-component-readers.js +287 -0
- package/dist/tools/nextjs-component-readers.js.map +1 -0
- package/dist/tools/nextjs-component-tools.d.ts +8 -78
- package/dist/tools/nextjs-component-tools.d.ts.map +1 -1
- package/dist/tools/nextjs-component-tools.js +9 -257
- package/dist/tools/nextjs-component-tools.js.map +1 -1
- package/dist/tools/nextjs-framework-audit-tools.d.ts +24 -1
- package/dist/tools/nextjs-framework-audit-tools.d.ts.map +1 -1
- package/dist/tools/nextjs-framework-audit-tools.js +184 -1
- package/dist/tools/nextjs-framework-audit-tools.js.map +1 -1
- package/dist/tools/nextjs-route-readers.d.ts +81 -0
- package/dist/tools/nextjs-route-readers.d.ts.map +1 -0
- package/dist/tools/nextjs-route-readers.js +340 -0
- package/dist/tools/nextjs-route-readers.js.map +1 -0
- package/dist/tools/nextjs-route-tools.d.ts +7 -71
- package/dist/tools/nextjs-route-tools.d.ts.map +1 -1
- package/dist/tools/nextjs-route-tools.js +9 -327
- package/dist/tools/nextjs-route-tools.js.map +1 -1
- package/dist/tools/pattern-tools.d.ts.map +1 -1
- package/dist/tools/pattern-tools.js +92 -2
- package/dist/tools/pattern-tools.js.map +1 -1
- package/dist/tools/php-tools.d.ts +14 -5
- package/dist/tools/php-tools.d.ts.map +1 -1
- package/dist/tools/php-tools.js +166 -64
- package/dist/tools/php-tools.js.map +1 -1
- package/dist/tools/plan-turn-tools.d.ts +89 -0
- package/dist/tools/plan-turn-tools.d.ts.map +1 -0
- package/dist/tools/plan-turn-tools.js +508 -0
- package/dist/tools/plan-turn-tools.js.map +1 -0
- package/dist/tools/project-tools.d.ts +1 -1
- package/dist/tools/project-tools.js +1 -1
- package/dist/tools/project-tools.js.map +1 -1
- package/dist/tools/pydantic-models.d.ts +46 -0
- package/dist/tools/pydantic-models.d.ts.map +1 -0
- package/dist/tools/pydantic-models.js +249 -0
- package/dist/tools/pydantic-models.js.map +1 -0
- package/dist/tools/python-audit.d.ts +40 -0
- package/dist/tools/python-audit.d.ts.map +1 -0
- package/dist/tools/python-audit.js +244 -0
- package/dist/tools/python-audit.js.map +1 -0
- package/dist/tools/python-constants-tools.d.ts +44 -0
- package/dist/tools/python-constants-tools.d.ts.map +1 -0
- package/dist/tools/python-constants-tools.js +525 -0
- package/dist/tools/python-constants-tools.js.map +1 -0
- package/dist/tools/react-tools.d.ts +46 -1
- package/dist/tools/react-tools.d.ts.map +1 -1
- package/dist/tools/react-tools.js +126 -1
- package/dist/tools/react-tools.js.map +1 -1
- package/dist/tools/review-diff-tools.d.ts +5 -0
- package/dist/tools/review-diff-tools.d.ts.map +1 -1
- package/dist/tools/review-diff-tools.js +109 -3
- package/dist/tools/review-diff-tools.js.map +1 -1
- package/dist/tools/search-tools.d.ts +3 -2
- package/dist/tools/search-tools.d.ts.map +1 -1
- package/dist/tools/search-tools.js +16 -3
- package/dist/tools/search-tools.js.map +1 -1
- package/dist/tools/sql-tools.d.ts +40 -0
- package/dist/tools/sql-tools.d.ts.map +1 -1
- package/dist/tools/sql-tools.js +123 -0
- package/dist/tools/sql-tools.js.map +1 -1
- package/dist/tools/symbol-tools.d.ts.map +1 -1
- package/dist/tools/symbol-tools.js +7 -10
- package/dist/tools/symbol-tools.js.map +1 -1
- package/dist/tools/taint-tools.d.ts +43 -0
- package/dist/tools/taint-tools.d.ts.map +1 -0
- package/dist/tools/taint-tools.js +922 -0
- package/dist/tools/taint-tools.js.map +1 -0
- package/dist/utils/import-graph.d.ts +6 -0
- package/dist/utils/import-graph.d.ts.map +1 -1
- package/dist/utils/import-graph.js +43 -7
- package/dist/utils/import-graph.js.map +1 -1
- package/package.json +3 -3
- package/rules/codesift.md +51 -13
- package/rules/codesift.mdc +51 -13
- package/rules/codex.md +51 -13
- package/rules/gemini.md +51 -13
|
@@ -0,0 +1,922 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { getParser } from "../parser/parser-manager.js";
|
|
4
|
+
import { detectSrcLayout, resolvePythonImport } from "../utils/python-import-resolver.js";
|
|
5
|
+
import { getCodeIndex } from "./index-tools.js";
|
|
6
|
+
const DEFAULT_MAX_DEPTH = 4;
|
|
7
|
+
const DEFAULT_MAX_TRACES = 50;
|
|
8
|
+
const DEFAULT_SOURCE_PATTERNS = [
|
|
9
|
+
"request.GET",
|
|
10
|
+
"request.POST",
|
|
11
|
+
"request.body",
|
|
12
|
+
"request.data",
|
|
13
|
+
"request.headers",
|
|
14
|
+
"request.COOKIES",
|
|
15
|
+
"request.META",
|
|
16
|
+
];
|
|
17
|
+
const DEFAULT_SINK_PATTERNS = [
|
|
18
|
+
"redirect",
|
|
19
|
+
"mark_safe",
|
|
20
|
+
"cursor.execute",
|
|
21
|
+
"subprocess",
|
|
22
|
+
"requests",
|
|
23
|
+
"httpx",
|
|
24
|
+
"open",
|
|
25
|
+
"session-write",
|
|
26
|
+
];
|
|
27
|
+
const KNOWN_SANITIZERS = new Set([
|
|
28
|
+
"escape",
|
|
29
|
+
"conditional_escape",
|
|
30
|
+
"urlquote",
|
|
31
|
+
"quote",
|
|
32
|
+
"quote_plus",
|
|
33
|
+
]);
|
|
34
|
+
function clonePath(path) {
|
|
35
|
+
return {
|
|
36
|
+
source: { ...path.source },
|
|
37
|
+
hops: path.hops.map((hop) => ({ ...hop })),
|
|
38
|
+
heuristic: path.heuristic,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function pathKey(path) {
|
|
42
|
+
return JSON.stringify({
|
|
43
|
+
source: path.source,
|
|
44
|
+
hops: path.hops,
|
|
45
|
+
heuristic: path.heuristic,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
function dedupePaths(paths) {
|
|
49
|
+
const seen = new Set();
|
|
50
|
+
const result = [];
|
|
51
|
+
for (const path of paths) {
|
|
52
|
+
const key = pathKey(path);
|
|
53
|
+
if (seen.has(key))
|
|
54
|
+
continue;
|
|
55
|
+
seen.add(key);
|
|
56
|
+
result.push(path);
|
|
57
|
+
}
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
function cloneEnv(env) {
|
|
61
|
+
const next = new Map();
|
|
62
|
+
for (const [name, paths] of env.entries()) {
|
|
63
|
+
next.set(name, paths.map(clonePath));
|
|
64
|
+
}
|
|
65
|
+
return next;
|
|
66
|
+
}
|
|
67
|
+
function mergeEnvs(...envs) {
|
|
68
|
+
const merged = new Map();
|
|
69
|
+
for (const env of envs) {
|
|
70
|
+
for (const [name, paths] of env.entries()) {
|
|
71
|
+
const existing = merged.get(name) ?? [];
|
|
72
|
+
merged.set(name, dedupePaths([...existing, ...paths.map(clonePath)]));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return merged;
|
|
76
|
+
}
|
|
77
|
+
function appendHop(paths, hop, options) {
|
|
78
|
+
return dedupePaths(paths.map((path) => ({
|
|
79
|
+
source: { ...path.source },
|
|
80
|
+
hops: [...path.hops.map((entry) => ({ ...entry })), { ...hop }],
|
|
81
|
+
heuristic: path.heuristic || Boolean(options?.heuristic),
|
|
82
|
+
})));
|
|
83
|
+
}
|
|
84
|
+
function computeConfidence(path) {
|
|
85
|
+
if (path.heuristic)
|
|
86
|
+
return "medium";
|
|
87
|
+
if (path.hops.length >= 4)
|
|
88
|
+
return "medium";
|
|
89
|
+
return "high";
|
|
90
|
+
}
|
|
91
|
+
function lineForNode(symbol, node) {
|
|
92
|
+
return symbol.start_line + node.startPosition.row;
|
|
93
|
+
}
|
|
94
|
+
function codeForNode(node) {
|
|
95
|
+
return node.text.split("\n")[0]?.trim() ?? node.text.trim();
|
|
96
|
+
}
|
|
97
|
+
function getAttributePath(node) {
|
|
98
|
+
if (!node)
|
|
99
|
+
return null;
|
|
100
|
+
if (node.type === "identifier")
|
|
101
|
+
return node.text;
|
|
102
|
+
if (node.type === "attribute") {
|
|
103
|
+
const objectNode = node.childForFieldName("object") ?? node.namedChild(0);
|
|
104
|
+
const attributeNode = node.childForFieldName("attribute") ?? node.namedChild(1);
|
|
105
|
+
const objectPath = getAttributePath(objectNode);
|
|
106
|
+
const attributePath = getAttributePath(attributeNode);
|
|
107
|
+
if (!objectPath || !attributePath)
|
|
108
|
+
return null;
|
|
109
|
+
return `${objectPath}.${attributePath}`;
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
function getCallArguments(argsNode) {
|
|
114
|
+
if (!argsNode)
|
|
115
|
+
return [];
|
|
116
|
+
const args = [];
|
|
117
|
+
let index = 0;
|
|
118
|
+
for (const child of argsNode.namedChildren) {
|
|
119
|
+
if (child.type === "keyword_argument") {
|
|
120
|
+
const keywordNode = child.namedChildren[0];
|
|
121
|
+
const valueNode = child.namedChildren[1];
|
|
122
|
+
if (!valueNode)
|
|
123
|
+
continue;
|
|
124
|
+
const arg = {
|
|
125
|
+
node: valueNode,
|
|
126
|
+
index,
|
|
127
|
+
};
|
|
128
|
+
if (keywordNode?.text)
|
|
129
|
+
arg.keyword = keywordNode.text;
|
|
130
|
+
args.push(arg);
|
|
131
|
+
index += 1;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
args.push({
|
|
135
|
+
node: child,
|
|
136
|
+
index,
|
|
137
|
+
});
|
|
138
|
+
index += 1;
|
|
139
|
+
}
|
|
140
|
+
return args;
|
|
141
|
+
}
|
|
142
|
+
function getParameterName(node) {
|
|
143
|
+
switch (node.type) {
|
|
144
|
+
case "identifier":
|
|
145
|
+
return node.text;
|
|
146
|
+
case "default_parameter":
|
|
147
|
+
case "typed_parameter":
|
|
148
|
+
case "typed_default_parameter":
|
|
149
|
+
case "list_splat_pattern":
|
|
150
|
+
case "dictionary_splat_pattern":
|
|
151
|
+
return node.namedChildren[0]?.text ?? null;
|
|
152
|
+
default:
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function findFunctionNode(node) {
|
|
157
|
+
if (node.type === "function_definition" || node.type === "async_function_definition") {
|
|
158
|
+
return node;
|
|
159
|
+
}
|
|
160
|
+
for (const child of node.namedChildren) {
|
|
161
|
+
const found = findFunctionNode(child);
|
|
162
|
+
if (found)
|
|
163
|
+
return found;
|
|
164
|
+
}
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
function createSourcePath(sourceKind, symbol, node) {
|
|
168
|
+
return {
|
|
169
|
+
source: {
|
|
170
|
+
kind: sourceKind,
|
|
171
|
+
label: node.text,
|
|
172
|
+
file: symbol.file,
|
|
173
|
+
line: lineForNode(symbol, node),
|
|
174
|
+
symbol_name: symbol.name,
|
|
175
|
+
code: codeForNode(node),
|
|
176
|
+
},
|
|
177
|
+
hops: [],
|
|
178
|
+
heuristic: false,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
function identifierPaths(env, name) {
|
|
182
|
+
return (env.get(name) ?? []).map(clonePath);
|
|
183
|
+
}
|
|
184
|
+
function isAllowedPattern(allowed, kind, label) {
|
|
185
|
+
if (allowed.length === 0)
|
|
186
|
+
return true;
|
|
187
|
+
return allowed.some((pattern) => pattern === kind
|
|
188
|
+
|| label === pattern
|
|
189
|
+
|| label.includes(pattern)
|
|
190
|
+
|| kind.includes(pattern));
|
|
191
|
+
}
|
|
192
|
+
function buildSinkDescriptors() {
|
|
193
|
+
return [
|
|
194
|
+
{
|
|
195
|
+
kind: "redirect",
|
|
196
|
+
matches: (calleeText) => calleeText === "redirect"
|
|
197
|
+
|| calleeText.endsWith(".redirect")
|
|
198
|
+
|| calleeText === "HttpResponseRedirect"
|
|
199
|
+
|| calleeText === "HttpResponsePermanentRedirect",
|
|
200
|
+
pickArgs: (args) => args[0] ? [args[0]] : [],
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
kind: "mark_safe",
|
|
204
|
+
matches: (calleeText) => calleeText === "mark_safe" || calleeText.endsWith(".mark_safe"),
|
|
205
|
+
pickArgs: (args) => args[0] ? [args[0]] : [],
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
kind: "cursor.execute",
|
|
209
|
+
matches: (calleeText) => calleeText === "cursor.execute" || calleeText.endsWith(".execute"),
|
|
210
|
+
pickArgs: (args) => args[0] ? [args[0]] : [],
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
kind: "subprocess",
|
|
214
|
+
matches: (calleeText) => calleeText.startsWith("subprocess.")
|
|
215
|
+
|| calleeText.endsWith(".Popen")
|
|
216
|
+
|| calleeText.endsWith(".run")
|
|
217
|
+
|| calleeText.endsWith(".call")
|
|
218
|
+
|| calleeText.endsWith(".check_call")
|
|
219
|
+
|| calleeText.endsWith(".check_output"),
|
|
220
|
+
pickArgs: (args) => args[0] ? [args[0]] : [],
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
kind: "requests",
|
|
224
|
+
matches: (calleeText) => calleeText.startsWith("requests.")
|
|
225
|
+
|| calleeText.includes(".requests.")
|
|
226
|
+
|| calleeText.startsWith("httpx.")
|
|
227
|
+
|| calleeText.includes(".httpx."),
|
|
228
|
+
pickArgs: (args) => {
|
|
229
|
+
if (args.length === 0)
|
|
230
|
+
return [];
|
|
231
|
+
const urlKeyword = args.find((arg) => arg.keyword === "url");
|
|
232
|
+
if (urlKeyword)
|
|
233
|
+
return [urlKeyword];
|
|
234
|
+
return args[1] ? [args[1]] : [args[0]];
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
kind: "httpx",
|
|
239
|
+
matches: (calleeText) => calleeText.startsWith("httpx.")
|
|
240
|
+
|| calleeText.includes(".httpx."),
|
|
241
|
+
pickArgs: (args) => {
|
|
242
|
+
if (args.length === 0)
|
|
243
|
+
return [];
|
|
244
|
+
const urlKeyword = args.find((arg) => arg.keyword === "url");
|
|
245
|
+
if (urlKeyword)
|
|
246
|
+
return [urlKeyword];
|
|
247
|
+
return args[1] ? [args[1]] : [args[0]];
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
kind: "open",
|
|
252
|
+
matches: (calleeText) => calleeText === "open" || calleeText.endsWith(".open"),
|
|
253
|
+
pickArgs: (args) => args[0] ? [args[0]] : [],
|
|
254
|
+
},
|
|
255
|
+
];
|
|
256
|
+
}
|
|
257
|
+
function getImportModule(node) {
|
|
258
|
+
const moduleNode = node.childForFieldName("module_name");
|
|
259
|
+
if (!moduleNode)
|
|
260
|
+
return { module: "", level: 0 };
|
|
261
|
+
if (moduleNode.type === "relative_import") {
|
|
262
|
+
let level = 0;
|
|
263
|
+
for (let i = 0; i < moduleNode.childCount; i++) {
|
|
264
|
+
const child = moduleNode.child(i);
|
|
265
|
+
if (!child)
|
|
266
|
+
continue;
|
|
267
|
+
if (child.type === "import_prefix") {
|
|
268
|
+
level += (child.text.match(/\./g) ?? []).length;
|
|
269
|
+
}
|
|
270
|
+
else if (child.type === ".") {
|
|
271
|
+
level += 1;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
const dotted = moduleNode.namedChildren.find((child) => child.type === "dotted_name");
|
|
275
|
+
return { module: dotted?.text ?? "", level };
|
|
276
|
+
}
|
|
277
|
+
return { module: moduleNode.text, level: 0 };
|
|
278
|
+
}
|
|
279
|
+
async function loadFileContext(state, filePath) {
|
|
280
|
+
const cached = state.fileContextCache.get(filePath);
|
|
281
|
+
if (cached !== undefined)
|
|
282
|
+
return cached;
|
|
283
|
+
let source;
|
|
284
|
+
try {
|
|
285
|
+
source = await readFile(join(state.index.root, filePath), "utf-8");
|
|
286
|
+
}
|
|
287
|
+
catch {
|
|
288
|
+
state.fileContextCache.set(filePath, null);
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
const tree = state.pythonParser.parse(source);
|
|
292
|
+
const files = state.index.files.map((entry) => entry.path);
|
|
293
|
+
const srcLayout = detectSrcLayout(files);
|
|
294
|
+
const imports = new Map();
|
|
295
|
+
for (const node of tree.rootNode.namedChildren) {
|
|
296
|
+
if (node.type !== "import_from_statement")
|
|
297
|
+
continue;
|
|
298
|
+
const { module, level } = getImportModule(node);
|
|
299
|
+
const resolvedFile = resolvePythonImport({ module, level }, filePath, files, srcLayout);
|
|
300
|
+
if (!resolvedFile)
|
|
301
|
+
continue;
|
|
302
|
+
for (const child of node.namedChildren) {
|
|
303
|
+
if (child.type === "aliased_import") {
|
|
304
|
+
const importedNode = child.namedChildren[0];
|
|
305
|
+
const aliasNode = child.namedChildren[1];
|
|
306
|
+
if (importedNode && aliasNode) {
|
|
307
|
+
imports.set(aliasNode.text, {
|
|
308
|
+
imported_name: importedNode.text,
|
|
309
|
+
source_file: resolvedFile,
|
|
310
|
+
line: node.startPosition.row + 1,
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
if (child.type === "dotted_name") {
|
|
316
|
+
const importedName = child.text;
|
|
317
|
+
const localName = importedName.split(".").pop() ?? importedName;
|
|
318
|
+
imports.set(localName, {
|
|
319
|
+
imported_name: importedName,
|
|
320
|
+
source_file: resolvedFile,
|
|
321
|
+
line: node.startPosition.row + 1,
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
const context = { imports };
|
|
327
|
+
state.fileContextCache.set(filePath, context);
|
|
328
|
+
return context;
|
|
329
|
+
}
|
|
330
|
+
function hasPotentialSource(symbol) {
|
|
331
|
+
return symbol.source?.includes("request.") ?? false;
|
|
332
|
+
}
|
|
333
|
+
function hasPotentialSink(symbol) {
|
|
334
|
+
const source = symbol.source ?? "";
|
|
335
|
+
return source.includes("mark_safe")
|
|
336
|
+
|| source.includes("redirect(")
|
|
337
|
+
|| source.includes(".execute(")
|
|
338
|
+
|| source.includes("subprocess.")
|
|
339
|
+
|| source.includes("requests.")
|
|
340
|
+
|| source.includes("httpx.")
|
|
341
|
+
|| source.includes("open(")
|
|
342
|
+
|| source.includes("request.session");
|
|
343
|
+
}
|
|
344
|
+
async function loadCallableContext(symbol, state) {
|
|
345
|
+
const cached = state.callableCache.get(symbol.id);
|
|
346
|
+
if (cached !== undefined)
|
|
347
|
+
return cached;
|
|
348
|
+
if (!symbol.source) {
|
|
349
|
+
state.callableCache.set(symbol.id, null);
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
const tree = state.pythonParser.parse(symbol.source);
|
|
353
|
+
const functionNode = findFunctionNode(tree.rootNode);
|
|
354
|
+
if (!functionNode) {
|
|
355
|
+
state.callableCache.set(symbol.id, null);
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
const paramsNode = functionNode.childForFieldName("parameters");
|
|
359
|
+
const parameterNames = paramsNode
|
|
360
|
+
? paramsNode.namedChildren
|
|
361
|
+
.map(getParameterName)
|
|
362
|
+
.filter((name) => Boolean(name))
|
|
363
|
+
: [];
|
|
364
|
+
const context = {
|
|
365
|
+
node: functionNode,
|
|
366
|
+
parameter_names: parameterNames,
|
|
367
|
+
};
|
|
368
|
+
state.callableCache.set(symbol.id, context);
|
|
369
|
+
return context;
|
|
370
|
+
}
|
|
371
|
+
function resolveSelfMethod(currentSymbol, propertyName, state) {
|
|
372
|
+
if (!currentSymbol.parent)
|
|
373
|
+
return null;
|
|
374
|
+
const methods = state.methodsByParent.get(currentSymbol.parent) ?? [];
|
|
375
|
+
return methods.find((symbol) => symbol.name === propertyName) ?? null;
|
|
376
|
+
}
|
|
377
|
+
async function resolveHelperTarget(currentSymbol, calleeNode, state) {
|
|
378
|
+
const calleeText = getAttributePath(calleeNode) ?? calleeNode.text;
|
|
379
|
+
if (calleeNode.type === "identifier") {
|
|
380
|
+
const sameFile = (state.symbolsByName.get(calleeText) ?? [])
|
|
381
|
+
.filter((symbol) => symbol.file === currentSymbol.file
|
|
382
|
+
&& symbol.id !== currentSymbol.id
|
|
383
|
+
&& (symbol.kind === "function" || symbol.kind === "class" || symbol.kind === "method"));
|
|
384
|
+
if (sameFile.length === 1)
|
|
385
|
+
return sameFile[0];
|
|
386
|
+
const fileContext = await loadFileContext(state, currentSymbol.file);
|
|
387
|
+
const imported = fileContext?.imports.get(calleeText);
|
|
388
|
+
if (imported) {
|
|
389
|
+
const importedMatch = (state.symbolsByName.get(imported.imported_name) ?? [])
|
|
390
|
+
.find((symbol) => symbol.file === imported.source_file);
|
|
391
|
+
if (importedMatch)
|
|
392
|
+
return importedMatch;
|
|
393
|
+
}
|
|
394
|
+
const unique = (state.symbolsByName.get(calleeText) ?? [])
|
|
395
|
+
.filter((symbol) => symbol.file.endsWith(".py") && symbol.id !== currentSymbol.id)
|
|
396
|
+
.filter((symbol) => symbol.kind === "function" || symbol.kind === "method" || symbol.kind === "class");
|
|
397
|
+
if (unique.length === 1)
|
|
398
|
+
return unique[0];
|
|
399
|
+
return null;
|
|
400
|
+
}
|
|
401
|
+
if (calleeNode.type === "attribute") {
|
|
402
|
+
const objectNode = calleeNode.childForFieldName("object") ?? calleeNode.namedChild(0);
|
|
403
|
+
const propertyNode = calleeNode.childForFieldName("attribute") ?? calleeNode.namedChild(1);
|
|
404
|
+
const objectName = getAttributePath(objectNode);
|
|
405
|
+
const propertyName = propertyNode?.text;
|
|
406
|
+
if ((objectName === "self" || objectName === "cls") && propertyName) {
|
|
407
|
+
return resolveSelfMethod(currentSymbol, propertyName, state);
|
|
408
|
+
}
|
|
409
|
+
const importedModule = objectName ? (await loadFileContext(state, currentSymbol.file))?.imports.get(objectName) : null;
|
|
410
|
+
if (importedModule && propertyName) {
|
|
411
|
+
const candidates = state.symbolsByName.get(propertyName) ?? [];
|
|
412
|
+
const importedMatch = candidates.find((symbol) => symbol.file === importedModule.source_file);
|
|
413
|
+
if (importedMatch)
|
|
414
|
+
return importedMatch;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
419
|
+
function matchesRequestSource(attributePath) {
|
|
420
|
+
if (!attributePath)
|
|
421
|
+
return null;
|
|
422
|
+
if (attributePath === "request.GET" || attributePath.startsWith("request.GET."))
|
|
423
|
+
return "request.GET";
|
|
424
|
+
if (attributePath === "request.POST" || attributePath.startsWith("request.POST."))
|
|
425
|
+
return "request.POST";
|
|
426
|
+
if (attributePath === "request.body")
|
|
427
|
+
return "request.body";
|
|
428
|
+
if (attributePath === "request.data" || attributePath.startsWith("request.data."))
|
|
429
|
+
return "request.data";
|
|
430
|
+
if (attributePath === "request.headers" || attributePath.startsWith("request.headers."))
|
|
431
|
+
return "request.headers";
|
|
432
|
+
if (attributePath === "request.COOKIES" || attributePath.startsWith("request.COOKIES."))
|
|
433
|
+
return "request.COOKIES";
|
|
434
|
+
if (attributePath === "request.META" || attributePath.startsWith("request.META."))
|
|
435
|
+
return "request.META";
|
|
436
|
+
return null;
|
|
437
|
+
}
|
|
438
|
+
function isSessionTarget(node) {
|
|
439
|
+
if (node.type === "attribute") {
|
|
440
|
+
const path = getAttributePath(node);
|
|
441
|
+
return path === "request.session";
|
|
442
|
+
}
|
|
443
|
+
if (node.type === "subscript") {
|
|
444
|
+
const base = node.childForFieldName("value") ?? node.namedChild(0);
|
|
445
|
+
return getAttributePath(base) === "request.session";
|
|
446
|
+
}
|
|
447
|
+
return false;
|
|
448
|
+
}
|
|
449
|
+
function sinkTraceKey(trace) {
|
|
450
|
+
return JSON.stringify({
|
|
451
|
+
entry_symbol: trace.entry_symbol,
|
|
452
|
+
entry_file: trace.entry_file,
|
|
453
|
+
source: trace.source,
|
|
454
|
+
sink: trace.sink,
|
|
455
|
+
hops: trace.hops,
|
|
456
|
+
heuristic: trace.heuristic,
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
function addTrace(state, entrySymbol, currentSymbol, sinkKind, sinkNode, paths) {
|
|
460
|
+
if (state.truncated)
|
|
461
|
+
return;
|
|
462
|
+
for (const path of paths) {
|
|
463
|
+
if (state.traces.length >= state.maxTraces) {
|
|
464
|
+
state.truncated = true;
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
const trace = {
|
|
468
|
+
entry_symbol: entrySymbol.name,
|
|
469
|
+
entry_file: entrySymbol.file,
|
|
470
|
+
source: { ...path.source },
|
|
471
|
+
sink: {
|
|
472
|
+
kind: sinkKind,
|
|
473
|
+
label: sinkNode.text,
|
|
474
|
+
file: currentSymbol.file,
|
|
475
|
+
line: lineForNode(currentSymbol, sinkNode),
|
|
476
|
+
symbol_name: currentSymbol.name,
|
|
477
|
+
code: codeForNode(sinkNode),
|
|
478
|
+
},
|
|
479
|
+
hops: path.hops.map((hop) => ({ ...hop })),
|
|
480
|
+
confidence: computeConfidence(path),
|
|
481
|
+
heuristic: path.heuristic,
|
|
482
|
+
};
|
|
483
|
+
const key = sinkTraceKey(trace);
|
|
484
|
+
if (state.traceKeys.has(key))
|
|
485
|
+
continue;
|
|
486
|
+
state.traceKeys.add(key);
|
|
487
|
+
state.traces.push(trace);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
async function evaluateExpression(node, symbol, env, state, context) {
|
|
491
|
+
switch (node.type) {
|
|
492
|
+
case "identifier":
|
|
493
|
+
return identifierPaths(env, node.text);
|
|
494
|
+
case "attribute": {
|
|
495
|
+
const sourceKind = matchesRequestSource(getAttributePath(node));
|
|
496
|
+
if (sourceKind)
|
|
497
|
+
return [createSourcePath(sourceKind, symbol, node)];
|
|
498
|
+
const objectNode = node.childForFieldName("object") ?? node.namedChild(0);
|
|
499
|
+
if (!objectNode)
|
|
500
|
+
return [];
|
|
501
|
+
const basePaths = await evaluateExpression(objectNode, symbol, env, state, context);
|
|
502
|
+
if (basePaths.length === 0)
|
|
503
|
+
return [];
|
|
504
|
+
return appendHop(basePaths, {
|
|
505
|
+
kind: "attribute",
|
|
506
|
+
file: symbol.file,
|
|
507
|
+
line: lineForNode(symbol, node),
|
|
508
|
+
symbol_name: symbol.name,
|
|
509
|
+
detail: `attribute access ${node.text}`,
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
case "subscript": {
|
|
513
|
+
const baseNode = node.childForFieldName("value") ?? node.namedChild(0);
|
|
514
|
+
const sourceKind = matchesRequestSource(getAttributePath(baseNode));
|
|
515
|
+
if (sourceKind)
|
|
516
|
+
return [createSourcePath(sourceKind, symbol, node)];
|
|
517
|
+
const basePaths = baseNode
|
|
518
|
+
? await evaluateExpression(baseNode, symbol, env, state, context)
|
|
519
|
+
: [];
|
|
520
|
+
if (basePaths.length === 0)
|
|
521
|
+
return [];
|
|
522
|
+
return appendHop(basePaths, {
|
|
523
|
+
kind: "container",
|
|
524
|
+
file: symbol.file,
|
|
525
|
+
line: lineForNode(symbol, node),
|
|
526
|
+
symbol_name: symbol.name,
|
|
527
|
+
detail: `container access ${node.text}`,
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
case "string": {
|
|
531
|
+
const interpolated = node.namedChildren
|
|
532
|
+
.filter((child) => child.type === "interpolation")
|
|
533
|
+
.flatMap((child) => child.namedChildren);
|
|
534
|
+
if (interpolated.length === 0)
|
|
535
|
+
return [];
|
|
536
|
+
const paths = [];
|
|
537
|
+
for (const child of interpolated) {
|
|
538
|
+
paths.push(...await evaluateExpression(child, symbol, env, state, context));
|
|
539
|
+
}
|
|
540
|
+
if (paths.length === 0)
|
|
541
|
+
return [];
|
|
542
|
+
return appendHop(paths, {
|
|
543
|
+
kind: "container",
|
|
544
|
+
file: symbol.file,
|
|
545
|
+
line: lineForNode(symbol, node),
|
|
546
|
+
symbol_name: symbol.name,
|
|
547
|
+
detail: `formatted string ${node.text}`,
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
case "list":
|
|
551
|
+
case "tuple":
|
|
552
|
+
case "dictionary":
|
|
553
|
+
case "set": {
|
|
554
|
+
const paths = [];
|
|
555
|
+
for (const child of node.namedChildren) {
|
|
556
|
+
if (child.type === "pair") {
|
|
557
|
+
const valueNode = child.namedChildren[1];
|
|
558
|
+
if (!valueNode)
|
|
559
|
+
continue;
|
|
560
|
+
paths.push(...await evaluateExpression(valueNode, symbol, env, state, context));
|
|
561
|
+
}
|
|
562
|
+
else {
|
|
563
|
+
paths.push(...await evaluateExpression(child, symbol, env, state, context));
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
if (paths.length === 0)
|
|
567
|
+
return [];
|
|
568
|
+
return appendHop(paths, {
|
|
569
|
+
kind: "container",
|
|
570
|
+
file: symbol.file,
|
|
571
|
+
line: lineForNode(symbol, node),
|
|
572
|
+
symbol_name: symbol.name,
|
|
573
|
+
detail: `container literal ${node.text}`,
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
case "binary_operator":
|
|
577
|
+
case "boolean_operator":
|
|
578
|
+
case "comparison_operator": {
|
|
579
|
+
const paths = [];
|
|
580
|
+
for (const child of node.namedChildren) {
|
|
581
|
+
paths.push(...await evaluateExpression(child, symbol, env, state, context));
|
|
582
|
+
}
|
|
583
|
+
return dedupePaths(paths);
|
|
584
|
+
}
|
|
585
|
+
case "parenthesized_expression":
|
|
586
|
+
return node.namedChildren[0]
|
|
587
|
+
? await evaluateExpression(node.namedChildren[0], symbol, env, state, context)
|
|
588
|
+
: [];
|
|
589
|
+
case "conditional_expression": {
|
|
590
|
+
const paths = [];
|
|
591
|
+
for (const child of node.namedChildren) {
|
|
592
|
+
paths.push(...await evaluateExpression(child, symbol, env, state, context));
|
|
593
|
+
}
|
|
594
|
+
return dedupePaths(paths);
|
|
595
|
+
}
|
|
596
|
+
case "call": {
|
|
597
|
+
const calleeNode = node.childForFieldName("function") ?? node.namedChild(0);
|
|
598
|
+
const argsNode = node.childForFieldName("arguments") ?? node.namedChild(1);
|
|
599
|
+
const callArgs = getCallArguments(argsNode);
|
|
600
|
+
const calleeText = getAttributePath(calleeNode) ?? calleeNode?.text ?? "";
|
|
601
|
+
const sourceKind = matchesRequestSource(calleeText);
|
|
602
|
+
if (sourceKind && calleeText.endsWith(".get")) {
|
|
603
|
+
return [createSourcePath(sourceKind, symbol, node)];
|
|
604
|
+
}
|
|
605
|
+
const evaluatedArgs = await Promise.all(callArgs.map(async (arg) => ({
|
|
606
|
+
arg,
|
|
607
|
+
paths: await evaluateExpression(arg.node, symbol, env, state, context),
|
|
608
|
+
})));
|
|
609
|
+
for (const descriptor of state.sinkDescriptors) {
|
|
610
|
+
if (!descriptor.matches(calleeText))
|
|
611
|
+
continue;
|
|
612
|
+
if (!isAllowedPattern(state.defaultSinks, descriptor.kind, calleeText))
|
|
613
|
+
continue;
|
|
614
|
+
const selectedArgs = descriptor.pickArgs(callArgs);
|
|
615
|
+
const taintedArgs = selectedArgs.flatMap((selected) => evaluatedArgs
|
|
616
|
+
.filter((entry) => entry.arg.index === selected.index)
|
|
617
|
+
.flatMap((entry) => entry.paths));
|
|
618
|
+
if (taintedArgs.length > 0) {
|
|
619
|
+
addTrace(state, context.entrySymbol, symbol, descriptor.kind, node, dedupePaths(taintedArgs));
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
const calleeLeaf = calleeText.split(".").pop() ?? calleeText;
|
|
623
|
+
if (KNOWN_SANITIZERS.has(calleeLeaf))
|
|
624
|
+
return [];
|
|
625
|
+
const taintedInputs = evaluatedArgs
|
|
626
|
+
.filter((entry) => entry.paths.length > 0)
|
|
627
|
+
.map((entry) => entry);
|
|
628
|
+
if (taintedInputs.length === 0)
|
|
629
|
+
return [];
|
|
630
|
+
const helperTarget = calleeNode
|
|
631
|
+
? await resolveHelperTarget(symbol, calleeNode, state)
|
|
632
|
+
: null;
|
|
633
|
+
if (helperTarget && context.depth < state.maxDepth && !context.callStack.includes(helperTarget.id)) {
|
|
634
|
+
const helperContext = await loadCallableContext(helperTarget, state);
|
|
635
|
+
if (helperContext) {
|
|
636
|
+
const helperEnv = new Map();
|
|
637
|
+
for (const entry of taintedInputs) {
|
|
638
|
+
const paramName = helperContext.parameter_names[entry.arg.index];
|
|
639
|
+
if (!paramName)
|
|
640
|
+
continue;
|
|
641
|
+
helperEnv.set(paramName, appendHop(entry.paths, {
|
|
642
|
+
kind: "call",
|
|
643
|
+
file: symbol.file,
|
|
644
|
+
line: lineForNode(symbol, node),
|
|
645
|
+
symbol_name: symbol.name,
|
|
646
|
+
detail: `call ${calleeText} -> parameter ${paramName}`,
|
|
647
|
+
}));
|
|
648
|
+
}
|
|
649
|
+
const helperResult = await analyzeCallableSymbol(helperTarget, helperEnv, state, {
|
|
650
|
+
entrySymbol: context.entrySymbol,
|
|
651
|
+
depth: context.depth + 1,
|
|
652
|
+
callStack: [...context.callStack, helperTarget.id],
|
|
653
|
+
});
|
|
654
|
+
if (helperResult.return_paths.length > 0) {
|
|
655
|
+
return appendHop(helperResult.return_paths, {
|
|
656
|
+
kind: "call",
|
|
657
|
+
file: symbol.file,
|
|
658
|
+
line: lineForNode(symbol, node),
|
|
659
|
+
symbol_name: symbol.name,
|
|
660
|
+
detail: `return from ${calleeText}`,
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
return [];
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
return appendHop(taintedInputs.flatMap((entry) => entry.paths), {
|
|
667
|
+
kind: "call",
|
|
668
|
+
file: symbol.file,
|
|
669
|
+
line: lineForNode(symbol, node),
|
|
670
|
+
symbol_name: symbol.name,
|
|
671
|
+
detail: `heuristic propagation through ${calleeText}`,
|
|
672
|
+
}, { heuristic: true });
|
|
673
|
+
}
|
|
674
|
+
default:
|
|
675
|
+
return [];
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
async function analyzeAssignment(assignmentNode, symbol, env, state, context) {
|
|
679
|
+
const lhs = assignmentNode.childForFieldName("left") ?? assignmentNode.namedChild(0);
|
|
680
|
+
const rhs = assignmentNode.childForFieldName("right") ?? assignmentNode.namedChild(1);
|
|
681
|
+
const nextEnv = cloneEnv(env);
|
|
682
|
+
if (!lhs || !rhs)
|
|
683
|
+
return nextEnv;
|
|
684
|
+
const rhsPaths = await evaluateExpression(rhs, symbol, env, state, context);
|
|
685
|
+
if (rhsPaths.length === 0)
|
|
686
|
+
return nextEnv;
|
|
687
|
+
if (lhs.type === "identifier") {
|
|
688
|
+
nextEnv.set(lhs.text, appendHop(rhsPaths, {
|
|
689
|
+
kind: "assignment",
|
|
690
|
+
file: symbol.file,
|
|
691
|
+
line: lineForNode(symbol, assignmentNode),
|
|
692
|
+
symbol_name: symbol.name,
|
|
693
|
+
detail: `${lhs.text} = ${rhs.text}`,
|
|
694
|
+
}));
|
|
695
|
+
return nextEnv;
|
|
696
|
+
}
|
|
697
|
+
if (isSessionTarget(lhs) && isAllowedPattern(state.defaultSinks, "session-write", lhs.text)) {
|
|
698
|
+
addTrace(state, context.entrySymbol, symbol, "session-write", assignmentNode, rhsPaths);
|
|
699
|
+
}
|
|
700
|
+
return nextEnv;
|
|
701
|
+
}
|
|
702
|
+
async function analyzeConditionalLike(node, symbol, env, state, context) {
|
|
703
|
+
const conditionNodes = node.namedChildren.filter((child) => child.type !== "block" && child.type !== "else_clause" && child.type !== "elif_clause");
|
|
704
|
+
for (const condition of conditionNodes) {
|
|
705
|
+
await evaluateExpression(condition, symbol, env, state, context);
|
|
706
|
+
}
|
|
707
|
+
const branchResults = [];
|
|
708
|
+
let hasElseLike = false;
|
|
709
|
+
for (const child of node.namedChildren) {
|
|
710
|
+
if (child.type === "block") {
|
|
711
|
+
branchResults.push(await analyzeBlock(child, symbol, cloneEnv(env), state, context));
|
|
712
|
+
continue;
|
|
713
|
+
}
|
|
714
|
+
if (child.type === "elif_clause") {
|
|
715
|
+
hasElseLike = true;
|
|
716
|
+
branchResults.push(await analyzeConditionalLike(child, symbol, cloneEnv(env), state, context));
|
|
717
|
+
continue;
|
|
718
|
+
}
|
|
719
|
+
if (child.type === "else_clause") {
|
|
720
|
+
hasElseLike = true;
|
|
721
|
+
const elseBlock = child.namedChildren.find((grandchild) => grandchild.type === "block");
|
|
722
|
+
if (elseBlock)
|
|
723
|
+
branchResults.push(await analyzeBlock(elseBlock, symbol, cloneEnv(env), state, context));
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
const baseEnvs = hasElseLike ? [] : [env];
|
|
727
|
+
return {
|
|
728
|
+
env: mergeEnvs(...baseEnvs, ...branchResults.map((entry) => entry.env)),
|
|
729
|
+
return_paths: dedupePaths(branchResults.flatMap((entry) => entry.return_paths)),
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
async function analyzeLoopLike(node, symbol, env, state, context) {
|
|
733
|
+
for (const child of node.namedChildren) {
|
|
734
|
+
if (child.type === "block")
|
|
735
|
+
continue;
|
|
736
|
+
await evaluateExpression(child, symbol, env, state, context);
|
|
737
|
+
}
|
|
738
|
+
const blockResults = [];
|
|
739
|
+
for (const child of node.namedChildren) {
|
|
740
|
+
if (child.type !== "block")
|
|
741
|
+
continue;
|
|
742
|
+
blockResults.push(await analyzeBlock(child, symbol, cloneEnv(env), state, context));
|
|
743
|
+
}
|
|
744
|
+
return {
|
|
745
|
+
env: mergeEnvs(env, ...blockResults.map((entry) => entry.env)),
|
|
746
|
+
return_paths: dedupePaths(blockResults.flatMap((entry) => entry.return_paths)),
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
async function analyzeStatement(node, symbol, env, state, context) {
|
|
750
|
+
switch (node.type) {
|
|
751
|
+
case "expression_statement": {
|
|
752
|
+
const inner = node.namedChildren[0];
|
|
753
|
+
if (!inner)
|
|
754
|
+
return { env, return_paths: [] };
|
|
755
|
+
if (inner.type === "assignment") {
|
|
756
|
+
return {
|
|
757
|
+
env: await analyzeAssignment(inner, symbol, env, state, context),
|
|
758
|
+
return_paths: [],
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
await evaluateExpression(inner, symbol, env, state, context);
|
|
762
|
+
return { env, return_paths: [] };
|
|
763
|
+
}
|
|
764
|
+
case "return_statement": {
|
|
765
|
+
const valueNode = node.namedChildren[0];
|
|
766
|
+
if (!valueNode)
|
|
767
|
+
return { env, return_paths: [] };
|
|
768
|
+
const valuePaths = await evaluateExpression(valueNode, symbol, env, state, context);
|
|
769
|
+
if (valuePaths.length === 0)
|
|
770
|
+
return { env, return_paths: [] };
|
|
771
|
+
return {
|
|
772
|
+
env,
|
|
773
|
+
return_paths: appendHop(valuePaths, {
|
|
774
|
+
kind: "return",
|
|
775
|
+
file: symbol.file,
|
|
776
|
+
line: lineForNode(symbol, node),
|
|
777
|
+
symbol_name: symbol.name,
|
|
778
|
+
detail: `return ${valueNode.text}`,
|
|
779
|
+
}),
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
case "if_statement":
|
|
783
|
+
case "elif_clause":
|
|
784
|
+
return await analyzeConditionalLike(node, symbol, env, state, context);
|
|
785
|
+
case "for_statement":
|
|
786
|
+
case "while_statement":
|
|
787
|
+
case "with_statement":
|
|
788
|
+
case "try_statement":
|
|
789
|
+
return await analyzeLoopLike(node, symbol, env, state, context);
|
|
790
|
+
case "pass_statement":
|
|
791
|
+
case "break_statement":
|
|
792
|
+
case "continue_statement":
|
|
793
|
+
return { env, return_paths: [] };
|
|
794
|
+
case "function_definition":
|
|
795
|
+
case "async_function_definition":
|
|
796
|
+
case "class_definition":
|
|
797
|
+
case "decorated_definition":
|
|
798
|
+
return { env, return_paths: [] };
|
|
799
|
+
default: {
|
|
800
|
+
for (const child of node.namedChildren) {
|
|
801
|
+
if (child.type === "block") {
|
|
802
|
+
await analyzeBlock(child, symbol, cloneEnv(env), state, context);
|
|
803
|
+
}
|
|
804
|
+
else {
|
|
805
|
+
await evaluateExpression(child, symbol, env, state, context);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
return { env, return_paths: [] };
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
async function analyzeBlock(blockNode, symbol, env, state, context) {
|
|
813
|
+
let currentEnv = cloneEnv(env);
|
|
814
|
+
let returnPaths = [];
|
|
815
|
+
for (const child of blockNode.namedChildren) {
|
|
816
|
+
if (state.truncated)
|
|
817
|
+
break;
|
|
818
|
+
const result = await analyzeStatement(child, symbol, currentEnv, state, context);
|
|
819
|
+
currentEnv = result.env;
|
|
820
|
+
if (result.return_paths.length > 0) {
|
|
821
|
+
returnPaths = dedupePaths([...returnPaths, ...result.return_paths]);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
return {
|
|
825
|
+
env: currentEnv,
|
|
826
|
+
return_paths: returnPaths,
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
async function analyzeCallableSymbol(symbol, initialEnv, state, context) {
|
|
830
|
+
const callableContext = await loadCallableContext(symbol, state);
|
|
831
|
+
if (!callableContext) {
|
|
832
|
+
return { env: initialEnv, return_paths: [] };
|
|
833
|
+
}
|
|
834
|
+
const bodyNode = callableContext.node.childForFieldName("body");
|
|
835
|
+
if (!bodyNode) {
|
|
836
|
+
return { env: initialEnv, return_paths: [] };
|
|
837
|
+
}
|
|
838
|
+
return await analyzeBlock(bodyNode, symbol, initialEnv, state, context);
|
|
839
|
+
}
|
|
840
|
+
function shouldAnalyzeSymbol(symbol, filePattern) {
|
|
841
|
+
if (!symbol.file.endsWith(".py"))
|
|
842
|
+
return false;
|
|
843
|
+
if (filePattern && !symbol.file.includes(filePattern))
|
|
844
|
+
return false;
|
|
845
|
+
if (!symbol.source)
|
|
846
|
+
return false;
|
|
847
|
+
if (symbol.kind !== "function" && symbol.kind !== "method")
|
|
848
|
+
return false;
|
|
849
|
+
return hasPotentialSource(symbol) || hasPotentialSink(symbol);
|
|
850
|
+
}
|
|
851
|
+
function buildState(index, pythonParser, options) {
|
|
852
|
+
const symbolsByName = new Map();
|
|
853
|
+
const methodsByParent = new Map();
|
|
854
|
+
for (const symbol of index.symbols) {
|
|
855
|
+
const named = symbolsByName.get(symbol.name) ?? [];
|
|
856
|
+
named.push(symbol);
|
|
857
|
+
symbolsByName.set(symbol.name, named);
|
|
858
|
+
if (symbol.parent && symbol.kind === "method") {
|
|
859
|
+
const methods = methodsByParent.get(symbol.parent) ?? [];
|
|
860
|
+
methods.push(symbol);
|
|
861
|
+
methodsByParent.set(symbol.parent, methods);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
return {
|
|
865
|
+
index,
|
|
866
|
+
pythonParser,
|
|
867
|
+
symbolsByName,
|
|
868
|
+
methodsByParent,
|
|
869
|
+
callableCache: new Map(),
|
|
870
|
+
fileContextCache: new Map(),
|
|
871
|
+
defaultSources: options?.source_patterns?.length
|
|
872
|
+
? [...options.source_patterns]
|
|
873
|
+
: [...DEFAULT_SOURCE_PATTERNS],
|
|
874
|
+
defaultSinks: options?.sink_patterns?.length
|
|
875
|
+
? [...options.sink_patterns]
|
|
876
|
+
: [...DEFAULT_SINK_PATTERNS],
|
|
877
|
+
maxDepth: options?.max_depth ?? DEFAULT_MAX_DEPTH,
|
|
878
|
+
maxTraces: options?.max_traces ?? DEFAULT_MAX_TRACES,
|
|
879
|
+
sinkDescriptors: buildSinkDescriptors(),
|
|
880
|
+
traceKeys: new Set(),
|
|
881
|
+
traces: [],
|
|
882
|
+
truncated: false,
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
export async function taintTrace(repo, options) {
|
|
886
|
+
const framework = options?.framework ?? "python-django";
|
|
887
|
+
if (framework !== "python-django") {
|
|
888
|
+
throw new Error(`taint_trace is not implemented for framework "${framework}" yet.`);
|
|
889
|
+
}
|
|
890
|
+
const index = await getCodeIndex(repo);
|
|
891
|
+
if (!index) {
|
|
892
|
+
throw new Error(`Repository "${repo}" not found.`);
|
|
893
|
+
}
|
|
894
|
+
const pythonParser = await getParser("python");
|
|
895
|
+
if (!pythonParser) {
|
|
896
|
+
throw new Error("Python parser unavailable");
|
|
897
|
+
}
|
|
898
|
+
const state = buildState(index, pythonParser, options);
|
|
899
|
+
const candidates = index.symbols
|
|
900
|
+
.filter((symbol) => shouldAnalyzeSymbol(symbol, options?.file_pattern))
|
|
901
|
+
.sort((a, b) => a.file.localeCompare(b.file) || a.start_line - b.start_line);
|
|
902
|
+
for (const symbol of candidates) {
|
|
903
|
+
if (state.truncated)
|
|
904
|
+
break;
|
|
905
|
+
await analyzeCallableSymbol(symbol, new Map(), state, {
|
|
906
|
+
entrySymbol: symbol,
|
|
907
|
+
depth: 0,
|
|
908
|
+
callStack: [symbol.id],
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
const filtered = state.traces.filter((trace) => isAllowedPattern(state.defaultSources, trace.source.kind, trace.source.label)
|
|
912
|
+
&& isAllowedPattern(state.defaultSinks, trace.sink.kind, trace.sink.label));
|
|
913
|
+
return {
|
|
914
|
+
framework,
|
|
915
|
+
analyzed_symbols: candidates.length,
|
|
916
|
+
source_patterns: [...state.defaultSources],
|
|
917
|
+
sink_patterns: [...state.defaultSinks],
|
|
918
|
+
traces: filtered,
|
|
919
|
+
truncated: state.truncated,
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
//# sourceMappingURL=taint-tools.js.map
|