progmune-runtime 3.7.48 → 3.7.49

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 CHANGED
@@ -1,5 +1,33 @@
1
1
  # Changelog
2
2
 
3
+ ## [3.7.49] — 2026-09-22
4
+
5
+ ### TS 侧「框架鉴权」语义标记:装饰器里的鉴权第一次对规则可见(E2)
6
+
7
+ - **缺口**:NestJS 的鉴权写在装饰器里(`@UseGuards(JwtAuthGuard)`,类级或方法级),
8
+ 既不在函数体文本里、也不在 `calls` 里 ⇒ 规则**无论如何都看不到**。
9
+ 探针:`@UseGuards(JwtAuthGuard)` 的 `createGroup` 被报
10
+ 「Authorization (Unauthenticated Mutation)」,而它明明有守卫。
11
+ - **更底层的发现**:规则侧定义了 23 个 `__progmune_*` 语义标记;Python 提取器产出
12
+ **23/23**,TS 提取器只产出 **4/23**(C/Go/Java **0/23**)。FP 观测池 100% 是 TS/JS
13
+ ⇒ 规则在 TS 上跑的时候 19 条证据通道是断的。本轮补的是其中一条(且是唯一
14
+ 「规则侧已经等着认、只是没人产」的那条)。
15
+ - **修复**(`src/extract-ir.ts`,类方法分支追加注入):
16
+ 类级 + 方法级装饰器 ⇒ `__progmune_auth_machinery__`(镜像 `tools/extract_ir.py:1134`
17
+ 的「类级框架守卫」)。**规则侧零改动**——Authorization 两条规则的 `auth_check`
18
+ safeguard 早已接受该标记。
19
+ - **边界(写死)**:`@Public()` / `@SkipAuth()` 显式免鉴权不注入;
20
+ `@UseGuards(ThrottlerGuard)` 等非鉴权守卫不注入;`@ApiBearerAuth()` 等 Swagger
21
+ 文档装饰器不注入;顶层函数装饰器不在本轮范围内。
22
+ - **实测效果**(FP 观测池 6 切片 / 367 条):Authorization 违规 **−24 条**
23
+ (Unauthenticated Access −11 / Mutation −13),**新增 0**,非 Authorization 规则
24
+ **0 变动**(与判据一致:只有接受标记的规则会受益;Input Validation 的 safeguard
25
+ 不接受任何标记,装饰器救不了它)。
26
+ - 回归:tsc ✓ / taintpath 160 条 ✓ / fr-007 pre 5·post 0、fr-016 pre 7·post 0 ✓ /
27
+ 定向测试 176 全绿(新增 E2 组 11 条)/ 反向验证四刀各落自己那组、残留 0。
28
+ **TS 盲测 LOST 0 / ADDED 0 —— 但这是空过**:generated 语料里装饰器命中数为 0,
29
+ 该门测不到本轮(R23 家族第三次),真证据来自 FP 观测池。
30
+
3
31
  ## [3.7.48] — 2026-09-22
4
32
 
5
33
  ### IR 提取失败必须发声(OOM 杀后的「0 违规假干净」根治)
@@ -2115,3 +2115,215 @@ export function dispatchTool(args: any) {
2115
2115
  }, 150000);
2116
2116
  }
2117
2117
  });
