progmune-runtime 3.7.49 → 3.7.51

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,63 @@
1
1
  # Changelog
2
2
 
3
+ ## [3.7.51] — 2026-09-22
4
+
5
+ ### Input Validation 的 trigger 只认函数名:不再被 callee 拖下水(F 轮)
6
+
7
+ - **缺口(不是词表问题,是「陈述对象」错位)**:本规则的 violationMessage 是
8
+ *"Content creation function does not validate…"* —— 陈述对象是
9
+ **「这个函数自己是不是内容创建函数」**。但 trigger 此前拿 **callee 名字**
10
+ 去匹配 `create*|add*|post*|upload*`,于是产生两类缺陷:
11
+ 1. **重复投射**:分发器 `handleRequest` 因调用 `createEvent` / `createRoute`
12
+ 被计一次,而那些 callee 自己也在报 ⇒ 同一条问题计两遍;
13
+ 2. **理由错**:`login` / `registerNewUser` / `authenticate` 因调用
14
+ **`createHash`**(密码哈希)被判成「内容创建函数未校验输入」。
15
+ - **修复**:新增规则字段 `triggerOwnNameOnly`(只拿 enclosing function 自身名字
16
+ 去匹配 trigger),在 `detectSafeguardViolations` 里加分支,并在
17
+ Input Validation 规则上开启。其余规则行为不变。
18
+ - **175 条损失逐条归类,无一真阳性**(R30):100 条重复投射(callee 自己仍报)、
19
+ 69 条 `createHash`、6 条 `list/get` 查询函数。真实池 37 条同理
20
+ (7 重复投射 / 3 被其他规则覆盖 / 27 触发源为 ORM·哈希·权限工厂·内部工具)。
21
+ - **实测**:TS 盲测 Input Validation **417 → 242(−175)**、ADDED 0、
22
+ **其他规则 0 变动**;FP 池 docmost 24 → 14(−10)、ADDED 0。
23
+ 离线重放(R29)的预测与真实重扫**逐条一致**(第二次确认保真度)。
24
+ - **已知边界(不掩饰)**:函数名不含 create/add/post/upload、但确实是
25
+ 「入口把用户输入交给内容创建」的函数(如 `handleSubmit` 调
26
+ `commentRepo.create`,而 create 不在扫描范围内)本轮后不再报。175 条里此类
27
+ 为 0;若要补回需要 `exposed`(web handler)通道 —— **按 R27 先查通道**。
28
+
29
+ ## [3.7.50] — 2026-09-22
30
+
31
+ ### TS 侧「入参已按 schema 校验」语义标记:DTO 的校验第一次对规则可见(E3)
32
+
33
+ - **缺口(R27 说的「证据通道断着」的实例)**:Input Validation 的 safeguard 是
34
+ **纯词形匹配,不接受任何 `__progmune_*` 标记**(`protocol-detector.ts:481`)。
35
+ NestJS 把校验写在**另一个文件**的 DTO 类属性装饰器上(`@IsString()` / `@MinLength(2)`),
36
+ 既不在函数体文本里、也不在 `calls` 里 ⇒ 规则永远看不到。E2 结束时已实测确认
37
+ 「装饰器救不了 Input Validation」,本轮是**接通道**,不是调词表。
38
+ - **修复(双侧,各一行)**:
39
+ - 提取器 `src/extract-ir.ts`:新增 `validatedDtoClassNames(project)` 预扫
40
+ (类自身或其属性上带校验装饰器 ⇒ 类名入集合),主循环后统一应用 ——
41
+ 入参类型是集合中的类 ⇒ 注入 `__progmune_input_schema__`。
42
+ 按**入参**判定、不按函数形态 ⇒ 函数声明 / 箭头 / 类方法全形态覆盖。
43
+ - 规则 `protocol-detector.ts`:Input Validation 的 safeguard 追加一个接受分支。
44
+ **只加在抑制位,不进 trigger**——新通道只能做减法。
45
+ - **标记命名是一个真陷阱(本轮抓到的)**:初版取名 `__progmune_validated_input__`,
46
+ 离线重放发现它会被 `Data Integrity (Foreign Key)` 的 safeguard
47
+ `\b(…|validate|…)(?:[A-Z]\w*|_\w+)\b` 命中(`validate` + `d_input__`),
48
+ 凭空压掉 **10 条无关违规**。改名 `__progmune_input_schema__`(不含
49
+ get/find/check/verify/validate/create/add/post/upload 任何动词词根)后归零。
50
+ 已加定向用例守住这条。
51
+ - **实测效果**(FP 观测池,docmost 切片真实重扫):Input Validation **39 → 24(−15)**,
52
+ **新增 0**,其他规则 **0 变动**。全池口径 −17(含 lujakob −1、w3tecch −1)。
53
+ - 回归:tsc ✓ / taintpath 160 条 ✓ / fr-007 pre 5·post 0、fr-016 pre 7·post 0 ✓ /
54
+ 定向测试 **187 全绿**(新增 E3 组 11 条)/ 反向验证两刀(拆规则侧接受位、关提取器
55
+ 发射)各自精确回到 39、残留 0。
56
+ - **TS 盲测 3201 → 3201、逐函数差异 0 —— 这是空过(R23 家族第四次)**,已核实:
57
+ generated 语料里校验装饰器命中数 **0**,该门测不到本轮;真证据来自 FP 观测池。
58
+ - 另附:本轮先用**离线重放**(拿已记录的 `calls + name` 直接喂 detector,不重新提取 IR)
59
+ 预测量三个候选变体的效果,再挑一个实现。重放预测的 −15 与真实重扫**逐条一致**。
60
+
3
61
  ## [3.7.49] — 2026-09-22
