progmune-runtime 3.7.13 → 3.7.15

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,17 @@
1
1
  # Changelog
2
2
 
3
+ ## [3.7.14] — 2026-09-02
4
+
5
+ ### Go 提取器修复 + 恢复率裁决 + 框架诚实分层 + 三语料实验
6
+
7
+ - **提取器两处修复**:①无函数体声明行(`//go:linkname` 类)吞并下一个函数(Getuid/RoundTrip 漏检根因——与 C 3.7.4「# 行吞函数」同族);②接收者方法 receiver 括号组误当参数组(returnType 取到整签名)——回归测试锁定
8
+ - **恢复率裁决**(评审第 3 条落地):tools/go-fn-list.go(go/parser 金标,同一文件集+只计有体函数)+ scan-go-recovery.ts——stdlib 四包 **2434/2434 = 100.0%** → **维持纯词法,go/parser 桥推迟**
9
+ - **命名鸿沟测量**(孵化器理论核心数据):词段可桥接 0-1.9%——Go stdlib 惯用命名无法桥接内置规则词汇(与 C 命名鸿沟同构)
10
+ - **框架诚实分层**(评审第 5 条落地):「12/13 结构级」→「**12 专用检测器 = 4 结构级(AST:NestJS/FastAPI/Django/Flask)+ 8 启发式(代码串:Express/tRPC/Fastify/Next.js/Koa/Hapi/Gin/Fiber)**」;转正门槛 = 真实项目 FP 数据点;CLAUDE.md 矛盾行修复;落地页/矩阵/README 双语同步
11
+ - **Go 三语料实验**(REALWORLD_GO_V1.md,评审第 4 条落地):stdlib 4 FP 全 FP(readColonFile 词段桥接跨窗口类);govwa(vendored benchmarks/go-apps/)B 路径 0 flags;**A 路径 demo-real-go-govwa**:真实代码 3 注解(loginAction=verify/SetSession=establish/AuthCheck=guard)→ 0 FP + 植入违规精确定位 + 3 注解/协议——**Go 定位维持注解驱动(Beta)**
12
+ - **TS 795 完整复跑**(评审第 6 条落地):recall 98.5%(有效 100%)/ precision 100% / FP 0——12 个框架探测器全上线后与历史基线一致
13
+ - 验证:35 定向测试;Python 盲测 v1.2 64 零漂移;check 免疫正常
14
+
3
15
  ## [3.7.13] — 2026-09-02
4
16
 
5
17
  ### Go 语言支持 + Gin/Fiber 框架适配——12/13
package/README.md CHANGED
@@ -109,7 +109,7 @@ Progmune is honest about what it can and cannot verify.
109
109
 
110
110
  **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.
111
111
 
112
- **Framework adapters: 12/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), NestJS ✅ (decorator route parsing + global APP_GUARD recognition + @Public exemption), Koa ✅, Hapi ✅ (route-config auth strategy analysis), Gin ✅ and Fiber ✅ (Go route/auth-middleware analysis) have dedicated detectors. 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
+ **Framework adapters: 12 dedicated detectors (4 structural + 8 heuristic).** 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), NestJS ✅ (decorator route parsing + global APP_GUARD recognition + @Public exemption), Koa ✅, Hapi ✅ (route-config auth strategy analysis), Gin ✅ and Fiber ✅ (Go route/auth-middleware analysis) have dedicated detectors. Next.js version-aware governance also exists. Spring Boot/Go frameworks and a few TS frameworks remain — framework adaptation is the #1 product gap.
113
113
 
114
114
  ### What Progmune does NOT cover (honest boundaries)
115
115
 
@@ -235,7 +235,7 @@ High-impact contribution areas:
235
235
  - **Runtime Pipeline:** Detect → Explain → Repair → Validate (L1–L4)
236
236
  - **Trust Engine:** 4-dimension scoring with binary explainability gate
237
237
  - **MCP Tools:** 19 — `progmune_trust_check`, `progmune_score`, `progmune_policy_check`, `progmune_certify`, and more
238
- - **Framework Adapters:** Express ✅, tRPC ✅, FastAPI ✅, Django ✅, Flask ✅, Fastify ✅, Next.js ✅, NestJS ✅ (12/13)
238
+ - **Framework Adapters:** 12 dedicated detectors 4 structural (NestJS/FastAPI/Django/Flask) + 8 heuristic (Express/tRPC/Fastify/Next.js/Koa/Hapi/Gin/Fiber)
239
239
  - **Knowledge Base:** 31 domains, 148 protocol rules, 22 detectors, 26 safeguards, PLSB 13/13 categories — plus 15 source-level detection rules (Python)
