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.
- package/dist/frameworks/django-detector.js +6 -0
- package/dist/frameworks/django-detector.test.js +29 -0
- package/dist/frameworks/express-detector.js +59 -32
- package/dist/frameworks/express-detector.test.js +69 -0
- package/dist/frameworks/fastify-detector.js +91 -27
- package/dist/frameworks/fastify-detector.test.js +99 -0
- package/dist/frameworks/fiber-detector.js +266 -9
- package/dist/frameworks/fiber-detector.test.js +188 -0
- package/dist/frameworks/gin-detector.js +123 -10
- package/dist/frameworks/gin-detector.test.js +183 -0
- package/dist/frameworks/hapi-detector.js +105 -31
- package/dist/frameworks/hapi-detector.test.js +91 -0
- package/dist/frameworks/koa-detector.js +25 -10
- package/dist/frameworks/koa-detector.test.js +38 -0
- package/dist/frameworks/nestjs-detector.js +145 -3
- package/dist/frameworks/nestjs-detector.test.js +103 -0
- package/dist/frameworks/nextjs-detector.js +4 -1
- package/dist/frameworks/nextjs-detector.test.js +35 -0
- package/dist/frameworks/route-window.js +124 -0
- package/dist/frameworks/trpc-detector.js +82 -16
- package/dist/frameworks/trpc-detector.test.js +177 -0
- package/dist/sdk.js +1 -1
- package/package.json +2 -2
- package/tools/extract_framework_django.py +62 -1
- package/tools/extract_framework_flask.py +3 -1
- package/tools/extract_framework_py.py +6 -2
|
@@ -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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
// (already reported as EXPRESS_NO_AUTH_MIDDLEWARE above)
|
|
295
|
+
if (!isMutation(route))
|
|
296
|
+
continue; // 读操作不查(公开读常见)
|
|
297
|
+
if (routeHasAuth(route))
|
|
277
298
|
continue;
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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 =>
|
|
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
|
|
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
|
-
|
|
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:
|
|
113
|
-
line: code.slice(0,
|
|
141
|
+
protected: protected_,
|
|
142
|
+
line: code.slice(0, at).split("\n").length,
|
|
114
143
|
});
|
|
115
|
-
|
|
116
|
-
|
|
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()} ${
|
|
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()} ${
|
|
124
|
-
line:
|
|
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
|
-
|
|
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
|
+
});
|