4
62
 
5
63
  ### TS 侧「框架鉴权」语义标记:装饰器里的鉴权第一次对规则可见(E2)
@@ -56,6 +56,7 @@ const fs = __importStar(require("fs"));
56
56
  const os = __importStar(require("os"));
57
57
  const path = __importStar(require("path"));
58
58
  const extract_ir_1 = require("./extract-ir");
59
+ const protocol_detector_1 = require("./protocol-detector");
59
60
  const TSCONFIG = JSON.stringify({
60
61
  compilerOptions: {
61
62
  target: "ES2020",
@@ -2327,3 +2328,260 @@ export class Uploader {
2327
2328
  }, 150000);
2328
2329
  }
2329
2330
  });
2331
+ // ═══════════════════════════════════════════════════════════════
2332
+ // E3:DTO 的 schema 校验必须对规则可见(2026-09-22)
2333
+ //
2334
+ // 判据(先抄准再写 expect,R22):
2335
+ // protocol-detector.ts Input Validation 的 safeguard **此前不接受任何
2336
+ // __progmune_* 标记**(纯词形匹配)⇒ NestJS 把校验写在 DTO 属性装饰器上时,
2337
+ // 规则永远看不到。本轮是「接通道」:提取器产 __progmune_input_schema__ +
2338
+ // 规则侧在**抑制位**加一个接受分支。只做减法,不进 trigger。
2339
+ //
2340
+ // 反面风险(注入过头 = 假阴性):
2341
+ // DTO 只有 @IsOptional / @Type / @Transform(不校验)⇒ 不得注入;
2342
+ // 入参是基本类型 / 无入参 ⇒ 不得注入;
2343
+ // 标记不得顺带压掉别的规则(Data Integrity 曾因标记名含 validate 被误压,
2344
+ // 见 extract-ir.ts 里标记命名的说明 ⇒ 这是本组专门要守的)。
2345
+ // ═══════════════════════════════════════════════════════════════
2346
+ (0, vitest_1.describe)("E3:DTO 的 schema 校验必须对 Input Validation 可见", () => {
2347
+ const MARK = "__progmune_input_schema__";
2348
+ const DECLS = `
2349
+ declare function Body(): any;
2350
+ declare function IsString(): any;
2351
+ declare function IsUUID(a?: any, b?: any): any;
2352
+ declare function MinLength(n: number): any;
2353
+ declare function IsOptional(): any;
2354
+ declare function Type(fn: any): any;
2355
+ declare function Transform(fn: any): any;
2356
+ `;
2357
+ const cases = [
2358
+ {
2359
+ name: "正例·① 跨文件 DTO(属性装饰器 @IsString)",
2360
+ fn: "GroupController.createGroup",
2361
+ files: {
2362
+ "dto.ts": DECLS + `
2363
+ export class CreateGroupDto {
2364
+ @IsString()
2365
+ name: string;
2366
+ }
2367
+ `,
2368
+ "a.ts": `
2369
+ import { CreateGroupDto } from "./dto";
2370
+ export class GroupController {
2371
+ createGroup(@Body() dto: CreateGroupDto) { return this.svc.createGroup(dto); }
2372
+ }
2373
+ `,
2374
+ },
2375
+ want: [MARK],
2376
+ why: "NestJS 最常见形态:校验写在另一个文件的 DTO 类上,函数体内什么都没有",
2377
+ },
2378
+ {
2379
+ name: "正例·② 同文件 DTO",
2380
+ fn: "C.createGroup",
2381
+ files: {
2382
+ "a.ts": DECLS + `
2383
+ export class CreateGroupDto {
2384
+ @IsUUID()
2385
+ id: string;
2386
+ }
2387
+ export class C {
2388
+ createGroup(dto: CreateGroupDto) { return 1; }
2389
+ }
2390
+ `,
2391
+ },
2392
+ want: [MARK],
2393
+ why: "DTO 与消费者同文件(小模块常见)",
2394
+ },
2395
+ {
2396
+ name: "正例·③ @MinLength 也算校验(不只是 Is* 族)",
2397
+ fn: "C.createGroup",
2398
+ files: {
2399
+ "a.ts": DECLS + `
2400
+ export class D {
2401
+ @MinLength(2)
2402
+ name: string;
2403
+ }
2404
+ export class C {
2405
+ createGroup(dto: D) { return 1; }
2406
+ }
2407
+ `,
2408
+ },
2409
+ want: [MARK],
2410
+ why: "R19:分母不只是 Is* 族;MinLength/MaxLength/Matches 都是真校验",
2411
+ },
2412
+ {
2413
+ name: "正例·④ 顶层函数同样覆盖(与 E2 的『仅类/方法』边界不同)",
2414
+ fn: "createGroup",
2415
+ files: {
2416
+ "a.ts": DECLS + `
2417
+ export class D {
2418
+ @IsString()
2419
+ name: string;
2420
+ }
2421
+ export function createGroup(dto: D) { return 1; }
2422
+ `,
2423
+ },
2424
+ want: [MARK],
2425
+ why: "按入参判定,不按函数形态 ⇒ 顶层函数同样接得上(E2 那只覆盖装饰器,本轮不看装饰器)",
2426
+ },
2427
+ {
2428
+ name: "负对照·① DTO 只有 @IsOptional/@Type/@Transform —— 不是校验",
2429
+ fn: "C.createGroup",
2430
+ files: {
2431
+ "a.ts": DECLS + `
2432
+ export class D {
2433
+ @IsOptional()
2434
+ @Type(() => String)
2435
+ @Transform(({ value }) => value)
2436
+ name?: string;
2437
+ }
2438
+ export class C {
2439
+ createGroup(dto: D) { return 1; }
2440
+ }
2441
+ `,
2442
+ },
2443
+ want: [],
2444
+ dontWant: [MARK],
2445
+ why: "可缺省 + 类型转换 ≠ 校验;判成已校验就是假阴性",
2446
+ },
2447
+ {
2448
+ name: "负对照·② 入参是基本类型(不是 DTO 类)",
2449
+ fn: "C.createGroup",
2450
+ files: {
2451
+ "a.ts": `
2452
+ export class C { createGroup(name: string) { return 1; } }
2453
+ `,
2454
+ },
2455
+ want: [],
2456
+ dontWant: [MARK],
2457
+ why: "基线:没有 schema 就不许凭空给出校验证据",
2458
+ },
2459
+ {
2460
+ name: "负对照·③ 无入参",
2461
+ fn: "C.createGroup",
2462
+ files: {
2463
+ "a.ts": DECLS + `
2464
+ export class D { @IsString() name: string; }
2465
+ export class C { createGroup() { return 1; } }
2466
+ `,
2467
+ },
2468
+ want: [],
2469
+ dontWant: [MARK],
2470
+ why: "DTO 存在但没被当入参 ⇒ 该函数的输入没有被校验",
2471
+ },
2472
+ {
2473
+ name: "正对照·⑤ 真实调用不得被挤掉(E1 不回归)",
2474
+ fn: "C.createGroup",
2475
+ files: {
2476
+ "a.ts": DECLS + `
2477
+ export class D { @IsString() name: string; }
2478
+ export class C {
2479
+ createGroup(dto: D) { return this.repo.createGroup(dto); }
2480
+ }
2481
+ `,
2482
+ },
2483
+ want: [MARK, "createGroup"],
2484
+ why: "本轮是**追加**标记;E1 补进来的真实调用必须仍在",
2485
+ },
2486
+ ];
2487
+ for (const c of cases) {
2488
+ (0, vitest_1.it)(`${c.name} —— ${c.why}`, () => {
2489
+ const dir = makeProject(c.files);
2490
+ try {
2491
+ const calls = marksFor(dir, c.fn);
2492
+ for (const w of c.want)
2493
+ (0, vitest_1.expect)(calls).toContain(w);
2494
+ for (const d of c.dontWant ?? [])
2495
+ (0, vitest_1.expect)(calls).not.toContain(d);
2496
+ }
2497
+ finally {
2498
+ fs.rmSync(dir, { recursive: true, force: true });
2499
+ }
2500
+ }, 150000);
2501
+ }
2502
+ // ── 规则侧:标记必须真的压得住 Input Validation,且只压这一条 ──
2503
+ (0, vitest_1.it)("规则侧·① 注入标记后 Input Validation 必须消失", () => {
2504
+ const before = (0, protocol_detector_1.detectSafeguardViolations)(["createPost"], "createPost");
2505
+ (0, vitest_1.expect)(before.some(v => v.rule === "Input Validation")).toBe(true);
2506
+ const after = (0, protocol_detector_1.detectSafeguardViolations)(["createPost", MARK], "createPost");
2507
+ (0, vitest_1.expect)(after.some(v => v.rule === "Input Validation")).toBe(false);
2508
+ });
2509
+ (0, vitest_1.it)("规则侧·② 标记不得压掉别的规则(Authorization 仍在)", () => {
2510
+ const after = (0, protocol_detector_1.detectSafeguardViolations)(["createPost", MARK], "createPost");
2511
+ (0, vitest_1.expect)(after.some(v => v.category === "authorization")).toBe(true);
2512
+ });
2513
+ (0, vitest_1.it)("规则侧·③ 标记名不得撞上 Data Integrity 的 safeguard(命名事故回归)", () => {
2514
+ // 曾用的名字 __progmune_validated_input__ 会被
2515
+ // \b(get|find|check|exists|lookup|status|validate|verify)(?:[A-Z]\w*|_\w+)\b 命中,
2516
+ // 凭空压掉 Foreign Key 校验。改名后必须仍报。
2517
+ const withOld = (0, protocol_detector_1.detectSafeguardViolations)(["addChild", "__progmune_validated_input__"], "addChild", undefined, ["parentId"], false);
2518
+ const withNew = (0, protocol_detector_1.detectSafeguardViolations)(["addChild", MARK], "addChild", undefined, ["parentId"], false);
2519
+ (0, vitest_1.expect)(withOld.some(v => v.rule === "Data Integrity (Foreign Key)")).toBe(false);
2520
+ (0, vitest_1.expect)(withNew.some(v => v.rule === "Data Integrity (Foreign Key)")).toBe(true);
2521
+ });
2522
+ });
2523
+ (0, vitest_1.describe)("F:Input Validation 的 trigger 只认函数名(不再被 callee 拖下水)", () => {
2524
+ // 立项依据(2026-09-22):本规则的陈述对象是「这个函数自己是不是内容创建函数」
2525
+ // (violationMessage 原文:"Content creation function does not validate...")。
2526
+ // 此前拿 callee 名字去匹配 trigger,产生两类缺陷,各占一半:
2527
+ // (a) 重复投射 —— 分发器 handleRequest 因调用 createEvent/createRoute 被计一次,
2528
+ // 而那些 callee 自己也在报(实测 100/175,全部 SAFE_DUP)。
2529
+ // (b) 理由错 —— login/register/authenticate 因调用 **createHash**(密码哈希)
2530
+ // 被判成「内容创建函数未校验输入」(69/175)。
2531
+ // 175 条已逐条归类,无一为真阳性;另有 6 条 list/get 查询函数。
2532
+ const IV = (calls, own) => (0, protocol_detector_1.detectSafeguardViolations)(calls, own).filter(v => v.rule === "Input Validation");
2533
+ const hit = (calls, own) => IV(calls, own).length > 0;
2534
+ // ── 正例:函数名自己就是创建函数 ⇒ 必须仍然报(本轮不得削弱真阳性) ──
2535
+ (0, vitest_1.it)("正例·① createPost 自身仍报", () => {
2536
+ (0, vitest_1.expect)(hit(["db.save"], "createPost")).toBe(true);
2537
+ });
2538
+ (0, vitest_1.it)("正例·② 类方法 C.createPost 仍报(ownName 取末段)", () => {
2539
+ (0, vitest_1.expect)(hit(["db.save"], "C.createPost")).toBe(true);
2540
+ });
2541
+ (0, vitest_1.it)("正例·③ addComment 仍报", () => {
2542
+ (0, vitest_1.expect)(hit(["db.save"], "addComment")).toBe(true);
2543
+ });
2544
+ (0, vitest_1.it)("正例·④ uploadFile 仍报", () => {
2545
+ (0, vitest_1.expect)(hit(["fs.write"], "uploadFile")).toBe(true);
2546
+ });
2547
+ (0, vitest_1.it)("正例·⑤ 下划线形态 create_post 仍报", () => {
2548
+ (0, vitest_1.expect)(hit(["db.save"], "create_post")).toBe(true);
2549
+ });
2550
+ // ── 负例:靠 callee 触发 ⇒ 必须不再报 ──
2551
+ (0, vitest_1.it)("负例·① 分发器 handleRequest 不再因 createEvent 被计(重复投射)", () => {
2552
+ (0, vitest_1.expect)(hit(["createEvent", "createMetric"], "handleRequest")).toBe(false);
2553
+ });
2554
+ (0, vitest_1.it)("负例·② registerNewUser 不再因 createHash 被判内容创建(密码哈希)", () => {
2555
+ (0, vitest_1.expect)(hit(["createHash", "bcrypt.hash"], "registerNewUser")).toBe(false);
2556
+ });
2557
+ (0, vitest_1.it)("负例·③ doLogin 同上", () => {
2558
+ (0, vitest_1.expect)(hit(["createHash"], "doLogin")).toBe(false);
2559
+ });
2560
+ (0, vitest_1.it)("负例·④ authenticate 同上", () => {
2561
+ (0, vitest_1.expect)(hit(["createHash"], "authenticate")).toBe(false);
2562
+ });
2563
+ (0, vitest_1.it)("负例·⑤ 查询函数 listPosts 不再被报", () => {
2564
+ (0, vitest_1.expect)(hit(["sort", "filter"], "listPosts")).toBe(false);
2565
+ });
2566
+ (0, vitest_1.it)("负例·⑥ ORM/建表类 callee 不再拖累调用者(createQueryBuilder)", () => {
2567
+ (0, vitest_1.expect)(hit(["createQueryBuilder"], "findArticles")).toBe(false);
2568
+ });
2569
+ // ── 边界与回归保护 ──
2570
+ (0, vitest_1.it)("边界·① ownName 缺失时不得误触发(triggerCalls 为空数组)", () => {
2571
+ (0, vitest_1.expect)(hit(["createPost"], "")).toBe(false);
2572
+ });
2573
+ (0, vitest_1.it)("回归·① E3 的 schema 标记仍然压得住(本轮不得破坏 E3)", () => {
2574
+ (0, vitest_1.expect)(hit(["createPost", "__progmune_input_schema__"], "createPost")).toBe(false);
2575
+ (0, vitest_1.expect)(hit(["createPost"], "createPost")).toBe(true);
2576
+ });
2577
+ (0, vitest_1.it)("回归·② 非目标规则不受影响(Authorization 仍在)", () => {
2578
+ const v = (0, protocol_detector_1.detectSafeguardViolations)(["createEvent"], "handleRequest");
2579
+ (0, vitest_1.expect)(v.some(x => x.category === "authorization")).toBe(true);
2580
+ });
2581
+ // ── 已知边界(登记,不断言)──
2582
+ // 函数名不含 create/add/post/upload、但确实是「入口把用户输入交给内容创建」
2583
+ // 的函数(如 handleSubmit 调 commentRepo.create,而 create 不在扫描范围内),
2584
+ // 本轮之后不再报。这是 triggerOwnNameOnly 的固有代价:175 条损失里此类为 0,
2585
+ // real-world 池里亦未发现(27 条触发源全为 ORM/哈希/权限工厂/内部工具)。
2586
+ // 若要补回,需要 `exposed`(web handler)通道 —— 按 R27 先查该通道通不通。
2587
+ });
@@ -441,6 +441,74 @@ function authMachineryFromDecorators(decorators) {
441
441
  }