240
240
  - **Corpus:** 2,500+ trajectories across 6+ repositories; blind benchmarks 100 (TS) + 90 (Python) projects; real-world validation on 4 application repos
241
241
  - **Current focus:** Enterprise PoC validation + remaining framework-internal boundary FPs
package/README.zh-CN.md CHANGED
@@ -109,7 +109,7 @@ Progmune 对能验证什么、不能验证什么保持诚实。
109
109
 
110
110
  **多语言 IR(注册表式)。** TypeScript(ts-morph)与 Python(AST)提取器是同一注册表(`src/extract-project-ir.ts`)中的注册项;`extractProjectIR` 把检测到的所有语言合并为一份函数 IR,由 agent loop、`execute()` 与 MCP server 共享——agent 可在两种语言上编排函数协议链。新增语言(Go、Java……)= 注册一条提取器,调用方零改动。
111
111
 
112
- **框架适配器:12/13。** Express ✅、tRPC ✅、FastAPI ✅、Django ✅(结构级路由认证分析——逐条检查写操作入口的认证)、Flask ✅(路由/before_request 认证守卫分析)、Fastify ✅(路由 preHandler/钩子认证分析)与 Next.js ✅(App Router 路由处理器认证分析)、NestJS ✅(装饰器路由解析 + 全局 APP_GUARD 守卫识别 + @Public 豁免)、Koa ✅、Hapi ✅(路由配置认证策略分析)、Gin ✅ 与 Fiber ✅(Go 路由/认证中间件分析)有专用检测器;Next.js 另有版本感知治理。Spring Boot、Go 框架及少数 TS 框架待适配——框架适配是 #1 产品缺口。
112
+ **框架适配器:12 专用检测器(4 结构级 + 8 启发式)。** Express ✅、tRPC ✅、FastAPI ✅、Django ✅(结构级路由认证分析——逐条检查写操作入口的认证)、Flask ✅(路由/before_request 认证守卫分析)、Fastify ✅(路由 preHandler/钩子认证分析)与 Next.js ✅(App Router 路由处理器认证分析)、NestJS ✅(装饰器路由解析 + 全局 APP_GUARD 守卫识别 + @Public 豁免)、Koa ✅、Hapi ✅(路由配置认证策略分析)、Gin ✅ 与 Fiber ✅(Go 路由/认证中间件分析)有专用检测器;Next.js 另有版本感知治理。结构级 = AST 解析(NestJS/FastAPI/Django/Flask——合成金标 P=R=100%);启发式 = 代码串模式(Express/tRPC/Fastify/Next.js/Koa/Hapi/Gin/Fiber——单测覆盖,真实项目 FP 数据待补)。Spring Boot 待适配——框架适配是 #1 产品缺口。
113
113
 
114
114
  ### Progmune 不覆盖什么(诚实边界)
115
115
 
@@ -234,7 +234,7 @@ Progmune 建立在"**LLM 输出是统计表演而非推理**"这一前提上—
234
234
  - **运行时管线:** 检测 → 解释 → 修复 → 验证(L1–L4)
235
235
  - **信任引擎:** 四维评分 + 二元可解释性门
236
236
  - **MCP 工具:** 19 个——`progmune_trust_check`、`progmune_score`、`progmune_policy_check`、`progmune_certify` 等
