progmune-runtime 3.7.7 → 3.7.8

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 CHANGED
@@ -1,5 +1,33 @@
1
1
  # Changelog
2
2
 
3
+ ## [3.7.8] — 2026-08-28
4
+
5
+ ### Django / DRF 框架结构适配(M2)——框架适配第 4 个
6
+
7
+ - **结构提取(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)
8
+ - **检测规则**(`src/frameworks/django-detector.ts`):
9
+ - `DJANGO_VIEW_NO_AUTH`——mutation 视图无保护:FBV 按动词名门控(add/create/update/delete/transfer 等——信息页 home/robots/error 不报);CBV 按方法含写操作(post/put/patch/delete/create/update/destroy)+ 无认证装饰器/无 LoginRequiredMixin
10
+ - `DRF_PERMISSION_BYPASS`——DRF 视图写方法 + 显式 AllowAny / 空权限类(非认证入口端点)
11
+ - 认证入口词汇豁免(login/signin/regist/token/health 等,视图名+URL 名+pattern)
12
+ - **引擎接线**:`collectDjangoViolations`(仅 Python,best-effort)+ `overall.djangoCoverage`——加性零漂移
13
+ - **合成金标**(`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%**
14
+ - **真实应用验证**: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 保持
15
+ - **修复(子串陷阱)**:`registration` 不含 `register` 子串、`register` 不含 `registr` 子串——认证入口词干统一为 `regist`(FastAPI/Django 两检测器同步)+ 回归测试锁定
16
+ - **验证**:19 个新单元测试(django-detector 10 + fastapi 豁免回归);套件 139/139;Python 盲测 v1.2 64 零漂移;C 演示 SSG 结果不变
17
+ - **边界(如实)**:FBV 无法静态区分 HTTP 方法(动词名门控口径,非方法级);include() 递归不展开(被 include 的应用 urls.py 单独成文件时已覆盖);自定义 permission 类视为保护(保守)
18
+
19
+ ### FastAPI 框架结构适配(M1)——框架适配第 3 个
20
+
21
+ - **结构提取(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 等非生产目录
22
+ - **检测规则**(`src/frameworks/fastapi-detector.ts`,结构提取与规则判定解耦):
23
+ - `FASTAPI_ROUTE_NO_AUTH`——写操作路由(post/put/patch/delete)无认证依赖且非认证入口端点(login/register/token/health 词汇豁免)→「每个 API 入口都有门禁」的精确形态;公开读(GET)不检查(realworld 的 tags/文章列表就是公开 GET——只盯 mutation 面把误报压到最低)
24
+ - `FASTAPI_DEAD_AUTH_SCHEME`——声明了认证方案但没有任何路由引用(认证设施是死的,装饰性声明)
25
+ - **引擎接线**:`collectFastapiViolations`(仅 Python 项目跑 python3 扫描;best-effort 永不阻断评估)+ `overall.fastapiCoverage`(apps/routes/filesScanned/issuesFound)——加性零漂移
26
+ - **合成金标**(`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%**
27
+ - **真实应用验证**:fastapi-realworld(19 路由全结构识别:签名 Depends 与装饰器 dependencies= 两种认证接线风格)——**0 FP**;django-realworld/django-unicorn 无 FastAPI 结构 → 适配器静默直通(结果不变)
28
+ - **零漂移**:Python 盲测 v1.2 64 违规零漂移;C 演示 SSG 结果不变;相关套件 128/128(含 fastapi-detector 9 个新回归)
29
+ - **边界(如实)**:全局中间件不视为认证(add_middleware 通常是 CORS;自定义认证中间件结构不可见);Depends 目标按 auth-like 词表+方案引用判定(无数据流分析);npm 安装态下 tools/ 不在包内 → 框架扫描与既有 Python IR 提取一样静默降级(安装态 Python 全链路为既有遗留问题,不随本版引入新差异)
30
+
3
31
  ## [3.7.7] — 2026-08-28
4
32
 
5
33
  ### 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: 2/13.** Express ✅ and tRPC ✅ have dedicated detectors; Next.js has version-aware governance; NestJS is partial. Django, FastAPI and 8 more remain — framework adaptation is the #1 product gap.
111
+ **Framework adapters: 4/13.** Express ✅, tRPC ✅, FastAPI (route/auth-dependency structural analysis — every mutation route checked for auth dependencies, dead auth schemes flagged) and Django(urlconf/CBV/DRF structural analysis — unprotected mutation views and `AllowAny` write endpoints flagged) have dedicated detectors; Next.js has version-aware governance; NestJS is partial. Next.js/Fastify/Flask and 5 more 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 (2/13)
237
+ - **Framework Adapters:** Express ✅, tRPC ✅, FastAPI ✅, Django ✅, NestJS partial (4/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
- **框架适配器:2/13。** Express tRPC 有专用检测器;Next.js 有版本感知治理;NestJS 部分支持。Django、FastAPI 及另外 8 个待适配——框架适配是 #1 产品缺口。
111
+ **框架适配器:4/13。** Express ✅、tRPC ✅、FastAPI ✅(路由/认证依赖结构分析——逐条检查写操作路由的认证依赖、标记死认证方案)与 Django ✅(URL 配置/CBV/DRF 结构分析——标记无保护的写视图与 `AllowAny` 写端点)有专用检测器;Next.js 有版本感知治理;NestJS 部分支持。Next.js/Fastify/Flask 及另外 5 个待适配——框架适配是 #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 部分(2/13)
236
+ - **框架适配器:** Express ✅、tRPC ✅、FastAPI ✅、Django ✅、NestJS 部分(4/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
+ });
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.7";
23
+ exports.RUNTIME_VERSION = "3.7.8";
24
24
  function verify(filePath) {
25
25
  const cert = (0, certify_1.certify)(filePath);
26
26
  const kb = (0, protocol_knowledge_1.buildKnowledgeBase)();
@@ -62,6 +62,8 @@ const api_semantic_mapper_1 = require("./api-semantic-mapper");
62
62
  const protocol_domain_validator_1 = require("./protocol-domain-validator");
63
63
  const annotation_suggest_1 = require("../annotation-suggest");
64
64
  const call_graph_propagator_1 = require("./call-graph-propagator");
65
+ const fastapi_detector_1 = require("../frameworks/fastapi-detector");
66
+ const django_detector_1 = require("../frameworks/django-detector");
65
67
  const ssg_bridge_1 = require("./ssg-bridge");
66
68
  const call_sequence_1 = require("../call-sequence");
67
69
  const extract_ir_1 = require("../extract-ir");
@@ -92,6 +94,8 @@ async function evaluateTrust(ctx) {
92
94
  const expressResult = collectExpressViolations(ctx);
93
95
  const nestjsResult = collectNestJSViolations(ctx);
94
96
  const trpcResult = collectTRPCViolations(ctx);
97
+ const fastapiResult = collectFastapiViolations(ctx);
98
+ const djangoResult = collectDjangoViolations(ctx);
95
99
  const coverageData = collectVerificationCoverage(ctx);
96
100
  const governanceDefects = collectGovernanceDefects(ctx);
97
101
  // ═══════════════════════════════════════
@@ -109,6 +113,8 @@ async function evaluateTrust(ctx) {
109
113
  ...expressViolations,
110
114
  ...nestjsResult.violations,
111
115
  ...trpcResult.violations,
116
+ ...fastapiResult.violations,
117
+ ...djangoResult.violations,
112
118
  ];
113
119
  // ═══════════════════════════════════════
114
120
  // PHASE 3: SCORE
@@ -228,6 +234,24 @@ async function evaluateTrust(ctx) {
228
234
  issuesFound: nestjsResult.violations.length,
229
235
  }
230
236
  : undefined,
237
+ /** FastAPI framework adapter coverage — route/auth-dependency analysis */
238
+ fastapiCoverage: fastapiResult.coverage.routes > 0
239
+ ? {
240
+ appsDetected: fastapiResult.coverage.apps,
241
+ totalRoutes: fastapiResult.coverage.routes,
242
+ filesScanned: fastapiResult.coverage.filesScanned,
243
+ issuesFound: fastapiResult.violations.length,
244
+ }
245
+ : undefined,
246
+ /** Django framework adapter coverage — urlconf/view/permission analysis */
247
+ djangoCoverage: djangoResult.coverage.routes > 0
248
+ ? {
249
+ appsDetected: djangoResult.coverage.apps,
250
+ totalRoutes: djangoResult.coverage.routes,
251
+ filesScanned: djangoResult.coverage.filesScanned,
252
+ issuesFound: djangoResult.violations.length,
253
+ }
254
+ : undefined,
231
255
  },
232
256
  dimensions: {
233
257
  policyCompliance: {
@@ -366,6 +390,102 @@ function mapPolicyViolation(rv, filePath, _enterprisePolicy) {
366
390
  * Collect Express-specific security violations from the framework detector.
367
391
  * Maps ExpressSecurityIssue[] → TrustViolation[].
368
392
  */
393
+ function collectDjangoViolations(ctx) {
394
+ const violations = [];
395
+ const coverage = { apps: 0, routes: 0, filesScanned: 0 };
396
+ // 仅 Python 项目跑框架结构扫描
397
+ const lang = ctx.language || "typescript";
398
+ if (lang !== "python")
399
+ return { violations, coverage };
400
+ try {
401
+ const { execSync } = require("child_process");
402
+ const fs = require("fs");
403
+ const os = require("os");
404
+ // engine.js 位于 dist/trust/ → 仓库根 tools/ 需要两级向上
405
+ const scriptPath = path.resolve(__dirname, "..", "..", "tools", "extract_framework_django.py");
406
+ const outPath = path.join(os.tmpdir(), `progmune-fwdj-${process.pid}-${Date.now()}.json`);
407
+ execSync(`python3 "${scriptPath}" "${ctx.projectPath}" "${outPath}"`, {
408
+ encoding: "utf-8",
409
+ stdio: "pipe",
410
+ timeout: 60000,
411
+ });
412
+ if (!fs.existsSync(outPath))
413
+ return { violations, coverage };
414
+ const structure = JSON.parse(fs.readFileSync(outPath, "utf-8"));
415
+ fs.unlinkSync(outPath);
416
+ const analysis = (0, django_detector_1.analyzeDjangoStructure)(structure);
417
+ if (!analysis.hasDjango)
418
+ return { violations, coverage };
419
+ coverage.apps = 1;
420
+ coverage.routes = (structure.routes || []).length;
421
+ coverage.filesScanned = structure.filesScanned || 0;
422
+ for (const issue of analysis.issues) {
423
+ violations.push({
424
+ severity: issue.severity === "low" ? "low" : issue.severity,
425
+ rule_id: issue.rule,
426
+ file: issue.file || "",
427
+ function: issue.handler || "unknown",
428
+ message: issue.message,
429
+ evidence: issue.route || "",
430
+ why: `Framework structural analysis: ${issue.message}`,
431
+ fix: issue.rule === "DRF_PERMISSION_BYPASS"
432
+ ? `Require authenticated permissions on write methods (e.g. permission_classes = (IsAuthenticated,)).`
433
+ : `Add a login decorator (@login_required) or an auth mixin (LoginRequiredMixin) to the view.`,
434
+ policy_ref: "framework-safety.django",
435
+ });
436
+ }
437
+ }
438
+ catch { /* best-effort — framework analysis must never break evaluation */ }
439
+ return { violations, coverage };
440
+ }
441
+ function collectFastapiViolations(ctx) {
442
+ const violations = [];
443
+ const coverage = { apps: 0, routes: 0, filesScanned: 0 };
444
+ // 仅 Python 项目跑框架结构扫描(避免 TS 项目无谓的 python3 子进程)
445
+ const lang = ctx.language || "typescript";
446
+ if (lang !== "python")
447
+ return { violations, coverage };
448
+ try {
449
+ const { execSync } = require("child_process");
450
+ const fs = require("fs");
451
+ const os = require("os");
452
+ // engine.js 位于 dist/trust/ → 仓库根 tools/ 需要两级向上
453
+ const scriptPath = path.resolve(__dirname, "..", "..", "tools", "extract_framework_py.py");
454
+ const outPath = path.join(os.tmpdir(), `progmune-fw-${process.pid}-${Date.now()}.json`);
455
+ execSync(`python3 "${scriptPath}" "${ctx.projectPath}" "${outPath}"`, {
456
+ encoding: "utf-8",
457
+ stdio: "pipe",
458
+ timeout: 60000,
459
+ });
460
+ if (!fs.existsSync(outPath))
461
+ return { violations, coverage };
462
+ const structure = JSON.parse(fs.readFileSync(outPath, "utf-8"));
463
+ fs.unlinkSync(outPath);
464
+ const analysis = (0, fastapi_detector_1.analyzeFastapiStructure)(structure);
465
+ if (!analysis.hasFastAPI)
466
+ return { violations, coverage };
467
+ coverage.apps = (structure.apps || []).length + (structure.routers || []).length;
468
+ coverage.routes = (structure.routes || []).length;
469
+ coverage.filesScanned = structure.filesScanned || 0;
470
+ for (const issue of analysis.issues) {
471
+ violations.push({
472
+ severity: issue.severity === "low" ? "low" : issue.severity,
473
+ rule_id: issue.rule,
474
+ file: issue.file || "",
475
+ function: issue.handler || "unknown",
476
+ message: issue.message,
477
+ evidence: issue.route || "",
478
+ why: `Framework structural analysis: ${issue.message}`,
479
+ fix: issue.rule === "FASTAPI_ROUTE_NO_AUTH"
480
+ ? `Add an auth dependency to the route (Depends/Security of an authenticator) or move it behind authenticated middleware.`
481
+ : `Reference the scheme from a route dependency or remove the unused scheme.`,
482
+ policy_ref: "framework-safety.fastapi",
483
+ });
484
+ }
485
+ }
486
+ catch { /* best-effort — framework analysis must never break evaluation */ }
487
+ return { violations, coverage };
488
+ }
369
489
  function collectExpressViolations(ctx) {
370
490
  const violations = [];
371
491
  let expressApps = 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "progmune-runtime",
3
- "version": "3.7.7",
3
+ "version": "3.7.8",
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/",