progmune-runtime 3.7.8 → 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 CHANGED
@@ -1,5 +1,16 @@
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
+
3
14
  ## [3.7.8] — 2026-08-28
4
15
 
5
16
  ### Django / DRF 框架结构适配(M2)——框架适配第 4 个
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: 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.
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 ✅, FastAPI ✅, Django ✅, NestJS partial (4/13)
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
- **框架适配器:4/13。** Express ✅、tRPC ✅、FastAPI ✅(路由/认证依赖结构分析——逐条检查写操作路由的认证依赖、标记死认证方案)与 Django ✅(URL 配置/CBV/DRF 结构分析——标记无保护的写视图与 `AllowAny` 写端点)有专用检测器;Next.js 有版本感知治理;NestJS 部分支持。Next.js/Fastify/Flask 及另外 5 个待适配——框架适配是 #1 产品缺口。
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 ✅、FastAPI ✅、Django ✅、NestJS 部分(4/13)
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,137 @@
1
+ "use strict";
2
+ /**
3
+ * Fastify Framework Adapter — Protocol Detection for Fastify
4
+ *
5
+ * 第 6 个框架适配(TS/JS 第 3 个专用检测器,镜像 express-detector 的
6
+ * 代码串分析风格):
7
+ *
8
+ * fastify.get('/x', { preHandler: [auth] }, handler) 路由级认证选项
9
+ * fastify.addHook('preHandler', authFn) 全局认证钩子
10
+ *
11
+ * 规则:
12
+ * FASTIFY_ROUTE_NO_AUTH mutation 路由注册(post/put/patch/delete)
13
+ * 无 preHandler/preValidation 认证选项,
14
+ * 且文件中无认证 addHook——路由级 missing-auth
15
+ *
16
+ * 口径(如实):
17
+ * - get 是读操作不检查(公开读是常见设计)
18
+ * - 认证钩子函数按名字词表识别(auth/login/permission/token/session…);
19
+ * 自定义认证函数若名字不含词表会漏判(保守方向是漏报不是误报)
20
+ * - 认证入口路径按词汇豁免(login/regist/auth/token/health)
21
+ * - 代码串级分析(与 Express 检测器同款):装饰器/配置展开不可见
22
+ */
23
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
24
+ if (k2 === undefined) k2 = k;
25
+ var desc = Object.getOwnPropertyDescriptor(m, k);
26
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
27
+ desc = { enumerable: true, get: function() { return m[k]; } };
28
+ }
29
+ Object.defineProperty(o, k2, desc);
30
+ }) : (function(o, m, k, k2) {
31
+ if (k2 === undefined) k2 = k;
32
+ o[k2] = m[k];
33
+ }));
34
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
35
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
36
+ }) : function(o, v) {
37
+ o["default"] = v;
38
+ });
39
+ var __importStar = (this && this.__importStar) || (function () {
40
+ var ownKeys = function(o) {
41
+ ownKeys = Object.getOwnPropertyNames || function (o) {
42
+ var ar = [];
43
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
44
+ return ar;
45
+ };
46
+ return ownKeys(o);
47
+ };
48
+ return function (mod) {
49
+ if (mod && mod.__esModule) return mod;
50
+ var result = {};
51
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
52
+ __setModuleDefault(result, mod);
53
+ return result;
54
+ };
55
+ })();
56
+ Object.defineProperty(exports, "__esModule", { value: true });
57
+ exports.analyzeFastifyApp = analyzeFastifyApp;
58
+ exports.analyzeFastifyFile = analyzeFastifyFile;
59
+ const fs = __importStar(require("fs"));
60
+ const MUTATION_METHODS = new Set(["post", "put", "patch", "delete"]);
61
+ const AUTH_ENTRY_WORDS = [
62
+ "login", "signin", "sign_in", "regist", "signup", "sign_up",
63
+ "token", "auth", "health", "status",
64
+ ];
65
+ const AUTH_FN_WORDS = [
66
+ "auth", "login", "permission", "token", "credential", "session",
67
+ "jwt", "verify", "guard", "protect",
68
+ ];
69
+ function isAuthEntryPath(pathName) {
70
+ const lower = pathName.toLowerCase();
71
+ return AUTH_ENTRY_WORDS.some((w) => lower.includes(w));
72
+ }
73
+ function isAuthFnName(name) {
74
+ const lower = name.toLowerCase();
75
+ return AUTH_FN_WORDS.some((w) => lower.includes(w));
76
+ }
77
+ // ── Analysis(代码串级,镜像 express-detector) ──
78
+ function analyzeFastifyApp(code) {
79
+ const issues = [];
80
+ const routes = [];
81
+ const authHooks = [];
82
+ const hasFastify = /\bFastify\b|\bfastify\b/.test(code);
83
+ if (!hasFastify) {
84
+ return { hasFastify: false, routes, authHooks, issues };
85
+ }
86
+ // 全局认证钩子:addHook('preHandler'|'preValidation'|'onRequest', authFn)
87
+ const hookRe = /\.addHook\s*\(\s*['"](preHandler|preValidation|onRequest)['"]\s*,\s*([A-Za-z_$][\w$]*)/g;
88
+ let m;
89
+ while ((m = hookRe.exec(code)) !== null) {
90
+ if (isAuthFnName(m[2]))
91
+ authHooks.push(m[2]);
92
+ }
93
+ // 路由注册:fastify.post('/x', {...}, handler) —— 捕获到 options 窗口
94
+ const routeRe = /\.(get|post|put|patch|delete|route)\s*\(\s*['"]([^'"]+)['"]/g;
95
+ while ((m = routeRe.exec(code)) !== null) {
96
+ const method = m[1].toLowerCase();
97
+ const pathName = m[2];
98
+ // 从路由调用起点向后截取 400 字符窗口,查 preHandler/preValidation 选项
99
+ const windowStart = m.index;
100
+ const window = code.slice(windowStart, windowStart + 400);
101
+ const hasAuthOption = /\b(preHandler|preValidation)\s*:/g.test(window)
102
+ && (() => {
103
+ // 选项值里出现 auth-like 函数名才认(保守)
104
+ const optMatch = window.match(/\b(preHandler|preValidation)\s*:\s*\[([^\]]*)\]/);
105
+ if (!optMatch)
106
+ return false;
107
+ return optMatch[2].split(",").some((name) => isAuthFnName(name.trim()));
108
+ })();
109
+ routes.push({
110
+ method,
111
+ path: pathName,
112
+ protected: hasAuthOption,
113
+ line: code.slice(0, m.index).split("\n").length,
114
+ });
115
+ if (MUTATION_METHODS.has(method) && !hasAuthOption
116
+ && authHooks.length === 0 && !isAuthEntryPath(pathName)) {
117
+ issues.push({
118
+ severity: "medium",
119
+ rule: "FASTIFY_ROUTE_NO_AUTH",
120
+ message: `Route ${method.toUpperCase()} ${pathName} is registered without ` +
121
+ `preHandler/preValidation auth and the app has no auth hook — ` +
122
+ `any caller can reach it.`,
123
+ route: `${method.toUpperCase()} ${pathName}`,
124
+ line: code.slice(0, m.index).split("\n").length,
125
+ });
126
+ }
127
+ }
128
+ return { hasFastify: true, routes, authHooks, issues };
129
+ }
130
+ function analyzeFastifyFile(filePath) {
131
+ if (!fs.existsSync(filePath))
132
+ return null;
133
+ const code = fs.readFileSync(filePath, "utf-8");
134
+ if (!/from\s+['"]fastify['"]|require\(['"]fastify['"]\)/.test(code))
135
+ return null;
136
+ return analyzeFastifyApp(code);
137
+ }
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ /**
4
+ * fastify-detector.test.ts — Fastify 框架适配器规则回归(纯函数,无文件 I/O)
5
+ *
6
+ * 代码串级分析(镜像 express-detector):路由注册 + preHandler/preValidation
7
+ * 认证选项 + addHook 认证钩子。
8
+ */
9
+ const vitest_1 = require("vitest");
10
+ const fastify_detector_1 = require("./fastify-detector");
11
+ const app = (routes, hooks = "") => `
12
+ import Fastify from "fastify";
13
+ const fastify = Fastify();
14
+ ${routes}
15
+ ${hooks}
16
+ `;
17
+ (0, vitest_1.describe)("fastify-detector", () => {
18
+ (0, vitest_1.it)("R1:无保护 mutation 路由 → FASTIFY_ROUTE_NO_AUTH", () => {
19
+ const { issues } = (0, fastify_detector_1.analyzeFastifyApp)(app(`
20
+ fastify.post("/transfer", async (req, reply) => ({ ok: true }));
21
+ `));
22
+ (0, vitest_1.expect)(issues.map((i) => i.rule)).toContain("FASTIFY_ROUTE_NO_AUTH");
23
+ });
24
+ (0, vitest_1.it)("R1:preHandler 认证选项保护不报", () => {
25
+ const { issues } = (0, fastify_detector_1.analyzeFastifyApp)(app(`
26
+ fastify.post("/transfer", { preHandler: [authenticate] }, async (req, reply) => ({ ok: true }));
27
+ `));
28
+ (0, vitest_1.expect)(issues).toHaveLength(0);
29
+ });
30
+ (0, vitest_1.it)("R1:preValidation 认证选项保护不报", () => {
31
+ const { issues } = (0, fastify_detector_1.analyzeFastifyApp)(app(`
32
+ fastify.put("/update", { preValidation: [checkToken] }, async (req, reply) => ({ ok: true }));
33
+ `));
34
+ (0, vitest_1.expect)(issues).toHaveLength(0);
35
+ });
36
+ (0, vitest_1.it)("R1:addHook 认证钩子全局保护不报", () => {
37
+ const { issues } = (0, fastify_detector_1.analyzeFastifyApp)(app(`fastify.post("/transfer", async (req, reply) => ({ ok: true }));`, `fastify.addHook("preHandler", authenticate);`));
38
+ (0, vitest_1.expect)(issues).toHaveLength(0);
39
+ });
40
+ (0, vitest_1.it)("R1:非认证 addHook(如日志)不视为保护", () => {
41
+ const { issues } = (0, fastify_detector_1.analyzeFastifyApp)(app(`fastify.post("/transfer", async (req, reply) => ({ ok: true }));`, `fastify.addHook("onRequest", logRequest);`));
42
+ (0, vitest_1.expect)(issues.map((i) => i.rule)).toContain("FASTIFY_ROUTE_NO_AUTH");
43
+ });
44
+ (0, vitest_1.it)("R1:GET 读操作不报", () => {
45
+ const { issues } = (0, fastify_detector_1.analyzeFastifyApp)(app(`
46
+ fastify.get("/articles", async (req, reply) => ({ items: [] }));
47
+ `));
48
+ (0, vitest_1.expect)(issues).toHaveLength(0);
49
+ });
50
+ (0, vitest_1.it)("R1 豁免:login/regist/token 认证入口路径不报", () => {
51
+ const { issues } = (0, fastify_detector_1.analyzeFastifyApp)(app(`
52
+ fastify.post("/login", async (req, reply) => ({ token: "t" }));
53
+ fastify.post("/register", async (req, reply) => ({ ok: true }));
54
+ `));
55
+ (0, vitest_1.expect)(issues).toHaveLength(0);
56
+ });
57
+ (0, vitest_1.it)("非 Fastify 代码不产生任何问题", () => {
58
+ const { hasFastify, issues } = (0, fastify_detector_1.analyzeFastifyApp)(`import express from "express"; const app = express(); app.post("/x", h);`);
59
+ (0, vitest_1.expect)(hasFastify).toBe(false);
60
+ (0, vitest_1.expect)(issues).toHaveLength(0);
61
+ });
62
+ });
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ /**
3
+ * Flask Framework Adapter — Protocol Detection for Flask
4
+ *
5
+ * 第 5 个框架适配(Python 第 3 个):结构扫描由 tools/extract_framework_flask.py
6
+ * (Python AST)完成——@app.route/@bp.route、methods kwarg、认证装饰器、
7
+ * before_request 认证守卫、Blueprint。
8
+ * 本模块消费结构 JSON 做规则判定:
9
+ *
10
+ * FLASK_ROUTE_NO_AUTH mutation 路由(methods 含 POST/PUT/PATCH/
11
+ * DELETE)无认证装饰器,且项目无认证
12
+ * before_request 守卫——路由级 missing-auth
13
+ *
14
+ * 口径(如实):
15
+ * - @app.route 缺省 methods = GET only(Flask 语义)——公开读不检查
16
+ * - 认证 before_request 按函数名词汇识别(auth/login/permission 等);
17
+ * 自定义守卫函数若名字不含词表会漏判(保守方向是漏报不是误报)
18
+ * - 认证入口端点按 处理器名+路径 词汇豁免(login/regist/token/health)
19
+ * - Blueprint 内的 before_request 与 app 级同权重(项目内任一认证守卫
20
+ * 存在即视为全局保护信号)
21
+ */
22
+ Object.defineProperty(exports, "__esModule", { value: true });
23
+ exports.analyzeFlaskStructure = analyzeFlaskStructure;
24
+ const MUTATION_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE"]);
25
+ const AUTH_ENTRY_WORDS = [
26
+ "login", "signin", "sign_in", "regist", "signup", "sign_up",
27
+ "token", "auth", "health", "status",
28
+ ];
29
+ function isAuthEntry(route) {
30
+ const haystack = `${route.handler} ${route.path}`.toLowerCase();
31
+ return AUTH_ENTRY_WORDS.some((w) => haystack.includes(w));
32
+ }
33
+ function analyzeFlaskStructure(data) {
34
+ if (!data.hasFlask || !Array.isArray(data.routes)) {
35
+ return { hasFlask: false, issues: [] };
36
+ }
37
+ const issues = [];
38
+ const hasGlobalAuthGuard = (data.beforeRequestAuth || []).length > 0;
39
+ for (const route of data.routes) {
40
+ const isMutation = (route.methods || []).some((m) => MUTATION_METHODS.has(m));
41
+ if (!isMutation)
42
+ continue;
43
+ const hasAuthDecorator = (route.authDecorators || []).length > 0;
44
+ if (hasAuthDecorator || hasGlobalAuthGuard)
45
+ continue;
46
+ if (isAuthEntry(route))
47
+ continue;
48
+ issues.push({
49
+ severity: "medium",
50
+ rule: "FLASK_ROUTE_NO_AUTH",
51
+ message: `Route ${(route.methods || []).join("/")} ${route.path || "(root)"} has no ` +
52
+ `auth decorator and the app has no auth before_request guard — any ` +
53
+ `visitor can invoke "${route.handler}".`,
54
+ route: `${(route.methods || []).join("/")} ${route.path || "(root)"}`,
55
+ handler: route.handler,
56
+ file: route.file,
57
+ line: route.line,
58
+ });
59
+ }
60
+ return { hasFlask: true, issues };
61
+ }
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ /**
4
+ * flask-detector.test.ts — Flask 框架适配器规则回归(纯函数,无文件 I/O)
5
+ */
6
+ const vitest_1 = require("vitest");
7
+ const flask_detector_1 = require("./flask-detector");
8
+ function structure(partial) {
9
+ return {
10
+ hasFlask: true,
11
+ apps: ["app"],
12
+ blueprints: [],
13
+ routes: [],
14
+ beforeRequestAuth: [],
15
+ filesScanned: 1,
16
+ ...partial,
17
+ };
18
+ }
19
+ const route = (p) => ({
20
+ methods: p.methods,
21
+ path: p.path || "",
22
+ handler: p.handler,
23
+ file: "app.py",
24
+ line: 1,
25
+ target: "app",
26
+ authDecorators: p.authDecorators || [],
27
+ });
28
+ (0, vitest_1.describe)("flask-detector", () => {
29
+ (0, vitest_1.it)("R1:无保护 mutation 路由 → FLASK_ROUTE_NO_AUTH", () => {
30
+ const { issues } = (0, flask_detector_1.analyzeFlaskStructure)(structure({
31
+ routes: [route({ methods: ["POST"], handler: "transfer_money" })],
32
+ }));
33
+ (0, vitest_1.expect)(issues.map((i) => i.rule)).toContain("FLASK_ROUTE_NO_AUTH");
34
+ });
35
+ (0, vitest_1.it)("R1:@login_required 保护不报", () => {
36
+ const { issues } = (0, flask_detector_1.analyzeFlaskStructure)(structure({
37
+ routes: [route({
38
+ methods: ["POST"], handler: "transfer_money",
39
+ authDecorators: ["login_required"],
40
+ })],
41
+ }));
42
+ (0, vitest_1.expect)(issues).toHaveLength(0);
43
+ });
44
+ (0, vitest_1.it)("R1:before_request 认证守卫存在时不报(全局保护)", () => {
45
+ const { issues } = (0, flask_detector_1.analyzeFlaskStructure)(structure({
46
+ beforeRequestAuth: ["authenticate"],
47
+ routes: [route({ methods: ["POST"], handler: "transfer_money" })],
48
+ }));
49
+ (0, vitest_1.expect)(issues).toHaveLength(0);
50
+ });
51
+ (0, vitest_1.it)("R1:缺省 methods 的 GET 路由不报(Flask 缺省=GET only)", () => {
52
+ const { issues } = (0, flask_detector_1.analyzeFlaskStructure)(structure({
53
+ routes: [route({ methods: ["GET"], handler: "home" })],
54
+ }));
55
+ (0, vitest_1.expect)(issues).toHaveLength(0);
56
+ });
57
+ (0, vitest_1.it)("R1 豁免:login/regist 认证入口端点不报", () => {
58
+ const { issues } = (0, flask_detector_1.analyzeFlaskStructure)(structure({
59
+ routes: [
60
+ route({ methods: ["POST"], path: "/login", handler: "login" }),
61
+ route({ methods: ["POST"], path: "/register", handler: "register_user" }),
62
+ ],
63
+ }));
64
+ (0, vitest_1.expect)(issues).toHaveLength(0);
65
+ });
66
+ (0, vitest_1.it)("非 Flask 结构不产生任何问题", () => {
67
+ const { hasFlask, issues } = (0, flask_detector_1.analyzeFlaskStructure)(structure({ hasFlask: false, routes: [] }));
68
+ (0, vitest_1.expect)(hasFlask).toBe(false);
69
+ (0, vitest_1.expect)(issues).toHaveLength(0);
70
+ });
71
+ });
@@ -8,11 +8,11 @@
8
8
  * Each adapter knows the API surface of a specific framework:
9
9
  * middleware chains, route handlers, dependency injection, guards, etc.
10
10
  *
11
- * Current: Express.js (first adapter broke the 0/13 gap)
12
- * Planned: Next.js, NestJS, Fastify, FastAPI, Django
11
+ * Adapters: Express, tRPC, NestJS(partial), FastAPI, Django, Flask, Fastify,
12
+ * Next.js (App Router) 7 dedicated detectors.
13
13
  */
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
- exports.generateVersionAwarenessReport = exports.checkFileRename = exports.checkFrameworkConventions = exports.detectFrameworks = exports.formatExpressReport = exports.classifyMiddleware = exports.extractGlobalMiddleware = exports.extractRoutes = exports.detectExpressApp = exports.analyzeExpressProject = exports.analyzeExpressFile = exports.analyzeExpressApp = void 0;
15
+ exports.generateVersionAwarenessReport = exports.checkFileRename = exports.checkFrameworkConventions = exports.detectFrameworks = exports.readNextMiddleware = exports.analyzeNextApp = exports.analyzeFastifyFile = exports.analyzeFastifyApp = exports.analyzeFlaskStructure = exports.analyzeDjangoStructure = exports.analyzeFastapiStructure = exports.formatExpressReport = exports.classifyMiddleware = exports.extractGlobalMiddleware = exports.extractRoutes = exports.detectExpressApp = exports.analyzeExpressProject = exports.analyzeExpressFile = exports.analyzeExpressApp = void 0;
16
16
  var express_detector_1 = require("./express-detector");
17
17
  Object.defineProperty(exports, "analyzeExpressApp", { enumerable: true, get: function () { return express_detector_1.analyzeExpressApp; } });
18
18
  Object.defineProperty(exports, "analyzeExpressFile", { enumerable: true, get: function () { return express_detector_1.analyzeExpressFile; } });
@@ -22,6 +22,18 @@ Object.defineProperty(exports, "extractRoutes", { enumerable: true, get: functio
22
22
  Object.defineProperty(exports, "extractGlobalMiddleware", { enumerable: true, get: function () { return express_detector_1.extractGlobalMiddleware; } });
23
23
  Object.defineProperty(exports, "classifyMiddleware", { enumerable: true, get: function () { return express_detector_1.classifyMiddleware; } });
24
24
  Object.defineProperty(exports, "formatExpressReport", { enumerable: true, get: function () { return express_detector_1.formatExpressReport; } });
25
+ var fastapi_detector_1 = require("./fastapi-detector");
26
+ Object.defineProperty(exports, "analyzeFastapiStructure", { enumerable: true, get: function () { return fastapi_detector_1.analyzeFastapiStructure; } });
27
+ var django_detector_1 = require("./django-detector");
28
+ Object.defineProperty(exports, "analyzeDjangoStructure", { enumerable: true, get: function () { return django_detector_1.analyzeDjangoStructure; } });
29
+ var flask_detector_1 = require("./flask-detector");
30
+ Object.defineProperty(exports, "analyzeFlaskStructure", { enumerable: true, get: function () { return flask_detector_1.analyzeFlaskStructure; } });
31
+ var fastify_detector_1 = require("./fastify-detector");
32
+ Object.defineProperty(exports, "analyzeFastifyApp", { enumerable: true, get: function () { return fastify_detector_1.analyzeFastifyApp; } });
33
+ Object.defineProperty(exports, "analyzeFastifyFile", { enumerable: true, get: function () { return fastify_detector_1.analyzeFastifyFile; } });
34
+ var nextjs_detector_1 = require("./nextjs-detector");
35
+ Object.defineProperty(exports, "analyzeNextApp", { enumerable: true, get: function () { return nextjs_detector_1.analyzeNextApp; } });
36
+ Object.defineProperty(exports, "readNextMiddleware", { enumerable: true, get: function () { return nextjs_detector_1.readNextMiddleware; } });
25
37
  // ── Framework Version-Aware Governance ──
26
38
  var version_awareness_1 = require("./version-awareness");
27
39
  Object.defineProperty(exports, "detectFrameworks", { enumerable: true, get: function () { return version_awareness_1.detectFrameworks; } });
@@ -0,0 +1,172 @@
1
+ "use strict";
2
+ /**
3
+ * Next.js Framework Adapter — App Router route handler analysis
4
+ *
5
+ * 第 7 个框架适配(TS/JS 第 4 个专用检测器)。Next.js 的「路由」是文件
6
+ * 而不是代码声明:app 下各段的 route.ts 每个导出 POST/PUT/PATCH/DELETE 的文件
7
+ * 就是一个对外写入口。本模块做文件级结构扫描:
8
+ *
9
+ * mutation 导出 export function POST/PUT/PATCH/DELETE(...)
10
+ * 路由级认证信号 route.ts 内调用 next-auth(getServerSession/auth())
11
+ * 或自定义认证(requireAuth/verifyToken/getToken/
12
+ * withAuth/isAuthenticated 等)
13
+ * 全局认证信号 middleware.ts(项目根或 src/)内容命中认证词表
14
+ *
15
+ * 规则:
16
+ * NEXT_ROUTE_NO_AUTH mutation 路由文件无路由级认证调用,且项目
17
+ * 无认证 middleware——路由级 missing-auth
18
+ *
19
+ * 口径(如实):
20
+ * - GET 导出不检查(公开读是常见设计)
21
+ * - 认证信号按词表识别;自定义认证名不含词表会漏判(保守方向是漏报)
22
+ * - 认证入口路径按词汇豁免(login/regist/auth/token)
23
+ * - Server Components / page.tsx 的非 API 页面不检查(只盯 route.ts
24
+ * 与 pages/api——对外 API 面)
25
+ */
26
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
27
+ if (k2 === undefined) k2 = k;
28
+ var desc = Object.getOwnPropertyDescriptor(m, k);
29
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
30
+ desc = { enumerable: true, get: function() { return m[k]; } };
31
+ }
32
+ Object.defineProperty(o, k2, desc);
33
+ }) : (function(o, m, k, k2) {
34
+ if (k2 === undefined) k2 = k;
35
+ o[k2] = m[k];
36
+ }));
37
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
38
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
39
+ }) : function(o, v) {
40
+ o["default"] = v;
41
+ });
42
+ var __importStar = (this && this.__importStar) || (function () {
43
+ var ownKeys = function(o) {
44
+ ownKeys = Object.getOwnPropertyNames || function (o) {
45
+ var ar = [];
46
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
47
+ return ar;
48
+ };
49
+ return ownKeys(o);
50
+ };
51
+ return function (mod) {
52
+ if (mod && mod.__esModule) return mod;
53
+ var result = {};
54
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
55
+ __setModuleDefault(result, mod);
56
+ return result;
57
+ };
58
+ })();
59
+ Object.defineProperty(exports, "__esModule", { value: true });
60
+ exports.analyzeNextApp = analyzeNextApp;
61
+ exports.readNextMiddleware = readNextMiddleware;
62
+ const fs = __importStar(require("fs"));
63
+ const path = __importStar(require("path"));
64
+ const MUTATION_EXPORTS = ["POST", "PUT", "PATCH", "DELETE"];
65
+ const AUTH_ENTRY_WORDS = [
66
+ "login", "signin", "sign_in", "regist", "signup", "sign_up",
67
+ "token", "auth", "health",
68
+ ];
69
+ const AUTH_CALL_RE = /\b(getServerSession|requireAuth|requireUser|verifyToken|verifyAuth|isAuthenticated|checkAuth|getToken|withAuth|authSession|authenticate)\s*\(/;
70
+ const AUTH_MIDDLEWARE_RE = /\b(getServerSession|requireAuth|verifyToken|getToken|withAuth|next-auth|authorization|authenticate)\b/;
71
+ function isAuthEntryFile(relFile) {
72
+ const lower = relFile.toLowerCase();
73
+ return AUTH_ENTRY_WORDS.some((w) => lower.includes(w));
74
+ }
75
+ // ── Analysis ──
76
+ /**
77
+ * 扫描 Next.js 项目的 API 路由文件(app 下各段的 route.ts + pages/api 下各段)。
78
+ * @param projectRoot — 项目根目录
79
+ * @param middlewareCode — middleware.ts 内容(调用方预读,可为空)
80
+ */
81
+ function analyzeNextApp(projectRoot, middlewareCode) {
82
+ const routeFiles = [];
83
+ const issues = [];
84
+ let legacyApiFiles = 0; // pages/api 旧式 handler(方法不可静态区分,只计数)
85
+ const hasAuthMiddleware = !!middlewareCode && AUTH_MIDDLEWARE_RE.test(middlewareCode);
86
+ const candidates = [];
87
+ for (const base of ["app", "src/app"]) {
88
+ const dir = path.join(projectRoot, base);
89
+ if (fs.existsSync(dir)) {
90
+ collectRouteFiles(dir, candidates);
91
+ }
92
+ }
93
+ const pagesApi = path.join(projectRoot, "pages", "api");
94
+ if (fs.existsSync(pagesApi)) {
95
+ collectRouteFiles(pagesApi, candidates);
96
+ }
97
+ for (const file of candidates) {
98
+ let code;
99
+ try {
100
+ code = fs.readFileSync(file, "utf-8");
101
+ }
102
+ catch {
103
+ continue;
104
+ }
105
+ const mutations = [];
106
+ for (const m of MUTATION_EXPORTS) {
107
+ if (new RegExp(`export\\s+(async\\s+)?function\\s+${m}\\b`).test(code)) {
108
+ mutations.push(m);
109
+ }
110
+ }
111
+ if (mutations.length === 0) {
112
+ legacyApiFiles++;
113
+ continue;
114
+ }
115
+ const hasAuthCall = AUTH_CALL_RE.test(code);
116
+ const rel = path.relative(projectRoot, file);
117
+ routeFiles.push({ file: rel, mutations, hasAuthCall });
118
+ if (!hasAuthCall && !hasAuthMiddleware && !isAuthEntryFile(rel)) {
119
+ issues.push({
120
+ severity: "medium",
121
+ rule: "NEXT_ROUTE_NO_AUTH",
122
+ message: `API route ${rel} exports ${mutations.join("/")} without an auth ` +
123
+ `check in the handler and the project has no auth middleware — ` +
124
+ `any caller can reach it.`,
125
+ route: rel,
126
+ file: rel,
127
+ });
128
+ }
129
+ }
130
+ return {
131
+ hasNext: routeFiles.length > 0 || legacyApiFiles > 0 || !!middlewareCode,
132
+ routeFiles,
133
+ hasAuthMiddleware,
134
+ issues,
135
+ };
136
+ }
137
+ function collectRouteFiles(dir, out) {
138
+ let entries;
139
+ try {
140
+ entries = fs.readdirSync(dir, { withFileTypes: true });
141
+ }
142
+ catch {
143
+ return;
144
+ }
145
+ for (const e of entries) {
146
+ const full = path.join(dir, e.name);
147
+ if (e.isDirectory()) {
148
+ collectRouteFiles(full, out);
149
+ }
150
+ else if (e.isFile() && e.name === "route.ts") {
151
+ out.push(full);
152
+ }
153
+ else if (e.isFile() && /\.(ts|js)$/.test(e.name) && dir.includes("pages" + path.sep + "api")) {
154
+ out.push(full);
155
+ }
156
+ }
157
+ }
158
+ /** 读取项目的 middleware 代码(根或 src/) */
159
+ function readNextMiddleware(projectRoot) {
160
+ for (const rel of ["middleware.ts", "middleware.js", "src/middleware.ts", "src/middleware.js"]) {
161
+ const p = path.join(projectRoot, rel);
162
+ if (fs.existsSync(p)) {
163
+ try {
164
+ return fs.readFileSync(p, "utf-8");
165
+ }
166
+ catch {
167
+ return undefined;
168
+ }
169
+ }
170
+ }
171
+ return undefined;
172
+ }
@@ -0,0 +1,113 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ /**
37
+ * nextjs-detector.test.ts — Next.js App Router 适配器规则回归(文件系统 I/O,
38
+ * 使用临时目录夹具——与 express-detector.test.ts 同款风格)
39
+ */
40
+ const vitest_1 = require("vitest");
41
+ const fs = __importStar(require("fs"));
42
+ const os = __importStar(require("os"));
43
+ const path = __importStar(require("path"));
44
+ const nextjs_detector_1 = require("./nextjs-detector");
45
+ let dir;
46
+ (0, vitest_1.beforeEach)(() => {
47
+ dir = fs.mkdtempSync(path.join(os.tmpdir(), "nextjs-det-"));
48
+ });
49
+ (0, vitest_1.afterEach)(() => {
50
+ fs.rmSync(dir, { recursive: true, force: true });
51
+ });
52
+ function writeRoute(rel, code) {
53
+ const full = path.join(dir, rel);
54
+ fs.mkdirSync(path.dirname(full), { recursive: true });
55
+ fs.writeFileSync(full, code);
56
+ }
57
+ const MUTATION_ROUTE = `export async function POST(req: Request) {
58
+ return Response.json({ ok: true });
59
+ }
60
+ `;
61
+ const AUTHED_ROUTE = `import { getServerSession } from "next-auth";
62
+ export async function POST(req: Request) {
63
+ const session = await getServerSession();
64
+ return Response.json({ ok: true });
65
+ }
66
+ `;
67
+ const AUTH_MIDDLEWARE = `import { withAuth } from "next-auth/middleware";
68
+ export default withAuth(function middleware(req) {});
69
+ `;
70
+ (0, vitest_1.describe)("nextjs-detector", () => {
71
+ (0, vitest_1.it)("R1:无认证 mutation 路由文件 → NEXT_ROUTE_NO_AUTH", () => {
72
+ writeRoute("app/api/transfer/route.ts", MUTATION_ROUTE);
73
+ const { hasNext, issues } = (0, nextjs_detector_1.analyzeNextApp)(dir);
74
+ (0, vitest_1.expect)(hasNext).toBe(true);
75
+ (0, vitest_1.expect)(issues.map((i) => i.rule)).toContain("NEXT_ROUTE_NO_AUTH");
76
+ });
77
+ (0, vitest_1.it)("R1:路由内 getServerSession 认证调用保护不报", () => {
78
+ writeRoute("app/api/transfer/route.ts", AUTHED_ROUTE);
79
+ const { issues } = (0, nextjs_detector_1.analyzeNextApp)(dir);
80
+ (0, vitest_1.expect)(issues).toHaveLength(0);
81
+ });
82
+ (0, vitest_1.it)("R1:认证 middleware 全局保护不报", () => {
83
+ writeRoute("app/api/transfer/route.ts", MUTATION_ROUTE);
84
+ writeRoute("middleware.ts", AUTH_MIDDLEWARE);
85
+ const mw = (0, nextjs_detector_1.readNextMiddleware)(dir);
86
+ const { issues } = (0, nextjs_detector_1.analyzeNextApp)(dir, mw);
87
+ (0, vitest_1.expect)(issues).toHaveLength(0);
88
+ });
89
+ (0, vitest_1.it)("R1:GET 导出不报(公开读)", () => {
90
+ writeRoute("app/api/articles/route.ts", `export async function GET() { return Response.json([]); }`);
91
+ const { issues } = (0, nextjs_detector_1.analyzeNextApp)(dir);
92
+ (0, vitest_1.expect)(issues).toHaveLength(0);
93
+ });
94
+ (0, vitest_1.it)("R1 豁免:login/auth 认证入口路径不报", () => {
95
+ writeRoute("app/api/auth/login/route.ts", MUTATION_ROUTE);
96
+ const { issues } = (0, nextjs_detector_1.analyzeNextApp)(dir);
97
+ (0, vitest_1.expect)(issues).toHaveLength(0);
98
+ });
99
+ (0, vitest_1.it)("pages/api 旧式路由同样覆盖", () => {
100
+ writeRoute("pages/api/transfer.ts", `export default function handler(req, res) { res.json({ok:true}); }`);
101
+ // 无 export function POST 的旧式 handler 不识别方法 → 无 flag(口径如实)
102
+ writeRoute("pages/api/transfer2.ts", `export default async function POST(req: Request) { return Response.json({}); }`);
103
+ const { hasNext, issues } = (0, nextjs_detector_1.analyzeNextApp)(dir);
104
+ (0, vitest_1.expect)(hasNext).toBe(true);
105
+ // transfer2 无 POST 导出匹配(default 导出非具名)——旧式页路由方法不可静态区分,如实
106
+ (0, vitest_1.expect)(issues).toHaveLength(0);
107
+ });
108
+ (0, vitest_1.it)("无 Next.js 结构的目录不产生问题", () => {
109
+ const { hasNext, issues } = (0, nextjs_detector_1.analyzeNextApp)(dir);
110
+ (0, vitest_1.expect)(hasNext).toBe(false);
111
+ (0, vitest_1.expect)(issues).toHaveLength(0);
112
+ });
113
+ });
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.8";
23
+ exports.RUNTIME_VERSION = "3.7.9";
24
24
  function verify(filePath) {
25
25
  const cert = (0, certify_1.certify)(filePath);
26
26
  const kb = (0, protocol_knowledge_1.buildKnowledgeBase)();
@@ -64,6 +64,9 @@ const annotation_suggest_1 = require("../annotation-suggest");
64
64
  const call_graph_propagator_1 = require("./call-graph-propagator");
65
65
  const fastapi_detector_1 = require("../frameworks/fastapi-detector");
66
66
  const django_detector_1 = require("../frameworks/django-detector");
67
+ const flask_detector_1 = require("../frameworks/flask-detector");
68
+ const fastify_detector_1 = require("../frameworks/fastify-detector");
69
+ const nextjs_detector_1 = require("../frameworks/nextjs-detector");
67
70
  const ssg_bridge_1 = require("./ssg-bridge");
68
71
  const call_sequence_1 = require("../call-sequence");
69
72
  const extract_ir_1 = require("../extract-ir");
@@ -96,6 +99,9 @@ async function evaluateTrust(ctx) {
96
99
  const trpcResult = collectTRPCViolations(ctx);
97
100
  const fastapiResult = collectFastapiViolations(ctx);
98
101
  const djangoResult = collectDjangoViolations(ctx);
102
+ const flaskResult = collectFlaskViolations(ctx);
103
+ const fastifyResult = collectFastifyViolations(ctx);
104
+ const nextjsResult = collectNextjsViolations(ctx);
99
105
  const coverageData = collectVerificationCoverage(ctx);
100
106
  const governanceDefects = collectGovernanceDefects(ctx);
101
107
  // ═══════════════════════════════════════
@@ -115,6 +121,9 @@ async function evaluateTrust(ctx) {
115
121
  ...trpcResult.violations,
116
122
  ...fastapiResult.violations,
117
123
  ...djangoResult.violations,
124
+ ...flaskResult.violations,
125
+ ...fastifyResult.violations,
126
+ ...nextjsResult.violations,
118
127
  ];
119
128
  // ═══════════════════════════════════════
120
129
  // PHASE 3: SCORE
@@ -252,6 +261,33 @@ async function evaluateTrust(ctx) {
252
261
  issuesFound: djangoResult.violations.length,
253
262
  }
254
263
  : undefined,
264
+ /** Flask framework adapter coverage — route/auth-guard analysis */
265
+ flaskCoverage: flaskResult.coverage.routes > 0
266
+ ? {
267
+ appsDetected: flaskResult.coverage.apps,
268
+ totalRoutes: flaskResult.coverage.routes,
269
+ filesScanned: flaskResult.coverage.filesScanned,
270
+ issuesFound: flaskResult.violations.length,
271
+ }
272
+ : undefined,
273
+ /** Fastify framework adapter coverage — route/auth-hook analysis */
274
+ fastifyCoverage: fastifyResult.coverage.routes > 0
275
+ ? {
276
+ appsDetected: fastifyResult.coverage.apps,
277
+ totalRoutes: fastifyResult.coverage.routes,
278
+ filesScanned: fastifyResult.coverage.filesScanned,
279
+ issuesFound: fastifyResult.violations.length,
280
+ }
281
+ : undefined,
282
+ /** Next.js framework adapter coverage — App Router route handler analysis */
283
+ nextjsCoverage: nextjsResult.coverage.routes > 0
284
+ ? {
285
+ appsDetected: nextjsResult.coverage.apps,
286
+ totalRoutes: nextjsResult.coverage.routes,
287
+ filesScanned: nextjsResult.coverage.filesScanned,
288
+ issuesFound: nextjsResult.violations.length,
289
+ }
290
+ : undefined,
255
291
  },
256
292
  dimensions: {
257
293
  policyCompliance: {
@@ -390,6 +426,129 @@ function mapPolicyViolation(rv, filePath, _enterprisePolicy) {
390
426
  * Collect Express-specific security violations from the framework detector.
391
427
  * Maps ExpressSecurityIssue[] → TrustViolation[].
392
428
  */
429
+ function collectNextjsViolations(ctx) {
430
+ const violations = [];
431
+ const coverage = { apps: 0, routes: 0, filesScanned: 0 };
432
+ try {
433
+ const middlewareCode = (0, nextjs_detector_1.readNextMiddleware)(ctx.projectPath);
434
+ const analysis = (0, nextjs_detector_1.analyzeNextApp)(ctx.projectPath, middlewareCode);
435
+ if (!analysis.hasNext)
436
+ return { violations, coverage };
437
+ coverage.apps = 1;
438
+ coverage.routes = analysis.routeFiles.length;
439
+ coverage.filesScanned = analysis.routeFiles.length;
440
+ for (const issue of analysis.issues) {
441
+ violations.push({
442
+ severity: issue.severity === "low" ? "low" : issue.severity,
443
+ rule_id: issue.rule,
444
+ file: issue.file || "",
445
+ function: "unknown",
446
+ message: issue.message,
447
+ evidence: issue.route || "",
448
+ why: `Framework structural analysis: ${issue.message}`,
449
+ fix: `Add an auth check inside the route handler (e.g. getServerSession) or protect the app with auth middleware.`,
450
+ policy_ref: "framework-safety.nextjs",
451
+ });
452
+ }
453
+ }
454
+ catch { /* best-effort */ }
455
+ return { violations, coverage };
456
+ }
457
+ function collectFastifyViolations(ctx) {
458
+ const violations = [];
459
+ const coverage = { apps: 0, routes: 0, filesScanned: 0 };
460
+ try {
461
+ const fs = require("fs");
462
+ const candidateDirs = ["src", "server", "app", "api", "routes", "lib"];
463
+ const extensions = languageToExtensions(ctx.language);
464
+ for (const dir of candidateDirs) {
465
+ const dirPath = path.join(ctx.projectPath, dir);
466
+ if (!fs.existsSync(dirPath))
467
+ continue;
468
+ let files;
469
+ try {
470
+ files = walkDir(dirPath, extensions, 100);
471
+ }
472
+ catch {
473
+ continue;
474
+ }
475
+ for (const file of files) {
476
+ if (/\.(test|spec)\.(ts|tsx|js|jsx)$/.test(file))
477
+ continue;
478
+ coverage.filesScanned++;
479
+ try {
480
+ const analysis = (0, fastify_detector_1.analyzeFastifyFile)(file);
481
+ if (!analysis || !analysis.hasFastify)
482
+ continue;
483
+ coverage.apps++;
484
+ coverage.routes += analysis.routes.length;
485
+ for (const issue of analysis.issues) {
486
+ violations.push({
487
+ severity: issue.severity === "low" ? "low" : issue.severity,
488
+ rule_id: issue.rule,
489
+ file: path.relative(ctx.projectPath, file),
490
+ function: "unknown",
491
+ message: issue.message,
492
+ evidence: issue.route || "",
493
+ why: `Framework structural analysis: ${issue.message}`,
494
+ fix: `Add preHandler/preValidation auth to the route options, or register an auth addHook.`,
495
+ policy_ref: "framework-safety.fastify",
496
+ });
497
+ }
498
+ }
499
+ catch { /* skip unreadable files */ }
500
+ }
501
+ }
502
+ }
503
+ catch { /* best-effort */ }
504
+ return { violations, coverage };
505
+ }
506
+ function collectFlaskViolations(ctx) {
507
+ const violations = [];
508
+ const coverage = { apps: 0, routes: 0, filesScanned: 0 };
509
+ // 仅 Python 项目跑框架结构扫描
510
+ const lang = ctx.language || "typescript";
511
+ if (lang !== "python")
512
+ return { violations, coverage };
513
+ try {
514
+ const { execSync } = require("child_process");
515
+ const fs = require("fs");
516
+ const os = require("os");
517
+ // engine.js 位于 dist/trust/ → 仓库根 tools/ 需要两级向上
518
+ const scriptPath = path.resolve(__dirname, "..", "..", "tools", "extract_framework_flask.py");
519
+ const outPath = path.join(os.tmpdir(), `progmune-fwfl-${process.pid}-${Date.now()}.json`);
520
+ execSync(`python3 "${scriptPath}" "${ctx.projectPath}" "${outPath}"`, {
521
+ encoding: "utf-8",
522
+ stdio: "pipe",
523
+ timeout: 60000,
524
+ });
525
+ if (!fs.existsSync(outPath))
526
+ return { violations, coverage };
527
+ const structure = JSON.parse(fs.readFileSync(outPath, "utf-8"));
528
+ fs.unlinkSync(outPath);
529
+ const analysis = (0, flask_detector_1.analyzeFlaskStructure)(structure);
530
+ if (!analysis.hasFlask)
531
+ return { violations, coverage };
532
+ coverage.apps = (structure.apps || []).length + (structure.blueprints || []).length;
533
+ coverage.routes = (structure.routes || []).length;
534
+ coverage.filesScanned = structure.filesScanned || 0;
535
+ for (const issue of analysis.issues) {
536
+ violations.push({
537
+ severity: issue.severity === "low" ? "low" : issue.severity,
538
+ rule_id: issue.rule,
539
+ file: issue.file || "",
540
+ function: issue.handler || "unknown",
541
+ message: issue.message,
542
+ evidence: issue.route || "",
543
+ why: `Framework structural analysis: ${issue.message}`,
544
+ fix: `Add an auth decorator (@login_required or custom) to the route handler, or register an auth before_request guard.`,
545
+ policy_ref: "framework-safety.flask",
546
+ });
547
+ }
548
+ }
549
+ catch { /* best-effort — framework analysis must never break evaluation */ }
550
+ return { violations, coverage };
551
+ }
393
552
  function collectDjangoViolations(ctx) {
394
553
  const violations = [];
395
554
  const coverage = { apps: 0, routes: 0, filesScanned: 0 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "progmune-runtime",
3
- "version": "3.7.8",
3
+ "version": "3.7.9",
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/",