progmune-runtime 3.7.14 → 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.
@@ -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
+ });
@@ -57,6 +57,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
57
57
  exports.analyzeFastifyApp = analyzeFastifyApp;
58
58
  exports.analyzeFastifyFile = analyzeFastifyFile;
59
59
  const fs = __importStar(require("fs"));
60
+ const route_window_1 = require("./route-window");
60
61
  const MUTATION_METHODS = new Set(["post", "put", "patch", "delete"]);
61
62
  const AUTH_ENTRY_WORDS = [
62
63
  "login", "signin", "sign_in", "regist", "signup", "sign_up",
@@ -74,7 +75,50 @@ function isAuthFnName(name) {
74
75
  const lower = name.toLowerCase();
75
76
  return AUTH_FN_WORDS.some((w) => lower.includes(w));
76
77
  }
77
- // ── Analysis(代码串级,镜像 express-detector) ──
78
+ // ── Analysis(代码串级) ──
79
+ /** 自 openIdx('{' 或 '(')取平衡块内文本(字符串感知) */
80
+ function balancedBlock(code, openIdx) {
81
+ const open = code[openIdx];
82
+ const close = open === "{" ? "}" : ")";
83
+ let depth = 1;
84
+ let end = openIdx + 1;
85
+ let quote = null;
86
+ while (end < code.length && depth > 0) {
87
+ const ch = code[end];
88
+ if (quote) {
89
+ if (ch === quote && code[end - 1] !== "\\")
90
+ quote = null;
91
+ }
92
+ else if (ch === '"' || ch === "'" || ch === "`") {
93
+ quote = ch;
94
+ }
95
+ else if (ch === open) {
96
+ depth++;
97
+ }
98
+ else if (ch === close) {
99
+ depth--;
100
+ }
101
+ end++;
102
+ }
103
+ return code.slice(openIdx + 1, Math.max(openIdx + 1, end - 1));
104
+ }
105
+ /** 认证选项名:位置形态(options 对象)与 object-form 路由均可出现 */
106
+ const AUTH_OPTION_NAMES = ["onRequest", "preHandler", "preValidation"];
107
+ /** 选项数组里是否有 auth-like 名(支持点限定 server.authenticate) */
108
+ function optionListHasAuth(listText) {
109
+ return listText.split(",").some((name) => isAuthFnName(name.trim()));
110
+ }
111
+ function hasAuthOptionInBlock(block) {
112
+ for (const opt of AUTH_OPTION_NAMES) {
113
+ const re = new RegExp(`\\b${opt}\\s*:\\s*\\[([^\\]]*)\\]`, "g");
114
+ let mm;
115
+ while ((mm = re.exec(block)) !== null) {
116
+ if (optionListHasAuth(mm[1]))
117
+ return true;
118
+ }
119
+ }
120
+ return false;
121
+ }
78
122
  function analyzeFastifyApp(code) {
79
123
  const issues = [];
80
124
  const routes = [];
@@ -84,44 +128,60 @@ function analyzeFastifyApp(code) {
84
128
  return { hasFastify: false, routes, authHooks, issues };
85
129
  }
86
130
  // 全局认证钩子:addHook('preHandler'|'preValidation'|'onRequest', authFn)
87
- const hookRe = /\.addHook\s*\(\s*['"](preHandler|preValidation|onRequest)['"]\s*,\s*([A-Za-z_$][\w$]*)/g;
131
+ const hookRe = /\.addHook\s*\(\s*['"](preHandler|preValidation|onRequest)['"]\s*,\s*([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)/g;
88
132
  let m;
89
133
  while ((m = hookRe.exec(code)) !== null) {
90
134
  if (isAuthFnName(m[2]))
91
135
  authHooks.push(m[2]);
92
136
  }
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
- })();
137
+ const pushRoute = (method, pathName, protected_, at) => {
109
138
  routes.push({
110
139
  method,
111
140
  path: pathName,
112
- protected: hasAuthOption,
113
- line: code.slice(0, m.index).split("\n").length,
141
+ protected: protected_,
142
+ line: code.slice(0, at).split("\n").length,
114
143
  });
115
- if (MUTATION_METHODS.has(method) && !hasAuthOption
116
- && authHooks.length === 0 && !isAuthEntryPath(pathName)) {
144
+ };
145
+ // ── object-form 路由:server.route({ method, path, onRequest:[auth], ... })
146
+ // (fastify-realworld 20/20 用此形态——V2 recall 失明根因)
147
+ const objRe = /\.route\s*\(\s*\{/g;
148
+ while ((m = objRe.exec(code)) !== null) {
149
+ const block = balancedBlock(code, m.index + m[0].length - 1); // 自 '{'
150
+ const methodM = block.match(/method\s*:\s*['"]([^'"]+)['"]/);
151
+ if (!methodM)
152
+ continue;
153
+ const method = methodM[1].toLowerCase();
154
+ // path 可能是拼接 options.prefix + 'users/login'——取首个引号字面量
155
+ const pathM = block.match(/path\s*:\s*[^,'"\n]*['"]([^'"]+)['"]/);
156
+ const pathName = pathM ? pathM[1] : "";
157
+ const protected_ = hasAuthOptionInBlock(block);
158
+ pushRoute(method, pathName, protected_, m.index);
159
+ }
160
+ // ── 位置形态:fastify.post('/x', { preHandler: [auth] }, handler) ──
161
+ const posRe = /\.(get|post|put|patch|delete)\s*\(\s*['"]([^'"]+)['"]/g;
162
+ while ((m = posRe.exec(code)) !== null) {
163
+ const method = m[1].toLowerCase();
164
+ const pathName = m[2];
165
+ const window = code.slice(m.index, m.index + 400);
166
+ const protected_ = hasAuthOptionInBlock(window);
167
+ pushRoute(method, pathName, protected_, m.index);
168
+ }
169
+ // register 集合豁免(语义层,同 Koa/Gin):有 <path>/login 姊妹佐证的
170
+ // 账户集合,其无认证 POST = 公开注册
171
+ const registerRoots = (0, route_window_1.collectRegisterRoots)(routes.map((r) => r.path));
172
+ for (const r of routes) {
173
+ if (MUTATION_METHODS.has(r.method) && !r.protected
174
+ && authHooks.length === 0
175
+ && !isAuthEntryPath(r.path)
176
+ && !(r.method === "post" && (0, route_window_1.isRegisterRoot)(r.path, registerRoots))) {
117
177
  issues.push({
118
178
  severity: "medium",
119
179
  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 — ` +
180
+ message: `Route ${r.method.toUpperCase()} ${r.path} is registered without ` +
181
+ `onRequest/preHandler/preValidation auth and the app has no auth hook — ` +
122
182
  `any caller can reach it.`,
123
- route: `${method.toUpperCase()} ${pathName}`,
124
- line: code.slice(0, m.index).split("\n").length,
183
+ route: `${r.method.toUpperCase()} ${r.path}`,
184
+ line: r.line,
125
185
  });
126
186
  }
127
187
  }
@@ -131,7 +191,11 @@ function analyzeFastifyFile(filePath) {
131
191
  if (!fs.existsSync(filePath))
132
192
  return null;
133
193
  const code = fs.readFileSync(filePath, "utf-8");
134
- if (!/from\s+['"]fastify['"]|require\(['"]fastify['"]\)/.test(code))
194
+ // 门:直接 import fastify(应用入口)或 fastify-plugin(真实插件模块——
195
+ // fastify-realworld 的路由模块是 fp(plugin) 包裹、接收 server 实例;
196
+ // 旧门只认 require('fastify') → 0/38 文件进门)
197
+ if (!/from\s+['"]fastify['"]|require\(['"]fastify['"]\)|fastify-plugin/.test(code)) {
135
198
  return null;
199
+ }
136
200
  return analyzeFastifyApp(code);
137
201
  }
@@ -1,4 +1,37 @@
1
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
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
36
  /**
4
37
  * fastify-detector.test.ts — Fastify 框架适配器规则回归(纯函数,无文件 I/O)
@@ -8,6 +41,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
8
41
  */
9
42
  const vitest_1 = require("vitest");
10
43
  const fastify_detector_1 = require("./fastify-detector");
44
+ const fs = __importStar(require("fs"));
45
+ const os = __importStar(require("os"));
46
+ const path = __importStar(require("path"));
11
47
  const app = (routes, hooks = "") => `
12
48
  import Fastify from "fastify";
13
49
  const fastify = Fastify();
@@ -60,3 +96,66 @@ fastify.post("/register", async (req, reply) => ({ ok: true }));
60
96
  (0, vitest_1.expect)(issues).toHaveLength(0);
61
97
  });
62
98
  });
99
+ // ── V2 结构性重写回归:object-form / onRequest / plugin 门 / register 豁免 ──
100
+ (0, vitest_1.describe)("fastify-detector object-form 路由(V2 修复回归)", () => {
101
+ (0, vitest_1.it)("server.route({method,path,onRequest:[server.authenticate]}) 受保护不报", () => {
102
+ const { issues, routes } = (0, fastify_detector_1.analyzeFastifyApp)(app(`
103
+ server.route({
104
+ method: 'POST',
105
+ path: options.prefix + 'articles',
106
+ onRequest: [server.authenticate],
107
+ handler: onCreate
108
+ });
109
+ `));
110
+ (0, vitest_1.expect)(routes.find((r) => r.path === "articles").protected).toBe(true);
111
+ (0, vitest_1.expect)(issues).toHaveLength(0);
112
+ });
113
+ (0, vitest_1.it)("object-form 无认证 mutation → 报", () => {
114
+ const { issues } = (0, fastify_detector_1.analyzeFastifyApp)(app(`
115
+ server.route({
116
+ method: 'POST',
117
+ path: options.prefix + 'payments',
118
+ handler: onPay
119
+ });
120
+ `));
121
+ (0, vitest_1.expect)(issues.map((i) => i.route)).toContain("POST payments");
122
+ });
123
+ (0, vitest_1.it)("点限定 server.authenticate 在 onRequest 数组被识别(词表含 auth)", () => {
124
+ const { routes } = (0, fastify_detector_1.analyzeFastifyApp)(app(`
125
+ server.route({ method: 'DELETE', path: 'x', onRequest: [server.authenticate], handler: h });
126
+ `));
127
+ (0, vitest_1.expect)(routes[0].protected).toBe(true);
128
+ });
129
+ (0, vitest_1.it)("register 集合豁免:POST users(有 users/login 姊妹)不报", () => {
130
+ const { issues } = (0, fastify_detector_1.analyzeFastifyApp)(app(`
131
+ server.route({ method: 'POST', path: options.prefix + 'users/login', handler: onLogin });
132
+ server.route({ method: 'POST', path: options.prefix + 'users', handler: onRegister });
133
+ `));
134
+ (0, vitest_1.expect)(issues.map((i) => i.route)).not.toContain("POST users");
135
+ (0, vitest_1.expect)(issues.map((i) => i.route)).not.toContain("POST users/login");
136
+ });
137
+ });
138
+ (0, vitest_1.describe)("fastify-detector plugin 门(V2 修复回归)", () => {
139
+ (0, vitest_1.it)("fastify-plugin 模块(fp(plugin) 路由文件)现可被分析", () => {
140
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "fastify-det-"));
141
+ try {
142
+ const fp = path.join(dir, "routes-users.js");
143
+ fs.writeFileSync(fp, `
144
+ const fp = require('fastify-plugin')
145
+ async function users (server, options, done) {
146
+ server.route({ method: 'POST', path: 'articles', onRequest: [server.authenticate], handler: h })
147
+ server.route({ method: 'POST', path: 'open', handler: h })
148
+ }
149
+ module.exports = fp(users)
150
+ `);
151
+ const a = (0, fastify_detector_1.analyzeFastifyFile)(fp);
152
+ (0, vitest_1.expect)(a).not.toBeNull();
153
+ (0, vitest_1.expect)(a.routes.length).toBe(2);
154
+ (0, vitest_1.expect)(a.issues.map((i) => i.route)).toContain("POST open");
155
+ (0, vitest_1.expect)(a.issues.map((i) => i.route)).not.toContain("POST articles");
156
+ }
157
+ finally {
158
+ fs.rmSync(dir, { recursive: true, force: true });
159
+ }
160
+ });
161
+ });