progmune-runtime 3.7.13 → 3.7.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +2 -2
- package/README.zh-CN.md +2 -2
- package/dist/extract-ir-go.js +42 -6
- package/dist/extract-ir-go.test.js +17 -0
- 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
|
@@ -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
|
* fiber-detector.test.ts — Fiber 框架适配器规则回归(纯函数,无文件 I/O)
|
|
5
38
|
*/
|
|
6
39
|
const vitest_1 = require("vitest");
|
|
7
40
|
const fiber_detector_1 = require("./fiber-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/gofiber/fiber/v2"
|
|
10
46
|
|
|
@@ -55,3 +91,155 @@ func main() {
|
|
|
55
91
|
(0, vitest_1.expect)(issues).toHaveLength(0);
|
|
56
92
|
});
|
|
57
93
|
});
|
|
94
|
+
// ── V8 修复轮回归:窗口边界(单点摘保护不再被后续路由掩盖)──
|
|
95
|
+
(0, vitest_1.describe)("fiber-detector V8 修复回归", () => {
|
|
96
|
+
(0, vitest_1.it)("窗口不跨路由串扰:下一路由的 Protected 不掩盖上一路由摘保护", () => {
|
|
97
|
+
const { issues, routes } = (0, fiber_detector_1.analyzeFiberApp)(app(`
|
|
98
|
+
api.Post("/logout", logoutHandler)
|
|
99
|
+
api.Post("/refresh-token", middleware.Protected(), refreshHandler)
|
|
100
|
+
`));
|
|
101
|
+
const logout = routes.find((x) => x.path === "/logout");
|
|
102
|
+
const refresh = routes.find((x) => x.path === "/refresh-token");
|
|
103
|
+
(0, vitest_1.expect)(logout.protected).toBe(false);
|
|
104
|
+
(0, vitest_1.expect)(refresh.protected).toBe(true);
|
|
105
|
+
(0, vitest_1.expect)(issues.map((i) => i.route)).toContain("POST /logout");
|
|
106
|
+
(0, vitest_1.expect)(issues.map((i) => i.route)).not.toContain("POST /refresh-token");
|
|
107
|
+
});
|
|
108
|
+
(0, vitest_1.it)("handler 名含 auth 词不误判(logoutHandler 不被当认证)", () => {
|
|
109
|
+
const { routes } = (0, fiber_detector_1.analyzeFiberApp)(app(`
|
|
110
|
+
api.Post("/logout", authHandler.Logout)
|
|
111
|
+
`));
|
|
112
|
+
(0, vitest_1.expect)(routes.find((x) => x.path === "/logout").protected).toBe(false);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
// ── Fiber 组认证跨文件传播(gin 同款模型移植)──
|
|
116
|
+
const FBOOT = `package main
|
|
117
|
+
import "github.com/gofiber/fiber/v2"
|
|
118
|
+
func main() {
|
|
119
|
+
app := fiber.New()
|
|
120
|
+
api := app.Group("/api")
|
|
121
|
+
users.UsersRegister(api.Group("/users"))
|
|
122
|
+
api.Use(middleware.Protected())
|
|
123
|
+
users.UserRegister(api.Group("/user"))
|
|
124
|
+
articles.ArticlesRegister(api.Group("/articles"))
|
|
125
|
+
}
|
|
126
|
+
`;
|
|
127
|
+
const FROUTERS = `package users
|
|
128
|
+
import "github.com/gofiber/fiber/v2"
|
|
129
|
+
func UsersRegister(router fiber.Router) {
|
|
130
|
+
router.Post("/login", UsersLogin)
|
|
131
|
+
router.Post("", UsersRegistration)
|
|
132
|
+
}
|
|
133
|
+
func UserRegister(router fiber.Router) {
|
|
134
|
+
router.Put("", UserUpdate)
|
|
135
|
+
}
|
|
136
|
+
`;
|
|
137
|
+
(0, vitest_1.describe)("fiberProtectedRegisterFns 组认证相位", () => {
|
|
138
|
+
(0, vitest_1.it)("Use 之后的 Register 受保护,Use 之前的公开", () => {
|
|
139
|
+
const p = (0, fiber_detector_1.fiberProtectedRegisterFns)(FBOOT);
|
|
140
|
+
(0, vitest_1.expect)(p.get("UserRegister")).toBe(true);
|
|
141
|
+
(0, vitest_1.expect)(p.get("ArticlesRegister")).toBe(true);
|
|
142
|
+
(0, vitest_1.expect)(p.get("UsersRegister")).toBeUndefined();
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
(0, vitest_1.describe)("fiberEnclosingFunc 归属", () => {
|
|
146
|
+
(0, vitest_1.it)("按 func 头行号归属", () => {
|
|
147
|
+
// FROUTERS:1 package / 2 import / 3 func UsersRegister / 6 func UserRegister
|
|
148
|
+
(0, vitest_1.expect)((0, fiber_detector_1.fiberEnclosingFunc)(FROUTERS, 4)).toBe("UsersRegister");
|
|
149
|
+
(0, vitest_1.expect)((0, fiber_detector_1.fiberEnclosingFunc)(FROUTERS, 7)).toBe("UserRegister");
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
(0, vitest_1.describe)("analyzeFiberProject 跨文件传播", () => {
|
|
153
|
+
function makeProject(withUse) {
|
|
154
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "fiber-proj-"));
|
|
155
|
+
const boot = withUse ? FBOOT : FBOOT.replace(/\s*api\.Use\(middleware\.Protected\(\)\)\n/, "");
|
|
156
|
+
fs.writeFileSync(path.join(dir, "main.go"), boot);
|
|
157
|
+
fs.writeFileSync(path.join(dir, "routers.go"), FROUTERS);
|
|
158
|
+
return dir;
|
|
159
|
+
}
|
|
160
|
+
(0, vitest_1.it)("Use 保护下跨文件 mutation 不报", () => {
|
|
161
|
+
const dir = makeProject(true);
|
|
162
|
+
try {
|
|
163
|
+
const a = (0, fiber_detector_1.analyzeFiberProject)(dir);
|
|
164
|
+
(0, vitest_1.expect)(a.issues.filter((i) => i.rule === "FIBER_ROUTE_NO_AUTH")).toHaveLength(0);
|
|
165
|
+
}
|
|
166
|
+
finally {
|
|
167
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
(0, vitest_1.it)("删 Use → mutation 重现(敏感性保留)", () => {
|
|
171
|
+
const dir = makeProject(false);
|
|
172
|
+
try {
|
|
173
|
+
const a = (0, fiber_detector_1.analyzeFiberProject)(dir);
|
|
174
|
+
const routes = a.issues.filter((i) => i.rule === "FIBER_ROUTE_NO_AUTH").map((i) => i.route);
|
|
175
|
+
(0, vitest_1.expect)(routes).toContain("PUT "); // UserRegister mutation 重现
|
|
176
|
+
// POST "" 是 register(/login 姊妹佐证豁免,公开)——不报正确
|
|
177
|
+
}
|
|
178
|
+
finally {
|
|
179
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
// ── 多层 Register 链(journalist 式 main→api(Group+Use)→v1→模块)──
|
|
184
|
+
function makeNestedProject(withUse) {
|
|
185
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "fiber-nest-"));
|
|
186
|
+
const mk = (rel, content) => {
|
|
187
|
+
const fp = path.join(dir, rel);
|
|
188
|
+
fs.mkdirSync(path.dirname(fp), { recursive: true });
|
|
189
|
+
fs.writeFileSync(fp, content);
|
|
190
|
+
};
|
|
191
|
+
mk("main/main.go", `package main
|
|
192
|
+
import "github.com/gofiber/fiber/v2"
|
|
193
|
+
func main() {
|
|
194
|
+
app := fiber.New()
|
|
195
|
+
api.Register(app)
|
|
196
|
+
}
|
|
197
|
+
`);
|
|
198
|
+
mk("api/api.go", `package api
|
|
199
|
+
import "github.com/gofiber/fiber/v2"
|
|
200
|
+
func Register(fiberApp *fiber.App) {
|
|
201
|
+
api := fiberApp.Group("/api")
|
|
202
|
+
${withUse ? "\tapi.Use(middleware.Protected())\n" : ""}\tv1.Register(&api)
|
|
203
|
+
}
|
|
204
|
+
`);
|
|
205
|
+
mk("v1/v1.go", `package v1
|
|
206
|
+
import "github.com/gofiber/fiber/v2"
|
|
207
|
+
func Register(router *fiber.Router) {
|
|
208
|
+
feeds.Register(router)
|
|
209
|
+
}
|
|
210
|
+
`);
|
|
211
|
+
mk("feeds/feeds.go", `package feeds
|
|
212
|
+
import "github.com/gofiber/fiber/v2"
|
|
213
|
+
func Register(router *fiber.Router) {
|
|
214
|
+
router.Post("/", CreateFeed)
|
|
215
|
+
router.Put("/:id", UpdateFeed)
|
|
216
|
+
}
|
|
217
|
+
`);
|
|
218
|
+
return dir;
|
|
219
|
+
}
|
|
220
|
+
(0, vitest_1.describe)("analyzeFiberProject 多层 Register 链(journalist 式)", () => {
|
|
221
|
+
(0, vitest_1.it)("api(Group+Use)→v1→feeds 链:跨层 mutation 不报", () => {
|
|
222
|
+
const dir = makeNestedProject(true);
|
|
223
|
+
try {
|
|
224
|
+
const a = (0, fiber_detector_1.analyzeFiberProject)(dir);
|
|
225
|
+
(0, vitest_1.expect)(a.issues.filter((i) => i.rule === "FIBER_ROUTE_NO_AUTH")).toHaveLength(0);
|
|
226
|
+
(0, vitest_1.expect)(a.protectedFunctions).toContain("feeds:Register");
|
|
227
|
+
(0, vitest_1.expect)(a.protectedFunctions).toContain("v1:Register");
|
|
228
|
+
}
|
|
229
|
+
finally {
|
|
230
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
(0, vitest_1.it)("删中间层 api.Use → feeds mutation 重现(敏感性穿透多层)", () => {
|
|
234
|
+
const dir = makeNestedProject(false);
|
|
235
|
+
try {
|
|
236
|
+
const a = (0, fiber_detector_1.analyzeFiberProject)(dir);
|
|
237
|
+
const routes = a.issues.filter((i) => i.rule === "FIBER_ROUTE_NO_AUTH").map((i) => i.route);
|
|
238
|
+
(0, vitest_1.expect)(routes).toContain("POST /");
|
|
239
|
+
(0, vitest_1.expect)(routes).toContain("PUT /:id");
|
|
240
|
+
}
|
|
241
|
+
finally {
|
|
242
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
});
|
|
@@ -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
|
+
});
|