2118
+ // ═══════════════════════════════════════════════════════════════
2119
+ // E2:框架鉴权(装饰器)必须对规则可见(2026-09-22)
2120
+ //
2121
+ // 判据(先抄准再写 expect,R22):
2122
+ // protocol-detector.ts:398 / :430 —— Authorization (Unauthenticated Access /
2123
+ // Mutation) 两条规则的 auth_check safeguard 接受 __progmune_auth_machinery__;
2124
+ // 探针实测:@UseGuards(JwtAuthGuard) 的 createGroup 修复前报
2125
+ // 「Authorization (Unauthenticated Mutation)」,注入标记后该条消失,
2126
+ // 而 Input Validation 仍在(它的 safeguard 不接受任何标记 ⇒ 装饰器救不了它)。
2127
+ //
2128
+ // 反面风险(注入过头 = 假阴性,比误报更危险):
2129
+ // @Public()/@SkipAuth() 是「显式免鉴权」,不得注入;
2130
+ // @UseGuards(ThrottlerGuard) 是限流不是鉴权,不得注入;
2131
+ // @ApiBearerAuth() 只是 Swagger 文档,不实施鉴权,不得注入。
2132
+ // ═══════════════════════════════════════════════════════════════
2133
+ (0, vitest_1.describe)("E2:框架鉴权装饰器必须对规则可见(此前 TS 侧从未产出该标记)", () => {
2134
+ const AUTH_MARK = "__progmune_auth_machinery__";
2135
+ const cases = [
2136
+ {
2137
+ name: "正例·① 类级 @UseGuards(JwtAuthGuard)",
2138
+ fn: "GroupController.createGroup",
2139
+ files: {
2140
+ "a.ts": `
2141
+ declare const UseGuards: any;
2142
+ declare const Controller: any;
2143
+ declare const Post: any;
2144
+ declare class JwtAuthGuard {}
2145
+ @UseGuards(JwtAuthGuard)
2146
+ @Controller('groups')
2147
+ export class GroupController {
2148
+ createGroup(dto: any) { return this.svc.create(dto); }
2149
+ }
2150
+ `,
2151
+ },
2152
+ want: [AUTH_MARK],
2153
+ why: "NestJS 最常见的形态:鉴权写在类上,方法体内什么都没有 ⇒ 规则原来看不见",
2154
+ },
2155
+ {
2156
+ name: "正例·② 方法级 @UseGuards(JwtAuthGuard)",
2157
+ fn: "C.m",
2158
+ files: {
2159
+ "a.ts": `
2160
+ declare const UseGuards: any;
2161
+ declare class JwtAuthGuard {}
2162
+ export class C {
2163
+ @UseGuards(JwtAuthGuard)
2164
+ m() { return 1; }
2165
+ }
2166
+ `,
2167
+ },
2168
+ want: [AUTH_MARK],
2169
+ why: "守卫挂在单个端点上(而不是整个控制器)",
2170
+ },
2171
+ {
2172
+ name: "正例·③ @UseGuards(AuthGuard('jwt'))",
2173
+ fn: "C.m",
2174
+ files: {
2175
+ "a.ts": `
2176
+ declare const UseGuards: any;
2177
+ declare function AuthGuard(x: string): any;
2178
+ export class C {
2179
+ @UseGuards(AuthGuard('jwt'))
2180
+ m() { return 1; }
2181
+ }
2182
+ `,
2183
+ },
2184
+ want: [AUTH_MARK],
2185
+ why: "NestJS 官方 passport 形态:守卫是工厂调用,实参里含 jwt",
2186
+ },
2187
+ {
2188
+ name: "正例·④ @Roles('admin')",
2189
+ fn: "C.m",
2190
+ files: {
2191
+ "a.ts": `
2192
+ declare const Roles: any;
2193
+ export class C {
2194
+ @Roles('admin')
2195
+ m() { return 1; }
2196
+ }
2197
+ `,
2198
+ },
2199
+ want: [AUTH_MARK],
2200
+ why: "角色/权限装饰器同样是框架鉴权",
2201
+ },
2202
+ {
2203
+ name: "正例·⑤ @Authorize()(无实参)",
2204
+ fn: "C.m",
2205
+ files: {
2206
+ "a.ts": `
2207
+ declare const Authorize: any;
2208
+ export class C {
2209
+ @Authorize()
2210
+ m() { return 1; }
2211
+ }
2212
+ `,
2213
+ },
2214
+ want: [AUTH_MARK],
2215
+ why: "无实参装饰器:无法靠实参判断,按装饰器名判定(保守放行)",
2216
+ },
2217
+ {
2218
+ name: "负对照·① @Public() 显式免鉴权,不得注入",
2219
+ fn: "C.m",
2220
+ files: {
2221
+ "a.ts": `
2222
+ declare const UseGuards: any;
2223
+ declare const Public: any;
2224
+ declare class JwtAuthGuard {}
2225
+ @UseGuards(JwtAuthGuard)
2226
+ export class C {
2227
+ @Public()
2228
+ m() { return 1; }
2229
+ }
2230
+ `,
2231
+ },
2232
+ want: [],
2233
+ dontWant: [AUTH_MARK],
2234
+ why: "公开端点被判成「有鉴权」= 假阴性,比误报危险得多,必须挡住",
2235
+ },
2236
+ {
2237
+ name: "负对照·② @UseGuards(ThrottlerGuard) 是限流不是鉴权",
2238
+ fn: "C.m",
2239
+ files: {
2240
+ "a.ts": `
2241
+ declare const UseGuards: any;
2242
+ declare class ThrottlerGuard {}
2243
+ export class C {
2244
+ @UseGuards(ThrottlerGuard)
2245
+ m() { return 1; }
2246
+ }
2247
+ `,
2248
+ },
2249
+ want: [],
2250
+ dontWant: [AUTH_MARK],
2251
+ why: "同名装饰器、非鉴权语义 ⇒ 实参必须看起来像鉴权守卫(R19:分母里最容易漏的就是被拒掉的那些)",
2252
+ },
2253
+ {
2254
+ name: "负对照·③ @ApiBearerAuth() 只是 Swagger 文档",
2255
+ fn: "C.m",
2256
+ files: {
2257
+ "a.ts": `
2258
+ declare const ApiBearerAuth: any;
2259
+ export class C {
2260
+ @ApiBearerAuth()
2261
+ m() { return 1; }
2262
+ }
2263
+ `,
2264
+ },
2265
+ want: [],
2266
+ dontWant: [AUTH_MARK],
2267
+ why: "名字里带 auth,但不实施鉴权 ⇒ Api* 一律跳过",
2268
+ },
2269
+ {
2270
+ name: "负对照·④ 无装饰器不得注入",
2271
+ fn: "C.m",
2272
+ files: {
2273
+ "a.ts": `
2274
+ export class C { m() { return 1; } }
2275
+ `,
2276
+ },
2277
+ want: [],
2278
+ dontWant: [AUTH_MARK],
2279
+ why: "基线:没有鉴权证据就不许凭空给出鉴权",
2280
+ },
2281
+ {
2282
+ name: "负对照·⑤ 顶层函数(非类)不在本轮范围内",
2283
+ fn: "handler",
2284
+ files: {
2285
+ "a.ts": `
2286
+ declare const UseGuards: any;
2287
+ declare class JwtAuthGuard {}
2288
+ @UseGuards(JwtAuthGuard)
2289
+ export function handler() { return 1; }
2290
+ `,
2291
+ },
2292
+ want: [],
2293
+ dontWant: [AUTH_MARK],
2294
+ why: "本轮只覆盖类/方法(镜像 Python 的『类级守卫』)。顶层函数装饰器罕见,边界写死在此",
2295
+ },
2296
+ {
2297
+ name: "正对照·⑥ 真实调用与既有标记不得被挤掉(E1 不回归)",
2298
+ fn: "Uploader.read",
2299
+ files: {
2300
+ "a.ts": `
2301
+ import * as fs from "fs";
2302
+ declare const UseGuards: any;
2303
+ declare class JwtAuthGuard {}
2304
+ @UseGuards(JwtAuthGuard)
2305
+ export class Uploader {
2306
+ read(filePath: string) { return fs.readFileSync(filePath, "utf-8"); }
2307
+ }
2308
+ `,
2309
+ },
2310
+ want: [AUTH_MARK, "readFileSync"],
2311
+ why: "本轮是**追加**标记,不是替换;E1 补进来的真实调用必须仍在",
2312
+ },
2313
+ ];
2314
+ for (const c of cases) {
2315
+ (0, vitest_1.it)(`${c.name} —— ${c.why}`, () => {
2316
+ const dir = makeProject(c.files);
2317
+ try {
2318
+ const calls = marksFor(dir, c.fn);
2319
+ for (const w of c.want)
2320
+ (0, vitest_1.expect)(calls).toContain(w);
2321
+ for (const d of c.dontWant ?? [])
2322
+ (0, vitest_1.expect)(calls).not.toContain(d);
2323
+ }
2324
+ finally {
2325
+ fs.rmSync(dir, { recursive: true, force: true });
2326
+ }
2327
+ }, 150000);
2328
+ }
2329
+ });
@@ -390,6 +390,57 @@ function extractDirectCalls(func, preText) {
390
390
  // 与 protocol-detector 的同名规则(source-level benchmark 路径)。
391
391
  // ═══════════════════════════════════════════════════════════════
392
392
  const PATH_TRAVERSAL_MARKER = "__progmune_path_traversal__";
393
+ // ═══════════════════════════════════════════════════════════════
394
+ // E2(2026-09-22):TS 侧「框架鉴权」语义标记 —— 镜像 tools/extract_ir.py:1134
395
+ //
396
+ // Python 侧由**类级框架守卫**触发(类名含 authenticator / DRF permission_classes);
397
+ // TS 侧的等价物是 NestJS 的类/方法级守卫装饰器:@UseGuards(JwtAuthGuard)、@Roles(...) 等。
398
+ // 装饰器里的鉴权既不在函数体文本里、也不在 calls 里,规则**无论如何都看不到**——
399
+ // 探针:@UseGuards(JwtAuthGuard) 的 createGroup 被报「未鉴权」,注入本标记后该条消失。
400
+ //
401
+ // 消费方:protocol-detector 的 Authorization (Unauthenticated Access / Mutation)
402
+ // 两条规则的 auth_check safeguard(protocol-detector.ts:398 / :430)——它们**早已接受**
403
+ // __progmune_auth_machinery__,TS 侧此前从未产出 ⇒ 规则侧零改动。
404
+ //
405
+ // 边界(写死在这里,别想当然):只有**接受标记的规则**会受益。
406
+ // Input Validation 的 safeguard 不接受任何 __progmune_* 标记(protocol-detector.ts:481
407
+ // 是纯词形匹配),装饰器对它无效;实测注入后 Input Validation 条仍在。
408
+ // ═══════════════════════════════════════════════════════════════
409
+ const AUTH_MACHINERY_MARKER = "__progmune_auth_machinery__";
410
+ /** 显式免鉴权装饰器 —— 出现即不注入。把公开端点判成「有鉴权」是最危险的假阴性。 */
411
+ const AUTH_SKIP_DECORATOR = /^(?:Public|SkipAuth|AllowAnonymous|IsPublic|NoAuth|ApiExcludeEndpoint)$/i;
412
+ /** 承载鉴权的装饰器名(NestJS 约定:Guard = 鉴权/授权)。 */
413
+ const AUTH_DECORATOR = /^(?:UseGuards|Roles|Permissions|Authorize|RequireAuth|RequiresAuth|Authenticated|UseAuth|CheckPermissions|RequirePermissions|AdminOnly)$/i;
414
+ /** @UseGuards(X) 的实参须像鉴权守卫 —— 用于排除 @UseGuards(ThrottlerGuard) 之类。 */
415
+ const GUARD_ARG_AUTH = /auth|jwt|session|login|permission|role|admin|bearer|credential/i;
416
+ /**
417
+ * 由类/方法装饰器判定是否注入框架鉴权标记。文本匹配(与 computeMarkerCalls 口径一致)。
418
+ * @returns [] 或 [AUTH_MACHINERY_MARKER]
419
+ */
420
+ function authMachineryFromDecorators(decorators) {
421
+ let skip = false;
422
+ let auth = false;
423
+ for (const d of decorators) {
424
+ const text = (d?.getText?.() ?? "").trim();
425
+ const name = /^@([A-Za-z_$][\w$]*)/.exec(text)?.[1];
426
+ if (!name)
427
+ continue;
428
+ if (AUTH_SKIP_DECORATOR.test(name)) {
429
+ skip = true;
430
+ continue;
431
+ }
432
+ // Swagger 文档装饰器(@ApiBearerAuth …)只描述、不实施鉴权
433
+ if (/^Api/i.test(name))
434
+ continue;
435
+ if (!AUTH_DECORATOR.test(name) && !/guard|auth/i.test(name))
436
+ continue;
437
+ const args = /^@[\w$]*\(([\s\S]*)\)$/.exec(text)?.[1] ?? "";
438
+ if (args.trim() && !GUARD_ARG_AUTH.test(args))
439
+ continue;
440
+ auth = true;
441
+ }
442
+ return auth && !skip ? [AUTH_MACHINERY_MARKER] : [];
443
+ }
393
444
  /**
394
445
  * 标识符是否「看起来像基目录」—— G-A2 的定案依据。
395
446
  *
@@ -2877,6 +2928,15 @@ function _extractSingleProject(absRoot, tsconfigPath) {
2877
2928
  // 顺序与函数声明 / 箭头两个分支保持一致:先真实调用,再追加语义标记。
2878
2929
  const mCalls = extractDirectCalls(m, mText);
2879
2930
  mCalls.push(...computeMarkerCalls(mText, mParams.map((p) => p.getName()), sinkParams, onMethodHit, guardFns, directGuardFns, shapers));
2931
+ // E2(2026-09-22):类级 + 方法级守卫装饰器 → 框架鉴权标记。鉴权写在装饰器里,
2932
+ // 函数体文本与 calls 都看不到(镜像 Python 提取器的「类级框架守卫」处理)。
2933
+ for (const mk of authMachineryFromDecorators([
2934
+ ...(cls.getDecorators?.() ?? []),
2935
+ ...(m.getDecorators?.() ?? []),
2936
+ ])) {
2937
+ if (!mCalls.includes(mk))
2938
+ mCalls.push(mk);
2939
+ }
2880
2940
  funcs.push({
2881
2941
  name: `${cn}.${mn}`,
2882
2942
  params: mParams.map((p) => ({ name: p.getName(), type: getParamType(p), typeDetail: getParamTypeDetail(p) })),
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.48";
23
+ exports.RUNTIME_VERSION = "3.7.49";
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.48",
3
+ "version": "3.7.49",
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/",