progmune-runtime 3.7.14 → 3.7.16
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 +4 -3
- package/tools/extract_framework_django.py +62 -1
- package/tools/extract_framework_flask.py +3 -1
- package/tools/extract_framework_py.py +6 -2
|
@@ -54,7 +54,12 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
54
54
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
55
55
|
exports.analyzeGinApp = analyzeGinApp;
|
|
56
56
|
exports.analyzeGinFile = analyzeGinFile;
|
|
57
|
+
exports.ginFuncStarts = ginFuncStarts;
|
|
58
|
+
exports.ginProtectedRegisterFns = ginProtectedRegisterFns;
|
|
59
|
+
exports.ginEnclosingFunc = ginEnclosingFunc;
|
|
60
|
+
exports.analyzeGinProject = analyzeGinProject;
|
|
57
61
|
const fs = __importStar(require("fs"));
|
|
62
|
+
const route_window_1 = require("./route-window");
|
|
58
63
|
const MUTATION_METHODS = new Set(["post", "put", "patch", "delete"]);
|
|
59
64
|
const AUTH_ENTRY_WORDS = [
|
|
60
65
|
"login", "signin", "sign_in", "regist", "signup", "sign_up",
|
|
@@ -84,24 +89,28 @@ function analyzeGinApp(code) {
|
|
|
84
89
|
return { hasGin: false, routes, authGroupMiddleware, issues };
|
|
85
90
|
}
|
|
86
91
|
// 组级认证中间件:r.Use(authMW) / r.Group("/api", authMW) / g.Use(authMW)
|
|
87
|
-
|
|
92
|
+
// 捕获支持点限定成员(users.AuthMiddleware)——修复旧版只捕限定符
|
|
93
|
+
// "users" 的缺陷(V7)。整名送词表判定(AuthMiddleware 含 auth ✓)
|
|
94
|
+
const useRe = /\.Use\s*\(\s*([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)/g;
|
|
88
95
|
let m;
|
|
89
96
|
while ((m = useRe.exec(code)) !== null) {
|
|
90
97
|
if (isAuthFnName(m[1]))
|
|
91
98
|
authGroupMiddleware.push(m[1]);
|
|
92
99
|
}
|
|
93
|
-
const groupRe = /\.Group\s*\(\s*"[^"]*"\s*,\s*([A-Za-z_][\w]*)/g;
|
|
100
|
+
const groupRe = /\.Group\s*\(\s*"[^"]*"\s*,\s*([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)/g;
|
|
94
101
|
while ((m = groupRe.exec(code)) !== null) {
|
|
95
102
|
if (isAuthFnName(m[1]))
|
|
96
103
|
authGroupMiddleware.push(m[1]);
|
|
97
104
|
}
|
|
98
105
|
// 路由注册:r.POST("/x", mw1, mw2, handler)——Go 方法名大写(POST 惯例)
|
|
99
|
-
|
|
106
|
+
// 空路径 "..." 允许(realworld 惯用 POST("", ...) 双注册)
|
|
107
|
+
const routeRe = /\.(get|post|put|patch|delete)\s*\(\s*"([^"]*)"(\s*,\s*|\s*\))/gi;
|
|
100
108
|
while ((m = routeRe.exec(code)) !== null) {
|
|
101
109
|
const method = m[1].toLowerCase();
|
|
102
110
|
const pathName = m[2];
|
|
103
|
-
|
|
104
|
-
const
|
|
111
|
+
// 认证窗口 = 本次调用边界内(括号感知),不跨路由(V7 缺陷修复)
|
|
112
|
+
const window = (0, route_window_1.routeCallWindow)(code, m.index + m[0].length);
|
|
113
|
+
const mwNames = (0, route_window_1.middlewareNamesFromWindow)(window);
|
|
105
114
|
const hasAuthMw = mwNames.some((name) => isAuthFnName(name));
|
|
106
115
|
routes.push({
|
|
107
116
|
method,
|
|
@@ -109,15 +118,23 @@ function analyzeGinApp(code) {
|
|
|
109
118
|
protected: hasAuthMw,
|
|
110
119
|
line: code.slice(0, m.index).split("\n").length,
|
|
111
120
|
});
|
|
112
|
-
|
|
113
|
-
|
|
121
|
+
}
|
|
122
|
+
// register 集合豁免(语义层,同 Koa):有 <path>/login 姊妹佐证的
|
|
123
|
+
// 账户集合,其无认证 POST = 公开注册(gin realworld 用 POST "" /
|
|
124
|
+
// "/" + /login 双注册——路径豁免词表认不出)
|
|
125
|
+
const registerRoots = (0, route_window_1.collectRegisterRoots)(routes.map((r) => r.path));
|
|
126
|
+
for (const r of routes) {
|
|
127
|
+
if (MUTATION_METHODS.has(r.method) && !r.protected
|
|
128
|
+
&& authGroupMiddleware.length === 0
|
|
129
|
+
&& !isAuthEntryPath(r.path)
|
|
130
|
+
&& !(r.method === "post" && (0, route_window_1.isRegisterRoot)(r.path, registerRoots))) {
|
|
114
131
|
issues.push({
|
|
115
132
|
severity: "medium",
|
|
116
133
|
rule: "GIN_ROUTE_NO_AUTH",
|
|
117
|
-
message: `Route ${method.toUpperCase()} ${
|
|
134
|
+
message: `Route ${r.method.toUpperCase()} ${r.path} has no auth middleware ` +
|
|
118
135
|
`and no auth Use/Group middleware — any caller can reach it.`,
|
|
119
|
-
route: `${method.toUpperCase()} ${
|
|
120
|
-
line:
|
|
136
|
+
route: `${r.method.toUpperCase()} ${r.path}`,
|
|
137
|
+
line: r.line,
|
|
121
138
|
});
|
|
122
139
|
}
|
|
123
140
|
}
|
|
@@ -131,3 +148,99 @@ function analyzeGinFile(filePath) {
|
|
|
131
148
|
return null;
|
|
132
149
|
return analyzeGinApp(code);
|
|
133
150
|
}
|
|
151
|
+
/** 顶层函数头(行号 1-based)——排除接收者方法(method receiver) */
|
|
152
|
+
function ginFuncStarts(text) {
|
|
153
|
+
const out = [];
|
|
154
|
+
const re = /^func\s+([A-Za-z_]\w*)\s*\(/gm;
|
|
155
|
+
let m;
|
|
156
|
+
while ((m = re.exec(text)) !== null) {
|
|
157
|
+
out.push({ name: m[1], line: text.slice(0, m.index).split("\n").length });
|
|
158
|
+
}
|
|
159
|
+
return out;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* 从 bootstrap 文本(含 gin.Default/New 的文件)推导:
|
|
163
|
+
* 哪些 Register 函数在「组已施加认证 Use 之后」被调用(其路由受保护)。
|
|
164
|
+
* 语句顺序敏感:Use 之前注册的(register/login)不算。
|
|
165
|
+
*/
|
|
166
|
+
function ginProtectedRegisterFns(bootstrapText) {
|
|
167
|
+
const state = new Map(); // 组变量 → 已认证
|
|
168
|
+
const protectedFns = new Map();
|
|
169
|
+
const re = /(\w+)\s*(?::=|=)\s*gin\.(Default|New)\s*\(|(\w+)\.Use\s*\(\s*([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)(?:\s*\(\s*(true|false))?|(\w+)\s*(?::=|=)\s*(?:[\w.]*\.)?([A-Za-z_]\w*)\.Group\s*\(|(?:[\w.]*\.)?([A-Z][A-Za-z0-9_]*)\s*\(\s*(?:[\w.]*\.)?([A-Za-z_]\w*)\.Group\s*\(/g;
|
|
170
|
+
let m;
|
|
171
|
+
// 组编号(按开括号序):1,2 根组 | 3,4,5 Use | 6,7 子组 | 8,9 注册调用
|
|
172
|
+
while ((m = re.exec(bootstrapText)) !== null) {
|
|
173
|
+
if (m[1] !== undefined) {
|
|
174
|
+
state.set(m[1], false); // 根组:gin.Default/New
|
|
175
|
+
}
|
|
176
|
+
else if (m[3] !== undefined) {
|
|
177
|
+
// 组.Use(认证);首参字面 false = 可选认证(public 读通行)不视为
|
|
178
|
+
// 保护——否则删掉 required Use 后 mutations 仍被可选 Use 掩盖(FN)
|
|
179
|
+
if (isAuthFnName(m[4]) && m[5] !== "false")
|
|
180
|
+
state.set(m[3], true);
|
|
181
|
+
}
|
|
182
|
+
else if (m[6] !== undefined) {
|
|
183
|
+
state.set(m[6], !!state.get(m[7])); // 子组继承父组状态
|
|
184
|
+
}
|
|
185
|
+
else if (m[8] !== undefined) {
|
|
186
|
+
if (state.get(m[9]))
|
|
187
|
+
protectedFns.set(m[8], true); // 认证相位调用的 Register fn
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return protectedFns;
|
|
191
|
+
}
|
|
192
|
+
/** routeLine 所在顶层函数名(按 func 头归属) */
|
|
193
|
+
function ginEnclosingFunc(text, routeLine) {
|
|
194
|
+
let name = null;
|
|
195
|
+
for (const f of ginFuncStarts(text)) {
|
|
196
|
+
if (f.line <= routeLine)
|
|
197
|
+
name = f.name;
|
|
198
|
+
else
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
return name;
|
|
202
|
+
}
|
|
203
|
+
/** 项目级分析:跨文件组认证传播 + 文件级判定兜底 */
|
|
204
|
+
function analyzeGinProject(projectRoot) {
|
|
205
|
+
const files = [];
|
|
206
|
+
// 简易 walk(.go,跳过 _test/vendor/node_modules)
|
|
207
|
+
const walk = (d) => {
|
|
208
|
+
for (const e of fs.readdirSync(d, { withFileTypes: true })) {
|
|
209
|
+
if (e.isDirectory()) {
|
|
210
|
+
if (["vendor", "node_modules", ".git"].includes(e.name))
|
|
211
|
+
continue;
|
|
212
|
+
walk(`${d}/${e.name}`);
|
|
213
|
+
}
|
|
214
|
+
else if (e.name.endsWith(".go") && !e.name.endsWith("_test.go")) {
|
|
215
|
+
const fp = `${d}/${e.name}`;
|
|
216
|
+
try {
|
|
217
|
+
files.push({ file: fp, text: fs.readFileSync(fp, "utf-8"), a: analyzeGinFile(fp) });
|
|
218
|
+
}
|
|
219
|
+
catch { /* skip unreadable */ }
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
if (fs.existsSync(projectRoot))
|
|
224
|
+
walk(projectRoot.replace(/\/$/, ""));
|
|
225
|
+
// bootstrap:含 gin.Default/gin.New 的文件(组认证相位在此推导)
|
|
226
|
+
const bootstrap = files.find((f) => /gin\.(Default|New)\s*\(/.test(f.text));
|
|
227
|
+
const protectedFns = bootstrap
|
|
228
|
+
? ginProtectedRegisterFns(bootstrap.text)
|
|
229
|
+
: new Map();
|
|
230
|
+
const issues = [];
|
|
231
|
+
for (const { file, text, a } of files) {
|
|
232
|
+
if (!a || a.issues.length === 0)
|
|
233
|
+
continue;
|
|
234
|
+
for (const issue of a.issues) {
|
|
235
|
+
const fn = issue.line ? ginEnclosingFunc(text, issue.line) : null;
|
|
236
|
+
if (fn && protectedFns.get(fn))
|
|
237
|
+
continue; // 认证相位注册 → 受保护
|
|
238
|
+
issues.push({ ...issue, route: issue.route });
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return {
|
|
242
|
+
filesScanned: files.length,
|
|
243
|
+
protectedFunctions: [...protectedFns.keys()].filter((k) => protectedFns.get(k)),
|
|
244
|
+
issues,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
@@ -1,10 +1,46 @@
|
|
|
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
|
* gin-detector.test.ts — Gin 框架适配器规则回归(纯函数,无文件 I/O)
|
|
5
38
|
*/
|
|
6
39
|
const vitest_1 = require("vitest");
|
|
7
40
|
const gin_detector_1 = require("./gin-detector");
|
|
41
|
+
const fs = __importStar(require("fs"));
|
|
42
|
+
const os = __importStar(require("os"));
|
|
43
|
+
const path = __importStar(require("path"));
|
|
8
44
|
const app = (routes, extra = "") => `
|
|
9
45
|
import "github.com/gin-gonic/gin"
|
|
10
46
|
|
|
@@ -62,3 +98,150 @@ func main() {
|
|
|
62
98
|
(0, vitest_1.expect)(issues).toHaveLength(0);
|
|
63
99
|
});
|
|
64
100
|
});
|
|
101
|
+
// ── V7 修复轮回归:Use 点限定捕获 / 窗口边界 / 空路径 ──
|
|
102
|
+
(0, vitest_1.describe)("gin-detector V7 修复回归", () => {
|
|
103
|
+
(0, vitest_1.it)("Use 点限定成员(users.AuthMiddleware)被识别为认证中间件(旧版只捕 'users')", () => {
|
|
104
|
+
const { issues, authGroupMiddleware } = (0, gin_detector_1.analyzeGinApp)(app(`
|
|
105
|
+
v1 := r.Group("/api")
|
|
106
|
+
v1.Use(users.AuthMiddleware(true))
|
|
107
|
+
v1.POST("/pay", createPayment)
|
|
108
|
+
`));
|
|
109
|
+
(0, vitest_1.expect)(authGroupMiddleware).toContain("users.AuthMiddleware");
|
|
110
|
+
(0, vitest_1.expect)(issues).toHaveLength(0);
|
|
111
|
+
});
|
|
112
|
+
(0, vitest_1.it)("窗口不跨路由串扰:后面路由的认证中间件不保护前面的公开路由", () => {
|
|
113
|
+
const { issues, routes } = (0, gin_detector_1.analyzeGinApp)(app(`
|
|
114
|
+
v1.POST("/users", UsersRegistration)
|
|
115
|
+
v1.POST("/articles", AuthMiddleware(), ArticleCreate)
|
|
116
|
+
`));
|
|
117
|
+
const reg = routes.find((x) => x.path === "/users");
|
|
118
|
+
const art = routes.find((x) => x.path === "/articles");
|
|
119
|
+
(0, vitest_1.expect)(reg.protected).toBe(false);
|
|
120
|
+
(0, vitest_1.expect)(art.protected).toBe(true);
|
|
121
|
+
(0, vitest_1.expect)(issues.map((i) => i.route)).toContain("POST /users");
|
|
122
|
+
(0, vitest_1.expect)(issues.map((i) => i.route)).not.toContain("POST /articles");
|
|
123
|
+
});
|
|
124
|
+
(0, vitest_1.it)("handler 名含 auth 词不误判为认证(UsersLogin 是 handler 不是中间件)", () => {
|
|
125
|
+
const { routes } = (0, gin_detector_1.analyzeGinApp)(app(`
|
|
126
|
+
v1.POST("/login", UsersLogin)
|
|
127
|
+
`));
|
|
128
|
+
const login = routes.find((x) => x.path === "/login");
|
|
129
|
+
(0, vitest_1.expect)(login.protected).toBe(false);
|
|
130
|
+
});
|
|
131
|
+
(0, vitest_1.it)("空路径注册可见(realworld 惯用 POST(\"\", ...))", () => {
|
|
132
|
+
const { routes } = (0, gin_detector_1.analyzeGinApp)(app(`
|
|
133
|
+
v1.POST("", UsersRegistration)
|
|
134
|
+
`));
|
|
135
|
+
(0, vitest_1.expect)(routes.some((x) => x.path === "")).toBe(true);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
// ── register 集合豁免(语义层)──
|
|
139
|
+
(0, vitest_1.describe)("gin-detector register 集合豁免", () => {
|
|
140
|
+
(0, vitest_1.it)("有 /login 姊妹佐证:POST \"\"/\"/\"(公开注册双注册)不报", () => {
|
|
141
|
+
const { issues } = (0, gin_detector_1.analyzeGinApp)(app(`
|
|
142
|
+
v1.POST("", UsersRegistration)
|
|
143
|
+
v1.POST("/", UsersRegistration)
|
|
144
|
+
v1.POST("/login", UsersLogin)
|
|
145
|
+
`));
|
|
146
|
+
(0, vitest_1.expect)(issues.map((i) => i.route)).not.toContain("POST ");
|
|
147
|
+
(0, vitest_1.expect)(issues.map((i) => i.route)).not.toContain("POST /");
|
|
148
|
+
(0, vitest_1.expect)(issues.map((i) => i.route)).not.toContain("POST /login");
|
|
149
|
+
});
|
|
150
|
+
(0, vitest_1.it)("POST-only:同根的 PUT 不豁免(user-update 类仍查)", () => {
|
|
151
|
+
const { issues } = (0, gin_detector_1.analyzeGinApp)(app(`
|
|
152
|
+
v1.POST("", UsersRegistration)
|
|
153
|
+
v1.POST("/login", UsersLogin)
|
|
154
|
+
v1.PUT("", UserUpdate)
|
|
155
|
+
`));
|
|
156
|
+
(0, vitest_1.expect)(issues.map((i) => i.route)).not.toContain("POST ");
|
|
157
|
+
(0, vitest_1.expect)(issues.map((i) => i.route)).toContain("PUT ");
|
|
158
|
+
});
|
|
159
|
+
(0, vitest_1.it)("无姊妹佐证:POST \"/users\" 仍报(管理员建用户不豁免)", () => {
|
|
160
|
+
const { issues } = (0, gin_detector_1.analyzeGinApp)(app(`
|
|
161
|
+
v1.POST("/users", AdminCreateUser)
|
|
162
|
+
`));
|
|
163
|
+
(0, vitest_1.expect)(issues.map((i) => i.route)).toContain("POST /users");
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
// ── V7 转正:组认证跨文件传播 ──
|
|
167
|
+
const BOOTSTRAP = `package main
|
|
168
|
+
import "github.com/gin-gonic/gin"
|
|
169
|
+
func main() {
|
|
170
|
+
r := gin.Default()
|
|
171
|
+
v1 := r.Group("/api")
|
|
172
|
+
users.UsersRegister(v1.Group("/users")) // Use 之前 → 公开
|
|
173
|
+
v1.Use(users.AuthMiddleware(false)) // 可选认证
|
|
174
|
+
tags.TagsRegister(v1.Group("/tags"))
|
|
175
|
+
v1.Use(users.AuthMiddleware(true))
|
|
176
|
+
users.UserRegister(v1.Group("/user"))
|
|
177
|
+
articles.ArticlesRegister(v1.Group("/articles"))
|
|
178
|
+
}
|
|
179
|
+
`;
|
|
180
|
+
const ROUTERS = `package users
|
|
181
|
+
import "github.com/gin-gonic/gin"
|
|
182
|
+
func UsersRegister(router *gin.RouterGroup) {
|
|
183
|
+
router.POST("/login", UsersLogin)
|
|
184
|
+
router.POST("", UsersRegistration)
|
|
185
|
+
}
|
|
186
|
+
func UserRegister(router *gin.RouterGroup) {
|
|
187
|
+
router.PUT("", UserUpdate)
|
|
188
|
+
router.PUT("/", UserUpdate)
|
|
189
|
+
}
|
|
190
|
+
`;
|
|
191
|
+
(0, vitest_1.describe)("ginProtectedRegisterFns 组认证相位推导", () => {
|
|
192
|
+
(0, vitest_1.it)("Use(true) 之后的 Register 调用受保护,Use 之前的公开", () => {
|
|
193
|
+
const p = (0, gin_detector_1.ginProtectedRegisterFns)(BOOTSTRAP);
|
|
194
|
+
(0, vitest_1.expect)(p.get("UserRegister")).toBe(true);
|
|
195
|
+
(0, vitest_1.expect)(p.get("ArticlesRegister")).toBe(true);
|
|
196
|
+
(0, vitest_1.expect)(p.get("UsersRegister")).toBeUndefined(); // Use 之前注册(register/login 公开)
|
|
197
|
+
(0, vitest_1.expect)(p.get("TagsRegister")).toBeUndefined(); // 仅可选 Use(false) 之下
|
|
198
|
+
});
|
|
199
|
+
(0, vitest_1.it)("可选认证 Use(false) 不视为保护(否则删 required 后被掩盖)", () => {
|
|
200
|
+
const p = (0, gin_detector_1.ginProtectedRegisterFns)(BOOTSTRAP.replace("v1.Use(users.AuthMiddleware(true))\n", ""));
|
|
201
|
+
(0, vitest_1.expect)(p.get("UserRegister")).toBeUndefined();
|
|
202
|
+
(0, vitest_1.expect)(p.get("ArticlesRegister")).toBeUndefined();
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
(0, vitest_1.describe)("ginEnclosingFunc 路由函数归属", () => {
|
|
206
|
+
(0, vitest_1.it)("按 func 头行号归属", () => {
|
|
207
|
+
const lines = ROUTERS.split("\n");
|
|
208
|
+
const lineNo = (i) => i + 1;
|
|
209
|
+
const fnAt = (text, ln) => (0, gin_detector_1.ginEnclosingFunc)(text, ln);
|
|
210
|
+
// ROUTERS:1 package / 2 import / 3 func UsersRegister … 7 func UserRegister
|
|
211
|
+
(0, vitest_1.expect)(fnAt(ROUTERS, lineNo(3))).toBe("UsersRegister");
|
|
212
|
+
(0, vitest_1.expect)(fnAt(ROUTERS, lineNo(7))).toBe("UserRegister");
|
|
213
|
+
(0, vitest_1.expect)(fnAt(ROUTERS, lineNo(1))).toBeNull(); // package 行无函数
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
(0, vitest_1.describe)("analyzeGinProject 跨文件传播", () => {
|
|
217
|
+
function makeProject(withRequiredUse) {
|
|
218
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "gin-proj-"));
|
|
219
|
+
const boot = withRequiredUse ? BOOTSTRAP : BOOTSTRAP.replace(/\tv1\.Use\(users\.AuthMiddleware\(true\)\)\n/, "");
|
|
220
|
+
fs.writeFileSync(path.join(dir, "hello.go"), boot);
|
|
221
|
+
fs.writeFileSync(path.join(dir, "routers.go"), ROUTERS);
|
|
222
|
+
return dir;
|
|
223
|
+
}
|
|
224
|
+
(0, vitest_1.it)("Use(true) 保护下:跨文件 mutation 不报", () => {
|
|
225
|
+
const dir = makeProject(true);
|
|
226
|
+
try {
|
|
227
|
+
const a = (0, gin_detector_1.analyzeGinProject)(dir);
|
|
228
|
+
// UserRegister 的 PUT 受组认证保护(跨文件传播)
|
|
229
|
+
(0, vitest_1.expect)(a.issues.filter((i) => i.rule === "GIN_ROUTE_NO_AUTH")).toHaveLength(0);
|
|
230
|
+
}
|
|
231
|
+
finally {
|
|
232
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
(0, vitest_1.it)("删掉 Use(true):mutation 重新被报(敏感性保留)", () => {
|
|
236
|
+
const dir = makeProject(false);
|
|
237
|
+
try {
|
|
238
|
+
const a = (0, gin_detector_1.analyzeGinProject)(dir);
|
|
239
|
+
const routes = a.issues.filter((i) => i.rule === "GIN_ROUTE_NO_AUTH").map((i) => i.route);
|
|
240
|
+
(0, vitest_1.expect)(routes).toContain("PUT ");
|
|
241
|
+
(0, vitest_1.expect)(routes).toContain("PUT /");
|
|
242
|
+
}
|
|
243
|
+
finally {
|
|
244
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
});
|
|
@@ -56,6 +56,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
56
56
|
exports.analyzeHapiApp = analyzeHapiApp;
|
|
57
57
|
exports.analyzeHapiFile = analyzeHapiFile;
|
|
58
58
|
const fs = __importStar(require("fs"));
|
|
59
|
+
const route_window_1 = require("./route-window");
|
|
59
60
|
const MUTATION_METHODS = new Set(["post", "put", "patch", "delete"]);
|
|
60
61
|
const AUTH_ENTRY_WORDS = [
|
|
61
62
|
"login", "signin", "sign_in", "regist", "signup", "sign_up",
|
|
@@ -66,11 +67,56 @@ function isAuthEntryPath(pathName) {
|
|
|
66
67
|
return AUTH_ENTRY_WORDS.some((w) => lower.includes(w));
|
|
67
68
|
}
|
|
68
69
|
// ── Analysis(代码串级) ──
|
|
70
|
+
/** 声明式 hapi 路由模块(V6 遗留缺口):真实 hapi 应用(glue/pal/插件)
|
|
71
|
+
* 以数组声明路由——module.exports = (server) => [ { method, path,
|
|
72
|
+
* config: { auth: 'jwt', ... }, handler }, ... ],由框架注册;
|
|
73
|
+
* 文件本身无 require('hapi')、无 server.route 调用 */
|
|
74
|
+
function isDeclarativeHapiModule(code) {
|
|
75
|
+
const hasRouteObject = /method\s*:\s*['"](GET|POST|PUT|PATCH|DELETE|get|post|put|patch|delete)['"]/.test(code) &&
|
|
76
|
+
/path\s*:\s*['"]/.test(code) &&
|
|
77
|
+
/\b(?:config|options)\s*:/.test(code);
|
|
78
|
+
if (!hasRouteObject)
|
|
79
|
+
return false;
|
|
80
|
+
return (/module\.exports\s*=\s*\(?\s*server\b/.test(code) ||
|
|
81
|
+
/module\.exports\s*=\s*function\s*\(\s*server\b/.test(code) ||
|
|
82
|
+
/return\s*\[/.test(code));
|
|
83
|
+
}
|
|
84
|
+
/** 自 routeObjStart('{') 取平衡块文本(含嵌套 config 对象;字符串感知) */
|
|
85
|
+
function hapiBalancedBlock(code, openIdx) {
|
|
86
|
+
let depth = 1;
|
|
87
|
+
let end = openIdx + 1;
|
|
88
|
+
let quote = null;
|
|
89
|
+
while (end < code.length && depth > 0) {
|
|
90
|
+
const ch = code[end];
|
|
91
|
+
if (quote) {
|
|
92
|
+
if (ch === quote && code[end - 1] !== "\\")
|
|
93
|
+
quote = null;
|
|
94
|
+
}
|
|
95
|
+
else if (ch === '"' || ch === "'" || ch === "`") {
|
|
96
|
+
quote = ch;
|
|
97
|
+
}
|
|
98
|
+
else if (ch === "{") {
|
|
99
|
+
depth++;
|
|
100
|
+
}
|
|
101
|
+
else if (ch === "}") {
|
|
102
|
+
depth--;
|
|
103
|
+
}
|
|
104
|
+
end++;
|
|
105
|
+
}
|
|
106
|
+
return code.slice(openIdx + 1, Math.max(openIdx + 1, end - 1));
|
|
107
|
+
}
|
|
108
|
+
function hapiAuthOption(block) {
|
|
109
|
+
const authM = block.match(/\bauth\s*:\s*(?:['"]([^'"]+)['"]|\{\s*strategy\s*:\s*['"]([^'"]+)['"]|\s*(false|true))/);
|
|
110
|
+
if (!authM)
|
|
111
|
+
return null;
|
|
112
|
+
return authM[3] === "false" ? "false" : authM[1] || authM[2] || authM[3] || null;
|
|
113
|
+
}
|
|
69
114
|
function analyzeHapiApp(code) {
|
|
70
115
|
const issues = [];
|
|
71
116
|
const routes = [];
|
|
72
117
|
const strategies = [];
|
|
73
|
-
|
|
118
|
+
// @hapi-scoped(v17+)、v16 require('hapi'),或声明式数组模块(V6 修复)
|
|
119
|
+
const hasHapi = /@hapi\/hapi|\bHapi\.server\b|\bhapi\.server\b|require\(\s*['"]hapi['"]\s*\)|from\s+['"]hapi['"]/.test(code) || isDeclarativeHapiModule(code);
|
|
74
120
|
if (!hasHapi) {
|
|
75
121
|
return { hasHapi: false, routes, strategies, issues };
|
|
76
122
|
}
|
|
@@ -80,43 +126,69 @@ function analyzeHapiApp(code) {
|
|
|
80
126
|
while ((m = strategyRe.exec(code)) !== null) {
|
|
81
127
|
strategies.push(m[1]);
|
|
82
128
|
}
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
129
|
+
const declarative = isDeclarativeHapiModule(code);
|
|
130
|
+
const lineAt = (idx) => code.slice(0, idx).split("\n").length;
|
|
131
|
+
if (!declarative) {
|
|
132
|
+
// 直连形态:server.route({ ... }) —— 500 字符窗口
|
|
133
|
+
const routeRe = /\.route\s*\(\s*\{/g;
|
|
134
|
+
while ((m = routeRe.exec(code)) !== null) {
|
|
135
|
+
const window = code.slice(m.index, m.index + 500);
|
|
136
|
+
const methodM = window.match(/method\s*:\s*['"]([^'"]+)['"]/);
|
|
137
|
+
const pathM = window.match(/path\s*:\s*['"]([^'"]+)['"]/);
|
|
138
|
+
if (!methodM || !pathM)
|
|
139
|
+
continue;
|
|
140
|
+
routes.push({
|
|
141
|
+
method: methodM[1].toLowerCase(),
|
|
142
|
+
path: pathM[1],
|
|
143
|
+
authOption: hapiAuthOption(window),
|
|
144
|
+
line: lineAt(m.index),
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
// 声明式数组:每个 { method, path, config:{auth} } 路由对象
|
|
150
|
+
const verbRe = /method\s*:\s*['"](GET|POST|PUT|PATCH|DELETE|get|post|put|patch|delete)['"]/g;
|
|
151
|
+
while ((m = verbRe.exec(code)) !== null) {
|
|
152
|
+
const verb = m[1].toLowerCase();
|
|
153
|
+
const verbLine = lineAt(m.index);
|
|
154
|
+
const objStart = code.lastIndexOf("{", m.index);
|
|
155
|
+
if (objStart < 0)
|
|
156
|
+
continue;
|
|
157
|
+
const block = hapiBalancedBlock(code, objStart);
|
|
158
|
+
const pathM = block.match(/path\s*:\s*['"]([^'"]+)['"]/);
|
|
159
|
+
if (!pathM)
|
|
160
|
+
continue;
|
|
161
|
+
// 重复对象去重
|
|
162
|
+
if (routes.some((r) => r.line === verbLine && r.method === verb))
|
|
163
|
+
continue;
|
|
164
|
+
routes.push({
|
|
165
|
+
method: verb,
|
|
166
|
+
path: pathM[1],
|
|
167
|
+
authOption: hapiAuthOption(block),
|
|
168
|
+
line: verbLine,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// register 集合豁免(语义层,同其他框架):users/login 姊妹 → POST users 公开
|
|
173
|
+
const registerRoots = (0, route_window_1.collectRegisterRoots)(routes.map((r) => r.path));
|
|
174
|
+
for (const r of routes) {
|
|
175
|
+
if (!MUTATION_METHODS.has(r.method))
|
|
91
176
|
continue;
|
|
92
|
-
|
|
93
|
-
const pathName = pathM[1];
|
|
94
|
-
// auth: false → 显式公开;无 auth 字段 → authM null
|
|
95
|
-
const authOption = authM
|
|
96
|
-
? (authM[3] === "false" ? "false" : authM[1] || authM[2] || authM[3] || null)
|
|
97
|
-
: null;
|
|
98
|
-
routes.push({
|
|
99
|
-
method,
|
|
100
|
-
path: pathName,
|
|
101
|
-
authOption,
|
|
102
|
-
line: code.slice(0, m.index).split("\n").length,
|
|
103
|
-
});
|
|
104
|
-
if (!MUTATION_METHODS.has(method))
|
|
177
|
+
if (isAuthEntryPath(r.path))
|
|
105
178
|
continue;
|
|
106
|
-
if (
|
|
179
|
+
if (r.method === "post" && (0, route_window_1.isRegisterRoot)(r.path, registerRoots))
|
|
107
180
|
continue;
|
|
108
|
-
|
|
109
|
-
if (authOption === null || authOption === "false") {
|
|
181
|
+
if (r.authOption === null || r.authOption === "false") {
|
|
110
182
|
issues.push({
|
|
111
183
|
severity: "medium",
|
|
112
184
|
rule: "HAPI_ROUTE_NO_AUTH",
|
|
113
|
-
message: authOption === "false"
|
|
114
|
-
? `Mutation route ${method.toUpperCase()} ${
|
|
185
|
+
message: r.authOption === "false"
|
|
186
|
+
? `Mutation route ${r.method.toUpperCase()} ${r.path} is explicitly ` +
|
|
115
187
|
`public (auth: false) — any caller can reach it.`
|
|
116
|
-
: `Mutation route ${method.toUpperCase()} ${
|
|
188
|
+
: `Mutation route ${r.method.toUpperCase()} ${r.path} has no auth ` +
|
|
117
189
|
`option in its route config — any caller can reach it.`,
|
|
118
|
-
route: `${method.toUpperCase()} ${
|
|
119
|
-
line:
|
|
190
|
+
route: `${r.method.toUpperCase()} ${r.path}`,
|
|
191
|
+
line: r.line,
|
|
120
192
|
});
|
|
121
193
|
}
|
|
122
194
|
}
|
|
@@ -126,7 +198,9 @@ function analyzeHapiFile(filePath) {
|
|
|
126
198
|
if (!fs.existsSync(filePath))
|
|
127
199
|
return null;
|
|
128
200
|
const code = fs.readFileSync(filePath, "utf-8");
|
|
129
|
-
|
|
201
|
+
// gate:@hapi-scoped / v16 require('hapi') / 声明式数组模块
|
|
202
|
+
const marker = /@hapi\/hapi|@hapi\/hawk|\bHapi\.server\b|require\(\s*['"]hapi['"]\s*\)|from\s+['"]hapi['"]/.test(code) || isDeclarativeHapiModule(code);
|
|
203
|
+
if (!marker)
|
|
130
204
|
return null;
|
|
131
205
|
return analyzeHapiApp(code);
|
|
132
206
|
}
|
|
@@ -58,3 +58,94 @@ server.route({ method: "POST", path: "/login", handler: () => "token" });
|
|
|
58
58
|
(0, vitest_1.expect)(issues).toHaveLength(0);
|
|
59
59
|
});
|
|
60
60
|
});
|
|
61
|
+
// ── V6 修复轮回归:v16 时代 require('hapi') gate 兼容 ──
|
|
62
|
+
(0, vitest_1.describe)("hapi-detector V6 gate 修复回归", () => {
|
|
63
|
+
(0, vitest_1.it)("v16 形态 require('hapi') + server.route 可被分析(旧 gate 只认 @hapi-scoped)", () => {
|
|
64
|
+
const code = `
|
|
65
|
+
const Hapi = require("hapi");
|
|
66
|
+
const server = new Hapi.Server();
|
|
67
|
+
server.connection({ port: 3000 });
|
|
68
|
+
server.auth.strategy("jwt", "jwt", { key: "s" });
|
|
69
|
+
server.route({ method: "POST", path: "/articles", config: { auth: "jwt" }, handler: (r, reply) => reply({}) });
|
|
70
|
+
server.route({ method: "POST", path: "/payments", handler: (r, reply) => reply({}) });
|
|
71
|
+
`;
|
|
72
|
+
const { hasHapi, strategies, routes, issues } = (0, hapi_detector_1.analyzeHapiApp)(code);
|
|
73
|
+
(0, vitest_1.expect)(hasHapi).toBe(true);
|
|
74
|
+
(0, vitest_1.expect)(strategies).toContain("jwt");
|
|
75
|
+
const articles = routes.find((r) => r.path === "/articles");
|
|
76
|
+
const payments = routes.find((r) => r.path === "/payments");
|
|
77
|
+
(0, vitest_1.expect)(articles).toBeDefined();
|
|
78
|
+
(0, vitest_1.expect)(articles.authOption).toBe("jwt"); // config.auth 嵌套亦被窗口文本捕获
|
|
79
|
+
(0, vitest_1.expect)(issues.map((i) => i.rule)).toContain("HAPI_ROUTE_NO_AUTH");
|
|
80
|
+
(0, vitest_1.expect)(issues.map((i) => i.route)).not.toContain("POST /articles");
|
|
81
|
+
});
|
|
82
|
+
(0, vitest_1.it)("gate 不误收 hapi-auth-jwt2(require('hapi') 需闭合引号紧随)", () => {
|
|
83
|
+
const { hasHapi } = (0, hapi_detector_1.analyzeHapiApp)(`
|
|
84
|
+
const hapiAuth = require("hapi-auth-jwt2");
|
|
85
|
+
module.exports = (server) => { return []; };
|
|
86
|
+
`);
|
|
87
|
+
(0, vitest_1.expect)(hasHapi).toBe(false);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
// ── V6 遗留缺口:声明式数组路由 + config.auth 嵌套 ──
|
|
91
|
+
const DECLARATIVE = `
|
|
92
|
+
module.exports = (server) => {
|
|
93
|
+
const handlers = require('./handlers')(server)
|
|
94
|
+
return [
|
|
95
|
+
// GET 公开
|
|
96
|
+
{
|
|
97
|
+
method: 'GET',
|
|
98
|
+
path: '/articles',
|
|
99
|
+
config: { description: 'list' },
|
|
100
|
+
handler: handlers.list
|
|
101
|
+
},
|
|
102
|
+
// mutation 受保护(config.auth 嵌套)
|
|
103
|
+
{
|
|
104
|
+
method: 'POST',
|
|
105
|
+
path: '/articles',
|
|
106
|
+
config: { auth: 'jwt', response: {} },
|
|
107
|
+
handler: handlers.create
|
|
108
|
+
},
|
|
109
|
+
// 无认证 mutation
|
|
110
|
+
{
|
|
111
|
+
method: 'POST',
|
|
112
|
+
path: '/payments',
|
|
113
|
+
config: {},
|
|
114
|
+
handler: handlers.pay
|
|
115
|
+
}
|
|
116
|
+
]
|
|
117
|
+
}
|
|
118
|
+
`;
|
|
119
|
+
const DECLARATIVE_USERS = `
|
|
120
|
+
module.exports = (server) => {
|
|
121
|
+
return [
|
|
122
|
+
{ method: 'POST', path: '/users/login', config: {}, handler: h },
|
|
123
|
+
{ method: 'POST', path: '/users', config: {}, handler: h },
|
|
124
|
+
{ method: 'PUT', path: '/user', config: { auth: 'jwt' }, handler: h }
|
|
125
|
+
]
|
|
126
|
+
}
|
|
127
|
+
`;
|
|
128
|
+
(0, vitest_1.describe)("hapi 声明式数组路由(V6 修复回归)", () => {
|
|
129
|
+
(0, vitest_1.it)("module.exports=(server)+数组路由对象被识别,config.auth 保护生效", () => {
|
|
130
|
+
const { hasHapi, routes, issues } = (0, hapi_detector_1.analyzeHapiApp)(DECLARATIVE);
|
|
131
|
+
(0, vitest_1.expect)(hasHapi).toBe(true);
|
|
132
|
+
(0, vitest_1.expect)(routes.length).toBe(3);
|
|
133
|
+
const create = routes.find((r) => r.method === "post" && r.path === "/articles");
|
|
134
|
+
(0, vitest_1.expect)(create.authOption).toBe("jwt");
|
|
135
|
+
const missing = issues.filter((i) => i.rule === "HAPI_ROUTE_NO_AUTH").map((i) => i.route);
|
|
136
|
+
(0, vitest_1.expect)(missing).toContain("POST /payments");
|
|
137
|
+
(0, vitest_1.expect)(missing).not.toContain("POST /articles");
|
|
138
|
+
});
|
|
139
|
+
(0, vitest_1.it)("摘 config.auth → mutation 报(敏感性)", () => {
|
|
140
|
+
const stripped = DECLARATIVE.replace("config: { auth: 'jwt', response: {} }", "config: { response: {} }");
|
|
141
|
+
const { issues } = (0, hapi_detector_1.analyzeHapiApp)(stripped);
|
|
142
|
+
(0, vitest_1.expect)(issues.some((i) => i.rule === "HAPI_ROUTE_NO_AUTH" && i.route === "POST /articles")).toBe(true);
|
|
143
|
+
});
|
|
144
|
+
(0, vitest_1.it)("register/login 公开:users/login + users(姊妹佐证)不报", () => {
|
|
145
|
+
const { issues } = (0, hapi_detector_1.analyzeHapiApp)(DECLARATIVE_USERS);
|
|
146
|
+
const missing = issues.filter((i) => i.rule === "HAPI_ROUTE_NO_AUTH").map((i) => i.route);
|
|
147
|
+
(0, vitest_1.expect)(missing).not.toContain("POST /users/login");
|
|
148
|
+
(0, vitest_1.expect)(missing).not.toContain("POST /users");
|
|
149
|
+
(0, vitest_1.expect)(missing).not.toContain("PUT /user");
|
|
150
|
+
});
|
|
151
|
+
});
|