progmune-runtime 3.7.54 → 3.7.56

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,65 @@
1
1
  # Changelog
2
2
 
3
+ ## [3.7.56] — 2026-10-02
4
+
5
+ ### 排序告警流进产品(§49.15 方案 b 落地,未发布)
6
+
7
+ **告警排序器产品化**:`alert-ranker.ts` 自 blind-benchmark 迁入 `src/trust/`(评估脚本
8
+ 改 import,对表测试差 0.000 确认同一实现)。`evaluateTrust` 在 IR 提取的同一批
9
+ `functions` 上跑 `detectSafeguardViolations`(batch-scan 同款口径:language 必传、
10
+ params 名/类型、exported 近似 exposed),产出**排序后的 safeguard 告警流**:
11
+
12
+ - `TrustDecision.overall.safeguardAlerts`:`total` + `groups`(推荐形态:按族分组,
13
+ 组间按先验、组内按语义分,防整族沉底)+ `topRanked`(全局平铺,minPerRule=1
14
+ 保底)。族先验来自 fp-gold 标注(DEFAULT_RULE_PRIOR,贝叶斯平滑 α=8),
15
+ `learnPrior` 可重估;排序不删告警(§49.16),输出条数 == 输入条数。
16
+ - **证据流与判定分离**:告警不进 violations、不扣分、不改 decision/score。
17
+ - 终端报告新增 "Ranked Safeguard Alerts" 区块(每族前 3 条 + 先验 + 语义理由)。
18
+
19
+ **实测**:docmost decision APPROVED(87) 不变(分离成立);告警流 1320 条 / 21 族,
20
+ 头部 = Input Validation / Data Integrity(FK) 族,语义理由可解释。
21
+ ⚠ 口径注记:1320 与 §49.17 的 1202 差 +9.8%——recall-probe 用 extractIRWithTypes、
22
+ 产品路径用 extractIR(两个提取器函数集略异);排序器的族先验基于 withTypes 口径
23
+ 的 fp-gold,迁移到产品口径的偏差未量化,如实记录待补核。
24
+
25
+ **测试**:engine 23/23(新增 safeguardAlerts 排序不变量 + 判定分离 2 条)、
26
+ trust 全家 67/67、alert-ranker-check 对表 0.000。
27
+
28
+ ## [3.7.55] — 2026-09-29
29
+
30
+ ### 主路径(trust 判定)真实项目首测 + NestJS 失明误报修复(§50)
31
+
32
+ **首次对产品判定路径做真实项目全量测量**(`blind-benchmark/REALWORLD_MAIN_PATH_V1.md`):
33
+ docmost(NestJS 5k★)修复前被主路径判 **BLOCKED(score 48,auth 0 分)**——132 条违规里
34
+ 126 条逐条核实为失明/语义误报,且同一批违规在 policyCompliance 与 protocolSafety
35
+ **双维度重复扣分**。主路径与旁路(safeguard,§49 加权精确率 5.8%)同病:
36
+ 真实公告位置命中 0/18(fr-corpus 17 条交叉证实协议状态机 0 检出),
37
+ 但主路径把噪声直接变成产品级判定——**虚假 BLOCKED 比虚假告警伤害大**。
38
+
39
+ **四刀修复(全部 src 接线层,判定核心不动)**:
40
+
41
+ 1. **E3 DTO 通道接入 NestJS 适配器**:`app.useGlobalPipes(...)` 全局管道识别 +
42
+ 路由入参类型 ∈ 已校验 DTO 集合(复用 `validatedDtoClassNames`,加继承闭包与
43
+ `createZodDto` Zod 形态)⇒ 不报 NO_VALIDATION。`extract-ir.ts` 导出 E3 集合函数。
44
+ 2. **无结构化输入豁免**:入参无 @Body/@Query/@Param(仅 AuthUser/Req/Res/
45
+ UploadedFile)的路由不报 NO_VALIDATION——没有可校验的输入。
46
+ 3. **auth 端点豁免扩展**:+ password-reset / reset-password / verify-token /
47
+ verify / setup / init(docmost 实测 3 条 NO_AUTH 误报)。
48
+ 4. **双维度去重**:policyCompliance 只吃策略类违规(policy_ref 以
49
+ framework/protocol-safety 开头的已在 protocolSafety 计分)。
50
+
51
+ **效果**:docmost **BLOCKED(48) → APPROVED(87)**,违规 132 → 6(NESTJS 误报
52
+ 126 → 0;残留 6 条含 1 条撞 CVE-2026-33193 攻击面的真实语义告警);
53
+ hedgedoc(Zod 校验体系 + public/private 双 API 面)47 → 32,剩余 19 条逐条定性
54
+ 为规则粒度边界(@Param 裸类型 / @Body('field') 单字段,真无校验,如实保留)。
55
+
56
+ **踩坑**:`param.getType().getText()` 返回 `import("...").DtoName` 形态——取
57
+ 首个标识符会取到 `import` 关键字,改取全部标识符任一命中;测试断言 route
58
+ 格式无前导斜杠(`POST pages/create`),3 个测试曾因此"假通过"。
59
+
60
+ **测试**:nestjs-detector 17/17(新增全局管道+DTO / Zod / 继承 / 无输入 7 条
61
+ 正反向);trust 全家 95/95。
62
+
3
63
  ## [3.7.54] — 2026-09-27
4
64
 
5
65
  ### §47 签名级会话证据 + §48 输入校验族摸底(含两条判据被数据否决)
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.validatedDtoClassNames = validatedDtoClassNames;
36
37
  exports.extractIR = extractIR;
37
38
  exports.extractIRWithTypes = extractIRWithTypes;
38
39
  const ts_morph_1 = require("ts-morph");