237
- - **框架适配器:** Express ✅、tRPC ✅、FastAPI ✅、Django ✅、Flask ✅、Fastify ✅、Next.js ✅、NestJS ✅(12/13
237
+ - **框架适配器:** 12 专用检测器——4 结构级(NestJS/FastAPI/Django/Flask)+ 8 启发式(Express/tRPC/Fastify/Next.js/Koa/Hapi/Gin/Fiber
238
238
  - **知识库:** 31 个域、148 条协议规则、22 个检测器、26 条防护规则、PLSB 13/13 类别——另加 15 条源码级检测规则(Python)
239
239
  - **语料:** 6+ 仓库 2,500+ 轨迹;盲测基准 100(TS)+ 90(Python)个项目;4 个应用仓库真实验证
240
240
  - **当前重点:** 企业 PoC 验证 + 剩余框架内部边界误报
@@ -230,11 +230,15 @@ function extractGoFile(filePath) {
230
230
  const name = plain ? plain[1] : receiver ? receiver[1] : null;
231
231
  if (!name)
232
232
  continue;
233
- // 多行签名:从 func 起拼接直到找到 { 或 ;
233
+ // 多行签名:从 func 起拼接直到找到 { 或 ;——遇到下一个 func 行即停
234
+ // (无函数体声明行如 `func runtime_args() []string // in package runtime`
235
+ // 不能吞掉紧随其后的函数定义——proc.go::Getuid 漏检根因)
234
236
  let sigText = lines[i];
235
237
  let j = i;
236
238
  while (!sigText.includes("{") && !sigText.includes(";") && j < lines.length - 1) {
237
239
  j++;
240
+ if (/^\s*func\b/.test(lines[j]) && j > i)
241
+ break;
238
242
  sigText += "\n" + lines[j];
239
243
  }
240
244
  const braceIdx = sigText.indexOf("{");
@@ -274,15 +278,47 @@ function extractGoFile(filePath) {
274
278
  const requires = docTag(commentText, "requires")?.split(/[,\s]+/).filter(Boolean);
275
279
  const produces = docTag(commentText, "produces")?.split(/[,\s]+/).filter(Boolean);
276
280
  const useWhen = docTag(commentText, "useWhen")?.split(/[,\s]+/).filter(Boolean);
277
- // 参数列表:签名中第一个 ( ... )
278
- const openParen = signature.indexOf("(");
279
- const closeParen = signature.indexOf(")", openParen);
281
+ // 参数列表:签名中第一个 ( ... )——接收者方法的 receiver 括号组要先跳过
282
+ // (bug 修复:func (s *Service) Login(...) 的 (s *Service) 曾被当作参数组,
283
+ // returnType 取到整个签名)
284
+ let sigAfterReceiver = signature;
285
+ if (receiver) {
286
+ let depth = 0;
287
+ for (let ci = signature.indexOf("("); ci < signature.length; ci++) {
288
+ if (signature[ci] === "(")
289
+ depth++;
290
+ else if (signature[ci] === ")") {
291
+ depth--;
292
+ if (depth === 0) {
293
+ sigAfterReceiver = signature.slice(ci + 1);
294
+ break;
295
+ }
296
+ }
297
+ }
298
+ }
299
+ const openParen = sigAfterReceiver.indexOf("(");
300
+ let closeParen = -1;
301
+ if (openParen >= 0) {
302
+ // 平衡括号匹配(参数可含 func 类型嵌套括号)
303
+ let depth = 0;
304
+ for (let ci = openParen; ci < sigAfterReceiver.length; ci++) {
305
+ if (sigAfterReceiver[ci] === "(")
306
+ depth++;
307
+ else if (sigAfterReceiver[ci] === ")") {
308
+ depth--;
309
+ if (depth === 0) {
310
+ closeParen = ci;
311
+ break;
312
+ }
313
+ }
314
+ }
315
+ }
280
316
  let params = [];
281
317
  if (openParen >= 0 && closeParen > openParen) {
282
- params = splitTopLevelParams(signature.slice(openParen + 1, closeParen));
318
+ params = splitTopLevelParams(sigAfterReceiver.slice(openParen + 1, closeParen));
283
319
  }
284
320
  // 返回类型:签名尾部(剥除泛型括号)
285
- const returnType = signature.slice(closeParen + 1).replace(/\[[^\]]*\]/g, "").trim();
321
+ const returnType = sigAfterReceiver.slice(closeParen + 1).replace(/\[[^\]]*\]/g, "").trim();
286
322
  // 函数体:从签名行(j)的 { 起逐行扫描到匹配 }(掩码后括号平衡,
287
323
  // 增量计数——每行只数一次,避免累积体重复计数)
288
324
  let body = "";
@@ -85,6 +85,23 @@ func (r *Repo) SaveUser(u User) error {
85
85
  (0, vitest_1.expect)(ir.some((f) => f.name === "SaveUser")).toBe(true);
86
86
  (0, vitest_1.expect)(ir.find((f) => f.name === "SaveUser")?.exported).toBe(true);
87
87
  });
88
+ (0, vitest_1.it)("接收者方法 returnType 与参数正确(receiver 括号组不被误取——回归)", () => {
89
+ write("svc.go", `
90
+ package main
91
+
92
+ func (s *Service) Login(ctx Context, user, password string) (Token, error) {
93
+ return Token{}, nil
94
+ }
95
+ `);
96
+ const ir = (0, extract_ir_go_1.extractIRGo)(dir);
97
+ const fn = ir.find((f) => f.name === "Login");
98
+ (0, vitest_1.expect)(fn?.returnType).toBe("(Token, error)");
99
+ // Go 语义:ctx / user / password 是 3 个参数(user 与 password 共享类型)
100
+ (0, vitest_1.expect)(fn?.params).toHaveLength(3);
101
+ (0, vitest_1.expect)(fn?.params[0].type).toContain("ctx");
102
+ (0, vitest_1.expect)(fn?.params[1].type).toContain("user");
103
+ (0, vitest_1.expect)(fn?.params[2].type).toContain("string");
104
+ });
88
105
  (0, vitest_1.it)("多行签名 + 返回类型", () => {
89
106
  write("multi.go", `
90
107
  package main
@@ -60,6 +60,9 @@ function analyzeDjangoStructure(data) {
60
60
  return { hasDjango: false, issues: [] };
61
61
  }
62
62
  const issues = [];
63
+ // ViewSet 经 DefaultRouter 展开为多条路由(集合+详情)——同一视图的
64
+ // DRF_PERMISSION_BYPASS 只报一次(变异形状是视图级的)
65
+ const reportedDrfBypass = new Set();
63
66
  for (const route of data.routes) {
64
67
  if (route.kind !== "fbv" && route.kind !== "cbv")
65
68
  continue;
@@ -101,6 +104,9 @@ function analyzeDjangoStructure(data) {
101
104
  if (!hasMutationMethod(view))
102
105
  continue;
103
106
  if (view.openPermission) {
107
+ if (reportedDrfBypass.has(viewName))
108
+ continue;
109
+ reportedDrfBypass.add(viewName);
104
110
  issues.push({
105
111
  severity: "medium",
106
112
  rule: "DRF_PERMISSION_BYPASS",
@@ -134,3 +134,32 @@ const cbv = (name, extra = {}) => ({
134
134
  (0, vitest_1.expect)(issues).toHaveLength(0);
135
135
  });
136
136
  });
137
+ (0, vitest_1.describe)("django-detector ViewSet 展开回归", () => {
138
+ (0, vitest_1.it)("同一 ViewSet 多条展开路由(集合+详情)DRF_PERMISSION_BYPASS 只报一次", () => {
139
+ const { issues } = (0, django_detector_1.analyzeDjangoStructure)(structure({
140
+ routes: [
141
+ { pattern: "^articles/?$", urlname: "", view: "ArticleViewSet", kind: "cbv", file: "urls.py" },
142
+ { pattern: "^articles/(?P<pk>[^/.]+)/?$", urlname: "", view: "ArticleViewSet", kind: "cbv", file: "urls.py" },
143
+ ],
144
+ views: {
145
+ ArticleViewSet: cbv("ArticleViewSet", {
146
+ isDrf: true, methods: ["create", "list", "retrieve", "update"],
147
+ permissionClasses: ["AllowAny"], openPermission: true,
148
+ }),
149
+ },
150
+ }));
151
+ (0, vitest_1.expect)(issues.filter((i) => i.rule === "DRF_PERMISSION_BYPASS")).toHaveLength(1);
152
+ });
153
+ (0, vitest_1.it)("受保护 ViewSet(IsAuthenticatedOrReadOnly)不报——写面现在被真正检查", () => {
154
+ const { issues } = (0, django_detector_1.analyzeDjangoStructure)(structure({
155
+ routes: [{ pattern: "^articles/?$", urlname: "", view: "ArticleViewSet", kind: "cbv", file: "urls.py" }],
156
+ views: {
157
+ ArticleViewSet: cbv("ArticleViewSet", {
158
+ isDrf: true, methods: ["create", "list", "retrieve", "update"],
159
+ permissionClasses: ["IsAuthenticatedOrReadOnly"], openPermission: false,
160
+ }),
161
+ },
162
+ }));
163
+ (0, vitest_1.expect)(issues).toHaveLength(0);
164
+ });
165
+ });
@@ -59,6 +59,7 @@ exports.analyzeExpressFile = analyzeExpressFile;
59
59
  exports.analyzeExpressProject = analyzeExpressProject;
60
60
  exports.formatExpressReport = formatExpressReport;
61
61
  const fs = __importStar(require("fs"));
62
+ const route_window_1 = require("./route-window");
62
63
  // ── Known auth/security middleware patterns ──
63
64
  const AUTH_MIDDLEWARE_PATTERNS = [
64
65
  /\bpassport\.initialize\b/,
@@ -79,6 +80,10 @@ const AUTH_MIDDLEWARE_PATTERNS = [
79
80
  /\bcheckAuth\b/,
80
81
  /\bprotect\b/,
81
82
  /\bauthenticateRequest\b/,
83
+ // realworld 惯用法:const auth = require('../middleware/auth') →
84
+ // router.get('/x', auth.required, ...)(V1 根因:点成员 auth.required 不可见)
85
+ /\bauth\s*\.\s*(required|optional)\b/i,
86
+ /\bauth\s*\./i,
82
87
  // Variable references — common Express convention (e.g., const auth = passport.authenticate(...))
83
88
  // These are standalone identifiers in middleware position
84
89
  /^auth$/i,
@@ -112,8 +117,6 @@ const VALIDATION_MIDDLEWARE_PATTERNS = [
112
117
  ];
113
118
  const SECURITY_HEADER_PATTERNS = [
114
119
  /\bhelmet\b/,
115
- /\bcors\s*\(/,
116
- /\bcors\b/,
117
120
  /\bcsp\b/,
118
121
  /\bcontentSecurityPolicy\b/,
119
122
  /\bhsts\b/,
@@ -150,10 +153,15 @@ function detectExpressApp(code) {
150
153
  function extractRoutes(code, appName) {
151
154
  const routes = [];
152
155
  const methods = ["get", "post", "put", "delete", "patch", "all", "use"];
156
+ // 接收者:真 app(appName)或路由对象(router/Router/*Router)——
157
+ // 真实 Express 应用把路由注册在 Router 实例上(V1 只提取 1/20+ 根因)
158
+ const receivers = appName && appName !== "app"
159
+ ? `(?:${appName}|router|Router|[A-Za-z_$][\\w$]*[Rr]outer)`
160
+ : `(?:router|Router|[A-Za-z_$][\\w$]*[Rr]outer|app)`;
153
161
  for (const method of methods) {
154
162
  // Pattern: app.get('/path', middleware1, middleware2, handler)
155
163
  // or: router.post('/path', handler)
156
- const routeRegex = new RegExp(`${appName}\\.${method}\\s*\\(\\s*['\"]([^'\"]+)['\"]\\s*,([^;]+)\\)`, "gi");
164
+ const routeRegex = new RegExp(`\\b${receivers}\\.${method}\\s*\\(\\s*['\"]([^'\"]+)['\"]\\s*,([^;]+)\\)`, "gi");
157
165
  let match;
158
166
  while ((match = routeRegex.exec(code)) !== null) {
159
167
  const rawArgs = match[2].trim();
@@ -240,9 +248,18 @@ function analyzeExpressApp(code) {
240
248
  const routes = extractRoutes(code, appName);
241
249
  const globalMiddleware = extractGlobalMiddleware(code, appName);
242
250
  const issues = [];
251
+ // 真 app:代码里实例化了 express()(route 模块只建 Router 不算 app——
252
+ // V1 per-file 计数虚高:6 个路由模块被当作独立 app 各报一遍)
253
+ const appIsCreator = /(?:const|let|var)\s+\w+\s*=\s*express\s*\(|require\(\s*['"]express['"]\s*\)\s*\(/.test(code);
254
+ // register 集合豁免(语义层):users/login 姊妹 → POST users 公开注册
255
+ const registerRoots = (0, route_window_1.collectRegisterRoots)(routes.map((r) => r.path));
256
+ const routeHasAuth = (r) => r.middlewares.some((mm) => AUTH_MIDDLEWARE_PATTERNS.some((pp) => pp.test(mm)));
257
+ const isMutation = (r) => ["post", "put", "patch", "delete"].includes(r.method);
258
+ const nonPublicMutation = (r) => isMutation(r) && !isPublicRoute(r.path)
259
+ && !(r.method === "post" && (0, route_window_1.isRegisterRoot)(r.path, registerRoots));
243
260
  // Check 1: Does the app have any auth middleware at all?
244
261
  const hasGlobalAuth = globalMiddleware.some(m => m.type === "auth");
245
- const hasAnyAuth = hasGlobalAuth || routes.some(r => r.middlewares.some(m => AUTH_MIDDLEWARE_PATTERNS.some(p => p.test(m))));
262
+ const hasAnyAuth = hasGlobalAuth || routes.some(routeHasAuth);
246
263
  // Check 2: Does the app have rate limiting on auth routes?
247
264
  const hasRateLimit = globalMiddleware.some(m => m.type === "rate_limit");
248
265
  const authRoutes = routes.filter(r => /\b(login|signin|signup|register|auth|token|password)\b/i.test(r.path));
@@ -256,7 +273,10 @@ function analyzeExpressApp(code) {
256
273
  // Check 6: Does the app have session management?
257
274
  const hasSession = globalMiddleware.some(m => m.type === "session") || /\bsession\s*\(/.test(code);
258
275
  // ── Generate Issues ──
259
- if (!hasAnyAuth) {
276
+ // 整 app 无认证:仅真 app(实例化 express())且自身有非公开 mutation
277
+ // 路由时报——main.ts 只挂载路由模块(真实认证在 controllers 内)不算裸
278
+ const hasNakedMutation = routes.some(nonPublicMutation);
279
+ if (!hasAnyAuth && appIsCreator && hasNakedMutation) {
260
280
  issues.push({
261
281
  severity: "critical",
262
282
  rule: "EXPRESS_NO_AUTH_MIDDLEWARE",
@@ -265,31 +285,33 @@ function analyzeExpressApp(code) {
265
285
  fix: "Add an auth middleware (e.g., passport.authenticate('jwt'), express-jwt, or a custom auth middleware) to protect routes.",
266
286
  });
267
287
  }
268
- // Check for routes without auth middleware (when auth middleware exists globally)
288
+ // 逐路由缺失认证:文件里已有认证(全局或路由级)时,未保护的非公开
289
+ // mutation 路由单独报——真实 Express 认证惯例是每路由 auth.required
290
+ // (V1 根因:检测器只认全局 app.use)
269
291
  if (hasAnyAuth) {
270
292
  for (const route of routes) {
271
293
  if (["use", "all"].includes(route.method))
272
294
  continue; // skip middleware registrations
273
- const routeHasAuth = route.middlewares.some(m => AUTH_MIDDLEWARE_PATTERNS.some(p => p.test(m)));
274
- if (!routeHasAuth && !hasGlobalAuth) {
275
- // Route has no auth middleware AND no global auth → each route needs its own
276
- // (already reported as EXPRESS_NO_AUTH_MIDDLEWARE above)
295
+ if (!isMutation(route))
296
+ continue; // 读操作不查(公开读常见)
297
+ if (routeHasAuth(route))
277
298
  continue;
278
- }
279
- if (!routeHasAuth && hasGlobalAuth && !isPublicRoute(route.path)) {
280
- issues.push({
281
- severity: "high",
282
- rule: "EXPRESS_ROUTE_MISSING_AUTH",
283
- message: `Route ${route.method.toUpperCase()} ${route.path} has no auth middleware. It may be inadvertently public.`,
284
- route: `${route.method.toUpperCase()} ${route.path}`,
285
- line: route.line,
286
- fix: `Add auth middleware to the route: app.${route.method}('${route.path}', authMiddleware, ${route.handler})`,
287
- });
288
- }
299
+ if (isPublicRoute(route.path))
300
+ continue;
301
+ if (route.method === "post" && (0, route_window_1.isRegisterRoot)(route.path, registerRoots))
302
+ continue;
303
+ issues.push({
304
+ severity: "high",
305
+ rule: "EXPRESS_ROUTE_MISSING_AUTH",
306
+ message: `Route ${route.method.toUpperCase()} ${route.path} has no auth middleware. It may be inadvertently public.`,
307
+ route: `${route.method.toUpperCase()} ${route.path}`,
308
+ line: route.line,
309
+ fix: `Add auth middleware to the route: app.${route.method}('${route.path}', authMiddleware, ${route.handler})`,
310
+ });
289
311
  }
290
312
  }
291
- // Auth routes without rate limiting
292
- if (authRoutes.length > 0 && !hasRateLimit) {
313
+ // Auth routes without rate limiting —— 仅真 app
314
+ if (appIsCreator && authRoutes.length > 0 && !hasRateLimit) {
293
315
  for (const route of authRoutes) {
294
316
  issues.push({
295
317
  severity: "high",
@@ -301,8 +323,8 @@ function analyzeExpressApp(code) {
301
323
  });
302
324
  }
303
325
  }
304
- // Missing security headers
305
- if (!hasHelmet) {
326
+ // Missing security headers —— 仅真 app(route 模块不重复报,V1 ×7 虚高)
327
+ if (appIsCreator && !hasHelmet) {
306
328
  issues.push({
307
329
  severity: "medium",
308
330
  rule: "EXPRESS_NO_HELMET",
@@ -311,9 +333,9 @@ function analyzeExpressApp(code) {
311
333
  fix: "Add helmet middleware: app.use(helmet())",
312
334
  });
313
335
  }
314
- // POST/PUT routes without validation
315
- const mutationRoutes = routes.filter(r => ["post", "put", "patch"].includes(r.method));
316
- if (mutationRoutes.length > 0 && !hasValidation) {
336
+ // POST/PUT routes without validation —— 仅真 app
337
+ const mutationRoutes = routes.filter(r => isMutation(r) && r.method !== "delete");
338
+ if (appIsCreator && mutationRoutes.length > 0 && !hasValidation) {
317
339
  issues.push({
318
340
  severity: "medium",
319
341
  rule: "EXPRESS_NO_INPUT_VALIDATION",
@@ -322,8 +344,8 @@ function analyzeExpressApp(code) {
322
344
  fix: "Add express-validator or zod validation to mutation routes.",
323
345
  });
324
346
  }
325
- // Missing CORS configuration
326
- if (!hasCors) {
347
+ // Missing CORS configuration —— 仅真 app
348
+ if (appIsCreator && !hasCors) {
327
349
  issues.push({
328
350
  severity: "low",
329
351
  rule: "EXPRESS_NO_CORS_CONFIG",
@@ -332,8 +354,8 @@ function analyzeExpressApp(code) {
332
354
  fix: "Add explicit CORS configuration: app.use(cors({ origin: 'https://your-domain.com' }))",
333
355
  });
334
356
  }
335
- // Session without secure settings (if present)
336
- if (hasSession) {
357
+ // Session without secure settings (if present) —— 仅真 app
358
+ if (appIsCreator && hasSession) {
337
359
  const sessionSecure = /secure\s*:\s*true/.test(code) && /httpOnly\s*:\s*true/.test(code) && /sameSite\s*:\s*['"](?:strict|lax)['"]/.test(code);
338
360
  if (!sessionSecure) {
339
361
  issues.push({
@@ -368,6 +390,11 @@ function isPublicRoute(path) {
368
390
  /^\/robots\.txt/i,
369
391
  /^\/$/,
370
392
  ];
393
+ // 尾段认证入口:/users/login、/api/register 等带前缀的真实 world 形态
394
+ // (V1 时代只认 ^/login 精确匹配——前缀登录入口被误报)
395
+ if (/\/?(login|signin|signup|sign_in|register|refresh)(\/|$)/i.test(path)) {
396
+ return true;
397
+ }
371
398
  return publicPatterns.some(p => p.test(path));
372
399
  }
373
400
  /**
@@ -125,6 +125,11 @@ app.listen(3000);
125
125
  (0, vitest_1.it)("should classify helmet as security_header", () => {
126
126
  (0, vitest_1.expect)((0, express_detector_1.classifyMiddleware)("", "helmet()")).toBe("security_header");
127
127
  });
128
+ (0, vitest_1.it)("should classify cors() as cors, NOT security_header (regression: SECURITY_HEADER_PATTERNS 曾含 cors 模式致 cors 恒被误分类 → 用 cors 的应用 NO_HELMET 漏报)", () => {
129
+ (0, vitest_1.expect)((0, express_detector_1.classifyMiddleware)("", "cors()")).toBe("cors");
130
+ (0, vitest_1.expect)((0, express_detector_1.classifyMiddleware)("", "cors({ origin: 'https://example.com' })")).toBe("cors");
131
+ (0, vitest_1.expect)((0, express_detector_1.classifyMiddleware)("", "cors(")).toBe("cors");
132
+ });
128
133
  });
129
134
  (0, vitest_1.describe)("extractGlobalMiddleware", () => {
130
135
  (0, vitest_1.it)("should extract app.use middleware", () => {
@@ -157,6 +162,22 @@ app.listen(3000);
157
162
  const result = (0, express_detector_1.analyzeExpressApp)(INSECURE_APP);
158
163
  (0, vitest_1.expect)(result.issues.some(i => i.rule === "EXPRESS_NO_HELMET")).toBe(true);
159
164
  });
165
+ (0, vitest_1.it)("should still flag NO_HELMET when app uses cors() but no helmet (regression: cors 曾误分类为 security_header → hasHelmet 误真 → FN)", () => {
166
+ const corsNoHelmet = `
167
+ const express = require('express');
168
+ const cors = require('cors');
169
+ const app = express();
170
+ app.use(cors());
171
+ app.get('/', (req, res) => { res.send('ok'); });
172
+ app.listen(3000);
173
+ `;
174
+ const result = (0, express_detector_1.analyzeExpressApp)(corsNoHelmet);
175
+ (0, vitest_1.expect)(result.issues.some(i => i.rule === "EXPRESS_NO_HELMET")).toBe(true);
176
+ // cors IS recognized — no NO_CORS_CONFIG flag on this app
177
+ (0, vitest_1.expect)(result.issues.some(i => i.rule === "EXPRESS_NO_CORS_CONFIG")).toBe(false);
178
+ // cors() must be typed cors, so engine cross-file suppression works
179
+ (0, vitest_1.expect)(result.globalMiddleware.some(m => m.type === "cors")).toBe(true);
180
+ });
160
181
  (0, vitest_1.it)("should detect missing CORS", () => {
161
182
  const result = (0, express_detector_1.analyzeExpressApp)(INSECURE_APP);
162
183
  (0, vitest_1.expect)(result.issues.some(i => i.rule === "EXPRESS_NO_CORS_CONFIG")).toBe(true);
@@ -204,3 +225,51 @@ app.listen(3000);
204
225
  (0, vitest_1.expect)(report).toContain("Security Issues:");
205
226
  });
206
227
  });
228
+ // ── V1 转正回归:接收者路由 / 路由级 auth / 逐路由缺失 / 真 app 门 / 前缀入口 ──
229
+ const ROUTER_MODULE = `
230
+ const express = require('express');
231
+ const router = express.Router();
232
+ const auth = require('../middleware/auth');
233
+ router.get('/articles', async (req, res) => { res.json([]); });
234
+ router.post('/articles', auth.required, async (req, res) => { res.json({}); });
235
+ router.post('/payments', async (req, res) => { res.json({}); });
236
+ module.exports = router;
237
+ `;
238
+ const ROUTER_WITH_LOGIN = `
239
+ const express = require('express');
240
+ const router = express.Router();
241
+ const auth = require('../middleware/auth');
242
+ router.post('/users/login', async (req, res) => { res.json({}); }); // 公开登录
243
+ router.post('/users', async (req, res) => { res.json({}); }); // 公开注册(有 login 姊妹)
244
+ router.put('/user', auth.required, async (req, res) => { res.json({}); });
245
+ module.exports = router;
246
+ `;
247
+ (0, vitest_1.describe)("express V1 转正回归", () => {
248
+ (0, vitest_1.it)("router 接收者路由可提取(旧版只认 app.——20+ 路由只提取 1 条)", () => {
249
+ const { routes } = (0, express_detector_1.analyzeExpressApp)(ROUTER_MODULE);
250
+ (0, vitest_1.expect)(routes.some((r) => r.method === "post" && r.path === "/articles")).toBe(true);
251
+ });
252
+ (0, vitest_1.it)("路由级 auth.required 保护不报;同文件其他无认证 mutation 报 ROUTE_MISSING_AUTH", () => {
253
+ const { issues } = (0, express_detector_1.analyzeExpressApp)(ROUTER_MODULE);
254
+ const missing = issues.filter((i) => i.rule === "EXPRESS_ROUTE_MISSING_AUTH").map((i) => i.route);
255
+ (0, vitest_1.expect)(missing).not.toContain("POST /articles");
256
+ (0, vitest_1.expect)(missing).toContain("POST /payments");
257
+ // 路由模块不是 app:NO_AUTH/NO_HELMET/NO_CORS 不报(V1 per-file 计数虚高修复)
258
+ (0, vitest_1.expect)(issues.some((i) => i.rule === "EXPRESS_NO_AUTH_MIDDLEWARE")).toBe(false);
259
+ (0, vitest_1.expect)(issues.some((i) => i.rule === "EXPRESS_NO_HELMET")).toBe(false);
260
+ });
261
+ (0, vitest_1.it)("前缀登录/注册入口豁免:/users/login 与 /users(姊妹佐证)不报", () => {
262
+ const { issues } = (0, express_detector_1.analyzeExpressApp)(ROUTER_WITH_LOGIN);
263
+ const missing = issues.filter((i) => i.rule === "EXPRESS_ROUTE_MISSING_AUTH").map((i) => i.route);
264
+ (0, vitest_1.expect)(missing).not.toContain("POST /users/login");
265
+ (0, vitest_1.expect)(missing).not.toContain("POST /users");
266
+ (0, vitest_1.expect)(missing).not.toContain("PUT /user");
267
+ });
268
+ (0, vitest_1.it)("摘掉某条 auth.required(其余仍受保护)→ 该 mutation 报 ROUTE_MISSING_AUTH(敏感性)", () => {
269
+ const twoProtected = ROUTER_MODULE.replace("router.post('/payments', async", "router.put('/admin', auth.required, async");
270
+ const stripped = twoProtected.replace("router.post('/articles', auth.required,", "router.post('/articles',");
271
+ const { issues } = (0, express_detector_1.analyzeExpressApp)(stripped);
272
+ (0, vitest_1.expect)(issues.some((i) => i.rule === "EXPRESS_ROUTE_MISSING_AUTH" && i.route === "POST /articles")).toBe(true);
273
+ (0, vitest_1.expect)(issues.some((i) => i.rule === "EXPRESS_ROUTE_MISSING_AUTH" && i.route === "PUT /admin")).toBe(false);
274
+ });
275
+ });