progmune-runtime 3.7.7 → 3.7.9
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 +39 -0
- package/README.md +2 -2
- package/README.zh-CN.md +2 -2
- package/dist/frameworks/django-detector.js +133 -0
- package/dist/frameworks/django-detector.test.js +136 -0
- package/dist/frameworks/fastapi-detector.js +94 -0
- package/dist/frameworks/fastapi-detector.test.js +144 -0
- package/dist/frameworks/fastify-detector.js +137 -0
- package/dist/frameworks/fastify-detector.test.js +62 -0
- package/dist/frameworks/flask-detector.js +61 -0
- package/dist/frameworks/flask-detector.test.js +71 -0
- package/dist/frameworks/index.js +15 -3
- package/dist/frameworks/nextjs-detector.js +172 -0
- package/dist/frameworks/nextjs-detector.test.js +113 -0
- package/dist/sdk.js +1 -1
- package/dist/trust/engine.js +279 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,44 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [3.7.9] — 2026-08-28
|
|
4
|
+
|
|
5
|
+
### Flask / Fastify / Next.js 框架结构适配(M4——框架适配 7/13)
|
|
6
|
+
|
|
7
|
+
- **Flask(Python 第 3 个)**:`tools/extract_framework_flask.py`(@app.route/@bp.route + methods kwarg、认证装饰器、before_request 认证守卫、Blueprint)+ `src/frameworks/flask-detector.ts`(`FLASK_ROUTE_NO_AUTH`——mutation 路由无认证装饰器且无认证 before_request 守卫)——合成金标 4 项目 P=R=100%;vendored flask 库结构识别冒烟通过;修复 before_request 误放 Assign 节点(应为 Expr 表达式语句)
|
|
8
|
+
- **Fastify(TS 第 3 个)**:`src/frameworks/fastify-detector.ts`(代码串级,镜像 express-detector——路由注册 + preHandler/preValidation 认证选项 + addHook 认证钩子;`FASTIFY_ROUTE_NO_AUTH`);引擎冒烟:钩子保护正确识别(0 误报);8 个单测
|
|
9
|
+
- **Next.js(TS 第 4 个)**:`src/frameworks/nextjs-detector.ts`(App Router 文件级结构——route.ts 的 POST/PUT/PATCH/DELETE 导出 + next-auth/自定义认证调用 + 认证 middleware;`NEXT_ROUTE_NO_AUTH`)——7 个单测;pages/api 旧式 handler 方法不可静态区分(只计数如实);修复注释内 `app/**/route.ts` 的 `*/` 提前终止块注释陷阱
|
|
10
|
+
- **引擎接线**:collectFlaskViolations / collectFastifyViolations / collectNextjsViolations + overall 三个 coverage 字段——全部加性 best-effort;frameworks/index.ts barrel 补 5 个适配器导出
|
|
11
|
+
- **验证**:新单测 21 个;全相关套件 160/160;Python 盲测 v1.2 64 零漂移;fastapi/django realworld 0 FP 保持;C 演示不变
|
|
12
|
+
- **边界(如实)**:Flask 认证 before_request 按函数名词汇识别(自定义名漏判=漏报方向);Fastify 代码串级(配置展开不可见);Next.js 只盯 API 面(route.ts/pages-api,页面组件不检查);npm 安装态 tools/ 不在包内→Python 框架扫描静默降级(既有遗留)
|
|
13
|
+
|
|
14
|
+
## [3.7.8] — 2026-08-28
|
|
15
|
+
|
|
16
|
+
### Django / DRF 框架结构适配(M2)——框架适配第 4 个
|
|
17
|
+
|
|
18
|
+
- **结构提取(Python AST)**:`tools/extract_framework_django.py`(与 extract_ir.py / extract_framework_py.py 解耦)——urlpatterns 解析(url()/path()/re_path() → FBV / CBV .as_view() / include)、FBV 登录装饰器(login_required/permission_required/staff_member_required/user_passes_test + 自定义 *auth* 装饰器)、CBV 基类与方法(View/generics + LoginRequiredMixin 等混入)、DRF permission_classes(AllowAny/IsAuthenticated/其他类名)与 @api_view(methods + permission_classes kwarg)
|
|
19
|
+
- **检测规则**(`src/frameworks/django-detector.ts`):
|
|
20
|
+
- `DJANGO_VIEW_NO_AUTH`——mutation 视图无保护:FBV 按动词名门控(add/create/update/delete/transfer 等——信息页 home/robots/error 不报);CBV 按方法含写操作(post/put/patch/delete/create/update/destroy)+ 无认证装饰器/无 LoginRequiredMixin
|
|
21
|
+
- `DRF_PERMISSION_BYPASS`——DRF 视图写方法 + 显式 AllowAny / 空权限类(非认证入口端点)
|
|
22
|
+
- 认证入口词汇豁免(login/signin/regist/token/health 等,视图名+URL 名+pattern)
|
|
23
|
+
- **引擎接线**:`collectDjangoViolations`(仅 Python,best-effort)+ `overall.djangoCoverage`——加性零漂移
|
|
24
|
+
- **合成金标**(`generate-projects-django.ts` + `scan-protocol-django.ts`):FBV/CBV/DRF × clean/V1/V2/V2b/V1V2 = 8 项目——**TP 6 / FP 0 / FN 0 → Precision 100% / Recall 100%**
|
|
25
|
+
- **真实应用验证**:django-realworld(15 路由结构全识别:DRF generics + permission_classes 接线、RegistrationAPIView/LoginAPIView AllowAny 正确豁免)——**0 FP**;PyGoat 133 路由结构全识别,适配器 2 条 flags 均为故意脆弱 lab(csrf_transfer_monei_api / DoItFast)——如实记录;fastapi-realworld 0 FP 保持
|
|
26
|
+
- **修复(子串陷阱)**:`registration` 不含 `register` 子串、`register` 不含 `registr` 子串——认证入口词干统一为 `regist`(FastAPI/Django 两检测器同步)+ 回归测试锁定
|
|
27
|
+
- **验证**:19 个新单元测试(django-detector 10 + fastapi 豁免回归);套件 139/139;Python 盲测 v1.2 64 零漂移;C 演示 SSG 结果不变
|
|
28
|
+
- **边界(如实)**:FBV 无法静态区分 HTTP 方法(动词名门控口径,非方法级);include() 递归不展开(被 include 的应用 urls.py 单独成文件时已覆盖);自定义 permission 类视为保护(保守)
|
|
29
|
+
|
|
30
|
+
### FastAPI 框架结构适配(M1)——框架适配第 3 个
|
|
31
|
+
|
|
32
|
+
- **结构提取(Python AST)**:`tools/extract_framework_py.py`(与 extract_ir.py 解耦,零风险)——路由(@app.get/@router.post/@r.api_route)、依赖注入(Depends()/Security(),含 Annotated[...] 订阅、嵌套调用解析、**装饰器级 dependencies=[...]**(realworld 风格)、认证方案声明(OAuth2PasswordBearer/HTTPBearer/APIKeyHeader 等 8 类)、全局中间件;跳过 tests/deps/venv 等非生产目录
|
|
33
|
+
- **检测规则**(`src/frameworks/fastapi-detector.ts`,结构提取与规则判定解耦):
|
|
34
|
+
- `FASTAPI_ROUTE_NO_AUTH`——写操作路由(post/put/patch/delete)无认证依赖且非认证入口端点(login/register/token/health 词汇豁免)→「每个 API 入口都有门禁」的精确形态;公开读(GET)不检查(realworld 的 tags/文章列表就是公开 GET——只盯 mutation 面把误报压到最低)
|
|
35
|
+
- `FASTAPI_DEAD_AUTH_SCHEME`——声明了认证方案但没有任何路由引用(认证设施是死的,装饰性声明)
|
|
36
|
+
- **引擎接线**:`collectFastapiViolations`(仅 Python 项目跑 python3 扫描;best-effort 永不阻断评估)+ `overall.fastapiCoverage`(apps/routes/filesScanned/issuesFound)——加性零漂移
|
|
37
|
+
- **合成金标**(`generate-projects-fastapi.ts` + `scan-protocol-fastapi.ts`,镜像 generate-projects-python 方法论):3 结构风格(直连/APIRouter/认证方案)× 4 违规变体(clean/V1 无认证写路由/V2 死方案/V1V2)= 12 项目——**TP 13 / FP 0 / FN 0 → Precision 100% / Recall 100%**
|
|
38
|
+
- **真实应用验证**:fastapi-realworld(19 路由全结构识别:签名 Depends 与装饰器 dependencies= 两种认证接线风格)——**0 FP**;django-realworld/django-unicorn 无 FastAPI 结构 → 适配器静默直通(结果不变)
|
|
39
|
+
- **零漂移**:Python 盲测 v1.2 64 违规零漂移;C 演示 SSG 结果不变;相关套件 128/128(含 fastapi-detector 9 个新回归)
|
|
40
|
+
- **边界(如实)**:全局中间件不视为认证(add_middleware 通常是 CORS;自定义认证中间件结构不可见);Depends 目标按 auth-like 词表+方案引用判定(无数据流分析);npm 安装态下 tools/ 不在包内 → 框架扫描与既有 Python IR 提取一样静默降级(安装态 Python 全链路为既有遗留问题,不随本版引入新差异)
|
|
41
|
+
|
|
3
42
|
## [3.7.7] — 2026-08-28
|
|
4
43
|
|
|
5
44
|
### C 注解采纳体验(采纳生死线工具)
|
package/README.md
CHANGED
|
@@ -108,7 +108,7 @@ Progmune is honest about what it can and cannot verify.
|
|
|
108
108
|
|
|
109
109
|
**Multi-language IR (registry-based).** TypeScript (ts-morph) and Python (AST) extractors are registered entries in one registry (`src/extract-project-ir.ts`); `extractProjectIR` merges every detected language into a single function IR shared by the agent loop, `execute()`, and the MCP server — the agent composes function-protocol chains across both languages. Adding a language (Go, Java, …) = register one extractor entry; callers don't change.
|
|
110
110
|
|
|
111
|
-
**Framework adapters:
|
|
111
|
+
**Framework adapters: 7/13.** Express ✅, tRPC ✅, FastAPI ✅, Django ✅ (structural route-level auth analysis — every mutation route checked for auth), Flask ✅ (route/before_request auth guard analysis), Fastify ✅ (route preHandler/hook auth analysis) and Next.js ✅ (App Router route-handler auth analysis) have dedicated detectors; NestJS is partial. Next.js version-aware governance also exists. Spring Boot/Go frameworks and a few TS frameworks remain — framework adaptation is the #1 product gap.
|
|
112
112
|
|
|
113
113
|
### What Progmune does NOT cover (honest boundaries)
|
|
114
114
|
|
|
@@ -234,7 +234,7 @@ High-impact contribution areas:
|
|
|
234
234
|
- **Runtime Pipeline:** Detect → Explain → Repair → Validate (L1–L4)
|
|
235
235
|
- **Trust Engine:** 4-dimension scoring with binary explainability gate
|
|
236
236
|
- **MCP Tools:** 19 — `progmune_trust_check`, `progmune_score`, `progmune_policy_check`, `progmune_certify`, and more
|
|
237
|
-
- **Framework Adapters:** Express ✅, tRPC ✅, NestJS partial (
|
|
237
|
+
- **Framework Adapters:** Express ✅, tRPC ✅, FastAPI ✅, Django ✅, Flask ✅, Fastify ✅, Next.js ✅, NestJS partial (7/13)
|
|
238
238
|
- **Knowledge Base:** 31 domains, 148 protocol rules, 22 detectors, 26 safeguards, PLSB 13/13 categories — plus 15 source-level detection rules (Python)
|
|
239
239
|
- **Corpus:** 2,500+ trajectories across 6+ repositories; blind benchmarks 100 (TS) + 90 (Python) projects; real-world validation on 4 application repos
|
|
240
240
|
- **Current focus:** Enterprise PoC validation + remaining framework-internal boundary FPs
|
package/README.zh-CN.md
CHANGED
|
@@ -108,7 +108,7 @@ Progmune 对能验证什么、不能验证什么保持诚实。
|
|
|
108
108
|
|
|
109
109
|
**多语言 IR(注册表式)。** TypeScript(ts-morph)与 Python(AST)提取器是同一注册表(`src/extract-project-ir.ts`)中的注册项;`extractProjectIR` 把检测到的所有语言合并为一份函数 IR,由 agent loop、`execute()` 与 MCP server 共享——agent 可在两种语言上编排函数协议链。新增语言(Go、Java……)= 注册一条提取器,调用方零改动。
|
|
110
110
|
|
|
111
|
-
**框架适配器:
|
|
111
|
+
**框架适配器:7/13。** Express ✅、tRPC ✅、FastAPI ✅、Django ✅(结构级路由认证分析——逐条检查写操作入口的认证)、Flask ✅(路由/before_request 认证守卫分析)、Fastify ✅(路由 preHandler/钩子认证分析)与 Next.js ✅(App Router 路由处理器认证分析)有专用检测器;NestJS 部分支持,Next.js 另有版本感知治理。Spring Boot、Go 框架及少数 TS 框架待适配——框架适配是 #1 产品缺口。
|
|
112
112
|
|
|
113
113
|
### Progmune 不覆盖什么(诚实边界)
|
|
114
114
|
|
|
@@ -233,7 +233,7 @@ Progmune 建立在"**LLM 输出是统计表演而非推理**"这一前提上—
|
|
|
233
233
|
- **运行时管线:** 检测 → 解释 → 修复 → 验证(L1–L4)
|
|
234
234
|
- **信任引擎:** 四维评分 + 二元可解释性门
|
|
235
235
|
- **MCP 工具:** 19 个——`progmune_trust_check`、`progmune_score`、`progmune_policy_check`、`progmune_certify` 等
|
|
236
|
-
- **框架适配器:** Express ✅、tRPC ✅、NestJS 部分(
|
|
236
|
+
- **框架适配器:** Express ✅、tRPC ✅、FastAPI ✅、Django ✅、Flask ✅、Fastify ✅、Next.js ✅、NestJS 部分(7/13)
|
|
237
237
|
- **知识库:** 31 个域、148 条协议规则、22 个检测器、26 条防护规则、PLSB 13/13 类别——另加 15 条源码级检测规则(Python)
|
|
238
238
|
- **语料:** 6+ 仓库 2,500+ 轨迹;盲测基准 100(TS)+ 90(Python)个项目;4 个应用仓库真实验证
|
|
239
239
|
- **当前重点:** 企业 PoC 验证 + 剩余框架内部边界误报
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Django Framework Adapter — Protocol Detection for Django / DRF
|
|
4
|
+
*
|
|
5
|
+
* 框架适配 M2:结构扫描由 tools/extract_framework_django.py(Python AST)
|
|
6
|
+
* 完成——urlpatterns 解析(FBV/CBV/DRF)、登录装饰器、DRF permission_classes。
|
|
7
|
+
* 本模块消费结构 JSON 做规则判定:
|
|
8
|
+
*
|
|
9
|
+
* DJANGO_VIEW_NO_AUTH mutation 视图(FBV 动词名门控 / CBV 方法
|
|
10
|
+
* 含写操作)无登录装饰器、无权限类、无
|
|
11
|
+
* LoginRequiredMixin 等保护——路由级
|
|
12
|
+
* missing-auth 的精确形态
|
|
13
|
+
* DRF_PERMISSION_BYPASS DRF 视图 mutation 方法 + 显式 AllowAny /
|
|
14
|
+
* 空权限类——显式绕过(非认证入口端点)
|
|
15
|
+
*
|
|
16
|
+
* 口径(如实):
|
|
17
|
+
* - FBV 无法静态区分 HTTP 方法——按视图名动词门控(add/create/update/
|
|
18
|
+
* delete/transfer 等),信息页(home/robots/error)不报;@api_view
|
|
19
|
+
* 声明的 methods 按真实方法判定
|
|
20
|
+
* - DRF 写方法 = post/put/patch/delete/create/update/destroy(generics
|
|
21
|
+
* 命名);read-only(list/retrieve/feed)不检查——公开读是常见设计
|
|
22
|
+
* - 认证入口端点按 视图名+URL 名 词汇豁免(login/register/signup/token/
|
|
23
|
+
* health/auth)
|
|
24
|
+
* - include() 递归与 admin.site.urls 等无法解析的引用跳过
|
|
25
|
+
*/
|
|
26
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
27
|
+
exports.analyzeDjangoStructure = analyzeDjangoStructure;
|
|
28
|
+
// ── Rule vocabulary ──
|
|
29
|
+
/** DRF 写方法(generics 命名 + HTTP 方法) */
|
|
30
|
+
const DRF_MUTATION_METHODS = new Set([
|
|
31
|
+
"post", "put", "patch", "delete", "create", "update", "destroy",
|
|
32
|
+
]);
|
|
33
|
+
/** FBV 动词名门控(信息页不报——home/robots/error 等是公开设计) */
|
|
34
|
+
const FBV_MUTATION_VERBS = [
|
|
35
|
+
"add", "create", "update", "delete", "remove", "transfer", "upload",
|
|
36
|
+
"submit", "edit", "change", "save", "write", "send", "post", "pay",
|
|
37
|
+
"checkout", "purchase", "reset", "revoke", "register_device",
|
|
38
|
+
];
|
|
39
|
+
const AUTH_ENTRY_WORDS = [
|
|
40
|
+
"login", "signin", "sign_in", "regist", "signup", "sign_up",
|
|
41
|
+
"token", "auth", "health", "status",
|
|
42
|
+
];
|
|
43
|
+
function isAuthEntry(route, viewName) {
|
|
44
|
+
const haystack = `${viewName || ""} ${route.urlname} ${route.pattern}`.toLowerCase();
|
|
45
|
+
return AUTH_ENTRY_WORDS.some((w) => haystack.includes(w));
|
|
46
|
+
}
|
|
47
|
+
function hasMutationMethod(view) {
|
|
48
|
+
if (view.apiViewMethods && view.apiViewMethods.length > 0) {
|
|
49
|
+
return view.apiViewMethods.some((m) => DRF_MUTATION_METHODS.has(m.toLowerCase()));
|
|
50
|
+
}
|
|
51
|
+
return (view.methods || []).some((m) => DRF_MUTATION_METHODS.has(m.toLowerCase()));
|
|
52
|
+
}
|
|
53
|
+
function fbvNameHasMutationVerb(name) {
|
|
54
|
+
const lower = name.toLowerCase();
|
|
55
|
+
return FBV_MUTATION_VERBS.some((v) => lower.includes(v));
|
|
56
|
+
}
|
|
57
|
+
// ── Analysis ──
|
|
58
|
+
function analyzeDjangoStructure(data) {
|
|
59
|
+
if (!data.hasDjango || !Array.isArray(data.routes)) {
|
|
60
|
+
return { hasDjango: false, issues: [] };
|
|
61
|
+
}
|
|
62
|
+
const issues = [];
|
|
63
|
+
for (const route of data.routes) {
|
|
64
|
+
if (route.kind !== "fbv" && route.kind !== "cbv")
|
|
65
|
+
continue;
|
|
66
|
+
const viewName = route.view;
|
|
67
|
+
if (!viewName)
|
|
68
|
+
continue;
|
|
69
|
+
const view = data.views[viewName];
|
|
70
|
+
if (!view)
|
|
71
|
+
continue; // 无法解析的引用(admin.site.urls 等)跳过
|
|
72
|
+
if (isAuthEntry(route, viewName))
|
|
73
|
+
continue;
|
|
74
|
+
if (view.kind === "fbv") {
|
|
75
|
+
const hasAuthDecorator = (view.authDecorators || []).length > 0;
|
|
76
|
+
if (hasAuthDecorator)
|
|
77
|
+
continue;
|
|
78
|
+
const protectedByApiView = (view.apiViewMethods || []).length > 0
|
|
79
|
+
&& (view.permissionClasses || []).length > 0
|
|
80
|
+
&& !(view.permissionClasses || []).every((p) => p === "AllowAny");
|
|
81
|
+
const isMutation = view.apiViewMethods && view.apiViewMethods.length > 0
|
|
82
|
+
? hasMutationMethod(view)
|
|
83
|
+
: fbvNameHasMutationVerb(viewName);
|
|
84
|
+
if (!isMutation)
|
|
85
|
+
continue;
|
|
86
|
+
if (protectedByApiView)
|
|
87
|
+
continue;
|
|
88
|
+
issues.push({
|
|
89
|
+
severity: "medium",
|
|
90
|
+
rule: "DJANGO_VIEW_NO_AUTH",
|
|
91
|
+
message: `View "${viewName}" (${route.pattern}) has no login decorator or ` +
|
|
92
|
+
`permission protection — any visitor can reach it.`,
|
|
93
|
+
route: route.pattern,
|
|
94
|
+
handler: viewName,
|
|
95
|
+
file: view.file,
|
|
96
|
+
});
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
// CBV
|
|
100
|
+
if (view.isDrf) {
|
|
101
|
+
if (!hasMutationMethod(view))
|
|
102
|
+
continue;
|
|
103
|
+
if (view.openPermission) {
|
|
104
|
+
issues.push({
|
|
105
|
+
severity: "medium",
|
|
106
|
+
rule: "DRF_PERMISSION_BYPASS",
|
|
107
|
+
message: `DRF view "${viewName}" (${route.pattern}) accepts write methods ` +
|
|
108
|
+
`with permission_classes allowing any caller.`,
|
|
109
|
+
route: route.pattern,
|
|
110
|
+
handler: viewName,
|
|
111
|
+
file: view.file,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
// 非 DRF CBV:写方法 + 无 method_decorator 认证 + 无 LoginRequiredMixin
|
|
117
|
+
if (!hasMutationMethod(view))
|
|
118
|
+
continue;
|
|
119
|
+
const authDecorators = (view.authDecorators || []).length > 0;
|
|
120
|
+
if (authDecorators || view.protectedByMixin)
|
|
121
|
+
continue;
|
|
122
|
+
issues.push({
|
|
123
|
+
severity: "medium",
|
|
124
|
+
rule: "DJANGO_VIEW_NO_AUTH",
|
|
125
|
+
message: `View "${viewName}" (${route.pattern}) has write methods without ` +
|
|
126
|
+
`login decorators or auth mixins.`,
|
|
127
|
+
route: route.pattern,
|
|
128
|
+
handler: viewName,
|
|
129
|
+
file: view.file,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
return { hasDjango: true, issues };
|
|
133
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/**
|
|
4
|
+
* django-detector.test.ts — Django 框架适配器规则回归(纯函数,无文件 I/O)
|
|
5
|
+
*
|
|
6
|
+
* 规则判定与结构提取解耦:扫描器(tools/extract_framework_django.py)只做
|
|
7
|
+
* 结构,本模块消费结构 JSON 做 DJANGO_VIEW_NO_AUTH / DRF_PERMISSION_BYPASS。
|
|
8
|
+
*/
|
|
9
|
+
const vitest_1 = require("vitest");
|
|
10
|
+
const django_detector_1 = require("./django-detector");
|
|
11
|
+
function structure(partial) {
|
|
12
|
+
return {
|
|
13
|
+
hasDjango: true,
|
|
14
|
+
routes: [],
|
|
15
|
+
views: {},
|
|
16
|
+
filesScanned: 1,
|
|
17
|
+
...partial,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
const fbv = (name, extra = {}) => ({
|
|
21
|
+
file: "views.py", kind: "fbv", decorators: [], authDecorators: [],
|
|
22
|
+
apiViewMethods: null, permissionClasses: [],
|
|
23
|
+
...extra,
|
|
24
|
+
});
|
|
25
|
+
const cbv = (name, extra = {}) => ({
|
|
26
|
+
file: "views.py", kind: "cbv", decorators: [], authDecorators: [],
|
|
27
|
+
apiViewMethods: null, permissionClasses: [], bases: [], methods: [],
|
|
28
|
+
isDrf: false, protectedByMixin: false, openPermission: false,
|
|
29
|
+
...extra,
|
|
30
|
+
});
|
|
31
|
+
(0, vitest_1.describe)("django-detector", () => {
|
|
32
|
+
(0, vitest_1.it)("R1:无保护的 FBV 动词名视图 → DJANGO_VIEW_NO_AUTH", () => {
|
|
33
|
+
const { issues } = (0, django_detector_1.analyzeDjangoStructure)(structure({
|
|
34
|
+
routes: [{ pattern: "transfer", urlname: "", view: "transfer_money", kind: "fbv", file: "urls.py" }],
|
|
35
|
+
views: { transfer_money: fbv("transfer_money") },
|
|
36
|
+
}));
|
|
37
|
+
(0, vitest_1.expect)(issues.map((i) => i.rule)).toContain("DJANGO_VIEW_NO_AUTH");
|
|
38
|
+
});
|
|
39
|
+
(0, vitest_1.it)("R1 识别 @login_required 保护", () => {
|
|
40
|
+
const { issues } = (0, django_detector_1.analyzeDjangoStructure)(structure({
|
|
41
|
+
routes: [{ pattern: "transfer", urlname: "", view: "transfer_money", kind: "fbv", file: "urls.py" }],
|
|
42
|
+
views: { transfer_money: fbv("transfer_money", { authDecorators: ["login_required"] }) },
|
|
43
|
+
}));
|
|
44
|
+
(0, vitest_1.expect)(issues).toHaveLength(0);
|
|
45
|
+
});
|
|
46
|
+
(0, vitest_1.it)("R1 豁免:home/robots/error 等信息页动词门控不报", () => {
|
|
47
|
+
const { issues } = (0, django_detector_1.analyzeDjangoStructure)(structure({
|
|
48
|
+
routes: [
|
|
49
|
+
{ pattern: "", urlname: "", view: "home", kind: "fbv", file: "urls.py" },
|
|
50
|
+
{ pattern: "robots.txt", urlname: "", view: "robots", kind: "fbv", file: "urls.py" },
|
|
51
|
+
],
|
|
52
|
+
views: { home: fbv("home"), robots: fbv("robots") },
|
|
53
|
+
}));
|
|
54
|
+
(0, vitest_1.expect)(issues).toHaveLength(0);
|
|
55
|
+
});
|
|
56
|
+
(0, vitest_1.it)("R1 豁免:registration 词干(不含 register 子串的陷阱回归)", () => {
|
|
57
|
+
const { issues } = (0, django_detector_1.analyzeDjangoStructure)(structure({
|
|
58
|
+
routes: [{ pattern: "^users/?$", urlname: "", view: "RegistrationAPIView", kind: "cbv", file: "urls.py" }],
|
|
59
|
+
views: {
|
|
60
|
+
RegistrationAPIView: cbv("RegistrationAPIView", {
|
|
61
|
+
isDrf: true, methods: ["post"],
|
|
62
|
+
permissionClasses: ["AllowAny"], openPermission: true,
|
|
63
|
+
}),
|
|
64
|
+
},
|
|
65
|
+
}));
|
|
66
|
+
(0, vitest_1.expect)(issues).toHaveLength(0);
|
|
67
|
+
});
|
|
68
|
+
(0, vitest_1.it)("R2:DRF 写方法 + AllowAny → DRF_PERMISSION_BYPASS", () => {
|
|
69
|
+
const { issues } = (0, django_detector_1.analyzeDjangoStructure)(structure({
|
|
70
|
+
routes: [{ pattern: "send", urlname: "", view: "SendMoneyAPIView", kind: "cbv", file: "urls.py" }],
|
|
71
|
+
views: {
|
|
72
|
+
SendMoneyAPIView: cbv("SendMoneyAPIView", {
|
|
73
|
+
isDrf: true, methods: ["post"],
|
|
74
|
+
permissionClasses: ["AllowAny"], openPermission: true,
|
|
75
|
+
}),
|
|
76
|
+
},
|
|
77
|
+
}));
|
|
78
|
+
(0, vitest_1.expect)(issues.map((i) => i.rule)).toContain("DRF_PERMISSION_BYPASS");
|
|
79
|
+
});
|
|
80
|
+
(0, vitest_1.it)("R2:IsAuthenticated 保护不报;只读视图(list/retrieve)不报", () => {
|
|
81
|
+
const { issues } = (0, django_detector_1.analyzeDjangoStructure)(structure({
|
|
82
|
+
routes: [
|
|
83
|
+
{ pattern: "send", urlname: "", view: "SendMoneyAPIView", kind: "cbv", file: "urls.py" },
|
|
84
|
+
{ pattern: "tags", urlname: "", view: "TagListAPIView", kind: "cbv", file: "urls.py" },
|
|
85
|
+
],
|
|
86
|
+
views: {
|
|
87
|
+
SendMoneyAPIView: cbv("SendMoneyAPIView", {
|
|
88
|
+
isDrf: true, methods: ["post"],
|
|
89
|
+
permissionClasses: ["IsAuthenticated"], openPermission: false,
|
|
90
|
+
}),
|
|
91
|
+
TagListAPIView: cbv("TagListAPIView", {
|
|
92
|
+
isDrf: true, methods: ["list"],
|
|
93
|
+
permissionClasses: ["AllowAny"], openPermission: true,
|
|
94
|
+
}),
|
|
95
|
+
},
|
|
96
|
+
}));
|
|
97
|
+
(0, vitest_1.expect)(issues).toHaveLength(0);
|
|
98
|
+
});
|
|
99
|
+
(0, vitest_1.it)("非 DRF CBV:写方法 + LoginRequiredMixin → 不报", () => {
|
|
100
|
+
const { issues } = (0, django_detector_1.analyzeDjangoStructure)(structure({
|
|
101
|
+
routes: [{ pattern: "edit", urlname: "", view: "EditView", kind: "cbv", file: "urls.py" }],
|
|
102
|
+
views: {
|
|
103
|
+
EditView: cbv("EditView", {
|
|
104
|
+
isDrf: false, methods: ["post"], protectedByMixin: true,
|
|
105
|
+
}),
|
|
106
|
+
},
|
|
107
|
+
}));
|
|
108
|
+
(0, vitest_1.expect)(issues).toHaveLength(0);
|
|
109
|
+
});
|
|
110
|
+
(0, vitest_1.it)("@api_view 写方法 + permission_classes 保护 → 不报", () => {
|
|
111
|
+
const { issues } = (0, django_detector_1.analyzeDjangoStructure)(structure({
|
|
112
|
+
routes: [{ pattern: "api", urlname: "", view: "api_transfer", kind: "fbv", file: "urls.py" }],
|
|
113
|
+
views: {
|
|
114
|
+
api_transfer: fbv("api_transfer", {
|
|
115
|
+
apiViewMethods: ["post"], permissionClasses: ["IsAuthenticated"],
|
|
116
|
+
}),
|
|
117
|
+
},
|
|
118
|
+
}));
|
|
119
|
+
(0, vitest_1.expect)(issues).toHaveLength(0);
|
|
120
|
+
});
|
|
121
|
+
(0, vitest_1.it)("include 与无法解析的引用(admin.site.urls)不报", () => {
|
|
122
|
+
const { issues } = (0, django_detector_1.analyzeDjangoStructure)(structure({
|
|
123
|
+
routes: [
|
|
124
|
+
{ pattern: "^admin/", urlname: "", view: "urls", kind: "fbv", file: "urls.py" },
|
|
125
|
+
{ pattern: "^api/", urlname: "", view: null, kind: "include", file: "urls.py" },
|
|
126
|
+
],
|
|
127
|
+
views: {},
|
|
128
|
+
}));
|
|
129
|
+
(0, vitest_1.expect)(issues).toHaveLength(0);
|
|
130
|
+
});
|
|
131
|
+
(0, vitest_1.it)("非 Django 结构不产生任何问题", () => {
|
|
132
|
+
const { hasDjango, issues } = (0, django_detector_1.analyzeDjangoStructure)(structure({ hasDjango: false, routes: [] }));
|
|
133
|
+
(0, vitest_1.expect)(hasDjango).toBe(false);
|
|
134
|
+
(0, vitest_1.expect)(issues).toHaveLength(0);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* FastAPI Framework Adapter — Protocol Detection for FastAPI
|
|
4
|
+
*
|
|
5
|
+
* 框架适配 M1(Python 侧结构提取 + TS 侧规则判定):
|
|
6
|
+
* 结构扫描由 tools/extract_framework_py.py(Python AST)完成——路由
|
|
7
|
+
* (@app.get/@router.post)、依赖注入(Depends()/Security(),含
|
|
8
|
+
* Annotated[...] 与装饰器级 dependencies=[...])、认证方案声明
|
|
9
|
+
* (OAuth2PasswordBearer/HTTPBearer/APIKeyHeader 等)、全局中间件。
|
|
10
|
+
* 本模块消费结构 JSON 做规则判定:
|
|
11
|
+
*
|
|
12
|
+
* FASTAPI_ROUTE_NO_AUTH 写操作路由(post/put/patch/delete)无
|
|
13
|
+
* 认证依赖,且不是认证入口端点(login/
|
|
14
|
+
* register/token 等)——「每个 API 入口
|
|
15
|
+
* 都有门禁」的精确形态
|
|
16
|
+
* FASTAPI_DEAD_AUTH_SCHEME 声明了认证方案但没有任何路由引用——
|
|
17
|
+
* 认证设施是死的(装饰性声明,防御性价值为零)
|
|
18
|
+
*
|
|
19
|
+
* 边界(如实):
|
|
20
|
+
* - 读操作(GET)不检查——公开读是常见设计(realworld 的 tags/文章
|
|
21
|
+
* 列表就是公开 GET),只盯写操作把误报压到最低(实测 realworld 0 FP)
|
|
22
|
+
* - 认证入口端点按 处理器名+路径 词汇豁免——登录/注册本身不能要求已认证
|
|
23
|
+
* - 不做数据流分析:Depends 目标是 auth-like 名或声明的方案即视为认证
|
|
24
|
+
* - 全局中间件不视为认证(add_middleware 通常是 CORS 等;认证中间件
|
|
25
|
+
* 是自定义 BaseHTTPMiddleware 且结构不可见——如实不认)
|
|
26
|
+
*/
|
|
27
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
28
|
+
exports.analyzeFastapiStructure = analyzeFastapiStructure;
|
|
29
|
+
// ── Rule vocabulary ──
|
|
30
|
+
/** 写操作方法——公开读是常见设计,只盯 mutation 面 */
|
|
31
|
+
const MUTATION_METHODS = new Set(["post", "put", "patch", "delete"]);
|
|
32
|
+
/** 认证入口端点豁免词(登录/注册/token 签发本身不能要求已认证) */
|
|
33
|
+
const AUTH_ENTRY_WORDS = [
|
|
34
|
+
"login", "signin", "sign_in", "regist", "signup", "sign_up",
|
|
35
|
+
"token", "auth", "health", "status",
|
|
36
|
+
];
|
|
37
|
+
/** 认证方案类型——引用即视为认证依赖 */
|
|
38
|
+
const SCHEME_TYPES = new Set([
|
|
39
|
+
"OAuth2PasswordBearer", "OAuth2AuthorizationCodeBearer", "HTTPBearer",
|
|
40
|
+
"HTTPBasic", "HTTPDigest", "APIKeyHeader", "APIKeyQuery", "APIKeyCookie",
|
|
41
|
+
"OpenIdConnect",
|
|
42
|
+
]);
|
|
43
|
+
function isAuthEntryRoute(route) {
|
|
44
|
+
const haystack = `${route.handler} ${route.path}`.toLowerCase();
|
|
45
|
+
return AUTH_ENTRY_WORDS.some((w) => haystack.includes(w));
|
|
46
|
+
}
|
|
47
|
+
/** 路由是否引用了某个认证方案(Depends(oauth2_scheme) 形态) */
|
|
48
|
+
function referencesScheme(route, schemeNames) {
|
|
49
|
+
return route.dependencies.some((d) => d.name !== null && schemeNames.has(d.name));
|
|
50
|
+
}
|
|
51
|
+
// ── Analysis ──
|
|
52
|
+
function analyzeFastapiStructure(data) {
|
|
53
|
+
if (!data.hasFastAPI || !Array.isArray(data.routes)) {
|
|
54
|
+
return { hasFastAPI: false, issues: [] };
|
|
55
|
+
}
|
|
56
|
+
const issues = [];
|
|
57
|
+
const schemeNames = new Set((data.authSchemes || []).map((s) => s.name));
|
|
58
|
+
// R1:无认证写操作路由
|
|
59
|
+
for (const route of data.routes) {
|
|
60
|
+
if (!MUTATION_METHODS.has(route.method.toLowerCase()))
|
|
61
|
+
continue;
|
|
62
|
+
const hasAuthDep = route.dependencies.some((d) => d.authLike);
|
|
63
|
+
if (hasAuthDep)
|
|
64
|
+
continue;
|
|
65
|
+
if (isAuthEntryRoute(route))
|
|
66
|
+
continue;
|
|
67
|
+
issues.push({
|
|
68
|
+
severity: "medium",
|
|
69
|
+
rule: "FASTAPI_ROUTE_NO_AUTH",
|
|
70
|
+
message: `Route ${route.method.toUpperCase()} ${route.path || "(root)"} has no ` +
|
|
71
|
+
`authentication dependency — any caller can invoke "${route.handler}" ` +
|
|
72
|
+
`without credentials.`,
|
|
73
|
+
route: `${route.method.toUpperCase()} ${route.path || "(root)"}`,
|
|
74
|
+
handler: route.handler,
|
|
75
|
+
file: route.file,
|
|
76
|
+
line: route.line,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
// R2:声明了认证方案但没有路由引用(死设施)
|
|
80
|
+
for (const scheme of data.authSchemes || []) {
|
|
81
|
+
if (!SCHEME_TYPES.has(scheme.type))
|
|
82
|
+
continue; // 非认证方案类跳过
|
|
83
|
+
const used = data.routes.some((r) => referencesScheme(r, schemeNames));
|
|
84
|
+
if (!used) {
|
|
85
|
+
issues.push({
|
|
86
|
+
severity: "medium",
|
|
87
|
+
rule: "FASTAPI_DEAD_AUTH_SCHEME",
|
|
88
|
+
message: `Auth scheme "${scheme.name}" (${scheme.type}) is declared but no ` +
|
|
89
|
+
`route references it — the scheme does not protect any endpoint.`,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return { hasFastAPI: true, issues };
|
|
94
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/**
|
|
4
|
+
* fastapi-detector.test.ts — FastAPI 框架适配器规则回归(纯函数,无文件 I/O)
|
|
5
|
+
*
|
|
6
|
+
* 规则判定与结构提取解耦:扫描器(tools/extract_framework_py.py)只做结构,
|
|
7
|
+
* 本模块消费结构 JSON 做 R1(无认证写操作路由)/ R2(死认证方案)。
|
|
8
|
+
*/
|
|
9
|
+
const vitest_1 = require("vitest");
|
|
10
|
+
const fastapi_detector_1 = require("./fastapi-detector");
|
|
11
|
+
function structure(partial) {
|
|
12
|
+
return {
|
|
13
|
+
hasFastAPI: true,
|
|
14
|
+
apps: ["app"],
|
|
15
|
+
routers: [],
|
|
16
|
+
authSchemes: [],
|
|
17
|
+
globalAuthMiddleware: [],
|
|
18
|
+
routes: [],
|
|
19
|
+
filesScanned: 1,
|
|
20
|
+
...partial,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
const route = (p) => ({
|
|
24
|
+
method: p.method,
|
|
25
|
+
path: p.path || "",
|
|
26
|
+
handler: p.handler,
|
|
27
|
+
file: "main.py",
|
|
28
|
+
line: 1,
|
|
29
|
+
dependencies: p.deps || [],
|
|
30
|
+
});
|
|
31
|
+
(0, vitest_1.describe)("fastapi-detector", () => {
|
|
32
|
+
(0, vitest_1.it)("R1:无认证写操作路由 → FASTAPI_ROUTE_NO_AUTH", () => {
|
|
33
|
+
const { issues } = (0, fastapi_detector_1.analyzeFastapiStructure)(structure({
|
|
34
|
+
routes: [route({ method: "post", handler: "create_article" })],
|
|
35
|
+
}));
|
|
36
|
+
(0, vitest_1.expect)(issues.map((i) => i.rule)).toContain("FASTAPI_ROUTE_NO_AUTH");
|
|
37
|
+
});
|
|
38
|
+
(0, vitest_1.it)("R1 豁免:登录/注册/token 认证入口端点不报", () => {
|
|
39
|
+
const { issues } = (0, fastapi_detector_1.analyzeFastapiStructure)(structure({
|
|
40
|
+
routes: [
|
|
41
|
+
route({ method: "post", path: "/login", handler: "login" }),
|
|
42
|
+
route({ method: "post", path: "/register", handler: "register_user" }),
|
|
43
|
+
route({ method: "post", path: "/token", handler: "issue_token" }),
|
|
44
|
+
],
|
|
45
|
+
}));
|
|
46
|
+
(0, vitest_1.expect)(issues).toHaveLength(0);
|
|
47
|
+
});
|
|
48
|
+
(0, vitest_1.it)("R1 豁免:registration 词干(不含 register 子串的陷阱回归)", () => {
|
|
49
|
+
const { issues } = (0, fastapi_detector_1.analyzeFastapiStructure)(structure({
|
|
50
|
+
routes: [
|
|
51
|
+
route({ method: "post", path: "/users", handler: "registration_api_view" }),
|
|
52
|
+
],
|
|
53
|
+
}));
|
|
54
|
+
(0, vitest_1.expect)(issues).toHaveLength(0);
|
|
55
|
+
});
|
|
56
|
+
(0, vitest_1.it)("R1 豁免:GET 读操作不报(公开读是常见设计)", () => {
|
|
57
|
+
const { issues } = (0, fastapi_detector_1.analyzeFastapiStructure)(structure({
|
|
58
|
+
routes: [route({ method: "get", path: "/articles", handler: "list_articles" })],
|
|
59
|
+
}));
|
|
60
|
+
(0, vitest_1.expect)(issues).toHaveLength(0);
|
|
61
|
+
});
|
|
62
|
+
(0, vitest_1.it)("R1 识别 Security/Depends 认证依赖(authLike 名)", () => {
|
|
63
|
+
const { issues } = (0, fastapi_detector_1.analyzeFastapiStructure)(structure({
|
|
64
|
+
routes: [
|
|
65
|
+
route({
|
|
66
|
+
method: "put", path: "/users/me", handler: "update_me",
|
|
67
|
+
deps: [{ name: "get_current_user_authorizer", via: "Security", authLike: true }],
|
|
68
|
+
}),
|
|
69
|
+
],
|
|
70
|
+
}));
|
|
71
|
+
(0, vitest_1.expect)(issues).toHaveLength(0);
|
|
72
|
+
});
|
|
73
|
+
(0, vitest_1.it)("R1 识别声明的认证方案引用(Depends(oauth2_scheme))", () => {
|
|
74
|
+
const { issues } = (0, fastapi_detector_1.analyzeFastapiStructure)(structure({
|
|
75
|
+
authSchemes: [{ name: "oauth2_scheme", type: "OAuth2PasswordBearer" }],
|
|
76
|
+
routes: [
|
|
77
|
+
route({
|
|
78
|
+
method: "get", path: "/me", handler: "read_me",
|
|
79
|
+
deps: [{ name: "oauth2_scheme", via: "Depends", authLike: true }],
|
|
80
|
+
}),
|
|
81
|
+
],
|
|
82
|
+
}));
|
|
83
|
+
(0, vitest_1.expect)(issues).toHaveLength(0);
|
|
84
|
+
});
|
|
85
|
+
(0, vitest_1.it)("R2:声明认证方案但无路由引用 → FASTAPI_DEAD_AUTH_SCHEME", () => {
|
|
86
|
+
const { issues } = (0, fastapi_detector_1.analyzeFastapiStructure)(structure({
|
|
87
|
+
authSchemes: [{ name: "oauth2_scheme", type: "OAuth2PasswordBearer" }],
|
|
88
|
+
routes: [route({ method: "get", handler: "list_things" })],
|
|
89
|
+
}));
|
|
90
|
+
(0, vitest_1.expect)(issues.map((i) => i.rule)).toContain("FASTAPI_DEAD_AUTH_SCHEME");
|
|
91
|
+
});
|
|
92
|
+
(0, vitest_1.it)("R2:方案被路由引用时不报", () => {
|
|
93
|
+
const { issues } = (0, fastapi_detector_1.analyzeFastapiStructure)(structure({
|
|
94
|
+
authSchemes: [{ name: "oauth2_scheme", type: "OAuth2PasswordBearer" }],
|
|
95
|
+
routes: [
|
|
96
|
+
route({
|
|
97
|
+
method: "get", handler: "read_me",
|
|
98
|
+
deps: [{ name: "oauth2_scheme", via: "Depends", authLike: true }],
|
|
99
|
+
}),
|
|
100
|
+
],
|
|
101
|
+
}));
|
|
102
|
+
(0, vitest_1.expect)(issues).toHaveLength(0);
|
|
103
|
+
});
|
|
104
|
+
(0, vitest_1.it)("非 FastAPI 结构不产生任何问题", () => {
|
|
105
|
+
const { hasFastAPI, issues } = (0, fastapi_detector_1.analyzeFastapiStructure)(structure({ hasFastAPI: false, routes: [] }));
|
|
106
|
+
(0, vitest_1.expect)(hasFastAPI).toBe(false);
|
|
107
|
+
(0, vitest_1.expect)(issues).toHaveLength(0);
|
|
108
|
+
});
|
|
109
|
+
(0, vitest_1.it)("realworld 快照:19 路由 0 问题(0 FP 回归锁)", () => {
|
|
110
|
+
const snapshot = {
|
|
111
|
+
hasFastAPI: true,
|
|
112
|
+
apps: ["application"],
|
|
113
|
+
routers: ["router"],
|
|
114
|
+
authSchemes: [],
|
|
115
|
+
globalAuthMiddleware: ["CORSMiddleware"],
|
|
116
|
+
routes: [
|
|
117
|
+
route({ method: "get", path: "", handler: "get_all_tags" }),
|
|
118
|
+
route({
|
|
119
|
+
method: "get", path: "", handler: "retrieve_current_user",
|
|
120
|
+
deps: [{ name: "get_current_user_authorizer", via: "Depends", authLike: true }],
|
|
121
|
+
}),
|
|
122
|
+
route({
|
|
123
|
+
method: "put", path: "", handler: "update_current_user",
|
|
124
|
+
deps: [{ name: "get_current_user_authorizer", via: "Depends", authLike: true }],
|
|
125
|
+
}),
|
|
126
|
+
route({ method: "post", path: "/login", handler: "login" }),
|
|
127
|
+
route({ method: "post", path: "", handler: "register" }),
|
|
128
|
+
route({
|
|
129
|
+
method: "post", path: "/{slug}/comments", handler: "create_comment",
|
|
130
|
+
deps: [{ name: "get_current_user_authorizer", via: "Security", authLike: true }],
|
|
131
|
+
}),
|
|
132
|
+
route({ method: "get", path: "/{slug}", handler: "retrieve_article_by_slug" }),
|
|
133
|
+
route({
|
|
134
|
+
method: "put", path: "/{slug}", handler: "update_article_by_slug",
|
|
135
|
+
deps: [{ name: "check_article_modification_permissions", via: "Depends", authLike: true }],
|
|
136
|
+
}),
|
|
137
|
+
],
|
|
138
|
+
filesScanned: 70,
|
|
139
|
+
};
|
|
140
|
+
const { hasFastAPI, issues } = (0, fastapi_detector_1.analyzeFastapiStructure)(structure(snapshot));
|
|
141
|
+
(0, vitest_1.expect)(hasFastAPI).toBe(true);
|
|
142
|
+
(0, vitest_1.expect)(issues).toHaveLength(0);
|
|
143
|
+
});
|
|
144
|
+
});
|