@@ -493,6 +494,8 @@ function decoratorIsValidator(text) {
493
494
  /**
494
495
  * 全项目扫描:类自身或其属性上带校验装饰器的类名集合。
495
496
  * 只看类名(不做类型解析)—— 与污点管线口径一致,跨文件引用靠名字对上。
497
+ * 导出供框架适配器复用(2026-09-29:NestJS 检测器的 NO_VALIDATION
498
+ * 失明误报——路由 DTO 在别的文件带 class-validator 装饰器,检测器看不到)。
496
499
  */
497
500
  function validatedDtoClassNames(project) {
498
501
  const out = new Set();
@@ -21,6 +21,7 @@ exports.analyzeNestJSFile = analyzeNestJSFile;
21
21
  exports.formatNestJSReport = formatNestJSReport;
22
22
  const ts_morph_1 = require("ts-morph");
23
23
  const route_window_1 = require("./route-window");
24
+ const extract_ir_1 = require("../extract-ir");
24
25
  // ── Core Analysis ──
25
26
  function analyzeNestJSProject(projectRoot) {
26
27
  let project;
@@ -65,6 +66,66 @@ function analyzeNestJSProject(projectRoot) {
65
66
  }
66
67
  }
67
68
  const hasGlobalAuthGuard = analysis.globalAuthGuards.length > 0;
69
+ // ── 全局校验管道(2026-09-29:docmost 123 条 NO_VALIDATION 失明误报的根因)──
70
+ // NestJS 标准做法是 bootstrap 里 app.useGlobalPipes(new ValidationPipe(...)),
71
+ // 路由本身不带 @UsePipes 装饰器 ⇒ 此前全部报 NO_VALIDATION。
72
+ // 检测方式:任意非 node_modules 源文件出现 .useGlobalPipes( 调用(参数不限——
73
+ // hedgedoc 形态是 useGlobalPipes(setupValidationPipe(logger)),管道经辅助函数
74
+ // 创建,字面 ValidationPipe 不在调用处)。
75
+ // 保守判定:全局管道只豁免「入参是已校验 DTO」的路由(见下),裸类型入参仍报。
76
+ let hasGlobalValidationPipe = false;
77
+ for (const file of project.getSourceFiles()) {
78
+ if (file.getFilePath().includes("node_modules"))
79
+ continue;
80
+ if (/\.(test|spec)\.ts$/.test(file.getFilePath()))
81
+ continue;
82
+ if (/\.useGlobalPipes\s*\(/.test(file.getText())) {
83
+ hasGlobalValidationPipe = true;
84
+ break;
85
+ }
86
+ }
87
+ // ── 已校验 DTO 类名集合(E3 通道同款实现,import 落地代码而非重写)──
88
+ const validatedDtos = hasGlobalValidationPipe
89
+ ? (0, extract_ir_1.validatedDtoClassNames)(project)
90
+ : new Set();
91
+ // ── 继承闭包(2026-09-29 docmost 实测:RemoveFavoriteDto extends AddFavoriteDto)──
92
+ // E3 集合只收「类自身带校验装饰器」的类,继承来的装饰器看不到。
93
+ // 只扩 detector 侧集合,不动 E3 函数(后者影响 safeguard 盲测基线)。
94
+ for (const file of project.getSourceFiles()) {
95
+ if (file.getFilePath().includes("node_modules"))
96
+ continue;
97
+ for (const cls of file.getClasses()) {
98
+ let base = cls.getBaseClass();
99
+ while (base) {
100
+ const baseName = base.getName();
101
+ if (baseName && validatedDtos.has(baseName)) {
102
+ const clsName = cls.getName();
103
+ if (clsName)
104
+ validatedDtos.add(clsName);
105
+ break;
106
+ }
107
+ base = base.getBaseClass();
108
+ }
109
+ }
110
+ }
111
+ // ── Zod 形态扩展(2026-09-29 hedgedoc 实测)──
112
+ // class LoginDto extends createZodDto(LoginSchema) {} —— nestjs-zod 的校验
113
+ // 体系不产 class-validator 装饰器,E3 通道看不见。extends 表达式文本
114
+ // 含 createZodDto( 即视为已校验 DTO。同样只扩 detector 侧集合。
115
+ for (const file of project.getSourceFiles()) {
116
+ if (file.getFilePath().includes("node_modules"))
117
+ continue;
118
+ for (const cls of file.getClasses()) {
119
+ const ext = cls.getExtends();
120
+ if (!ext)
121
+ continue;
122
+ if (/createZodDto\s*\(/.test(ext.getText())) {
123
+ const clsName = cls.getName();
124
+ if (clsName)
125
+ validatedDtos.add(clsName);
126
+ }
127
+ }
128
+ }
68
129
  // ── 第二遍:模块级中间件保护(Nest 5 时代惯用法)──
69
130
  // class XxxModule implements NestModule { configure(consumer) {
70
131
  // consumer.apply(AuthMiddleware).forRoutes({path, method}, ...) } }
@@ -154,6 +215,25 @@ function analyzeNestJSProject(projectRoot) {
154
215
  || method.getDecorator("AllowAnon") !== undefined;
155
216
  // 认证守卫 = 认证名分类后的守卫(ThrottlerGuard 等限流守卫不算认证)
156
217
  const authGuards = guards.filter(isAuthGuardName);
218
+ // 入参是否指向已校验 DTO:@Body()/@Query()/@Param() 参数的
219
+ // 类型名 ∈ class-validator 装饰类集合(E3 通道语义)。
220
+ // ts-morph 的类型文本会带 import 前缀:import(".../page.dto").PageInfoDto
221
+ // 或泛型 Selectable<import("...").Users> —— 取全部标识符,
222
+ // 任一命中已校验 DTO 集合即豁免(联合类型/裸类型/数组皆可对上;
223
+ // db 类型 Users 等不在集合,不会误豁免)。
224
+ const hasValidatedDto = method.getParameters().some((param) => {
225
+ const decNames = param.getDecorators().map((d) => d.getName());
226
+ if (!decNames.some((n) => ["Body", "Query", "Param"].includes(n)))
227
+ return false;
228
+ const typeText = param.getType().getText();
229
+ const ids = typeText.match(/[A-Za-z_$][\w$]*/g) || [];
230
+ return ids.some((id) => validatedDtos.has(id));
231
+ });
232
+ // 无结构化输入:入参没有 @Body/@Query/@Param——只有 AuthUser/Req/Res/
233
+ // UploadedFile 等上下文注入或文件对象(2026-09-29 docmost 实测:
234
+ // auth/collab-token、auth/logout 等无 body 路由被误报 NO_VALIDATION)。
235
+ // 没有可校验的 DTO 输入,规则判定面不覆盖,不报。
236
+ const hasStructuredInput = method.getParameters().some((param) => param.getDecorators().some((d) => ["Body", "Query", "Param"].includes(d.getName())));
157
237
  const route = {
158
238
  method: httpMethod,
159
239
  path: fullPath,
@@ -161,6 +241,7 @@ function analyzeNestJSProject(projectRoot) {
161
241
  handler: method.getName() || "unknown",
162
242
  hasAuthGuard: authGuards.length > 0,
163
243
  hasValidationPipe: pipes.length > 0,
244
+ hasValidatedDto,
164
245
  guards,
165
246
  pipes,
166
247
  isPublicDecorated,
@@ -188,7 +269,12 @@ function analyzeNestJSProject(projectRoot) {
188
269
  fix: `Add @UseGuards(AuthGuard) to the method or controller class, or remove the @Public marker.`,
189
270
  });
190
271
  }
191
- if (!route.hasValidationPipe) {
272
+ // 豁免:@UsePipes 装饰器,或「全局 ValidationPipe + 入参是已校验 DTO」
273
+ // (docmost 实测 123 条失明误报的修复——路由 DTO 在别的文件带
274
+ // class-validator 装饰器,只看 @UsePipes 永远看不到),
275
+ // 或路由无结构化输入(没有可校验的东西)
276
+ const validatedByGlobalPipe = hasGlobalValidationPipe && route.hasValidatedDto;
277
+ if (!route.hasValidationPipe && !validatedByGlobalPipe && hasStructuredInput) {
192
278
  analysis.issues.push({
193
279
  type: "NESTJS_NO_VALIDATION",
194
280
  severity: "medium",
@@ -367,6 +453,12 @@ function isPublicRoute(path) {
367
453
  /\/auth\/register$/i, /\/register$/i, /\/signup$/i,
368
454
  /\/auth\/signup$/i, /\/auth\/signin$/i, /\/signin$/i,
369
455
  /\/auth\/refresh$/i, /\/auth\/forgot/i, /\/auth\/reset/i,
456
+ // auth 流程自身端点(2026-09-29 docmost 实测:auth/setup、
457
+ // auth/password-reset、auth/verify-token 被误报 NO_AUTH——
458
+ // 用户尚未登录,这些端点天生无认证;原 /auth/reset 匹配不了 password-reset)
459
+ /\/auth\/password-reset$/i, /\/auth\/reset-password$/i,
460
+ /\/auth\/verify-token$/i, /\/auth\/verify$/i,
461
+ /\/auth\/setup$/i, /\/auth\/init$/i,
370
462
  /\/health$/i, /\/healthcheck$/i, /\/ping$/i, /\/status$/i,
371
463
  /\/public\//i, /\/static\//i,
372
464
  ];
@@ -462,6 +554,7 @@ function analyzeNestJSFile(filePath) {
462
554
  handler: method.getName() || "unknown",
463
555
  hasAuthGuard: guards.filter(isAuthGuardName).length > 0,
464
556
  hasValidationPipe: pipes.length > 0,
557
+ hasValidatedDto: false, // 单文件分析无跨文件 DTO 上下文(项目级用 analyzeNestJSProject)
465
558
  guards,
466
559
  pipes,
467
560
  isPublicDecorated: false, // 单文件分析不解析 @Public(项目级用 analyzeNestJSProject)
@@ -241,3 +241,180 @@ export class AdminController {
241
241
  (0, vitest_1.expect)(a.issues.some((i) => i.type === "NESTJS_NO_AUTH" && i.route === "POST /users")).toBe(true);
242
242
  });
243
243
  });
244
+ // ═══════════════════════════════════════════════════════════
245
+ // 全局 ValidationPipe + DTO 校验识别(2026-09-29 docmost 实测:
246
+ // 123 条 NO_VALIDATION 失明误报的修复——路由 DTO 在别的文件带
247
+ // class-validator 装饰器,只看 @UsePipes 永远看不到)
248
+ // ═══════════════════════════════════════════════════════════
249
+ (0, vitest_1.describe)("nestjs-detector 全局管道 + DTO 校验通道", () => {
250
+ function writeValidatedApp() {
251
+ write("tsconfig.json", TSCONFIG);
252
+ write("src/main.ts", `
253
+ import { ValidationPipe } from "@nestjs/common";
254
+ import { NestFactory } from "@nestjs/core";
255
+ import { AppModule } from "./app.module";
256
+ async function bootstrap() {
257
+ const app = await NestFactory.create(AppModule);
258
+ app.useGlobalPipes(
259
+ new ValidationPipe({ whitelist: true, transform: true }),
260
+ );
261
+ await app.listen(3000);
262
+ }
263
+ `);
264
+ write("src/app.module.ts", `
265
+ import { Module } from "@nestjs/common";
266
+ import { PageController } from "./page.controller";
267
+ @Module({ controllers: [PageController] })
268
+ export class AppModule {}
269
+ `);
270
+ write("src/create-page.dto.ts", `
271
+ import { IsString, IsUUID, IsOptional } from "class-validator";
272
+ export class CreatePageDto {
273
+ @IsOptional() @IsString() title?: string;
274
+ @IsUUID() spaceId: string;
275
+ }
276
+ `);
277
+ write("src/page.controller.ts", `
278
+ import { Controller, Post, Body } from "@nestjs/common";
279
+ import { CreatePageDto } from "./create-page.dto";
280
+ @Controller("pages")
281
+ export class PageController {
282
+ @Post("create")
283
+ create(@Body() dto: CreatePageDto) { return {}; }
284
+ }
285
+ `);
286
+ }
287
+ (0, vitest_1.it)("全局 ValidationPipe + 入参是已校验 DTO → 不报 NO_VALIDATION", () => {
288
+ writeValidatedApp();
289
+ const a = (0, nestjs_detector_1.analyzeNestJSProject)(dir);
290
+ const nv = a.issues.filter((i) => i.type === "NESTJS_NO_VALIDATION");
291
+ (0, vitest_1.expect)(nv).toEqual([]);
292
+ });
293
+ (0, vitest_1.it)("反向:DTO 摘掉 class-validator 装饰器 → 精确转红", () => {
294
+ writeValidatedApp();
295
+ write("src/create-page.dto.ts", `
296
+ export class CreatePageDto {
297
+ title?: string;
298
+ spaceId: string;
299
+ }
300
+ `);
301
+ const a = (0, nestjs_detector_1.analyzeNestJSProject)(dir);
302
+ (0, vitest_1.expect)(a.issues.some((i) => i.type === "NESTJS_NO_VALIDATION" && i.route === "POST pages/create")).toBe(true);
303
+ });
304
+ (0, vitest_1.it)("反向:摘掉全局管道 → 精确转红(无 @UsePipes 的路由失去豁免源)", () => {
305
+ writeValidatedApp();
306
+ write("src/main.ts", `
307
+ async function bootstrap() { await (await import("@nestjs/core").then(m => m.NestFactory.create(Object))).listen(3000); }
308
+ `);
309
+ const a = (0, nestjs_detector_1.analyzeNestJSProject)(dir);
310
+ (0, vitest_1.expect)(a.issues.some((i) => i.type === "NESTJS_NO_VALIDATION" && i.route === "POST pages/create")).toBe(true);
311
+ });
312
+ (0, vitest_1.it)("继承 DTO:子类 extends 已校验父类 → 不报(RemoveFavoriteDto 形态)", () => {
313
+ writeValidatedApp();
314
+ write("src/create-page.dto.ts", `
315
+ import { IsString, IsUUID, IsOptional } from "class-validator";
316
+ export class CreatePageDto {
317
+ @IsOptional() @IsString() title?: string;
318
+ @IsUUID() spaceId: string;
319
+ }
320
+ export class UpdatePageDto extends CreatePageDto {}
321
+ `);
322
+ write("src/page.controller.ts", `
323
+ import { Controller, Post, Body } from "@nestjs/common";
324
+ import { UpdatePageDto } from "./create-page.dto";
325
+ @Controller("pages")
326
+ export class PageController {
327
+ @Post("update")
328
+ update(@Body() dto: UpdatePageDto) { return {}; }
329
+ }
330
+ `);
331
+ const a = (0, nestjs_detector_1.analyzeNestJSProject)(dir);
332
+ (0, vitest_1.expect)(a.issues.some((i) => i.type === "NESTJS_NO_VALIDATION" && i.route === "POST pages/update")).toBe(false);
333
+ });
334
+ (0, vitest_1.it)("无结构化输入(仅 AuthUser/Req)→ 不报 NO_VALIDATION(collab-token/logout 形态)", () => {
335
+ write("tsconfig.json", TSCONFIG);
336
+ write("src/main.ts", `
337
+ import { ValidationPipe } from "@nestjs/common";
338
+ async function bootstrap() { (globalThis as any).app.useGlobalPipes(new ValidationPipe()); }
339
+ `);
340
+ write("src/app.module.ts", `
341
+ import { Module } from "@nestjs/common";
342
+ import { AuthController } from "./auth.controller";
343
+ @Module({ controllers: [AuthController] })
344
+ export class AppModule {}
345
+ `);
346
+ write("src/auth.controller.ts", `
347
+ import { Controller, Post, Req } from "@nestjs/common";
348
+ @Controller("auth")
349
+ export class AuthController {
350
+ @Post("logout")
351
+ logout(@Req() req: any) { return {}; }
352
+ }
353
+ `);
354
+ const a = (0, nestjs_detector_1.analyzeNestJSProject)(dir);
355
+ (0, vitest_1.expect)(a.issues.some((i) => i.type === "NESTJS_NO_VALIDATION" && i.route === "POST auth/logout")).toBe(false);
356
+ });
357
+ (0, vitest_1.it)("Zod 形态:class XDto extends createZodDto(schema) + 辅助函数全局管道 → 不报(hedgedoc 形态)", () => {
358
+ write("tsconfig.json", TSCONFIG);
359
+ write("src/main.ts", `
360
+ import { setupValidationPipe } from "./setup-pipes";
361
+ async function bootstrap() { (globalThis as any).app.useGlobalPipes(setupValidationPipe()); }
362
+ `);
363
+ write("src/setup-pipes.ts", `
364
+ import { createZodValidationPipe } from "nestjs-zod";
365
+ export function setupValidationPipe(): any { return new (createZodValidationPipe({}))(); }
366
+ `);
367
+ write("src/app.module.ts", `
368
+ import { Module } from "@nestjs/common";
369
+ import { NotesController } from "./notes.controller";
370
+ @Module({ controllers: [NotesController] })
371
+ export class AppModule {}
372
+ `);
373
+ write("src/login.dto.ts", `
374
+ import { createZodDto } from "nestjs-zod";
375
+ export class LoginDto extends createZodDto({}) {}
376
+ `);
377
+ write("src/notes.controller.ts", `
378
+ import { Controller, Post, Body } from "@nestjs/common";
379
+ import { LoginDto } from "./login.dto";
380
+ @Controller("notes")
381
+ export class NotesController {
382
+ @Post("create")
383
+ create(@Body() dto: LoginDto) { return {}; }
384
+ }
385
+ `);
386
+ const a = (0, nestjs_detector_1.analyzeNestJSProject)(dir);
387
+ (0, vitest_1.expect)(a.issues.some((i) => i.type === "NESTJS_NO_VALIDATION" && i.route === "POST notes/create")).toBe(false);
388
+ });
389
+ (0, vitest_1.it)("反向:Zod DTO 摘掉 createZodDto 继承 → 精确转红", () => {
390
+ write("tsconfig.json", TSCONFIG);
391
+ write("src/main.ts", `
392
+ import { setupValidationPipe } from "./setup-pipes";
393
+ async function bootstrap() { (globalThis as any).app.useGlobalPipes(setupValidationPipe()); }
394
+ `);
395
+ write("src/setup-pipes.ts", `
396
+ import { createZodValidationPipe } from "nestjs-zod";
397
+ export function setupValidationPipe(): any { return new (createZodValidationPipe({}))(); }
398
+ `);
399
+ write("src/app.module.ts", `
400
+ import { Module } from "@nestjs/common";
401
+ import { NotesController } from "./notes.controller";
402
+ @Module({ controllers: [NotesController] })
403
+ export class AppModule {}
404
+ `);
405
+ write("src/login.dto.ts", `
406
+ export class LoginDto {}
407
+ `);
408
+ write("src/notes.controller.ts", `
409
+ import { Controller, Post, Body } from "@nestjs/common";
410
+ import { LoginDto } from "./login.dto";
411
+ @Controller("notes")
412
+ export class NotesController {
413
+ @Post("create")
414
+ create(@Body() dto: LoginDto) { return {}; }
415
+ }
416
+ `);
417
+ const a = (0, nestjs_detector_1.analyzeNestJSProject)(dir);
418
+ (0, vitest_1.expect)(a.issues.some((i) => i.type === "NESTJS_NO_VALIDATION" && i.route === "POST notes/create")).toBe(true);
419
+ });
420
+ });
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.52";
23
+ exports.RUNTIME_VERSION = "3.7.56";
24
24
  function verify(filePath) {
25
25
  const cert = (0, certify_1.certify)(filePath);
26
26
  const kb = (0, protocol_knowledge_1.buildKnowledgeBase)();
@@ -0,0 +1,218 @@
1
+ "use strict";
2
+ /**
3
+ * §49.13 告警排序器 —— 把 §49.12 的评估结论变成可执行代码(2026-09-28)
4
+ *
5
+ * 一、为什么是排序而不是压制
6
+ * §49.9 把压制型判据的三个方向全否决了(缺席型伤真漏洞 3/18;组合型在真漏洞上 0/18)。
7
+ * 剩下的只有**排序**:告警一条不少,但让真漏洞浮到前面。这不删除信息,
8
+ * 所以对判据的要求比压制低——压制错了不可逆,排错了只是顺序次优。
9
+ *
10
+ * 二、两个信号为什么是**正交**的
11
+ * ① 族先验 rulePrior:这条规则**历史上**有多大比例是真漏洞(族级、来自标注)
12
+ * ② 语义自身信号 semanticScore:这个**函数自身**做了什么(函数级、来自 IR)
13
+ * ① 是「这一类告警值不值得看」,② 是「这一条告警像不像真的」。
14
+ * 实测留一仓:单独 ① AUC 0.891、单独 ② AUC 0.744、合起来 **0.895**,
15
+ * 且合起来前 10% 密度从 30.6% 提到 45.8% ⇒ ② 在 ① 分好的层里还能再分,确证互补。
16
+ *
17
+ * 三、三条必须知道的口径(不知道就会用错)
18
+ * ① **先验来自真值标注 ⇒ 有自证风险**。某族先验一旦是 0,它就被压到最后,
19
+ * 没人看 ⇒ 永远发现不了它的真漏洞 ⇒ 先验永远是 0。缓解靠**调用方式**:
20
+ * 默认用 `groupAlerts()` 分组(每个族都有一块,不会被整体沉底),
21
+ * 或 `rankAlerts({ minPerRule })` 给每族保底名额。**不要**用无保底的纯全局排序。
22
+ * 曾试图用「规则宽泛度(命中密度)」替代先验以绕开这个风险,实测与真漏洞率
23
+ * 相关系数 r = -0.026(无关)⇒ 替代方案不成立,已否决。
24
+ * ② **信号必须语义定义,不能数据挑选**。§49.12 先让程序从数据里挑「真漏洞率高的
25
+ * call 名」,嵌套留一仓(选信号也只在训练仓内做)后前 10% 从 76.9% 掉到 25.3%
26
+ * ——挑出来的 `notifyothers` / `dbactor` / `createbytecountingstream` 是**项目专有
27
+ * 函数名**,过拟合。这里全部改成语义集合(数据操作 / 防护证据 / 鉴权 / token)。
28
+ * ③ **这是排序,不是压制**:输出条数 == 输入条数,一条都不会少。
29
+ *
30
+ * 四、落地契约(要接进产品时照这个做)
31
+ * 输入:告警 + 该函数的 IR(calls / params / 同函数命中了几条规则)。产品路径
32
+ * FunctionInfo 里这几项都有 ⇒ 不需要扩 IR,不需要改任何判定逻辑。
33
+ * 先验:内置 DEFAULT_RULE_PRIOR(本轮从 fp-gold 学出)。**线上应该用自己项目
34
+ * 的反馈数据重估**(learnPrior),并保留关闭先验的能力(usePrior:false,
35
+ * 此时只剩语义信号,实测仍有前 10% 3.06× 的提升,不会归零)。
36
+ *
37
+ * 用法:
38
+ * import { rankAlerts, groupAlerts, learnPrior } from "./alert-ranker";
39
+ * const ranked = rankAlerts(alerts, { minPerRule: 1 });
40
+ *
41
+ * 2026-10-02:自 blind-benchmark/alert-ranker.ts 迁入产品代码(§49.15 方案 b
42
+ * 落地——产品新增 safeguard 告警流)。评估脚本(alert-ranker-check /
43
+ * rank-robustness / rank-alerts-cli)改为 import 本文件,对表测试保证同一实现。
44
+ */
45
+ Object.defineProperty(exports, "__esModule", { value: true });
46
+ exports.DEFAULT_BASE = exports.DEFAULT_RULE_PRIOR = exports.W_PRIOR = void 0;
47
+ exports.semanticScore = semanticScore;
48
+ exports.scoreOne = scoreOne;
49
+ exports.rankAlerts = rankAlerts;
50
+ exports.groupAlerts = groupAlerts;
51
+ exports.learnPrior = learnPrior;
52
+ /** 先验权重:先验取值 0~0.41,×3 后与语义分量级相当(§49.12 实测最优组合用的就是这个比例) */
53
+ exports.W_PRIOR = 3.0;
54
+ /**
55
+ * 内置族先验。来源:fp-gold.jsonl,2026-09-28,n=244(真漏洞 24 / 误报 220),
56
+ * 贝叶斯平滑 alpha=8(伪计数按全局基线 0.0984 注入)。
57
+ *
58
+ * ⚠ 这张表反映的是**本项目这批标注**的分布,换项目必须重估(learnPrior)。
59
+ * 尤其注意 "Data Mutation Without Audit Trail" = 0.0057:它在本批标注里 140 条
60
+ * 全是误报,但那不代表它在**任何**项目上都是噪音(比如有审计合规要求的场景)。
61
+ */
62
+ exports.DEFAULT_RULE_PRIOR = {
63
+ "Data Integrity (Foreign Key)": 0.4098,
64
+ "Input Validation": 0.3456,
65
+ "No Input Sanitization": 0.2367,
66
+ "File Upload Without Validation": 0.2144,
67
+ "Session Fixation (Logout without Invalidation)": 0.0874,
68
+ "No Token Rotation After Privilege Change": 0.0874,
69
+ "Token Security (Weak Generation)": 0.0874,
70
+ "Authorization (Resource Ownership)": 0.0787,
71
+ "Authorization (Unauthenticated Access)": 0.0787,
72
+ "TLS Enforcement": 0.0656,
73
+ "Rate Limiting": 0.0656,
74
+ "API Without Rate Limiting": 0.0656,
75
+ "Authorization (Ownership Check)": 0.0656,
76
+ "Session No Timeout": 0.0605,
77
+ "Notification Without Retry": 0.0605,
78
+ "Authorization (Unauthenticated Mutation)": 0.0562,
79
+ "Password Hashing": 0.0525,
80
+ "Password Hashing (Weak)": 0.0525,
81
+ "Registration Without Email Verification": 0.0525,
82
+ "Data Mutation Without Audit Trail": 0.0057,
83
+ };
84
+ /** 未知族的兜底先验 = 全局基线 */
85
+ exports.DEFAULT_BASE = 0.0984;
86
+ // ── 语义信号集合(全部**语义定义**,不是从数据里挑出来的,见文件头 ③)──
87
+ /** 直接的数据操作:真漏洞多半要真的动数据 */
88
+ const DATA_OP = new Set([
89
+ "insert", "select", "where", "update", "delete", "save", "remove", "upsert",
90
+ "patch", "create", "destroy", "findone", "findmany", "find", "executetx",
91
+ "add", "write", "execute",
92
+ ]);
93
+ /** 提取器注入的「有校验证据」标记 —— 有防护 ⇒ 更像误报 */
94
+ const GUARD_MARKS = new Set(["__progmune_input_guard__", "__progmune_input_schema__"]);
95
+ /** 鉴权判定调用 */
96
+ const AUTHZ = new Set([
97
+ "can", "cannot", "authorize", "checkpermission", "haspermission",
98
+ "verifyjwt", "isauthorized",
99
+ ]);
100
+ const INPUT_EFFECT = "__progmune_input_effect__";
101
+ const TOKEN_ISSUED = "__progmune_token_issued__";
102
+ // 权重(§49.12 评估用的就是这组,语义分与先验量级匹配)
103
+ const W = {
104
+ dataOp: 0.5, // 每个命中的数据操作名
105
+ multiRule: 0.4, // 同函数被多条规则命中
106
+ inputEffect: 0.3, // 输入确实被使用
107
+ guard: -0.6, // 有校验证据 ⇒ 降权
108
+ authz: -0.4, // 有鉴权判定 ⇒ 降权
109
+ token: -0.3, // 已签发 token ⇒ 降权
110
+ };
111
+ /** 语义自身信号打分:这个函数**自身**做了什么。只吃 IR 可得字段。 */
112
+ function semanticScore(a) {
113
+ const calls = a.calls || [];
114
+ const lower = new Set(calls.map((c) => String(c).toLowerCase()));
115
+ const reasons = [];
116
+ let s = 0;
117
+ const nData = [...DATA_OP].filter((d) => lower.has(d)).length;
118
+ if (nData) {
119
+ s += W.dataOp * nData;
120
+ reasons.push(`直接数据操作 ×${nData}`);
121
+ }
122
+ if ((a.nRules ?? 1) >= 2) {
123
+ s += W.multiRule;
124
+ reasons.push("同函数命中多条规则");
125
+ }
126
+ if (calls.includes(INPUT_EFFECT)) {
127
+ s += W.inputEffect;
128
+ reasons.push("输入被使用");
129
+ }
130
+ if (calls.some((c) => GUARD_MARKS.has(String(c)))) {
131
+ s += W.guard;
132
+ reasons.push("有校验证据(降权)");
133
+ }
134
+ if ([...AUTHZ].some((x) => lower.has(x))) {
135
+ s += W.authz;
136
+ reasons.push("有鉴权判定(降权)");
137
+ }
138
+ if (calls.includes(TOKEN_ISSUED)) {
139
+ s += W.token;
140
+ reasons.push("已签发 token(降权)");
141
+ }
142
+ return { score: s, reasons };
143
+ }
144
+ function scoreOne(a, o = {}) {
145
+ const prior = (o.prior ?? exports.DEFAULT_RULE_PRIOR)[a.rule] ?? (o.base ?? exports.DEFAULT_BASE);
146
+ const sem = semanticScore(a);
147
+ const p = o.usePrior === false ? 0 : exports.W_PRIOR * prior;
148
+ return { alert: a, score: p + sem.score, prior, semantic: sem.score, reasons: sem.reasons };
149
+ }
150
+ /**
151
+ * 全局排序:输出一条平铺列表。
152
+ * minPerRule>0 时先给每族保底名额,再排剩下的 —— 避免整族沉底。
153
+ */
154
+ function rankAlerts(alerts, o = {}) {
155
+ const all = alerts.map((a) => scoreOne(a, o));
156
+ const min = o.minPerRule ?? 0;
157
+ if (min <= 0)
158
+ return all.sort((x, y) => y.score - x.score);
159
+ const byRule = new Map();
160
+ for (const r of all) {
161
+ const k = r.alert.rule;
162
+ if (!byRule.has(k))
163
+ byRule.set(k, []);
164
+ byRule.get(k).push(r);
165
+ }
166
+ const head = [];
167
+ const tail = [];
168
+ for (const [, list] of byRule) {
169
+ list.sort((x, y) => y.score - x.score);
170
+ head.push(...list.slice(0, min));
171
+ tail.push(...list.slice(min));
172
+ }
173
+ const cmp = (x, y) => y.score - x.score;
174
+ return [...head.sort(cmp), ...tail.sort(cmp)];
175
+ }
176
+ /**
177
+ * **推荐形态**:按族分组,组间按先验排序,组内按语义分排序。
178
+ * 每个族都有一块 ⇒ 天然不会因为先验低而被整体沉底(缓解自证风险)。
179
+ */
180
+ function groupAlerts(alerts, o = {}) {
181
+ const scored = alerts.map((a) => scoreOne(a, o));
182
+ const m = new Map();
183
+ for (const r of scored) {
184
+ const k = r.alert.rule;
185
+ if (!m.has(k))
186
+ m.set(k, []);
187
+ m.get(k).push(r);
188
+ }
189
+ const groups = [...m.entries()].map(([rule, list]) => ({
190
+ rule,
191
+ prior: list[0].prior,
192
+ count: list.length,
193
+ alerts: list.sort((x, y) => y.score - x.score),
194
+ }));
195
+ return groups.sort((a, b) => b.prior - a.prior);
196
+ }
197
+ /**
198
+ * 从**已标注**的告警里学族先验。线上应该用自己项目的反馈数据调它重估。
199
+ * alpha 越大越保守(越接近全局基线)——样本少的时候调大。
200
+ */
201
+ function learnPrior(labeled, alpha = 8) {
202
+ const cnt = new Map();
203
+ let tpAll = 0;
204
+ for (const r of labeled) {
205
+ const c = cnt.get(r.rule) ?? { tp: 0, n: 0 };
206
+ c.n++;
207
+ if (r.gold === "TP") {
208
+ c.tp++;
209
+ tpAll++;
210
+ }
211
+ cnt.set(r.rule, c);
212
+ }
213
+ const base = labeled.length ? tpAll / labeled.length : exports.DEFAULT_BASE;
214
+ const prior = {};
215
+ for (const [rule, c] of cnt)
216
+ prior[rule] = (c.tp + alpha * base) / (c.n + alpha);
217
+ return { base, prior };
218
+ }
@@ -74,6 +74,8 @@ const gin_detector_1 = require("../frameworks/gin-detector");
74
74
  const fiber_detector_1 = require("../frameworks/fiber-detector");
75
75
  const ssg_bridge_1 = require("./ssg-bridge");
76
76
  const call_sequence_1 = require("../call-sequence");
77
+ const protocol_detector_1 = require("../protocol-detector");
78
+ const alert_ranker_1 = require("./alert-ranker");
77
79
  const extract_ir_1 = require("../extract-ir");
78
80
  const extract_ir_python_1 = require("../extract-ir-python");
79
81
  const extract_ir_c_1 = require("../extract-ir-c");
@@ -103,7 +105,7 @@ async function evaluateTrust(ctx) {
103
105
  // ── Phase 5: Build call graph for cross-function propagation ──
104
106
  const callGraph = (0, call_graph_propagator_1.buildCallGraphFromIR)(path.join(ctx.projectPath, "ir.json"));
105
107
  const enterpriseViolations = collectEnterpriseViolations(ctx);
106
- const { violations: protocolViolations, coverage: mappingCoverageData, ssgCoverage: ssgCov, annotationSuggestions: cAnnotationSuggestions, extractionWarning } = await collectProtocolViolations(ctx, callGraph);
108
+ const { violations: protocolViolations, coverage: mappingCoverageData, ssgCoverage: ssgCov, annotationSuggestions: cAnnotationSuggestions, extractionWarning, safeguardAlerts } = await collectProtocolViolations(ctx, callGraph);
107
109
  const expressResult = collectExpressViolations(ctx);
108
110
  const nestjsResult = collectNestJSViolations(ctx);
109
111
  const trpcResult = collectTRPCViolations(ctx);
@@ -151,7 +153,14 @@ async function evaluateTrust(ctx) {
151
153
  writeTrustFailuresToCorpus(ctx, allViolations);
152
154
  // PHASE 3: SCORE
153
155
  // ═══════════════════════════════════════
154
- const policyResult = (0, score_calculator_1.scorePolicyCompliance)(allViolations);
156
+ // 企业策略维度只吃策略类违规——框架适配器(framework.*)与协议检测
157
+ // (protocol-safety.*)的违规在 protocolSafety 维度已计分,同池双计
158
+ // 会把失明误报放大成双倍扣分(2026-09-29 docmost 实测:132 条
159
+ // NestJS 违规双计,auth 0 分 + score 48 + BLOCKED,逐条核实 126 条为 FP)。
160
+ const policyViolations = allViolations.filter((v) => !v.policy_ref.startsWith("framework") &&
161
+ !v.policy_ref.startsWith("protocol-safety") &&
162
+ v.policy_ref !== "protocol");
163
+ const policyResult = (0, score_calculator_1.scorePolicyCompliance)(policyViolations);
155
164
  const protocolResult = (0, score_calculator_1.scoreProtocolSafety)(allViolations);
156
165
  const coverageResult = (0, score_calculator_1.scoreVerificationCoverage)(coverageData);
157
166
  const governanceResult = (0, score_calculator_1.scoreGovernanceIntegrity)(governanceDefects);
@@ -256,6 +265,10 @@ async function evaluateTrust(ctx) {
256
265
  : undefined,
257
266
  /** SSG State Machine coverage — how many calls were matched to protocol rules */
258
267
  ssgCoverage: ssgCov,
268
+ /** 2026-10-02(§49.15 方案 b):排序后的 safeguard 告警流——证据流,
269
+ * 不进判定。groups 为推荐形态(按族分组防整族沉底),topRanked
270
+ * 为全局平铺(minPerRule=1 保底)。total=0 时字段仍在(0 必须自证)。 */
271
+ safeguardAlerts,
259
272
  /** Express framework adapter coverage — routes & middleware analyzed */
260
273
  expressCoverage: expressResult.coverage.expressApps > 0
261
274
  ? {
@@ -361,7 +374,7 @@ async function evaluateTrust(ctx) {
361
374
  score: policyResult.score,
362
375
  weight: types_1.DEFAULT_DIMENSION_WEIGHTS.policyCompliance,
363
376
  confidence: policyResult.hasCritical ? "LOW" : "HIGH",
364
- violations: allViolations.filter((v) => v.severity !== "low" || v.policy_ref !== "protocol"),
377
+ violations: policyViolations,
365
378
  },
366
379
  protocolSafety: protocolResult,
367
380
  verificationCoverage: coverageResult,
@@ -1218,6 +1231,9 @@ async function collectProtocolViolations(ctx, callGraph) {
1218
1231
  let annotationSuggestions;
1219
1232
  // 2026-09-22:提取失败标记(跨 try 作用域,随结果返回)
1220
1233
  let extractionWarning;
1234
+ // 2026-10-02:排序后的 safeguard 告警流(§49.15 方案 b)——同一批 IR 上
1235
+ // 收集,try 内赋值、try 外返回
1236
+ let safeguardAlerts;
1221
1237
  try {
1222
1238
  // ── P4.5 校准:TS/JS 项目在 ir.json 缺失时先提取 IR ──
1223
1239
  // 序列必须来自「函数体内真实调用」而非「文件内函数声明名单」——
@@ -1455,6 +1471,42 @@ async function collectProtocolViolations(ctx, callGraph) {
1455
1471
  if ((ctx.language || "typescript") === "c") {
1456
1472
  annotationSuggestions = (0, annotation_suggest_1.suggestAnnotations)(functions, protocolRulesData ? new Set(protocolRulesData.rules.keys()) : undefined);
1457
1473
  }
1474
+ // ── safeguard 告警流(2026-10-02,§49.15 方案 b 落地)──
1475
+ // 同一批 IR 上跑 detectSafeguardViolations(batch-scan 同款口径:
1476
+ // language 必传,params 名/类型、exported 近似 exposed),产出
1477
+ // **排序后的告警流**。证据流与判定分离:不进 violations、不扣分、
1478
+ // 不改 decision/score——排序告警是给人工审查的入口(§49.16:
1479
+ // 排序不删告警,一条不少;groupAlerts 分组防整族沉底)。
1480
+ const safeguardAlertsRaw = [];
1481
+ const svLang = ctx.language || "typescript";
1482
+ for (const f of functions) {
1483
+ const fCalls = f.calls || [];
1484
+ let svs;
1485
+ try {
1486
+ svs = (0, protocol_detector_1.detectSafeguardViolations)(fCalls, f.name, svLang, (f.params || []).map((p) => p.name), !!f.exported, (f.params || []).map((p) => p.type || ""));
1487
+ }
1488
+ catch {
1489
+ continue; // 单函数失败不阻断整批(与 batch-scan 容错一致)
1490
+ }
1491
+ if (svs.length === 0)
1492
+ continue;
1493
+ const params = (f.params || []).map((p) => ({ n: p.name, t: p.type || "" }));
1494
+ for (const v of svs) {
1495
+ safeguardAlertsRaw.push({
1496
+ rule: v.rule,
1497
+ file: f.file,
1498
+ function: f.name,
1499
+ calls: fCalls,
1500
+ nRules: svs.length,
1501
+ params,
1502
+ });
1503
+ }
1504
+ }
1505
+ safeguardAlerts = {
1506
+ total: safeguardAlertsRaw.length,
1507
+ groups: (0, alert_ranker_1.groupAlerts)(safeguardAlertsRaw),
1508
+ topRanked: (0, alert_ranker_1.rankAlerts)(safeguardAlertsRaw, { minPerRule: 1 }),
1509
+ };
1458
1510
  }
1459
1511
  }
1460
1512
  catch { /* best-effort */ }
@@ -1595,6 +1647,7 @@ async function collectProtocolViolations(ctx, callGraph) {
1595
1647
  catch { /* best-effort */ }
1596
1648
  return {
1597
1649
  violations,
1650
+ safeguardAlerts,
1598
1651
  coverage: {
1599
1652
  totalApis,
1600
1653
  lookupHits,
@@ -138,6 +138,29 @@ function formatTrustTerminal(decision) {
138
138
  lines.push(`${BOX_V}${" ".repeat(w - 2)}${BOX_V}`);
139
139
  }
140
140
  }
141
+ // Ranked Safeguard Alerts — 证据流(2026-10-02,§49.15 方案 b):
142
+ // 排序后的 safeguard 告警,不进判定(decision/score 不受影响)。
143
+ // 展示分组形态的头部:组间按族先验、组内按语义分;每组最多 3 条。
144
+ const sa = overall.safeguardAlerts;
145
+ if (sa && sa.total > 0) {
146
+ lines.push(`${BOX_ML}${BOX_H.repeat(w - 2)}${BOX_MR}`);
147
+ lines.push(`${BOX_V} ${BOLD}Ranked Safeguard Alerts${RESET} (evidence stream — does not affect the decision)${BOX_V}`.slice(0, w + 1));
148
+ lines.push(`${BOX_V}${" ".repeat(w - 2)}${BOX_V}`);
149
+ lines.push(`${BOX_V} ${DIM}${sa.total} alert(s), ${sa.groups.length} rule group(s). Groups ordered by rule prior; within each group by semantic signals.${RESET}`.slice(0, w + 1));
150
+ lines.push(`${BOX_V}${" ".repeat(w - 2)}${BOX_V}`);
151
+ for (const g of sa.groups.slice(0, 8)) {
152
+ lines.push(`${BOX_V} ${CYAN}◆ ${g.rule}${RESET} ${DIM}(prior ${(g.prior * 100).toFixed(1)}%, ${g.count} alerts)${RESET}`.slice(0, w + 1));
153
+ for (const r of g.alerts.slice(0, 3)) {
154
+ const a = r.alert;
155
+ const fn = `${a.function || "?"} @ ${a.file || "?"}`;
156
+ lines.push(`${BOX_V} ${DIM}${fn.slice(0, w - 8)}${RESET}`);
157
+ if (r.reasons.length > 0) {
158
+ lines.push(`${BOX_V} ${DIM}↳ ${r.reasons.join(" · ").slice(0, w - 9)}${RESET}`);
159
+ }
160
+ }
161
+ lines.push(`${BOX_V}${" ".repeat(w - 2)}${BOX_V}`);
162
+ }
163
+ }
141
164
  // Audit Trail
142
165
  lines.push(`${BOX_ML}${BOX_H.repeat(w - 2)}${BOX_MR}`);
143
166
  lines.push(`${BOX_V} ${BOLD}Audit Trail${RESET}${" ".repeat(w - 16)}${BOX_V}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "progmune-runtime",
3
- "version": "3.7.54",
3
+ "version": "3.7.56",
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/",