progmune-runtime 3.7.55 → 3.7.57
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 +62 -0
- package/dist/frameworks/nestjs-detector.js +428 -161
- package/dist/frameworks/nestjs-detector.test.js +220 -0
- package/dist/protocol-detector.js +11 -11
- package/dist/sdk.js +1 -1
- package/dist/trust/alert-ranker.js +220 -0
- package/dist/trust/confidence-calculator.js +3 -1
- package/dist/trust/engine.js +67 -3
- package/dist/trust/formatters/terminal.js +23 -0
- package/dist/trust/score-calculator.js +88 -8
- package/dist/trust/score-calculator.test.js +149 -0
- package/dist/trust/types.js +17 -1
- package/package.json +1 -1
|
@@ -418,3 +418,223 @@ export class NotesController {
|
|
|
418
418
|
(0, vitest_1.expect)(a.issues.some((i) => i.type === "NESTJS_NO_VALIDATION" && i.route === "POST notes/create")).toBe(true);
|
|
419
419
|
});
|
|
420
420
|
});
|
|
421
|
+
// ═══════════════════════════════════════════════════════════
|
|
422
|
+
// §52(2026-10-03):app-level provider 注册事件
|
|
423
|
+
//
|
|
424
|
+
// immich held-out 实测倒在了第三种注册途径上:
|
|
425
|
+
// const commonMiddleware = [{ provide: APP_PIPE, useClass: ZodValidationPipe }, ...];
|
|
426
|
+
// const apiMiddleware = [FileUploadInterceptor, ...commonMiddleware, {...}];
|
|
427
|
+
// @Module({ providers: [...common, ...apiMiddleware, ...] })
|
|
428
|
+
// `APP_PIPE` 与 `@Module` 之间隔了**两跳数组 spread**,只看装饰器那一层永远
|
|
429
|
+
// 看不到 ⇒ 153 条 NO_VALIDATION 误报。修复原则是「注册事件」而非「写法」:
|
|
430
|
+
// 哪里有注册意图,哪里就是证据,不再要求它必须直连某个 @Module。
|
|
431
|
+
//
|
|
432
|
+
// 同组反向测试是硬要求(R75):压制放宽后必须证明「该报的还在报」。
|
|
433
|
+
// ═══════════════════════════════════════════════════════════
|
|
434
|
+
(0, vitest_1.describe)("nestjs-detector app-level provider 注册事件(§52)", () => {
|
|
435
|
+
const ZOD_STUB = `
|
|
436
|
+
export function createZodDto(schema: any): any { return class {} as any; }
|
|
437
|
+
export const CreateAlbumSchema = { albumName: "string" };
|
|
438
|
+
export class CreateAlbumDto extends createZodDto(CreateAlbumSchema) {}
|
|
439
|
+
export class PlainAlbumDto { albumName: string = ""; }
|
|
440
|
+
`;
|
|
441
|
+
const ALBUM_CONTROLLER = `
|
|
442
|
+
import { Body, Controller, Post } from "@nestjs/common";
|
|
443
|
+
import { CreateAlbumDto } from "./album.dto";
|
|
444
|
+
@Controller("albums")
|
|
445
|
+
export class AlbumController {
|
|
446
|
+
@Post()
|
|
447
|
+
create(@Body() dto: CreateAlbumDto) { return {}; }
|
|
448
|
+
}
|
|
449
|
+
`;
|
|
450
|
+
const APP_MODULE_SPREAD = `
|
|
451
|
+
import { Module } from "@nestjs/common";
|
|
452
|
+
import { APP_PIPE } from "@nestjs/core";
|
|
453
|
+
import { ZodValidationPipe } from "nestjs-zod";
|
|
454
|
+
import { FileUploadInterceptor } from "./middleware";
|
|
455
|
+
|
|
456
|
+
const commonMiddleware = [
|
|
457
|
+
{ provide: APP_PIPE, useClass: ZodValidationPipe },
|
|
458
|
+
];
|
|
459
|
+
const apiMiddleware = [FileUploadInterceptor, ...commonMiddleware];
|
|
460
|
+
|
|
461
|
+
@Module({ providers: [...apiMiddleware] })
|
|
462
|
+
export class AppModule {}
|
|
463
|
+
`;
|
|
464
|
+
function writeImmishApp() {
|
|
465
|
+
write("tsconfig.json", TSCONFIG);
|
|
466
|
+
write("src/album.dto.ts", ZOD_STUB);
|
|
467
|
+
write("src/album.controller.ts", ALBUM_CONTROLLER);
|
|
468
|
+
write("src/middleware.ts", `
|
|
469
|
+
export class FileUploadInterceptor {}
|
|
470
|
+
`);
|
|
471
|
+
}
|
|
472
|
+
(0, vitest_1.it)("APP_PIPE 经两跳 spread 进 @Module ⇒ 已校验 DTO 的 mutation 不报 NO_VALIDATION(immich 形态)", () => {
|
|
473
|
+
writeImmishApp();
|
|
474
|
+
write("src/app.module.ts", APP_MODULE_SPREAD);
|
|
475
|
+
const a = (0, nestjs_detector_1.analyzeNestJSProject)(dir);
|
|
476
|
+
(0, vitest_1.expect)(a.globalValidationPipes).toContain("ZodValidationPipe");
|
|
477
|
+
const nv = a.issues.filter((i) => i.type === "NESTJS_NO_VALIDATION");
|
|
478
|
+
(0, vitest_1.expect)(nv.map((i) => i.route)).not.toContain("POST albums");
|
|
479
|
+
});
|
|
480
|
+
(0, vitest_1.it)("反向:同一形态但摘掉 APP_PIPE 注册 ⇒ 精确转红(不是无差别压制)", () => {
|
|
481
|
+
writeImmishApp();
|
|
482
|
+
write("src/app.module.ts", `
|
|
483
|
+
import { Module } from "@nestjs/common";
|
|
484
|
+
import { FileUploadInterceptor } from "./middleware";
|
|
485
|
+
const apiMiddleware = [FileUploadInterceptor];
|
|
486
|
+
@Module({ providers: [...apiMiddleware] })
|
|
487
|
+
export class AppModule {}
|
|
488
|
+
`);
|
|
489
|
+
const a = (0, nestjs_detector_1.analyzeNestJSProject)(dir);
|
|
490
|
+
(0, vitest_1.expect)(a.globalValidationPipes).toEqual([]);
|
|
491
|
+
(0, vitest_1.expect)(a.issues.some((i) => i.type === "NESTJS_NO_VALIDATION" && i.route === "POST albums")).toBe(true);
|
|
492
|
+
});
|
|
493
|
+
(0, vitest_1.it)("反向:有全局管道但入参不是已校验 DTO ⇒ 仍报(保守豁免不扩散到裸类型)", () => {
|
|
494
|
+
writeImmishApp();
|
|
495
|
+
write("src/app.module.ts", APP_MODULE_SPREAD);
|
|
496
|
+
write("src/album.controller.ts", `
|
|
497
|
+
import { Body, Controller, Post } from "@nestjs/common";
|
|
498
|
+
import { PlainAlbumDto } from "./album.dto";
|
|
499
|
+
@Controller("albums")
|
|
500
|
+
export class AlbumController {
|
|
501
|
+
@Post()
|
|
502
|
+
create(@Body() dto: PlainAlbumDto) { return {}; }
|
|
503
|
+
}
|
|
504
|
+
`);
|
|
505
|
+
const a = (0, nestjs_detector_1.analyzeNestJSProject)(dir);
|
|
506
|
+
(0, vitest_1.expect)(a.issues.some((i) => i.type === "NESTJS_NO_VALIDATION" && i.route === "POST albums")).toBe(true);
|
|
507
|
+
});
|
|
508
|
+
(0, vitest_1.it)("反向:注释里的 APP_PIPE 不算注册事件 ⇒ 仍报(text-fallback 抗噪)", () => {
|
|
509
|
+
writeImmishApp();
|
|
510
|
+
write("src/app.module.ts", `
|
|
511
|
+
import { Module } from "@nestjs/common";
|
|
512
|
+
// 计划中:providers: [{ provide: APP_PIPE, useClass: ZodValidationPipe }]
|
|
513
|
+
// * 文档注释形态同样不应被当作注册证据
|
|
514
|
+
@Module({ providers: [] })
|
|
515
|
+
export class AppModule {}
|
|
516
|
+
`);
|
|
517
|
+
const a = (0, nestjs_detector_1.analyzeNestJSProject)(dir);
|
|
518
|
+
(0, vitest_1.expect)(a.globalValidationPipes).toEqual([]);
|
|
519
|
+
(0, vitest_1.expect)(a.issues.some((i) => i.type === "NESTJS_NO_VALIDATION" && i.route === "POST albums")).toBe(true);
|
|
520
|
+
});
|
|
521
|
+
(0, vitest_1.it)("@UsePipes 自定义管道名(不带 Pipe 后缀)⇒ 识别为已注册管道", () => {
|
|
522
|
+
writeImmishApp();
|
|
523
|
+
write("src/album.controller.ts", `
|
|
524
|
+
import { Body, Controller, Post, UsePipes } from "@nestjs/common";
|
|
525
|
+
import { CreateAlbumDto } from "./album.dto";
|
|
526
|
+
@Controller("albums")
|
|
527
|
+
export class AlbumController {
|
|
528
|
+
@Post()
|
|
529
|
+
@UsePipes(bodyTrimSanitizer)
|
|
530
|
+
create(@Body() dto: CreateAlbumDto) { return {}; }
|
|
531
|
+
}
|
|
532
|
+
`);
|
|
533
|
+
const a = (0, nestjs_detector_1.analyzeNestJSProject)(dir);
|
|
534
|
+
(0, vitest_1.expect)(a.issues.filter((i) => i.type === "NESTJS_NO_VALIDATION" && i.route === "POST albums")).toEqual([]);
|
|
535
|
+
});
|
|
536
|
+
(0, vitest_1.it)("@UseGuards(AuthGuard('JWT')) 的参数字符串不产生假认证证据(反向 Sophie)", () => {
|
|
537
|
+
write("tsconfig.json", TSCONFIG);
|
|
538
|
+
write("src/auth.guard.ts", GUARD);
|
|
539
|
+
write("src/api.controller.ts", `
|
|
540
|
+
import { Controller, Post, UseGuards } from "@nestjs/common";
|
|
541
|
+
import { JWTStrategy } from "./jwt";
|
|
542
|
+
@Controller("api")
|
|
543
|
+
export class ApiController {
|
|
544
|
+
@Post("transfer")
|
|
545
|
+
@UseGuards(JWTStrategy)
|
|
546
|
+
transfer() { return {}; }
|
|
547
|
+
}
|
|
548
|
+
`);
|
|
549
|
+
write("src/jwt.ts", `
|
|
550
|
+
export const JWT = "Bearer";
|
|
551
|
+
export class JWTStrategy { canActivate(): boolean { return false; } }
|
|
552
|
+
`);
|
|
553
|
+
write("src/app.module.ts", MODULE_EMPTY);
|
|
554
|
+
const a = (0, nestjs_detector_1.analyzeNestJSProject)(dir);
|
|
555
|
+
// JWTStrategy 名字里带 jwt ⇒ 认证守卫(EXPECT 有保护)
|
|
556
|
+
(0, vitest_1.expect)(a.issues.filter((i) => i.type === "NESTJS_NO_AUTH")).toEqual([]);
|
|
557
|
+
// 但若写成字符串参数,不得凭 'JWT' 这个 token 冒充守卫名
|
|
558
|
+
write("src/api.controller.ts", `
|
|
559
|
+
import { Controller, Post, UseGuards } from "@nestjs/common";
|
|
560
|
+
import { AnonymousGate } from "./gate";
|
|
561
|
+
@Controller("api")
|
|
562
|
+
export class ApiController {
|
|
563
|
+
@Post("transfer")
|
|
564
|
+
@UseGuards(AnonymousGate("JWT"))
|
|
565
|
+
transfer() { return {}; }
|
|
566
|
+
}
|
|
567
|
+
`);
|
|
568
|
+
write("src/gate.ts", `
|
|
569
|
+
export function AnonymousGate(scheme: string): any { return class { canActivate() { return true; } }; }
|
|
570
|
+
`);
|
|
571
|
+
const b = (0, nestjs_detector_1.analyzeNestJSProject)(dir);
|
|
572
|
+
(0, vitest_1.expect)(b.issues.some((i) => i.type === "NESTJS_NO_AUTH" && i.route === "POST api/transfer")).toBe(true);
|
|
573
|
+
});
|
|
574
|
+
(0, vitest_1.it)("registrations 带可复核的 (file, line) 与发现途径", () => {
|
|
575
|
+
writeImmishApp();
|
|
576
|
+
write("src/app.module.ts", APP_MODULE_SPREAD);
|
|
577
|
+
const a = (0, nestjs_detector_1.analyzeNestJSProject)(dir);
|
|
578
|
+
const pipeReg = a.registrations.filter((r) => r.token === "APP_PIPE");
|
|
579
|
+
(0, vitest_1.expect)(pipeReg.length).toBeGreaterThan(0);
|
|
580
|
+
for (const r of pipeReg) {
|
|
581
|
+
// 可复核:每条注册都必须能指回某个文件的某一行
|
|
582
|
+
(0, vitest_1.expect)(r.file).toMatch(/app\.module\.ts$/);
|
|
583
|
+
(0, vitest_1.expect)(r.line).toBeGreaterThan(0);
|
|
584
|
+
(0, vitest_1.expect)(r.impl).toBe("ZodValidationPipe");
|
|
585
|
+
(0, vitest_1.expect)(["module-decorator", "module-property", "array-literal", "text-fallback"]).toContain(r.source);
|
|
586
|
+
}
|
|
587
|
+
});
|
|
588
|
+
(0, vitest_1.it)("数组路径别名:展开成多条 route,公开豁免重新生效(nocodb 形态,普查 72%)", () => {
|
|
589
|
+
write("tsconfig.json", TSCONFIG);
|
|
590
|
+
write("src/app.module.ts", MODULE_EMPTY);
|
|
591
|
+
write("src/token.controller.ts", `
|
|
592
|
+
import { Controller, Post } from "@nestjs/common";
|
|
593
|
+
@Controller(["meta/bases/:id", "api/v1/db/meta/projects/:id"])
|
|
594
|
+
export class TokenController {
|
|
595
|
+
@Post(["login", "signin"])
|
|
596
|
+
login() { return {}; }
|
|
597
|
+
@Post(["api-tokens"])
|
|
598
|
+
createToken() { return {}; }
|
|
599
|
+
}
|
|
600
|
+
`);
|
|
601
|
+
const a = (0, nestjs_detector_1.analyzeNestJSProject)(dir);
|
|
602
|
+
const paths = a.routes.map((r) => `${r.method} ${r.path}`).sort();
|
|
603
|
+
// 2 个 controller 别名 × 3 个方法别名 = 6 条
|
|
604
|
+
(0, vitest_1.expect)(paths).toEqual([
|
|
605
|
+
"POST api/v1/db/meta/projects/:id/api-tokens",
|
|
606
|
+
"POST api/v1/db/meta/projects/:id/login",
|
|
607
|
+
"POST api/v1/db/meta/projects/:id/signin",
|
|
608
|
+
"POST meta/bases/:id/api-tokens",
|
|
609
|
+
"POST meta/bases/:id/login",
|
|
610
|
+
"POST meta/bases/:id/signin",
|
|
611
|
+
]);
|
|
612
|
+
// 旧实现下 path 是整段数组文本 ⇒ /login 豁免匹配不到 ⇒ 4 条 login/signin 全被误报
|
|
613
|
+
const noAuth = a.issues.filter((i) => i.type === "NESTJS_NO_AUTH").map((i) => i.route);
|
|
614
|
+
(0, vitest_1.expect)(noAuth.sort()).toEqual([
|
|
615
|
+
"POST api/v1/db/meta/projects/:id/api-tokens",
|
|
616
|
+
"POST meta/bases/:id/api-tokens",
|
|
617
|
+
]);
|
|
618
|
+
});
|
|
619
|
+
(0, vitest_1.it)("@Module(configVar):装饰器参数是变量时仍能读到 providers(nocodb 形态,普查 71%)", () => {
|
|
620
|
+
writeImmishApp();
|
|
621
|
+
write("src/app.module.ts", `
|
|
622
|
+
import { Module } from "@nestjs/common";
|
|
623
|
+
import { APP_PIPE } from "@nestjs/core";
|
|
624
|
+
import { ZodValidationPipe } from "nestjs-zod";
|
|
625
|
+
|
|
626
|
+
const moduleMetadata = {
|
|
627
|
+
imports: [],
|
|
628
|
+
controllers: [],
|
|
629
|
+
providers: [{ provide: APP_PIPE, useClass: ZodValidationPipe }],
|
|
630
|
+
};
|
|
631
|
+
|
|
632
|
+
@Module(moduleMetadata)
|
|
633
|
+
export class AppModule {}
|
|
634
|
+
`);
|
|
635
|
+
const a = (0, nestjs_detector_1.analyzeNestJSProject)(dir);
|
|
636
|
+
(0, vitest_1.expect)(a.globalValidationPipes).toContain("ZodValidationPipe");
|
|
637
|
+
const nv = a.issues.filter((i) => i.type === "NESTJS_NO_VALIDATION");
|
|
638
|
+
(0, vitest_1.expect)(nv.map((i) => i.route)).not.toContain("POST albums");
|
|
639
|
+
});
|
|
640
|
+
});
|
|
@@ -1121,17 +1121,17 @@ const SAFEGUARD_RULES = [
|
|
|
1121
1121
|
conceptExpected: ["checkExpiry", "autoRenew", "certificate watch"],
|
|
1122
1122
|
},
|
|
1123
1123
|
// ── Data Integrity: Mutation without audit ──
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1124
|
+
// 2026-10-04(修冤枉第一刀):移除 "Data Mutation Without Audit Trail"。
|
|
1125
|
+
// 数据依据:fp-gold 131/131 全 FP(docmost/hedgedoc/hoppscotch/verdaccio/
|
|
1126
|
+
// 3×realworld 七仓九片);TS 盲测 gold 795 条 0 条此族;PyGoat 232 条里
|
|
1127
|
+
// 此族 8 条全 unlabeled 且样本全 FP 形态;84 条 GHSA 无一条审计轨迹类。
|
|
1128
|
+
// 根因是概念性失明(非词表问题):真实架构里审计在框架/中间件层
|
|
1129
|
+
// (ORM 拦截器/数据库触发器/日志系统),函数名里永远不会出现 audit
|
|
1130
|
+
// 字样 ⇒ 判据「mutation 函数名附近有审计词汇」在真实代码形态上永远
|
|
1131
|
+
// 不可能满足。与授权族保留的理由对照(R92):授权族在真实世界有 TP
|
|
1132
|
+
// (fr 语料 8/9 位置命中是授权类),此族在所有口径 TP=0。
|
|
1133
|
+
// 若未来有函数级审计判据(如 decorator/@Audited 标记通道),按
|
|
1134
|
+
// 「先语料后规则」重新提案。
|
|
1135
1135
|
// ═══════════════════════════════════════════════════════════════
|
|
1136
1136
|
// Governance meta-rule: Framework Version Convention Check
|
|
1137
1137
|
// Lesson from proxy.ts incident (2026-08-03)
|
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.57";
|
|
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,220 @@
|
|
|
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
|
+
// 2026-10-04:Data Mutation Without Audit Trail 规则已移除(fp-gold
|
|
83
|
+
// 131/131 全 FP、全口径 TP=0,概念性失明——见 protocol-detector.ts 注记),
|
|
84
|
+
// 先验条目随之删除。
|
|
85
|
+
};
|
|
86
|
+
/** 未知族的兜底先验 = 全局基线 */
|
|
87
|
+
exports.DEFAULT_BASE = 0.0984;
|
|
88
|
+
// ── 语义信号集合(全部**语义定义**,不是从数据里挑出来的,见文件头 ③)──
|
|
89
|
+
/** 直接的数据操作:真漏洞多半要真的动数据 */
|
|
90
|
+
const DATA_OP = new Set([
|
|
91
|
+
"insert", "select", "where", "update", "delete", "save", "remove", "upsert",
|
|
92
|
+
"patch", "create", "destroy", "findone", "findmany", "find", "executetx",
|
|
93
|
+
"add", "write", "execute",
|
|
94
|
+
]);
|
|
95
|
+
/** 提取器注入的「有校验证据」标记 —— 有防护 ⇒ 更像误报 */
|
|
96
|
+
const GUARD_MARKS = new Set(["__progmune_input_guard__", "__progmune_input_schema__"]);
|
|
97
|
+
/** 鉴权判定调用 */
|
|
98
|
+
const AUTHZ = new Set([
|
|
99
|
+
"can", "cannot", "authorize", "checkpermission", "haspermission",
|
|
100
|
+
"verifyjwt", "isauthorized",
|
|
101
|
+
]);
|
|
102
|
+
const INPUT_EFFECT = "__progmune_input_effect__";
|
|
103
|
+
const TOKEN_ISSUED = "__progmune_token_issued__";
|
|
104
|
+
// 权重(§49.12 评估用的就是这组,语义分与先验量级匹配)
|
|
105
|
+
const W = {
|
|
106
|
+
dataOp: 0.5, // 每个命中的数据操作名
|
|
107
|
+
multiRule: 0.4, // 同函数被多条规则命中
|
|
108
|
+
inputEffect: 0.3, // 输入确实被使用
|
|
109
|
+
guard: -0.6, // 有校验证据 ⇒ 降权
|
|
110
|
+
authz: -0.4, // 有鉴权判定 ⇒ 降权
|
|
111
|
+
token: -0.3, // 已签发 token ⇒ 降权
|
|
112
|
+
};
|
|
113
|
+
/** 语义自身信号打分:这个函数**自身**做了什么。只吃 IR 可得字段。 */
|
|
114
|
+
function semanticScore(a) {
|
|
115
|
+
const calls = a.calls || [];
|
|
116
|
+
const lower = new Set(calls.map((c) => String(c).toLowerCase()));
|
|
117
|
+
const reasons = [];
|
|
118
|
+
let s = 0;
|
|
119
|
+
const nData = [...DATA_OP].filter((d) => lower.has(d)).length;
|
|
120
|
+
if (nData) {
|
|
121
|
+
s += W.dataOp * nData;
|
|
122
|
+
reasons.push(`直接数据操作 ×${nData}`);
|
|
123
|
+
}
|
|
124
|
+
if ((a.nRules ?? 1) >= 2) {
|
|
125
|
+
s += W.multiRule;
|
|
126
|
+
reasons.push("同函数命中多条规则");
|
|
127
|
+
}
|
|
128
|
+
if (calls.includes(INPUT_EFFECT)) {
|
|
129
|
+
s += W.inputEffect;
|
|
130
|
+
reasons.push("输入被使用");
|
|
131
|
+
}
|
|
132
|
+
if (calls.some((c) => GUARD_MARKS.has(String(c)))) {
|
|
133
|
+
s += W.guard;
|
|
134
|
+
reasons.push("有校验证据(降权)");
|
|
135
|
+
}
|
|
136
|
+
if ([...AUTHZ].some((x) => lower.has(x))) {
|
|
137
|
+
s += W.authz;
|
|
138
|
+
reasons.push("有鉴权判定(降权)");
|
|
139
|
+
}
|
|
140
|
+
if (calls.includes(TOKEN_ISSUED)) {
|
|
141
|
+
s += W.token;
|
|
142
|
+
reasons.push("已签发 token(降权)");
|
|
143
|
+
}
|
|
144
|
+
return { score: s, reasons };
|
|
145
|
+
}
|
|
146
|
+
function scoreOne(a, o = {}) {
|
|
147
|
+
const prior = (o.prior ?? exports.DEFAULT_RULE_PRIOR)[a.rule] ?? (o.base ?? exports.DEFAULT_BASE);
|
|
148
|
+
const sem = semanticScore(a);
|
|
149
|
+
const p = o.usePrior === false ? 0 : exports.W_PRIOR * prior;
|
|
150
|
+
return { alert: a, score: p + sem.score, prior, semantic: sem.score, reasons: sem.reasons };
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* 全局排序:输出一条平铺列表。
|
|
154
|
+
* minPerRule>0 时先给每族保底名额,再排剩下的 —— 避免整族沉底。
|
|
155
|
+
*/
|
|
156
|
+
function rankAlerts(alerts, o = {}) {
|
|
157
|
+
const all = alerts.map((a) => scoreOne(a, o));
|
|
158
|
+
const min = o.minPerRule ?? 0;
|
|
159
|
+
if (min <= 0)
|
|
160
|
+
return all.sort((x, y) => y.score - x.score);
|
|
161
|
+
const byRule = new Map();
|
|
162
|
+
for (const r of all) {
|
|
163
|
+
const k = r.alert.rule;
|
|
164
|
+
if (!byRule.has(k))
|
|
165
|
+
byRule.set(k, []);
|
|
166
|
+
byRule.get(k).push(r);
|
|
167
|
+
}
|
|
168
|
+
const head = [];
|
|
169
|
+
const tail = [];
|
|
170
|
+
for (const [, list] of byRule) {
|
|
171
|
+
list.sort((x, y) => y.score - x.score);
|
|
172
|
+
head.push(...list.slice(0, min));
|
|
173
|
+
tail.push(...list.slice(min));
|
|
174
|
+
}
|
|
175
|
+
const cmp = (x, y) => y.score - x.score;
|
|
176
|
+
return [...head.sort(cmp), ...tail.sort(cmp)];
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* **推荐形态**:按族分组,组间按先验排序,组内按语义分排序。
|
|
180
|
+
* 每个族都有一块 ⇒ 天然不会因为先验低而被整体沉底(缓解自证风险)。
|
|
181
|
+
*/
|
|
182
|
+
function groupAlerts(alerts, o = {}) {
|
|
183
|
+
const scored = alerts.map((a) => scoreOne(a, o));
|
|
184
|
+
const m = new Map();
|
|
185
|
+
for (const r of scored) {
|
|
186
|
+
const k = r.alert.rule;
|
|
187
|
+
if (!m.has(k))
|
|
188
|
+
m.set(k, []);
|
|
189
|
+
m.get(k).push(r);
|
|
190
|
+
}
|
|
191
|
+
const groups = [...m.entries()].map(([rule, list]) => ({
|
|
192
|
+
rule,
|
|
193
|
+
prior: list[0].prior,
|
|
194
|
+
count: list.length,
|
|
195
|
+
alerts: list.sort((x, y) => y.score - x.score),
|
|
196
|
+
}));
|
|
197
|
+
return groups.sort((a, b) => b.prior - a.prior);
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* 从**已标注**的告警里学族先验。线上应该用自己项目的反馈数据调它重估。
|
|
201
|
+
* alpha 越大越保守(越接近全局基线)——样本少的时候调大。
|
|
202
|
+
*/
|
|
203
|
+
function learnPrior(labeled, alpha = 8) {
|
|
204
|
+
const cnt = new Map();
|
|
205
|
+
let tpAll = 0;
|
|
206
|
+
for (const r of labeled) {
|
|
207
|
+
const c = cnt.get(r.rule) ?? { tp: 0, n: 0 };
|
|
208
|
+
c.n++;
|
|
209
|
+
if (r.gold === "TP") {
|
|
210
|
+
c.tp++;
|
|
211
|
+
tpAll++;
|
|
212
|
+
}
|
|
213
|
+
cnt.set(r.rule, c);
|
|
214
|
+
}
|
|
215
|
+
const base = labeled.length ? tpAll / labeled.length : exports.DEFAULT_BASE;
|
|
216
|
+
const prior = {};
|
|
217
|
+
for (const [rule, c] of cnt)
|
|
218
|
+
prior[rule] = (c.tp + alpha * base) / (c.n + alpha);
|
|
219
|
+
return { base, prior };
|
|
220
|
+
}
|
|
@@ -207,6 +207,8 @@ function computeCoverageConfidence(projectPath) {
|
|
|
207
207
|
// 1. Load protocol transition space
|
|
208
208
|
const space = loadTransitionSpace(projectPath);
|
|
209
209
|
const namespaces = [...space.perNamespace.keys()];
|
|
210
|
+
// §53:协议定义不存在 ⇒ 本指标不可测(0 分是「没得测」,不是「测得低」)
|
|
211
|
+
const applicable = fs.existsSync(path.join(projectPath, "protocols.json"));
|
|
210
212
|
// 2. Load trajectory coverage
|
|
211
213
|
const coverage = loadTrajectoryCoverage(projectPath);
|
|
212
214
|
// 3. Compute per-namespace confidence
|
|
@@ -250,7 +252,7 @@ function computeCoverageConfidence(projectPath) {
|
|
|
250
252
|
const summary = `Weighted coverage: ${score}% ±${margin}%. ` +
|
|
251
253
|
`${saturated} namespaces saturated, ${partial} partial, ${noVocab} no vocabulary. ` +
|
|
252
254
|
(noVocab > 0 ? `Top gap: add trajectories for ${noVocab} uncovered namespaces.` : "");
|
|
253
|
-
return { score, margin, level, breakdown, summary };
|
|
255
|
+
return { score, margin, level, breakdown, summary, applicable };
|
|
254
256
|
}
|
|
255
257
|
/**
|
|
256
258
|
* Clear internal caches (for testing or when corpus is updated).
|
package/dist/trust/engine.js
CHANGED
|
@@ -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);
|
|
@@ -176,16 +178,30 @@ async function evaluateTrust(ctx) {
|
|
|
176
178
|
const effectiveScore = policyResult.hasCritical
|
|
177
179
|
? Math.min(overallScore, types_1.DECISION_THRESHOLDS.criticalLock)
|
|
178
180
|
: overallScore;
|
|
179
|
-
const decision = (0, score_calculator_1.determineDecision)(effectiveScore, policyResult.hasCritical, explainResult.status);
|
|
180
181
|
// ── Phase 1: Coverage-based confidence (replaces qualitative labels) ──
|
|
181
182
|
const coverageConfidence = (0, confidence_calculator_1.computeCoverageConfidence)(ctx.projectPath);
|
|
183
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
184
|
+
// §53 / R97:决策门禁
|
|
185
|
+
//
|
|
186
|
+
// 实测触发场景(immich / nocodb):authentication 子协议 0 分(21 / 586 条违规),
|
|
187
|
+
// coverageConfidence 0% LOW,mappingCoverage 4%~8% LOW —— 三项独立的
|
|
188
|
+
// 「其实没看到什么」的指标全部触底,聚合结果却仍是 83 / APPROVED / HIGH。
|
|
189
|
+
//
|
|
190
|
+
// 门禁只做**封顶**(不许说通过),不做扣分:没有观察就没有扣分依据。
|
|
191
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
192
|
+
const decisionGates = (0, score_calculator_1.evaluateDecisionGates)({
|
|
193
|
+
protocolSafety: protocolResult,
|
|
194
|
+
coverageLevel: coverageConfidence.level,
|
|
195
|
+
coverageApplicable: coverageConfidence.applicable,
|
|
196
|
+
});
|
|
197
|
+
const decision = (0, score_calculator_1.determineDecision)(effectiveScore, policyResult.hasCritical, explainResult.status, decisionGates);
|
|
182
198
|
const dimConfidences = [
|
|
183
199
|
policyResult.hasCritical ? "LOW" : coverageResult.confidence,
|
|
184
200
|
protocolResult.confidence,
|
|
185
201
|
coverageResult.confidence,
|
|
186
202
|
governanceResult.confidence,
|
|
187
203
|
];
|
|
188
|
-
const confidence = (0, score_calculator_1.determineConfidence)(dimConfidences, explainResult.status);
|
|
204
|
+
const confidence = (0, score_calculator_1.determineConfidence)(dimConfidences, explainResult.status, decisionGates.observationIncomplete);
|
|
189
205
|
// ═══════════════════════════════════════
|
|
190
206
|
// PHASE 5: ASSEMBLE
|
|
191
207
|
// ═══════════════════════════════════════
|
|
@@ -225,17 +241,21 @@ async function evaluateTrust(ctx) {
|
|
|
225
241
|
score: effectiveScore,
|
|
226
242
|
decision,
|
|
227
243
|
confidence,
|
|
244
|
+
decisionGates,
|
|
228
245
|
coverageConfidence: extractionWarning
|
|
229
246
|
? {
|
|
230
247
|
score: 0,
|
|
231
248
|
margin: 25,
|
|
232
249
|
level: "LOW",
|
|
250
|
+
// §53:IR 提取失败同样是「没得测」,不是「测得低」
|
|
251
|
+
applicable: false,
|
|
233
252
|
summary: `${extractionWarning}——本次扫描是废票,不得据此得出「干净」结论`,
|
|
234
253
|
}
|
|
235
254
|
: {
|
|
236
255
|
score: coverageConfidence.score,
|
|
237
256
|
margin: coverageConfidence.margin,
|
|
238
257
|
level: coverageConfidence.level,
|
|
258
|
+
applicable: coverageConfidence.applicable,
|
|
239
259
|
summary: coverageConfidence.summary,
|
|
240
260
|
},
|
|
241
261
|
extractionWarning,
|
|
@@ -263,6 +283,10 @@ async function evaluateTrust(ctx) {
|
|
|
263
283
|
: undefined,
|
|
264
284
|
/** SSG State Machine coverage — how many calls were matched to protocol rules */
|
|
265
285
|
ssgCoverage: ssgCov,
|
|
286
|
+
/** 2026-10-02(§49.15 方案 b):排序后的 safeguard 告警流——证据流,
|
|
287
|
+
* 不进判定。groups 为推荐形态(按族分组防整族沉底),topRanked
|
|
288
|
+
* 为全局平铺(minPerRule=1 保底)。total=0 时字段仍在(0 必须自证)。 */
|
|
289
|
+
safeguardAlerts,
|
|
266
290
|
/** Express framework adapter coverage — routes & middleware analyzed */
|
|
267
291
|
expressCoverage: expressResult.coverage.expressApps > 0
|
|
268
292
|
? {
|
|
@@ -1225,6 +1249,9 @@ async function collectProtocolViolations(ctx, callGraph) {
|
|
|
1225
1249
|
let annotationSuggestions;
|
|
1226
1250
|
// 2026-09-22:提取失败标记(跨 try 作用域,随结果返回)
|
|
1227
1251
|
let extractionWarning;
|
|
1252
|
+
// 2026-10-02:排序后的 safeguard 告警流(§49.15 方案 b)——同一批 IR 上
|
|
1253
|
+
// 收集,try 内赋值、try 外返回
|
|
1254
|
+
let safeguardAlerts;
|
|
1228
1255
|
try {
|
|
1229
1256
|
// ── P4.5 校准:TS/JS 项目在 ir.json 缺失时先提取 IR ──
|
|
1230
1257
|
// 序列必须来自「函数体内真实调用」而非「文件内函数声明名单」——
|
|
@@ -1462,6 +1489,42 @@ async function collectProtocolViolations(ctx, callGraph) {
|
|
|
1462
1489
|
if ((ctx.language || "typescript") === "c") {
|
|
1463
1490
|
annotationSuggestions = (0, annotation_suggest_1.suggestAnnotations)(functions, protocolRulesData ? new Set(protocolRulesData.rules.keys()) : undefined);
|
|
1464
1491
|
}
|
|
1492
|
+
// ── safeguard 告警流(2026-10-02,§49.15 方案 b 落地)──
|
|
1493
|
+
// 同一批 IR 上跑 detectSafeguardViolations(batch-scan 同款口径:
|
|
1494
|
+
// language 必传,params 名/类型、exported 近似 exposed),产出
|
|
1495
|
+
// **排序后的告警流**。证据流与判定分离:不进 violations、不扣分、
|
|
1496
|
+
// 不改 decision/score——排序告警是给人工审查的入口(§49.16:
|
|
1497
|
+
// 排序不删告警,一条不少;groupAlerts 分组防整族沉底)。
|
|
1498
|
+
const safeguardAlertsRaw = [];
|
|
1499
|
+
const svLang = ctx.language || "typescript";
|
|
1500
|
+
for (const f of functions) {
|
|
1501
|
+
const fCalls = f.calls || [];
|
|
1502
|
+
let svs;
|
|
1503
|
+
try {
|
|
1504
|
+
svs = (0, protocol_detector_1.detectSafeguardViolations)(fCalls, f.name, svLang, (f.params || []).map((p) => p.name), !!f.exported, (f.params || []).map((p) => p.type || ""));
|
|
1505
|
+
}
|
|
1506
|
+
catch {
|
|
1507
|
+
continue; // 单函数失败不阻断整批(与 batch-scan 容错一致)
|
|
1508
|
+
}
|
|
1509
|
+
if (svs.length === 0)
|
|
1510
|
+
continue;
|
|
1511
|
+
const params = (f.params || []).map((p) => ({ n: p.name, t: p.type || "" }));
|
|
1512
|
+
for (const v of svs) {
|
|
1513
|
+
safeguardAlertsRaw.push({
|
|
1514
|
+
rule: v.rule,
|
|
1515
|
+
file: f.file,
|
|
1516
|
+
function: f.name,
|
|
1517
|
+
calls: fCalls,
|
|
1518
|
+
nRules: svs.length,
|
|
1519
|
+
params,
|
|
1520
|
+
});
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
safeguardAlerts = {
|
|
1524
|
+
total: safeguardAlertsRaw.length,
|
|
1525
|
+
groups: (0, alert_ranker_1.groupAlerts)(safeguardAlertsRaw),
|
|
1526
|
+
topRanked: (0, alert_ranker_1.rankAlerts)(safeguardAlertsRaw, { minPerRule: 1 }),
|
|
1527
|
+
};
|
|
1465
1528
|
}
|
|
1466
1529
|
}
|
|
1467
1530
|
catch { /* best-effort */ }
|
|
@@ -1602,6 +1665,7 @@ async function collectProtocolViolations(ctx, callGraph) {
|
|
|
1602
1665
|
catch { /* best-effort */ }
|
|
1603
1666
|
return {
|
|
1604
1667
|
violations,
|
|
1668
|
+
safeguardAlerts,
|
|
1605
1669
|
coverage: {
|
|
1606
1670
|
totalApis,
|
|
1607
1671
|
lookupHits,
|