progmune-runtime 3.7.70 → 3.7.71
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 +32 -0
- package/dist/frameworks/express-detector.js +116 -7
- package/dist/frameworks/express-detector.test.js +90 -0
- package/dist/sdk.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [3.7.71] — 2026-10-18
|
|
4
|
+
|
|
5
|
+
### Express 匿名中间件函数体鉴权证据(fr-014 残留归零)
|
|
6
|
+
|
|
7
|
+
**根因**:旧 `extractGlobalMiddleware` 用 `[^)]+` 捕获 `app.use(...)`
|
|
8
|
+
实参——匿名中间件函数体的鉴权证据(`app.use(\`${prefix}*\`,
|
|
9
|
+
(req,res,next) => { extractBearerToken(...) ... })`)被截断在第一个
|
|
10
|
+
`)` 处(`(req,res,next)` 的右括号),`classifyMiddleware` 永远看不
|
|
11
|
+
到函数体 ⇒ `hasAnyAuth` 恒 false ⇒ mockoon 管理 API 修复版本
|
|
12
|
+
(CVE-2026-59148 / GHSA-rqx4-3f6q-3x2v)仍残留 1 条
|
|
13
|
+
EXPRESS_NO_AUTH_MIDDLEWARE critical。
|
|
14
|
+
|
|
15
|
+
- **顶层实参解析(parseUseArgs)**:括号/花括号/字符串/注释感知的
|
|
16
|
+
顶层逗号切分——匿名中间件函数体完整进入 classifyMiddleware,
|
|
17
|
+
函数体内的 extractBearerToken/timingSafeEqual 等强证据词命中
|
|
18
|
+
auth 分类。边界如实记录:模板串按无嵌套 ${} 处理;正则字面量
|
|
19
|
+
不特殊处理(与检测器其他文本级判定同口径)
|
|
20
|
+
- **模板串路径归属边界**:动态路径路由(`${prefix}/x`)跳过逐路由
|
|
21
|
+
EXPRESS_ROUTE_MISSING_AUTH 检查——全局守卫挂载路径同样是变量
|
|
22
|
+
拼接,检测器无法证明「该路由不在守卫覆盖内」,逐路由告警是不
|
|
23
|
+
可归属的噪声(mockoon 修复形态实测 12 条全 FP)。保守边界:整
|
|
24
|
+
app 无认证的 critical 不受影响(动态路径照常进 hasNakedMutation
|
|
25
|
+
计数,修复前形态照报 critical)
|
|
26
|
+
- **fr-014 三态复测(mini 语料,真值文件 raw 直链)**:修复前
|
|
27
|
+
admin-api.ts 报 critical + server.ts 3 条加固建议类(两版本一致);
|
|
28
|
+
修复后 0 critical + 同 3 条建议类——**注册表升级为干净 DETECTED**
|
|
29
|
+
(fr-corpus 有效 11/17 的 with-boundary 项收窄)
|
|
30
|
+
- **回归锁定**:express 检测器 33/33(+6 例:函数体证据/三态/嵌套
|
|
31
|
+
实参不切碎/字面量路由照报/注释括号);框架全族 188/188;引擎 +
|
|
32
|
+
检测器族 245/245;express-realworld 20 路由 0 协议级 FP 保持
|
|
33
|
+
(仅已知 NO_HELMET 加固建议边界)
|
|
34
|
+
|
|
3
35
|
## [3.7.70] — 2026-10-18
|
|
4
36
|
|
|
5
37
|
### 裸名族兜底传播豁免(修冤枉第十一刀)
|
|
@@ -223,23 +223,124 @@ function classifyMiddleware(code, name) {
|
|
|
223
223
|
return "logging";
|
|
224
224
|
return "unknown";
|
|
225
225
|
}
|
|
226
|
+
/**
|
|
227
|
+
* 解析 app.use(...) 的实参列表(2026-10-18,fr-014 残留根因修复):
|
|
228
|
+
* 顶层逗号切分 + 括号/花括号/字符串/注释感知。
|
|
229
|
+
*
|
|
230
|
+
* 根因:旧实现用 `[^)]+` 捕获实参——匿名中间件函数体的鉴权证据
|
|
231
|
+
* (app.use(`${prefix}*`, (req,res,next) => { extractBearerToken(...)
|
|
232
|
+
* ... }))被截断在第一个 `)` 处((req,res,next) 的右括号),
|
|
233
|
+
* classifyMiddleware 永远看不到函数体 ⇒ hasAnyAuth 恒 false。
|
|
234
|
+
*
|
|
235
|
+
* 边界(如实记录):模板串按无嵌套 ${} 处理(路径字面量形态;
|
|
236
|
+
* ${} 内含反引号/顶层括号的极端形态会错切);正则字面量不特殊
|
|
237
|
+
* 处理(检测器不做语法树,与其他文本级判定同口径)。
|
|
238
|
+
*/
|
|
239
|
+
function parseUseArgs(code, openParenIndex) {
|
|
240
|
+
const args = [];
|
|
241
|
+
let i = openParenIndex + 1;
|
|
242
|
+
let paren = 1; // app.use( 的实参层
|
|
243
|
+
let brace = 0;
|
|
244
|
+
let argStart = i;
|
|
245
|
+
let quote = null;
|
|
246
|
+
let lineComment = false;
|
|
247
|
+
let blockComment = false;
|
|
248
|
+
while (i < code.length) {
|
|
249
|
+
const ch = code[i];
|
|
250
|
+
if (lineComment) {
|
|
251
|
+
if (ch === "\n")
|
|
252
|
+
lineComment = false;
|
|
253
|
+
i++;
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
if (blockComment) {
|
|
257
|
+
if (ch === "*" && code[i + 1] === "/") {
|
|
258
|
+
blockComment = false;
|
|
259
|
+
i += 2;
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
i++;
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
if (quote) {
|
|
266
|
+
if (ch === "\\") {
|
|
267
|
+
i += 2;
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
if (ch === quote)
|
|
271
|
+
quote = null;
|
|
272
|
+
i++;
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
if (ch === "'" || ch === '"' || ch === "`") {
|
|
276
|
+
quote = ch;
|
|
277
|
+
i++;
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
if (ch === "/" && code[i + 1] === "/") {
|
|
281
|
+
lineComment = true;
|
|
282
|
+
i += 2;
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
if (ch === "/" && code[i + 1] === "*") {
|
|
286
|
+
blockComment = true;
|
|
287
|
+
i += 2;
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
if (ch === "(") {
|
|
291
|
+
paren++;
|
|
292
|
+
i++;
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
if (ch === ")") {
|
|
296
|
+
paren--;
|
|
297
|
+
if (paren === 0) {
|
|
298
|
+
const text = code.slice(argStart, i).trim();
|
|
299
|
+
if (text)
|
|
300
|
+
args.push({ text, start: argStart });
|
|
301
|
+
return args;
|
|
302
|
+
}
|
|
303
|
+
i++;
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
if (ch === "{") {
|
|
307
|
+
brace++;
|
|
308
|
+
i++;
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
if (ch === "}") {
|
|
312
|
+
brace = Math.max(0, brace - 1);
|
|
313
|
+
i++;
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
if (ch === "," && paren === 1 && brace === 0) {
|
|
317
|
+
const text = code.slice(argStart, i).trim();
|
|
318
|
+
if (text)
|
|
319
|
+
args.push({ text, start: argStart });
|
|
320
|
+
argStart = i + 1;
|
|
321
|
+
}
|
|
322
|
+
i++;
|
|
323
|
+
}
|
|
324
|
+
return args;
|
|
325
|
+
}
|
|
226
326
|
/**
|
|
227
327
|
* Extract global middleware registrations (app.use(fn)).
|
|
228
328
|
*/
|
|
229
329
|
function extractGlobalMiddleware(code, appName) {
|
|
230
330
|
const middleware = [];
|
|
231
|
-
const useRegex = new RegExp(`${appName}\\.use\\s*\\(
|
|
331
|
+
const useRegex = new RegExp(`${appName}\\.use\\s*\\(`, "gi");
|
|
232
332
|
let match;
|
|
233
333
|
while ((match = useRegex.exec(code)) !== null) {
|
|
234
|
-
const
|
|
235
|
-
for (const arg of
|
|
236
|
-
|
|
334
|
+
const openParen = match.index + match[0].length - 1;
|
|
335
|
+
for (const arg of parseUseArgs(code, openParen)) {
|
|
336
|
+
const t = arg.text;
|
|
337
|
+
if (/^['"`]/.test(t))
|
|
237
338
|
continue; // skip path strings
|
|
238
|
-
if (/^(express|router|Router)\./.test(
|
|
339
|
+
if (/^(express|router|Router)\./.test(t))
|
|
239
340
|
continue; // skip sub-routers (handled separately)
|
|
240
341
|
middleware.push({
|
|
241
|
-
name:
|
|
242
|
-
type: classifyMiddleware(code,
|
|
342
|
+
name: t,
|
|
343
|
+
type: classifyMiddleware(code, t),
|
|
243
344
|
registeredAt: code.slice(0, match.index).split("\n").length,
|
|
244
345
|
});
|
|
245
346
|
}
|
|
@@ -311,6 +412,14 @@ function analyzeExpressApp(code) {
|
|
|
311
412
|
continue; // 读操作不查(公开读常见)
|
|
312
413
|
if (routeHasAuth(route))
|
|
313
414
|
continue;
|
|
415
|
+
// 模板串路径(如 `${adminApiPrefix}/env-vars`)不可静态归属——
|
|
416
|
+
// 全局守卫挂载路径同样是变量拼接(app.use(`${prefix}*`, auth)),
|
|
417
|
+
// 检测器无法证明「该路由不在守卫覆盖内」,逐路由告警是不可归属的
|
|
418
|
+
// 噪声(fr-014 mockoon 修复形态实测 12 条全 FP)。保守边界:动态
|
|
419
|
+
// 路径跳过逐路由检查;整 app 无认证的 critical 检查不受影响
|
|
420
|
+
// (动态路径照常进入 hasNakedMutation 计数)。
|
|
421
|
+
if (route.path.includes("${"))
|
|
422
|
+
continue;
|
|
314
423
|
if (isPublicRoute(route.path))
|
|
315
424
|
continue;
|
|
316
425
|
if (route.method === "post" && (0, route_window_1.isRegisterRoot)(route.path, registerRoots))
|
|
@@ -273,3 +273,93 @@ module.exports = router;
|
|
|
273
273
|
(0, vitest_1.expect)(issues.some((i) => i.rule === "EXPRESS_ROUTE_MISSING_AUTH" && i.route === "PUT /admin")).toBe(false);
|
|
274
274
|
});
|
|
275
275
|
});
|
|
276
|
+
// ═══════════════════════════════════════════════════════════════
|
|
277
|
+
// fr-014 残留修复(2026-10-18):匿名中间件函数体鉴权证据 + 模板
|
|
278
|
+
// 串路径归属边界(mockoon admin-api 修复形态)
|
|
279
|
+
//
|
|
280
|
+
// 根因:extractGlobalMiddleware 旧实现 [^)]+ 把 app.use(`${prefix}*`,
|
|
281
|
+
// (req,res,next) => { ...extractBearerToken... }) 截断在第一个 `)`,
|
|
282
|
+
// 函数体证据永远不可见 ⇒ hasAnyAuth 恒 false ⇒ 修复后仍报 critical。
|
|
283
|
+
// 修复:顶层实参解析(括号/花括号/字符串感知)+ 模板串路径跳过
|
|
284
|
+
// 逐路由检查(动态路径不可静态归属,12 条实测全 FP)。
|
|
285
|
+
// ═══════════════════════════════════════════════════════════════
|
|
286
|
+
(0, vitest_1.describe)("express-detector:匿名中间件函数体鉴权证据(fr-014 残留)", () => {
|
|
287
|
+
const ADMIN_API_FIX_SHAPE = `
|
|
288
|
+
const express = require('express');
|
|
289
|
+
const { timingSafeEqual } = require('node:crypto');
|
|
290
|
+
const setupAdminApi = (app: Express) => {
|
|
291
|
+
const adminApiPrefix = '/mockoon-admin';
|
|
292
|
+
const hasValidAdminApiToken = (token: string) => timingSafeEqual(Buffer.from('x'), Buffer.from(token));
|
|
293
|
+
|
|
294
|
+
app.use(\`\${adminApiPrefix}*\`, (req, res, next) => {
|
|
295
|
+
const bearerToken = extractBearerToken(req.get('authorization'));
|
|
296
|
+
if (!bearerToken || !hasValidAdminApiToken(bearerToken)) {
|
|
297
|
+
res.status(401).send({ message: 'Unauthorized' });
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
next();
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
app.post(\`\${adminApiPrefix}/env-vars\`, setEnvVarHandler);
|
|
304
|
+
app.put(\`\${adminApiPrefix}/env-vars\`, setEnvVarHandler);
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
export { setupAdminApi };
|
|
308
|
+
`;
|
|
309
|
+
const ADMIN_API_PRE_SHAPE = ADMIN_API_FIX_SHAPE.replace(` app.use(\`\${adminApiPrefix}*\`, (req, res, next) => {
|
|
310
|
+
const bearerToken = extractBearerToken(req.get('authorization'));
|
|
311
|
+
if (!bearerToken || !hasValidAdminApiToken(bearerToken)) {
|
|
312
|
+
res.status(401).send({ message: 'Unauthorized' });
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
next();
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
`, "");
|
|
319
|
+
(0, vitest_1.it)("匿名中间件函数体内的 bearer 证据 ⇒ 分类为 auth(修复形态)", () => {
|
|
320
|
+
const mw = (0, express_detector_1.extractGlobalMiddleware)(ADMIN_API_FIX_SHAPE, "app");
|
|
321
|
+
const auth = mw.find((m) => m.type === "auth");
|
|
322
|
+
(0, vitest_1.expect)(auth).toBeDefined();
|
|
323
|
+
(0, vitest_1.expect)(auth.name).toContain("extractBearerToken");
|
|
324
|
+
});
|
|
325
|
+
(0, vitest_1.it)("修复形态(全局匿名 auth + 模板串 mutation 路由)⇒ 0 issues", () => {
|
|
326
|
+
const { issues } = (0, express_detector_1.analyzeExpressApp)(ADMIN_API_FIX_SHAPE);
|
|
327
|
+
(0, vitest_1.expect)(issues).toEqual([]);
|
|
328
|
+
});
|
|
329
|
+
(0, vitest_1.it)("修复前形态(无任何鉴权证据 + 模板串 mutation 路由)⇒ critical 照报", () => {
|
|
330
|
+
const { issues } = (0, express_detector_1.analyzeExpressApp)(ADMIN_API_PRE_SHAPE);
|
|
331
|
+
(0, vitest_1.expect)(issues.some((i) => i.rule === "EXPRESS_NO_AUTH_MIDDLEWARE")).toBe(true);
|
|
332
|
+
});
|
|
333
|
+
(0, vitest_1.it)("嵌套调用实参不被切碎(app.use(cors({origin:'x,y'})) 分类 cors)", () => {
|
|
334
|
+
const code = `
|
|
335
|
+
const app = require('express')();
|
|
336
|
+
app.use(cors({ origin: 'a,b', methods: ['GET','POST'] }));
|
|
337
|
+
app.post('/x', handler);
|
|
338
|
+
`;
|
|
339
|
+
const mw = (0, express_detector_1.extractGlobalMiddleware)(code, "app");
|
|
340
|
+
(0, vitest_1.expect)(mw.some((m) => m.type === "cors")).toBe(true);
|
|
341
|
+
// 逗号在字符串/嵌套括号内不产生碎实参
|
|
342
|
+
(0, vitest_1.expect)(mw.length).toBe(1);
|
|
343
|
+
});
|
|
344
|
+
(0, vitest_1.it)("字面量路径 mutation 路由在 hasAnyAuth 下仍逐路由报(回归锁定)", () => {
|
|
345
|
+
const code = `
|
|
346
|
+
const app = require('express')();
|
|
347
|
+
app.use(passport.initialize());
|
|
348
|
+
app.post('/admin/secret', handler);
|
|
349
|
+
`;
|
|
350
|
+
const { issues } = (0, express_detector_1.analyzeExpressApp)(code);
|
|
351
|
+
(0, vitest_1.expect)(issues.some((i) => i.rule === "EXPRESS_ROUTE_MISSING_AUTH" && i.route === "POST /admin/secret")).toBe(true);
|
|
352
|
+
});
|
|
353
|
+
(0, vitest_1.it)("注释内的括号/花括号不破坏实参解析", () => {
|
|
354
|
+
const code = `
|
|
355
|
+
const app = require('express')();
|
|
356
|
+
app.use(
|
|
357
|
+
// (注释里的括号) 与 /* 块注释 { } */
|
|
358
|
+
(req, res, next) => { /* } */ if (req.url) { next(); } }
|
|
359
|
+
);
|
|
360
|
+
app.post('/x', handler);
|
|
361
|
+
`;
|
|
362
|
+
const mw = (0, express_detector_1.extractGlobalMiddleware)(code, "app");
|
|
363
|
+
(0, vitest_1.expect)(mw.length).toBe(1);
|
|
364
|
+
});
|
|
365
|
+
});
|
package/dist/sdk.js
CHANGED
|
@@ -20,7 +20,7 @@ const risk_model_1 = require("./risk-model");
|
|
|
20
20
|
const protocol_knowledge_1 = require("./protocol-knowledge");
|
|
21
21
|
const evidence_repository_1 = require("./evidence-repository");
|
|
22
22
|
/** Runtime version — stable public identifier. Internal layers evolve underneath. */
|
|
23
|
-
exports.RUNTIME_VERSION = "3.7.
|
|
23
|
+
exports.RUNTIME_VERSION = "3.7.71";
|
|
24
24
|
function verify(filePath) {
|
|
25
25
|
const cert = (0, certify_1.certify)(filePath);
|
|
26
26
|
const kb = (0, protocol_knowledge_1.buildKnowledgeBase)();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "progmune-runtime",
|
|
3
|
-
"version": "3.7.
|
|
3
|
+
"version": "3.7.71",
|
|
4
4
|
"description": "Progmune — AI Trust Decision Engine. Verify AI-generated code before it reaches production. Outputs APPROVED / NEEDS_REVIEW / BLOCKED with evidence.",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist/",
|