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
|
@@ -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
|
+
});
|
|
@@ -53,7 +53,14 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
53
53
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
54
54
|
exports.analyzeFiberApp = analyzeFiberApp;
|
|
55
55
|
exports.analyzeFiberFile = analyzeFiberFile;
|
|
56
|
+
exports.fiberFuncStarts = fiberFuncStarts;
|
|
57
|
+
exports.fiberEnclosingFunc = fiberEnclosingFunc;
|
|
58
|
+
exports.fiberProtectedRegisterFns = fiberProtectedRegisterFns;
|
|
59
|
+
exports.analyzeFiberProject = analyzeFiberProject;
|
|
60
|
+
exports.goFuncsOf = goFuncsOf;
|
|
61
|
+
exports.fiberProjectProtectedFns = fiberProjectProtectedFns;
|
|
56
62
|
const fs = __importStar(require("fs"));
|
|
63
|
+
const route_window_1 = require("./route-window");
|
|
57
64
|
const MUTATION_METHODS = new Set(["post", "put", "patch", "delete"]);
|
|
58
65
|
const AUTH_ENTRY_WORDS = [
|
|
59
66
|
"login", "signin", "sign_in", "regist", "signup", "sign_up",
|
|
@@ -81,19 +88,22 @@ function analyzeFiberApp(code) {
|
|
|
81
88
|
return { hasFiber: false, routes, authMiddleware, issues };
|
|
82
89
|
}
|
|
83
90
|
// 全局/组级认证中间件:app.Use(authMW) / group.Use(authMW)
|
|
84
|
-
|
|
91
|
+
// 捕获支持点限定成员(jwtware.New 前的一般为 middleware.Protected 等)
|
|
92
|
+
const useRe = /\.Use\s*\(\s*([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)/g;
|
|
85
93
|
let m;
|
|
86
94
|
while ((m = useRe.exec(code)) !== null) {
|
|
87
95
|
if (isAuthFnName(m[1]))
|
|
88
96
|
authMiddleware.push(m[1]);
|
|
89
97
|
}
|
|
90
98
|
// 路由注册:app.Post("/x", mw1, mw2, handler)——Go 方法名大写(Post 惯例)
|
|
91
|
-
|
|
99
|
+
// 空路径 "..." 允许
|
|
100
|
+
const routeRe = /\.(get|post|put|patch|delete)\s*\(\s*"([^"]*)"(\s*,\s*|\s*\))/gi;
|
|
92
101
|
while ((m = routeRe.exec(code)) !== null) {
|
|
93
102
|
const method = m[1].toLowerCase();
|
|
94
103
|
const pathName = m[2];
|
|
95
|
-
|
|
96
|
-
const
|
|
104
|
+
// 认证窗口 = 本次调用边界内(括号感知),不跨路由(V8 缺陷修复)
|
|
105
|
+
const window = (0, route_window_1.routeCallWindow)(code, m.index + m[0].length);
|
|
106
|
+
const mwNames = (0, route_window_1.middlewareNamesFromWindow)(window);
|
|
97
107
|
const hasAuthMw = mwNames.some((name) => isAuthFnName(name));
|
|
98
108
|
routes.push({
|
|
99
109
|
method,
|
|
@@ -101,15 +111,21 @@ function analyzeFiberApp(code) {
|
|
|
101
111
|
protected: hasAuthMw,
|
|
102
112
|
line: code.slice(0, m.index).split("\n").length,
|
|
103
113
|
});
|
|
104
|
-
|
|
105
|
-
|
|
114
|
+
}
|
|
115
|
+
// register 集合豁免(语义层,同 Koa/Gin)
|
|
116
|
+
const registerRoots = (0, route_window_1.collectRegisterRoots)(routes.map((r) => r.path));
|
|
117
|
+
for (const r of routes) {
|
|
118
|
+
if (MUTATION_METHODS.has(r.method) && !r.protected
|
|
119
|
+
&& authMiddleware.length === 0
|
|
120
|
+
&& !isAuthEntryPath(r.path)
|
|
121
|
+
&& !(r.method === "post" && (0, route_window_1.isRegisterRoot)(r.path, registerRoots))) {
|
|
106
122
|
issues.push({
|
|
107
123
|
severity: "medium",
|
|
108
124
|
rule: "FIBER_ROUTE_NO_AUTH",
|
|
109
|
-
message: `Route ${method.toUpperCase()} ${
|
|
125
|
+
message: `Route ${r.method.toUpperCase()} ${r.path} has no auth middleware ` +
|
|
110
126
|
`and no auth Use middleware — any caller can reach it.`,
|
|
111
|
-
route: `${method.toUpperCase()} ${
|
|
112
|
-
line:
|
|
127
|
+
route: `${r.method.toUpperCase()} ${r.path}`,
|
|
128
|
+
line: r.line,
|
|
113
129
|
});
|
|
114
130
|
}
|
|
115
131
|
}
|
|
@@ -123,3 +139,244 @@ function analyzeFiberFile(filePath) {
|
|
|
123
139
|
return null;
|
|
124
140
|
return analyzeFiberApp(code);
|
|
125
141
|
}
|
|
142
|
+
/** 顶层函数头(行号 1-based) */
|
|
143
|
+
function fiberFuncStarts(text) {
|
|
144
|
+
const out = [];
|
|
145
|
+
const re = /^func\s+([A-Za-z_]\w*)\s*\(/gm;
|
|
146
|
+
let m;
|
|
147
|
+
while ((m = re.exec(text)) !== null) {
|
|
148
|
+
out.push({ name: m[1], line: text.slice(0, m.index).split("\n").length });
|
|
149
|
+
}
|
|
150
|
+
return out;
|
|
151
|
+
}
|
|
152
|
+
/** routeLine 所在顶层函数名 */
|
|
153
|
+
function fiberEnclosingFunc(text, routeLine) {
|
|
154
|
+
let name = null;
|
|
155
|
+
for (const f of fiberFuncStarts(text)) {
|
|
156
|
+
if (f.line <= routeLine)
|
|
157
|
+
name = f.name;
|
|
158
|
+
else
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
return name;
|
|
162
|
+
}
|
|
163
|
+
/** bootstrap 相位推导:认证 Use 之后调用的 Register fn(组编号 1,2|3,4|5,6|7,8) */
|
|
164
|
+
function fiberProtectedRegisterFns(bootstrapText) {
|
|
165
|
+
const state = new Map();
|
|
166
|
+
const protectedFns = new Map();
|
|
167
|
+
const re = /(\w+)\s*(?::=|=)\s*fiber\.New\s*\(|(\w+)\.Use\s*\(\s*([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)|(\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;
|
|
168
|
+
let m;
|
|
169
|
+
while ((m = re.exec(bootstrapText)) !== null) {
|
|
170
|
+
if (m[1] !== undefined) {
|
|
171
|
+
state.set(m[1], false);
|
|
172
|
+
}
|
|
173
|
+
else if (m[2] !== undefined) {
|
|
174
|
+
if (isAuthFnName(m[3]))
|
|
175
|
+
state.set(m[2], true);
|
|
176
|
+
}
|
|
177
|
+
else if (m[4] !== undefined) {
|
|
178
|
+
state.set(m[4], !!state.get(m[5]));
|
|
179
|
+
}
|
|
180
|
+
else if (m[6] !== undefined) {
|
|
181
|
+
if (state.get(m[7]))
|
|
182
|
+
protectedFns.set(m[6], true);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return protectedFns;
|
|
186
|
+
}
|
|
187
|
+
/** 项目级分析:跨文件组认证传播(同 gin analyzeGinProject) */
|
|
188
|
+
function analyzeFiberProject(projectRoot) {
|
|
189
|
+
const files = [];
|
|
190
|
+
const walk = (d) => {
|
|
191
|
+
for (const e of fs.readdirSync(d, { withFileTypes: true })) {
|
|
192
|
+
if (e.isDirectory()) {
|
|
193
|
+
if (["vendor", "node_modules", ".git"].includes(e.name))
|
|
194
|
+
continue;
|
|
195
|
+
walk(`${d}/${e.name}`);
|
|
196
|
+
}
|
|
197
|
+
else if (e.name.endsWith(".go") && !e.name.endsWith("_test.go")) {
|
|
198
|
+
const fp = `${d}/${e.name}`;
|
|
199
|
+
try {
|
|
200
|
+
files.push({ file: fp, text: fs.readFileSync(fp, "utf-8"), a: analyzeFiberFile(fp) });
|
|
201
|
+
}
|
|
202
|
+
catch { /* skip */ }
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
if (fs.existsSync(projectRoot))
|
|
207
|
+
walk(projectRoot.replace(/\/$/, ""));
|
|
208
|
+
// 多层 Register 链传播(journalist 式 main→api.Register(Group+Use)→…)
|
|
209
|
+
const protectedFns = fiberProjectProtectedFns(files);
|
|
210
|
+
const issues = [];
|
|
211
|
+
for (const { file, text, a } of files) {
|
|
212
|
+
if (!a || a.issues.length === 0)
|
|
213
|
+
continue;
|
|
214
|
+
const pkgM = text.match(/^package\s+(\w+)/m);
|
|
215
|
+
const filePkg = pkgM ? pkgM[1] : "";
|
|
216
|
+
for (const issue of a.issues) {
|
|
217
|
+
const fn = issue.line ? fiberEnclosingFunc(text, issue.line) : null;
|
|
218
|
+
if (fn && protectedFns.get(`${filePkg}:${fn}`))
|
|
219
|
+
continue;
|
|
220
|
+
issues.push({ ...issue });
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return {
|
|
224
|
+
filesScanned: files.length,
|
|
225
|
+
protectedFunctions: [...protectedFns.keys()].filter((k) => protectedFns.get(k)),
|
|
226
|
+
issues,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
/** 解析一个 Go 文件的顶层函数 */
|
|
230
|
+
function goFuncsOf(text, file) {
|
|
231
|
+
const out = [];
|
|
232
|
+
const pkgM = text.match(/^package\s+(\w+)/m);
|
|
233
|
+
const pkg = pkgM ? pkgM[1] : "";
|
|
234
|
+
const headerRe = /^func\s+([A-Za-z_]\w*)\s*\(/gm;
|
|
235
|
+
let m;
|
|
236
|
+
while ((m = headerRe.exec(text)) !== null) {
|
|
237
|
+
const hStart = m.index;
|
|
238
|
+
// 平衡取参数
|
|
239
|
+
let depth = 1;
|
|
240
|
+
let end = m.index + m[0].length;
|
|
241
|
+
while (end < text.length && depth > 0) {
|
|
242
|
+
if (text[end] === "(")
|
|
243
|
+
depth++;
|
|
244
|
+
else if (text[end] === ")")
|
|
245
|
+
depth--;
|
|
246
|
+
end++;
|
|
247
|
+
}
|
|
248
|
+
const paramsText = text.slice(m.index + m[0].length, end - 1);
|
|
249
|
+
// 找下一个 func 头作为 body 终点
|
|
250
|
+
const nxt = out.length ? text.indexOf("func ", end) : text.indexOf("\nfunc ", end);
|
|
251
|
+
const bodyEnd = (() => {
|
|
252
|
+
const nextHeader = text.slice(end).search(/^func\s/m);
|
|
253
|
+
return nextHeader === -1 ? text.length : end + nextHeader;
|
|
254
|
+
})();
|
|
255
|
+
const body = text.slice(end, bodyEnd);
|
|
256
|
+
const routerParams = [];
|
|
257
|
+
// 参数切分(顶层逗号)
|
|
258
|
+
let d = 0;
|
|
259
|
+
let cur = "";
|
|
260
|
+
const parts = [];
|
|
261
|
+
for (const ch of paramsText) {
|
|
262
|
+
if (ch === "(")
|
|
263
|
+
d++;
|
|
264
|
+
else if (ch === ")")
|
|
265
|
+
d--;
|
|
266
|
+
if (ch === "," && d === 0) {
|
|
267
|
+
parts.push(cur.trim());
|
|
268
|
+
cur = "";
|
|
269
|
+
}
|
|
270
|
+
else
|
|
271
|
+
cur += ch;
|
|
272
|
+
}
|
|
273
|
+
if (cur.trim())
|
|
274
|
+
parts.push(cur.trim());
|
|
275
|
+
for (const p of parts) {
|
|
276
|
+
// name *pkg.fiber.Router | name fiber.App …
|
|
277
|
+
const pm = p.match(/^([A-Za-z_]\w*)\s+[\w./*]*(\bfiber\b[\w./]*(?:Router|App|Group))/);
|
|
278
|
+
if (pm && /Router|App|Group/.test(pm[2]))
|
|
279
|
+
routerParams.push(pm[1]);
|
|
280
|
+
}
|
|
281
|
+
out.push({ file, pkg, name: m[1], routerParams, body: text.slice(end, bodyEnd) });
|
|
282
|
+
headerRe.lastIndex = bodyEnd; // 跳过函数体
|
|
283
|
+
}
|
|
284
|
+
return out;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* 项目级保护函数集(多层 Register 链,包限定键):
|
|
288
|
+
* 队列自全部函数「参数未认证」种子起,凡函数体在「组已 Use 认证」后以
|
|
289
|
+
* 认证组调用项目内 Register 函数 → 该函数入队(参数认证)——支持
|
|
290
|
+
* journalist 式 main→api.Register(Group+Use)→v1.Register→模块 多层链。
|
|
291
|
+
* 键 = pkg:name,避免跨包同名函数(feeds.Register/tokens.Register…)串扰。
|
|
292
|
+
*/
|
|
293
|
+
function fiberProjectProtectedFns(files) {
|
|
294
|
+
const funcs = [];
|
|
295
|
+
for (const f of files)
|
|
296
|
+
funcs.push(...goFuncsOf(f.text, f.file));
|
|
297
|
+
const keyOf = (pkg, name) => `${pkg}:${name}`;
|
|
298
|
+
const byKey = new Map();
|
|
299
|
+
for (const fn of funcs) {
|
|
300
|
+
byKey.set(keyOf(fn.pkg, fn.name), [...(byKey.get(keyOf(fn.pkg, fn.name)) || []), fn]);
|
|
301
|
+
}
|
|
302
|
+
const protectedFns = new Map();
|
|
303
|
+
const done = new Set();
|
|
304
|
+
const queue = [];
|
|
305
|
+
for (const fn of funcs)
|
|
306
|
+
queue.push({ pkg: fn.pkg, name: fn.name, authed: false });
|
|
307
|
+
while (queue.length) {
|
|
308
|
+
const { pkg, name, authed } = queue.shift();
|
|
309
|
+
const dk = `${pkg}|${name}|${authed}`;
|
|
310
|
+
if (done.has(dk))
|
|
311
|
+
continue;
|
|
312
|
+
done.add(dk);
|
|
313
|
+
if (authed)
|
|
314
|
+
protectedFns.set(keyOf(pkg, name), true);
|
|
315
|
+
const candidates = byKey.get(keyOf(pkg, name)) || [];
|
|
316
|
+
for (const fn of candidates) {
|
|
317
|
+
const authedParams = new Set();
|
|
318
|
+
if (authed)
|
|
319
|
+
fn.routerParams.forEach((pp) => authedParams.add(pp));
|
|
320
|
+
const callees = fiberBodyProtectedCalls(fn.body, authedParams, funcs);
|
|
321
|
+
for (const c of callees)
|
|
322
|
+
queue.push({ pkg: c.pkg, name: c.name, authed: true });
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return protectedFns;
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* 函数体内事件模拟 v2:返回在其认证相位被调用的项目函数(包限定)。
|
|
329
|
+
* 调用限定符 feeds.Register → pkg 匹配 feeds;同包调用按当前函数包。
|
|
330
|
+
*/
|
|
331
|
+
function fiberBodyProtectedCalls(body, authedParams, allFuncs) {
|
|
332
|
+
const calls = [];
|
|
333
|
+
const b = body.replace(/\(\s*\*\s*(\w+)\s*\)/g, "$1");
|
|
334
|
+
const curPkg = allFuncs.find((f) => f.body === body)?.pkg || "";
|
|
335
|
+
const state = new Map();
|
|
336
|
+
for (const p of authedParams)
|
|
337
|
+
state.set(p, true);
|
|
338
|
+
const re = /(\w+)\s*(?::=|=)\s*(?:[\w.]*\.)?([A-Za-z_]\w*)\.Group\s*\(|(\w+)\.Use\s*\(\s*([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)|(?:([A-Za-z_]\w*)\.)?([A-Z][A-Za-z0-9_]*)\s*\(/g;
|
|
339
|
+
let m;
|
|
340
|
+
while ((m = re.exec(b)) !== null) {
|
|
341
|
+
if (m[1] !== undefined) {
|
|
342
|
+
state.set(m[1], !!state.get(m[2]));
|
|
343
|
+
}
|
|
344
|
+
else if (m[3] !== undefined) {
|
|
345
|
+
if (isAuthFnName(m[4]))
|
|
346
|
+
state.set(m[3], true);
|
|
347
|
+
}
|
|
348
|
+
else if (m[5] !== undefined || m[6] !== undefined) {
|
|
349
|
+
const qual = m[5];
|
|
350
|
+
const fname = m[6];
|
|
351
|
+
// 项目内函数匹配:限定符 → pkg==qual;无限定符 → 同包
|
|
352
|
+
const matches = allFuncs.filter((f) => f.name === fname && (qual ? f.pkg === qual : f.pkg === curPkg));
|
|
353
|
+
if (matches.length === 0)
|
|
354
|
+
continue;
|
|
355
|
+
const openIdx = b.indexOf("(", m.index + m[0].length - 1);
|
|
356
|
+
if (openIdx < 0)
|
|
357
|
+
continue;
|
|
358
|
+
let depth = 1;
|
|
359
|
+
let i = openIdx + 1;
|
|
360
|
+
while (i < b.length && depth > 0) {
|
|
361
|
+
if (b[i] === "(")
|
|
362
|
+
depth++;
|
|
363
|
+
else if (b[i] === ")")
|
|
364
|
+
depth--;
|
|
365
|
+
i++;
|
|
366
|
+
}
|
|
367
|
+
const args = b.slice(openIdx + 1, i - 1).split(",").map((x) => x.trim().replace(/^[&*]+/, ""));
|
|
368
|
+
// 实参认证判定:直接组变量 或 内联组派生 X.Group(...)(X 已认证)
|
|
369
|
+
const argAuthed = (a) => {
|
|
370
|
+
if (state.get(a))
|
|
371
|
+
return true;
|
|
372
|
+
const g = a.match(/^([A-Za-z_]\w*)\.Group\s*\(/);
|
|
373
|
+
return !!g && !!state.get(g[1]);
|
|
374
|
+
};
|
|
375
|
+
if (args.some(argAuthed)) {
|
|
376
|
+
for (const mm of matches)
|
|
377
|
+
calls.push({ pkg: mm.pkg, name: mm.name });
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return calls;
|
|
382
|
+
}
|