progmune-runtime 3.7.9 → 3.7.10
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/CHANGELOG.md +9 -0
- package/dist/sdk.js +1 -1
- package/package.json +7 -3
- package/tools/extract_framework_django.py +269 -0
- package/tools/extract_framework_flask.py +169 -0
- package/tools/extract_framework_py.py +237 -0
- package/tools/extract_ir.py +1109 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [3.7.10] — 2026-09-01
|
|
4
|
+
|
|
5
|
+
### 安装态 Python 全链路修复(npm 包正确性)
|
|
6
|
+
|
|
7
|
+
- **修复**:`tools/extract_ir.py` + 三个框架扫描器(`extract_framework_py.py` / `extract_framework_django.py` / `extract_framework_flask.py`)加入 npm 包 `files` 白名单——**此前安装态(MCP 主产品形态)的 Python IR 提取与框架结构扫描全部静默失效**(脚本不在包内,execSync 失败被 best-effort 吞掉)。M1/M2 验证时发现的既有遗留,非 3.7.8/3.7.9 引入
|
|
8
|
+
- **安装态端到端验证**(此前从未有过):打包 tarball → 临时目录 npm install → 用安装态的 dist 跑 evaluateTrust——FastAPI 合成项目检出 `FASTAPI_ROUTE_NO_AUTH`(框架扫描生效)、Python 盲测项目检出 SSG 违规(IR 提取生效)、**安装态与仓库态 A/B 完全一致**;`tools/__pycache__` 未入包(files 精确文件路径)
|
|
9
|
+
- **落地页**:语言覆盖现状更新入版(C 注解驱动 Beta 行 + 框架适配 7/13 行 + 双语 i18n + 证据链接)
|
|
10
|
+
- 包体:377 文件(+4 个 .py,约 71.5KB)
|
|
11
|
+
|
|
3
12
|
## [3.7.9] — 2026-08-28
|
|
4
13
|
|
|
5
14
|
### Flask / Fastify / Next.js 框架结构适配(M4——框架适配 7/13)
|
package/dist/sdk.js
CHANGED
|
@@ -20,7 +20,7 @@ const risk_model_1 = require("./risk-model");
|
|
|
20
20
|
const protocol_knowledge_1 = require("./protocol-knowledge");
|
|
21
21
|
const evidence_repository_1 = require("./evidence-repository");
|
|
22
22
|
/** Runtime version — stable public identifier. Internal layers evolve underneath. */
|
|
23
|
-
exports.RUNTIME_VERSION = "3.7.
|
|
23
|
+
exports.RUNTIME_VERSION = "3.7.10";
|
|
24
24
|
function verify(filePath) {
|
|
25
25
|
const cert = (0, certify_1.certify)(filePath);
|
|
26
26
|
const kb = (0, protocol_knowledge_1.buildKnowledgeBase)();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "progmune-runtime",
|
|
3
|
-
"version": "3.7.
|
|
3
|
+
"version": "3.7.10",
|
|
4
4
|
"description": "Progmune — AI Trust Decision Engine. Verify AI-generated code before it reaches production. Outputs APPROVED / NEEDS_REVIEW / BLOCKED with evidence.",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist/",
|
|
@@ -8,7 +8,11 @@
|
|
|
8
8
|
"c-aliases.json",
|
|
9
9
|
"CHANGELOG.md",
|
|
10
10
|
"docs/Progmune_项目全解.html",
|
|
11
|
-
"docs/Progmune_投资人白皮书_v2.0.html"
|
|
11
|
+
"docs/Progmune_投资人白皮书_v2.0.html",
|
|
12
|
+
"tools/extract_ir.py",
|
|
13
|
+
"tools/extract_framework_py.py",
|
|
14
|
+
"tools/extract_framework_django.py",
|
|
15
|
+
"tools/extract_framework_flask.py"
|
|
12
16
|
],
|
|
13
17
|
"main": "dist/mcp-server.mjs",
|
|
14
18
|
"bin": {
|
|
@@ -110,4 +114,4 @@
|
|
|
110
114
|
"tsx": "^4.22.4",
|
|
111
115
|
"vitest": "^3.2.6"
|
|
112
116
|
}
|
|
113
|
-
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Django Framework Structure Extractor — 框架结构扫描(M2)
|
|
4
|
+
|
|
5
|
+
从 Python AST 提取 Django 结构:
|
|
6
|
+
1) urlpatterns 解析:url()/path()/re_path() → 视图引用(FBV views.foo /
|
|
7
|
+
CBV Foo.as_view() / include('module.urls') 递归)
|
|
8
|
+
2) 视图注册表:
|
|
9
|
+
- FBV:函数装饰器(login_required/permission_required/staff_member_required/
|
|
10
|
+
user_passes_test/authentication_decorator 等 *auth* 名装饰器)
|
|
11
|
+
- CBV:基类(APIView/View/generics.*/LoginRequiredMixin/
|
|
12
|
+
PermissionRequiredMixin)、方法(get/post/put/patch/delete)、
|
|
13
|
+
@method_decorator、DRF permission_classes(AllowAny/IsAuthenticated/
|
|
14
|
+
IsAdminUser/其他类名)
|
|
15
|
+
- @api_view 装饰器:methods + permission_classes kwarg
|
|
16
|
+
3) 每个 urlpattern → 视图解析(按短名匹配注册表)
|
|
17
|
+
|
|
18
|
+
输出 JSON:{hasDjango, routes:[{pattern,urlname,view,kind,file}],
|
|
19
|
+
views:{name:{file,decorators,methods,permissionClasses,isDrf,isProtected}},
|
|
20
|
+
filesScanned}
|
|
21
|
+
|
|
22
|
+
用法:python3 extract_framework_django.py <projectRoot> <outJson>
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import ast
|
|
26
|
+
import json
|
|
27
|
+
import os
|
|
28
|
+
import re
|
|
29
|
+
import sys
|
|
30
|
+
|
|
31
|
+
SKIP_DIRS = {"tests", "test", "deps", "venv", "env", "node_modules", "vendor",
|
|
32
|
+
".git", "migrations", "__pycache__", "scripts", "docs",
|
|
33
|
+
"staticfiles", "static"}
|
|
34
|
+
|
|
35
|
+
AUTH_DECORATORS = {
|
|
36
|
+
"login_required", "permission_required", "staff_member_required",
|
|
37
|
+
"user_passes_test", "authentication_decorator", "login_required_decorator",
|
|
38
|
+
"require_http_methods",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
AUTH_MIXINS = {"LoginRequiredMixin", "PermissionRequiredMixin",
|
|
42
|
+
"UserPassesTestMixin", "StaffMemberRequiredMixin"}
|
|
43
|
+
|
|
44
|
+
DRF_BASES = {"APIView", "GenericAPIView", "RetrieveAPIView", "ListAPIView",
|
|
45
|
+
"CreateAPIView", "UpdateAPIView", "DestroyAPIView",
|
|
46
|
+
"RetrieveUpdateAPIView", "ListCreateAPIView",
|
|
47
|
+
"RetrieveUpdateDestroyAPIView", "ViewSet", "ModelViewSet",
|
|
48
|
+
"GenericViewSet", "ReadOnlyModelViewSet"}
|
|
49
|
+
|
|
50
|
+
MUTATION_METHODS = {"post", "put", "patch", "delete"}
|
|
51
|
+
|
|
52
|
+
VIEW_BASES = DRF_BASES | {"View", "TemplateView", "FormView", "CreateView",
|
|
53
|
+
"UpdateView", "DeleteView", "ListView", "DetailView"}
|
|
54
|
+
|
|
55
|
+
OPEN_PERMISSIONS = {"AllowAny"}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def name_of(node):
|
|
59
|
+
if isinstance(node, ast.Name):
|
|
60
|
+
return node.id
|
|
61
|
+
if isinstance(node, ast.Attribute):
|
|
62
|
+
return node.attr
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def short_view_name(ref):
|
|
67
|
+
"""views.auth_lab → auth_lab;Foo.as_view() → Foo"""
|
|
68
|
+
if isinstance(ref, ast.Attribute):
|
|
69
|
+
return ref.attr
|
|
70
|
+
if isinstance(ref, ast.Call) and isinstance(ref.func, ast.Attribute):
|
|
71
|
+
return ref.func.value.attr if isinstance(ref.func.value, ast.Attribute) \
|
|
72
|
+
else name_of(ref.func.value)
|
|
73
|
+
if isinstance(ref, ast.Name):
|
|
74
|
+
return ref.id
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class ViewCollector(ast.NodeVisitor):
|
|
79
|
+
"""收集全部函数/类的视图特征(与 urlpatterns 解耦)"""
|
|
80
|
+
|
|
81
|
+
def __init__(self, filepath):
|
|
82
|
+
self.file = filepath
|
|
83
|
+
self.fbvs = {} # name -> {decorators, apiView: [methods]}
|
|
84
|
+
self.classes = {} # name -> {bases, methods, decorators, permissionClasses, isDrf, protected}
|
|
85
|
+
|
|
86
|
+
def visit_FunctionDef(self, node):
|
|
87
|
+
self._scan_function(node, async_=False)
|
|
88
|
+
self.generic_visit(node)
|
|
89
|
+
|
|
90
|
+
def visit_AsyncFunctionDef(self, node):
|
|
91
|
+
self._scan_function(node, async_=True)
|
|
92
|
+
self.generic_visit(node)
|
|
93
|
+
|
|
94
|
+
def _scan_function(self, node, async_):
|
|
95
|
+
decorators = []
|
|
96
|
+
api_view_methods = None
|
|
97
|
+
permission_classes = []
|
|
98
|
+
for dec in node.decorator_list:
|
|
99
|
+
if isinstance(dec, ast.Name):
|
|
100
|
+
decorators.append(dec.id)
|
|
101
|
+
elif isinstance(dec, ast.Call) and isinstance(dec.func, ast.Name):
|
|
102
|
+
decorators.append(dec.func.id)
|
|
103
|
+
if dec.func.id == "api_view":
|
|
104
|
+
if dec.args and isinstance(dec.args[0], (ast.List, ast.Tuple)):
|
|
105
|
+
api_view_methods = [
|
|
106
|
+
e.value.lower() for e in dec.args[0].elts
|
|
107
|
+
if isinstance(e, ast.Constant)
|
|
108
|
+
]
|
|
109
|
+
for kw in dec.keywords:
|
|
110
|
+
if kw.arg == "permission_classes" and isinstance(kw.value, (ast.List, ast.Tuple)):
|
|
111
|
+
permission_classes = [name_of(e) for e in kw.value.elts if name_of(e)]
|
|
112
|
+
elif isinstance(dec, ast.Call) and isinstance(dec.func, ast.Attribute):
|
|
113
|
+
decorators.append(dec.func.attr) # django.views.decorators.login_required
|
|
114
|
+
self.fbvs[node.name] = {
|
|
115
|
+
"file": self.file,
|
|
116
|
+
"decorators": decorators,
|
|
117
|
+
"apiViewMethods": api_view_methods,
|
|
118
|
+
"permissionClasses": permission_classes,
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
def visit_ClassDef(self, node):
|
|
122
|
+
bases = [name_of(b) for b in node.bases if name_of(b)]
|
|
123
|
+
methods = []
|
|
124
|
+
permission_classes = []
|
|
125
|
+
decorators = []
|
|
126
|
+
for item in node.body:
|
|
127
|
+
if isinstance(item, ast.FunctionDef) and item.name in (
|
|
128
|
+
"get", "post", "put", "patch", "delete", "create",
|
|
129
|
+
"update", "destroy", "list", "retrieve"):
|
|
130
|
+
methods.append(item.name)
|
|
131
|
+
if isinstance(item, ast.Assign):
|
|
132
|
+
for t in item.targets:
|
|
133
|
+
if isinstance(t, ast.Name) and t.id == "permission_classes":
|
|
134
|
+
if isinstance(item.value, (ast.List, ast.Tuple)):
|
|
135
|
+
permission_classes = [name_of(e) for e in item.value.elts if name_of(e)]
|
|
136
|
+
elif isinstance(item.value, ast.Name):
|
|
137
|
+
permission_classes = [item.value.id]
|
|
138
|
+
for dec in node.decorator_list:
|
|
139
|
+
if isinstance(dec, ast.Name):
|
|
140
|
+
decorators.append(dec.id)
|
|
141
|
+
elif isinstance(dec, ast.Call) and isinstance(dec.func, ast.Name):
|
|
142
|
+
decorators.append(dec.func.id)
|
|
143
|
+
elif isinstance(dec, ast.Call) and isinstance(dec.func, ast.Attribute):
|
|
144
|
+
decorators.append(dec.func.attr)
|
|
145
|
+
is_drf = any(b in DRF_BASES for b in bases)
|
|
146
|
+
has_mixin = any(b in AUTH_MIXINS for b in bases)
|
|
147
|
+
self.classes[node.name] = {
|
|
148
|
+
"file": self.file,
|
|
149
|
+
"bases": bases,
|
|
150
|
+
"methods": methods,
|
|
151
|
+
"decorators": decorators,
|
|
152
|
+
"permissionClasses": permission_classes,
|
|
153
|
+
"isDrf": is_drf,
|
|
154
|
+
"protectedByMixin": has_mixin,
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class UrlCollector(ast.NodeVisitor):
|
|
159
|
+
"""收集 urlpatterns 列表里的视图引用"""
|
|
160
|
+
|
|
161
|
+
def __init__(self, filepath):
|
|
162
|
+
self.file = filepath
|
|
163
|
+
self.routes = []
|
|
164
|
+
|
|
165
|
+
def visit_Assign(self, node):
|
|
166
|
+
for t in node.targets:
|
|
167
|
+
if isinstance(t, ast.Name) and t.id == "urlpatterns" and isinstance(node.value, (ast.List, ast.Tuple)):
|
|
168
|
+
for elt in node.value.elts:
|
|
169
|
+
self._scan_entry(elt)
|
|
170
|
+
self.generic_visit(node)
|
|
171
|
+
|
|
172
|
+
def _scan_entry(self, elt):
|
|
173
|
+
if not isinstance(elt, ast.Call):
|
|
174
|
+
return
|
|
175
|
+
fn = name_of(elt.func) # url / path / re_path
|
|
176
|
+
if fn not in ("url", "path", "re_path"):
|
|
177
|
+
return
|
|
178
|
+
pattern = ""
|
|
179
|
+
if elt.args and isinstance(elt.args[0], ast.Constant):
|
|
180
|
+
pattern = str(elt.args[0].value)
|
|
181
|
+
view_ref = elt.args[1] if len(elt.args) > 1 else None
|
|
182
|
+
if view_ref is None:
|
|
183
|
+
return
|
|
184
|
+
kind = "other"
|
|
185
|
+
view_name = None
|
|
186
|
+
if isinstance(view_ref, ast.Call) and isinstance(view_ref.func, ast.Attribute):
|
|
187
|
+
if view_ref.func.attr == "as_view":
|
|
188
|
+
kind = "cbv"
|
|
189
|
+
view_name = short_view_name(view_ref)
|
|
190
|
+
elif view_ref.func.id == "include" if isinstance(view_ref.func, ast.Name) else False:
|
|
191
|
+
kind = "include"
|
|
192
|
+
if view_name is None:
|
|
193
|
+
if isinstance(view_ref, ast.Name):
|
|
194
|
+
kind = "include" if False else "fbv"
|
|
195
|
+
view_name = view_ref.id
|
|
196
|
+
elif isinstance(view_ref, ast.Attribute):
|
|
197
|
+
kind = "fbv"
|
|
198
|
+
view_name = view_ref.attr
|
|
199
|
+
elif isinstance(view_ref, ast.Call) and isinstance(view_ref.func, ast.Name):
|
|
200
|
+
if view_ref.func.id == "include":
|
|
201
|
+
kind = "include"
|
|
202
|
+
else:
|
|
203
|
+
kind = "fbv"
|
|
204
|
+
view_name = view_ref.func.id
|
|
205
|
+
urlname = ""
|
|
206
|
+
for kw in elt.keywords:
|
|
207
|
+
if kw.arg == "name" and isinstance(kw.value, ast.Constant):
|
|
208
|
+
urlname = str(kw.value)
|
|
209
|
+
self.routes.append({
|
|
210
|
+
"pattern": pattern,
|
|
211
|
+
"urlname": urlname,
|
|
212
|
+
"view": view_name,
|
|
213
|
+
"kind": kind,
|
|
214
|
+
"file": self.file,
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def walk_py_files(root):
|
|
219
|
+
for dirpath, dirnames, filenames in os.walk(root):
|
|
220
|
+
dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS]
|
|
221
|
+
for fn in filenames:
|
|
222
|
+
if fn.endswith(".py") and not fn.startswith("test_"):
|
|
223
|
+
yield os.path.join(dirpath, fn)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def main():
|
|
227
|
+
root, out = sys.argv[1], sys.argv[2]
|
|
228
|
+
views, routes, files_scanned = {}, [], 0
|
|
229
|
+
for fp in walk_py_files(root):
|
|
230
|
+
files_scanned += 1
|
|
231
|
+
try:
|
|
232
|
+
with open(fp, "r", encoding="utf-8", errors="replace") as f:
|
|
233
|
+
tree = ast.parse(f.read(), filename=fp)
|
|
234
|
+
except (SyntaxError, UnicodeDecodeError, OSError):
|
|
235
|
+
continue
|
|
236
|
+
vc, uc = ViewCollector(fp), UrlCollector(fp)
|
|
237
|
+
vc.visit(tree)
|
|
238
|
+
uc.visit(tree)
|
|
239
|
+
for name, info in vc.fbvs.items():
|
|
240
|
+
info["kind"] = "fbv"
|
|
241
|
+
views[name] = info
|
|
242
|
+
for name, info in vc.classes.items():
|
|
243
|
+
info["kind"] = "cbv"
|
|
244
|
+
views[name] = info
|
|
245
|
+
routes.extend(uc.routes)
|
|
246
|
+
|
|
247
|
+
# 保护判定(扫描器只算事实,规则在 TS 检测器侧)
|
|
248
|
+
for name, info in views.items():
|
|
249
|
+
auth_dec = [d for d in info.get("decorators", [])
|
|
250
|
+
if d in AUTH_DECORATORS or "auth" in d.lower() or "login" in d.lower()]
|
|
251
|
+
info["authDecorators"] = auth_dec
|
|
252
|
+
if info["kind"] == "cbv":
|
|
253
|
+
pc = info.get("permissionClasses") or []
|
|
254
|
+
info["openPermission"] = (len(pc) == 0) or all(
|
|
255
|
+
p in OPEN_PERMISSIONS for p in pc
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
result = {
|
|
259
|
+
"hasDjango": bool(routes),
|
|
260
|
+
"routes": routes,
|
|
261
|
+
"views": views,
|
|
262
|
+
"filesScanned": files_scanned,
|
|
263
|
+
}
|
|
264
|
+
with open(out, "w", encoding="utf-8") as f:
|
|
265
|
+
json.dump(result, f, ensure_ascii=False, indent=2)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
if __name__ == "__main__":
|
|
269
|
+
main()
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Flask Framework Structure Extractor — 框架结构扫描(第 5 个框架适配)
|
|
4
|
+
|
|
5
|
+
从 Python AST 提取 Flask 结构:
|
|
6
|
+
1) 应用与蓝图:app = Flask(...) / bp = Blueprint(...) / register_blueprint
|
|
7
|
+
2) 路由:@app.route("/x", methods=[...]) / @bp.route(...) → handler 函数
|
|
8
|
+
3) 认证信号:
|
|
9
|
+
- handler 装饰器:login_required / permission_required / 自定义 *auth* 名
|
|
10
|
+
- 全局守卫:app.before_request(auth_fn) / bp.before_request(auth_fn),
|
|
11
|
+
auth_fn 名命中认证词表
|
|
12
|
+
4) 路由方法:methods kwarg(缺省 = GET only)
|
|
13
|
+
|
|
14
|
+
输出 JSON:{hasFlask, apps, blueprints, routes:[{method,path,handler,file,line,
|
|
15
|
+
authDecorators}], beforeRequestAuth:[names], filesScanned}
|
|
16
|
+
|
|
17
|
+
用法:python3 extract_framework_flask.py <projectRoot> <outJson>
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import ast
|
|
21
|
+
import json
|
|
22
|
+
import os
|
|
23
|
+
import sys
|
|
24
|
+
|
|
25
|
+
SKIP_DIRS = {"tests", "test", "deps", "venv", "env", "node_modules", "vendor",
|
|
26
|
+
".git", "migrations", "__pycache__", "scripts", "docs",
|
|
27
|
+
"staticfiles", "static"}
|
|
28
|
+
|
|
29
|
+
AUTH_WORDS = ("auth", "login", "permission", "token", "credential", "session",
|
|
30
|
+
"user")
|
|
31
|
+
|
|
32
|
+
MUTATION_METHODS = {"POST", "PUT", "PATCH", "DELETE"}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def name_of(node):
|
|
36
|
+
if isinstance(node, ast.Name):
|
|
37
|
+
return node.id
|
|
38
|
+
if isinstance(node, ast.Attribute):
|
|
39
|
+
return node.attr
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def is_auth_like(name):
|
|
44
|
+
if not name:
|
|
45
|
+
return False
|
|
46
|
+
ln = name.lower()
|
|
47
|
+
return any(w in ln for w in AUTH_WORDS)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class FlaskCollector(ast.NodeVisitor):
|
|
51
|
+
def __init__(self, filepath):
|
|
52
|
+
self.file = filepath
|
|
53
|
+
self.apps = []
|
|
54
|
+
self.blueprints = []
|
|
55
|
+
self.routes = []
|
|
56
|
+
self.before_requests = [] # 注册的 before_request 函数名
|
|
57
|
+
|
|
58
|
+
def visit_Assign(self, node):
|
|
59
|
+
if len(node.targets) == 1 and isinstance(node.targets[0], ast.Name):
|
|
60
|
+
v = node.value
|
|
61
|
+
if isinstance(v, ast.Call) and isinstance(v.func, ast.Name):
|
|
62
|
+
if v.func.id == "Flask":
|
|
63
|
+
self.apps.append(node.targets[0].id)
|
|
64
|
+
elif v.func.id == "Blueprint":
|
|
65
|
+
self.blueprints.append(node.targets[0].id)
|
|
66
|
+
self.generic_visit(node)
|
|
67
|
+
|
|
68
|
+
def visit_Expr(self, node):
|
|
69
|
+
# app.before_request(auth_fn) / app.register_blueprint(bp) 是表达式语句
|
|
70
|
+
v = node.value
|
|
71
|
+
if isinstance(v, ast.Call) and isinstance(v.func, ast.Attribute):
|
|
72
|
+
attr = v.func.attr
|
|
73
|
+
if attr == "before_request" and v.args:
|
|
74
|
+
fn_name = name_of(v.args[0])
|
|
75
|
+
if fn_name:
|
|
76
|
+
self.before_requests.append(fn_name)
|
|
77
|
+
if attr == "register_blueprint" and v.args:
|
|
78
|
+
bp = name_of(v.args[0])
|
|
79
|
+
if bp and bp not in self.blueprints:
|
|
80
|
+
self.blueprints.append(bp)
|
|
81
|
+
self.generic_visit(node)
|
|
82
|
+
|
|
83
|
+
def visit_FunctionDef(self, node):
|
|
84
|
+
self._scan(node)
|
|
85
|
+
self.generic_visit(node)
|
|
86
|
+
|
|
87
|
+
def visit_AsyncFunctionDef(self, node):
|
|
88
|
+
self._scan(node)
|
|
89
|
+
self.generic_visit(node)
|
|
90
|
+
|
|
91
|
+
def _scan(self, node):
|
|
92
|
+
auth_decorators = []
|
|
93
|
+
for dec in node.decorator_list:
|
|
94
|
+
if isinstance(dec, ast.Call) and isinstance(dec.func, ast.Attribute):
|
|
95
|
+
# @app.route("/x", methods=["POST"]) / @bp.route(...)
|
|
96
|
+
if dec.func.attr == "route":
|
|
97
|
+
target = name_of(dec.func.value)
|
|
98
|
+
path = dec.args[0].value if dec.args and isinstance(dec.args[0], ast.Constant) else ""
|
|
99
|
+
methods = ["GET"]
|
|
100
|
+
for kw in dec.keywords:
|
|
101
|
+
if kw.arg == "methods" and isinstance(kw.value, (ast.List, ast.Tuple)):
|
|
102
|
+
methods = [
|
|
103
|
+
e.value.upper() for e in kw.value.elts
|
|
104
|
+
if isinstance(e, ast.Constant)
|
|
105
|
+
]
|
|
106
|
+
self.routes.append({
|
|
107
|
+
"methods": methods,
|
|
108
|
+
"path": path,
|
|
109
|
+
"handler": node.name,
|
|
110
|
+
"file": self.file,
|
|
111
|
+
"line": node.lineno,
|
|
112
|
+
"target": target, # app / bp 变量名
|
|
113
|
+
})
|
|
114
|
+
elif isinstance(dec, ast.Name):
|
|
115
|
+
if is_auth_like(dec.id):
|
|
116
|
+
auth_decorators.append(dec.id)
|
|
117
|
+
elif isinstance(dec, ast.Call) and isinstance(dec.func, ast.Name):
|
|
118
|
+
if is_auth_like(dec.func.id):
|
|
119
|
+
auth_decorators.append(dec.func.id)
|
|
120
|
+
elif isinstance(dec, ast.Call) and isinstance(dec.func, ast.Attribute):
|
|
121
|
+
if is_auth_like(dec.func.attr):
|
|
122
|
+
auth_decorators.append(dec.func.attr)
|
|
123
|
+
# 把装饰器信息挂到该函数名对应的 routes(同函数多装饰器顺序不定)
|
|
124
|
+
for r in self.routes:
|
|
125
|
+
if r["handler"] == node.name and r["file"] == self.file:
|
|
126
|
+
r["authDecorators"] = auth_decorators
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def walk_py_files(root):
|
|
130
|
+
for dirpath, dirnames, filenames in os.walk(root):
|
|
131
|
+
dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS]
|
|
132
|
+
for fn in filenames:
|
|
133
|
+
if fn.endswith(".py") and not fn.startswith("test_"):
|
|
134
|
+
yield os.path.join(dirpath, fn)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def main():
|
|
138
|
+
root, out = sys.argv[1], sys.argv[2]
|
|
139
|
+
apps, blueprints, routes, before_requests, files_scanned = [], [], [], [], 0
|
|
140
|
+
for fp in walk_py_files(root):
|
|
141
|
+
files_scanned += 1
|
|
142
|
+
try:
|
|
143
|
+
with open(fp, "r", encoding="utf-8", errors="replace") as f:
|
|
144
|
+
tree = ast.parse(f.read(), filename=fp)
|
|
145
|
+
except (SyntaxError, UnicodeDecodeError, OSError):
|
|
146
|
+
continue
|
|
147
|
+
collector = FlaskCollector(fp)
|
|
148
|
+
collector.visit(tree)
|
|
149
|
+
apps.extend(collector.apps)
|
|
150
|
+
blueprints.extend(collector.blueprints)
|
|
151
|
+
routes.extend(collector.routes)
|
|
152
|
+
before_requests.extend(collector.before_requests)
|
|
153
|
+
|
|
154
|
+
result = {
|
|
155
|
+
"hasFlask": bool(apps or blueprints or routes),
|
|
156
|
+
"apps": sorted(set(apps)),
|
|
157
|
+
"blueprints": sorted(set(blueprints)),
|
|
158
|
+
"routes": routes,
|
|
159
|
+
"beforeRequestAuth": sorted(set(
|
|
160
|
+
fn for fn in before_requests if is_auth_like(fn)
|
|
161
|
+
)),
|
|
162
|
+
"filesScanned": files_scanned,
|
|
163
|
+
}
|
|
164
|
+
with open(out, "w", encoding="utf-8") as f:
|
|
165
|
+
json.dump(result, f, ensure_ascii=False, indent=2)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
if __name__ == "__main__":
|
|
169
|
+
main()
|