442
442
  return auth && !skip ? [AUTH_MACHINERY_MARKER] : [];
443
443
  }
444
+ // ═══════════════════════════════════════════════════════════════
445
+ // E3(2026-09-22):TS 侧「入参已按 schema 校验」语义标记
446
+ //
447
+ // 判据(先抄准再写 expect,R22):
448
+ // Input Validation 的 safeguard(protocol-detector.ts:481)此前是**纯词形匹配**,
449
+ // 不接受任何 __progmune_* 标记 ⇒ NestJS 把校验写在 DTO 类的属性装饰器上时,
450
+ // 规则无论如何都看不到 —— 这是 R27 说的「证据通道断着」。
451
+ // 本轮做的是「接通道」:提取器产标记 + 规则侧开一个接受位,**只抑制,不新增触发**。
452
+ //
453
+ // 为什么标记名叫 __progmune_input_schema__ 而不是 ..._validated_input__:
454
+ // 标记本身会被别的规则当普通调用名匹配。实测 __progmune_validated_input__ 会
455
+ // 撞上 Data Integrity (Foreign Key) 的 safeguard
456
+ // `\b(get|find|check|exists|lookup|status|validate|verify)(?:[A-Z]\w*|_\w+)\b`
457
+ // ("validate" + "d_input__" 命中),凭空压掉 10 条无关违规。
458
+ // 命名必须避开 get/find/check/verify/validate/create/add/post/upload 等动词词根。
459
+ //
460
+ // 边界(写死在这里):只在**入参类型**是本项目里带校验装饰器的类时才注入;
461
+ // 装饰器的「校验性」判定见下,@IsOptional / @Type / @Transform 单独出现不算。
462
+ // ═══════════════════════════════════════════════════════════════
463
+ const INPUT_SCHEMA_MARKER = "__progmune_input_schema__";
464
+ /** 前缀式:MinLength / MaxLength / Matches / ValidateNested … 都是真校验 */
465
+ const VALIDATOR_DECORATOR_PREFIX = /^(?:Min|Max|Length|Matches|ArrayMinSize|ArrayMaxSize|Validate|Equals|NotEquals|Allow|Contains|NotContains)/;
466
+ /** class-validator 的 Is* 族;@IsOptional 只是「可缺省」,不校验 ⇒ 排除 */
467
+ const VALIDATOR_DECORATOR_IS = /^Is(?!Optional$)[A-Z]/;
468
+ function decoratorIsValidator(text) {
469
+ const name = /^@([A-Za-z_$][\w$]*)/.exec((text ?? "").trim())?.[1];
470
+ if (!name)
471
+ return false;
472
+ return VALIDATOR_DECORATOR_PREFIX.test(name) || VALIDATOR_DECORATOR_IS.test(name);
473
+ }
474
+ /**
475
+ * 全项目扫描:类自身或其属性上带校验装饰器的类名集合。
476
+ * 只看类名(不做类型解析)—— 与污点管线口径一致,跨文件引用靠名字对上。
477
+ */
478
+ function validatedDtoClassNames(project) {
479
+ const out = new Set();
480
+ for (const sf of project.getSourceFiles()) {
481
+ if (sf.getFilePath().includes("node_modules"))
482
+ continue;
483
+ for (const cls of sf.getClasses()) {
484
+ const name = cls.getName();
485
+ if (!name)
486
+ continue;
487
+ let hit = false;
488
+ for (const d of cls.getDecorators()) {
489
+ if (decoratorIsValidator(d?.getText?.() ?? "")) {
490
+ hit = true;
491
+ break;
492
+ }
493
+ }
494
+ if (!hit) {
495
+ for (const prop of cls.getProperties()) {
496
+ for (const d of prop.getDecorators()) {
497
+ if (decoratorIsValidator(d?.getText?.() ?? "")) {
498
+ hit = true;
499
+ break;
500
+ }
501
+ }
502
+ if (hit)
503
+ break;
504
+ }
505
+ }
506
+ if (hit)
507
+ out.add(name);
508
+ }
509
+ }
510
+ return out;
511
+ }
444
512
  /**
445
513
  * 标识符是否「看起来像基目录」—— G-A2 的定案依据。
446
514
  *
@@ -2969,6 +3037,20 @@ function _extractSingleProject(absRoot, tsconfigPath) {
2969
3037
  }
2970
3038
  }
2971
3039
  }
3040
+ const validatedDtoClasses = validatedDtoClassNames(project);
3041
+ // ── E3(2026-09-22):入参已按 schema 校验 ⇒ 注入 __progmune_input_schema__
3042
+ // 放在主循环之后统一应用,覆盖全部函数形态(函数声明 / 箭头 / 类方法),
3043
+ // 与 pendingMethodMarks 的跨函数标记同一处收口。
3044
+ for (const f of funcs) {
3045
+ if (!f.params || f.params.length === 0)
3046
+ continue;
3047
+ const hit = f.params.some((p) => p.type && validatedDtoClasses.has(p.type));
3048
+ if (!hit)
3049
+ continue;
3050
+ f.calls = f.calls || [];
3051
+ if (!f.calls.includes(INPUT_SCHEMA_MARKER))
3052
+ f.calls.push(INPUT_SCHEMA_MARKER);
3053
+ }
2972
3054
  // ═══════════════════════════════════════════════════════════════
2973
3055
  // Phase 5: Dynamic external function resolution
2974
3056
  // Replaces hardcoded knownExternals
@@ -429,9 +429,20 @@ const SAFEGUARD_RULES = [
429
429
  {
430
430
  name: "Input Validation",
431
431
  category: "input_validation",
432
+ // F 轮(2026-09-22):本规则的陈述对象是「这个函数自己是不是内容创建函数」
433
+ // (violationMessage: "Content creation function ...")。此前拿 callee 名字
434
+ // 去匹配 trigger,造成两类缺陷:① 分发器 handleRequest 因调用 createEvent 等
435
+ // 被重复计一次,而那些 callee 自己也在报;② login/register 因调用
436
+ // **createHash**(密码哈希)被判成「内容创建函数未校验输入」。
437
+ // 175 条损失已逐条归类,无一为真阳性。⇒ trigger 只看函数名。
438
+ triggerOwnNameOnly: true,
432
439
  trigger: /\b(create|add|post|upload)(?:[A-Z]\w*|_\w+)|(?:[A-Z]\w*|_\w+)(Create|Add|Post|Upload)\b/i,
433
440
  safeguards: [
434
- { pattern: /\b(validate|sanitize|check|verify)(?:[A-Z]\w*|_\w*)(Content|Input|Length|Title|Body|Type|Size|File|Data|Param|Arg|Field|Value)\b|\b(validateContent|sanitizeInput|checkLength|verifyType|checkSize)\b/i, label: "input_validation" },
441
+ // E3(2026-09-22):__progmune_input_schema__ —— 提取器在「入参类型是带校验
442
+ // 装饰器的 DTO 类」时产出(NestJS 把校验写在 DTO 属性装饰器上,词形匹配永远
443
+ // 看不到)。本规则此前不接受任何 __progmune_* 标记 ⇒ 该证据通道是断的。
444
+ // 注意只加在**抑制位**(safeguard),不进 trigger:新通道只能做减法。
445
+ { pattern: /\b(validate|sanitize|check|verify)(?:[A-Z]\w*|_\w*)(Content|Input|Length|Title|Body|Type|Size|File|Data|Param|Arg|Field|Value)\b|\b(validateContent|sanitizeInput|checkLength|verifyType|checkSize)\b|__progmune_input_schema__/i, label: "input_validation" },
435
446
  ],
436
447
  violationMessage: "Content creation function does not validate or sanitize input. XSS, injection, and oversized content possible.",
437
448
  conceptMissing: ["InputSanitization", "ContentValidation", "SizeLimit"],
@@ -1129,7 +1140,11 @@ function detectSafeguardViolations(calls, enclosingFuncName, language, params, e
1129
1140
  : SAFEGUARD_RULES;
1130
1141
  for (const rule of activeRules) {
1131
1142
  // Check if trigger matches
1132
- const triggerCalls = rule.triggerCallsOnly ? rawCalls : effectiveCalls;
1143
+ const triggerCalls = rule.triggerOwnNameOnly
1144
+ ? (ownName ? [ownName] : [])
1145
+ : rule.triggerCallsOnly
1146
+ ? rawCalls
1147
+ : effectiveCalls;
1133
1148
  const triggerMatch = triggerCalls.some(c => rule.trigger.test(c));
1134
1149
  if (!triggerMatch)
1135
1150
  continue;
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.49";
23
+ exports.RUNTIME_VERSION = "3.7.51";
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.49",
3
+ "version": "3.7.51",
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/",