progmune-runtime 3.7.66 → 3.7.68
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,85 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [3.7.68] — 2026-10-16
|
|
4
|
+
|
|
5
|
+
### 调用方混合并集豁免(修冤枉第九刀)
|
|
6
|
+
|
|
7
|
+
**根因**:第五刀(全部调用方已守卫)与第七刀(全部调用方是作业)
|
|
8
|
+
各留下边界:① 守卫传播 ≤3 跳,第 4 跳的服务层函数(如
|
|
9
|
+
ExportService.getWorkspaceBaseUrl)在守卫集外;② 第七刀 B 判据是
|
|
10
|
+
**裸名族级**保守——同名族任一成员被 HTTP 面调用则全族不豁免,
|
|
11
|
+
族内「只被作业调用」的成员(如 VerificationNotificationService.
|
|
12
|
+
getPageContext,同为 getPageContext 裸名的 Page 族成员被 controller
|
|
13
|
+
调用)被连坐照报。两者语义上都没有未鉴权请求面:每条到达 F 的
|
|
14
|
+
路径要么已过守卫、要么源起作业处理器。
|
|
15
|
+
|
|
16
|
+
- **判据(computeMixedSurfacePropagatedSet)**:函数 F 的每个直接
|
|
17
|
+
调用方都 ∈ {已守卫传播集} ∪ {作业面传播集} ⇒ 豁免。保守规则:
|
|
18
|
+
R1 调用方集合非空(HTTP 入口无项目调用方,永不豁免);R2 每条
|
|
19
|
+
调用边必须可解析(与第五刀共享 computeResolvedCallerMap 分层消歧,
|
|
20
|
+
歧义 fail-safe);R3 任一调用方在并集外 → 照报;R4 快照轮次语义,
|
|
21
|
+
默认 1 轮(传递闭包留数据驱动);R5 返回集合只含并集新增成员
|
|
22
|
+
- **引擎接线**:豁免仅 authorization 类、函数自身不直接带标记
|
|
23
|
+
(R5/R6);被压告警进 `safeguardAlerts.suppressed` 带
|
|
24
|
+
`suppressedByMixedSurfacePropagation` 标记(标记而非丢弃,R9);
|
|
25
|
+
`PROGMUNE_MIXED_SURFACE_TOGGLE=off` 逃生门
|
|
26
|
+
- **同条件 A/B(docmost,各 fresh IR)**:主告警流 685→669
|
|
27
|
+
(−16 = Access 13 + Mutation 2 + Ownership 1,消失清单与离线重放
|
|
28
|
+
预测逐条一致、新增 0);**control(toggle off)685/140 与 3.7.67
|
|
29
|
+
基线逐条一致**;主路径 violations 11=11 / score 84=84 /
|
|
30
|
+
NEEDS_REVIEW 不变;suppressed 140→156(156=117 守卫 +23 作业
|
|
31
|
+
+16 混合,669+156=825 证据守恒)
|
|
32
|
+
- **FN 风险逐条复核**:16 条全核调用方来源——3 条守卫类
|
|
33
|
+
(@UseGuards(JwtAuthGuard) 导出路由 → export 工具函数,守卫传播
|
|
34
|
+
第 4 跳);13 条作业类(@Processor 队列 → 通知/历史服务方法 →
|
|
35
|
+
下游 getter/写入,B 判据族级连坐的成员)。TP 0
|
|
36
|
+
- **C4/C5 主力类探针结论(knife9-mixed-surface-probe)**:
|
|
37
|
+
repo-update 类 137 / getter 类 213 中,无调用方(HTTP 入口)62/92、
|
|
38
|
+
歧义边 50/70、并集外调用方 23/42——「公开设计端点/可达但非
|
|
39
|
+
鉴权敏感」是语义问题,调用方面传播已到边界(守卫深度敏感性:
|
|
40
|
+
maxDepth 3→6 仅 +6 条)
|
|
41
|
+
- **回归锁定**:单测 11 例(protocol-detector-mixed-surface.test.ts,
|
|
42
|
+
含与第七刀族级保守的差异用例)+ 引擎产品路径 2 + 检测器族
|
|
43
|
+
199/199 + 引擎 239/239;三闸门(taintpath 160 / webshape 72 /
|
|
44
|
+
fr-007 pre5-post0 + fr-016 pre7-post0);**TS 盲测 119 项目对
|
|
45
|
+
3.7.67 提交基线 LOST 0 / ADDED 0**
|
|
46
|
+
|
|
47
|
+
## [3.7.67] — 2026-10-12
|
|
48
|
+
|
|
49
|
+
### 词撞类豁免(修冤枉第八刀:mutation/access trigger 词段碰撞)
|
|
50
|
+
|
|
51
|
+
**根因**:mutation/access trigger 的 `/i` 标志让 `[A-Z]\w*` 退化为
|
|
52
|
+
`[A-Za-z]\w*`——第二分支 `(?:[A-Z]\w*|_\w+)(Add|Create|Update|...)`
|
|
53
|
+
实际匹配任何**含** mutation 词段的 camelCase 词:`for+Update`(Kysely
|
|
54
|
+
悲观锁)、`create+Cipheriv`(crypto AEAD 构造器)、`Last+UpdatedBy`
|
|
55
|
+
(select 子查询构建器)、`getConfig`(存储驱动配置)全数误命中。
|
|
56
|
+
与 3.7.63 第四刀(NIS 触发词收窄)同族,但 mutation 动词是核心词
|
|
57
|
+
不能删,改为 excludePatterns 精确豁免。
|
|
58
|
+
|
|
59
|
+
- **Mutation 规则 excludePatterns 增补**:`/\bforUpdate\b/`(Kysely/
|
|
60
|
+
TypeORM SELECT … FOR UPDATE 锁子句——逐条核对纯 SELECT 非变更)、
|
|
61
|
+
`/\bcreateCipheriv\b|\bcreateDecipheriv\b/`(crypto 构造器)、
|
|
62
|
+
`/\bwith[A-Z]\w*UpdatedBy\b/`(select 子查询构建器)
|
|
63
|
+
- **Access 规则增补**:`/\bgetConfig\b/`(存储驱动配置读取,
|
|
64
|
+
snake_case 形态已有 /get_config/)
|
|
65
|
+
- **已知 FN 边界(记录在案)**:excludePatterns 是函数级豁免——
|
|
66
|
+
若未来出现「锁后写/加密后存」形态(函数同时含撞词与真实写调用),
|
|
67
|
+
会被整体豁免。docmost 全 IR 扫描 12 个含撞词函数中唯一「写调用」
|
|
68
|
+
是 cipher.update()(加密流 API),TP 0;盲测语料 0 个函数含候选
|
|
69
|
+
token ⇒ 零漂移由构造保证(knife8-replay-probe.ts 复核)
|
|
70
|
+
- **同条件 A/B(docmost,同一 fresh IR)**:主告警流 697→685
|
|
71
|
+
(−12 = Mutation 9 + Access 3,消失清单与离线重放预测逐条一致,
|
|
72
|
+
新增 0);suppressed 140 不变;**主路径 violations 11=11 内容级
|
|
73
|
+
零差 / score 84=84 / NEEDS_REVIEW 不变**
|
|
74
|
+
- **回归锁定**:单测 6 例(protocol-detector-knife8-word-collision
|
|
75
|
+
.test.ts:四个词撞形状 + updateTable/getUserData 反例照常触发);
|
|
76
|
+
检测器套件 49/49;引擎套件 27/27;taintpath 160 / webshape 72 /
|
|
77
|
+
fr-007 5-0 / fr-016 7-0 维持;TS 盲测对 HEAD 基线 LOST 0/ADDED 0
|
|
78
|
+
- **预研探针(可复跑)**:knife8-word-collision-probe.ts(577 条授权
|
|
79
|
+
族分类:词撞类仅 ~15 条=2.6%,主力是 repo-update 149/ getter 198
|
|
80
|
+
语义类——下刀候选)+ knife8-trigger-origin-probe.ts(触发来源定位:
|
|
81
|
+
/i 退化机制)+ knife8-replay-probe.ts(离线重放预测 12=实际 12)
|
|
82
|
+
|
|
3
83
|
## [3.7.66] — 2026-10-12
|
|
4
84
|
|
|
5
85
|
### 队列作业调用方传播(修冤枉第七刀:授权族告警的「只被作业调用的函数没有请求面」根因)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vitest_1 = require("vitest");
|
|
4
|
+
const protocol_detector_1 = require("./protocol-detector");
|
|
5
|
+
/**
|
|
6
|
+
* 修冤枉第八刀(2026-10-12)词撞类回归测试。
|
|
7
|
+
*
|
|
8
|
+
* 根因:mutation/access trigger 的 /i 标志让 [A-Z]\w* 退化为 [A-Za-z]\w*,
|
|
9
|
+
* 「for+Update」「create+Cipheriv」「Last+UpdatedBy」等非变更词段全数命中。
|
|
10
|
+
* 修法:excludePatterns 增补四个形状(docmost 12 条告警逐条核过 TP 0——
|
|
11
|
+
* forUpdate=Kysely 悲观锁 SELECT、createCipheriv/Decipheriv=crypto 构造器、
|
|
12
|
+
* withLastUpdatedBy=select 子查询构建器、getConfig=存储驱动配置读取)。
|
|
13
|
+
* 已知 FN 边界(记录在案):函数若同时含撞词与真实写调用(锁后写/
|
|
14
|
+
* 加密后存),excludePatterns 会整体豁免该函数——docmost 与盲测语料
|
|
15
|
+
* 均无此形态(knife8-replay-probe.ts 复核)。
|
|
16
|
+
*/
|
|
17
|
+
(0, vitest_1.describe)("修冤枉第八刀:词撞类 excludePatterns", () => {
|
|
18
|
+
(0, vitest_1.it)("forUpdate(Kysely 悲观锁 SELECT)不再触发 Unauthenticated Mutation", () => {
|
|
19
|
+
const v = (0, protocol_detector_1.detectSafeguardViolations)(["select", "selectFrom", "where", "forUpdate"], "PageRepo.findById", "typescript");
|
|
20
|
+
(0, vitest_1.expect)(v.some((x) => x.rule === "Authorization (Unauthenticated Mutation)")).toBe(false);
|
|
21
|
+
});
|
|
22
|
+
(0, vitest_1.it)("createCipheriv/createDecipheriv(crypto 构造器)不再触发 Mutation", () => {
|
|
23
|
+
const v = (0, protocol_detector_1.detectSafeguardViolations)(["randomBytes", "createCipheriv", "concat", "update", "final"], "EncryptionService.encrypt", "typescript");
|
|
24
|
+
(0, vitest_1.expect)(v.some((x) => x.rule === "Authorization (Unauthenticated Mutation)")).toBe(false);
|
|
25
|
+
const v2 = (0, protocol_detector_1.detectSafeguardViolations)(["createDecipheriv", "setAuthTag", "concat", "update"], "EncryptionService.decrypt", "typescript");
|
|
26
|
+
(0, vitest_1.expect)(v2.some((x) => x.rule === "Authorization (Unauthenticated Mutation)")).toBe(false);
|
|
27
|
+
});
|
|
28
|
+
(0, vitest_1.it)("withLastUpdatedBy(select 子查询构建器)不再触发 Mutation", () => {
|
|
29
|
+
const v = (0, protocol_detector_1.detectSafeguardViolations)(["as", "jsonObjectFrom", "whereRef", "select"], "PageRepo.withLastUpdatedBy", "typescript");
|
|
30
|
+
(0, vitest_1.expect)(v.some((x) => x.rule === "Authorization (Unauthenticated Mutation)")).toBe(false);
|
|
31
|
+
});
|
|
32
|
+
(0, vitest_1.it)("getConfig(存储驱动配置读取)不再触发 Unauthenticated Access", () => {
|
|
33
|
+
const v = (0, protocol_detector_1.detectSafeguardViolations)([], "S3Driver.getConfig", "typescript");
|
|
34
|
+
(0, vitest_1.expect)(v.some((x) => x.rule === "Authorization (Unauthenticated Access)")).toBe(false);
|
|
35
|
+
});
|
|
36
|
+
(0, vitest_1.it)("反例:真实写调用(updateTable/insertInto)照常触发 Mutation", () => {
|
|
37
|
+
const v = (0, protocol_detector_1.detectSafeguardViolations)(["dbOrTx", "updateTable", "set", "where", "execute"], "PageRepo.updatePages", "typescript");
|
|
38
|
+
(0, vitest_1.expect)(v.some((x) => x.rule === "Authorization (Unauthenticated Mutation)")).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
(0, vitest_1.it)("反例:真实数据读取(getUserData)照常触发 Access", () => {
|
|
41
|
+
const v = (0, protocol_detector_1.detectSafeguardViolations)(["selectFrom", "where", "executeTakeFirst"], "UserRepo.getUserData", "typescript");
|
|
42
|
+
(0, vitest_1.expect)(v.some((x) => x.rule === "Authorization (Unauthenticated Access)")).toBe(true);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vitest_1 = require("vitest");
|
|
4
|
+
const protocol_detector_1 = require("./protocol-detector");
|
|
5
|
+
/**
|
|
6
|
+
* 调用方混合并集豁免(修冤枉第九刀,2026-10-16)单元测试。
|
|
7
|
+
*
|
|
8
|
+
* 判据(见 computeMixedSurfacePropagatedSet 注释):
|
|
9
|
+
* 函数 F 的每个直接调用方都 ∈ {已守卫传播集} ∪ {作业面传播集} ⇒
|
|
10
|
+
* F 无未鉴权请求面(每条到达 F 的路径要么已过守卫、要么源起作业)。
|
|
11
|
+
* R1-R5:调用方非空 / 边全解析 / 全部落在并集内 / 快照轮次默认 1 /
|
|
12
|
+
* 返回集合只含并集新增成员。
|
|
13
|
+
* 消费方(engine)负责 R5/R6:只压 authorization 类、且不压直接标记函数。
|
|
14
|
+
*/
|
|
15
|
+
const MG = "__progmune_auth_machinery__";
|
|
16
|
+
const MJ = "__progmune_no_request_surface__";
|
|
17
|
+
/** 便捷:guard/job 集 + 混合集一并算 */
|
|
18
|
+
function sets(fns, opts) {
|
|
19
|
+
const guard = (0, protocol_detector_1.computeGuardPropagatedSet)(fns);
|
|
20
|
+
const job = (0, protocol_detector_1.computeJobSurfacePropagatedSet)(fns);
|
|
21
|
+
const mixed = (0, protocol_detector_1.computeMixedSurfacePropagatedSet)(fns, guard, job, opts);
|
|
22
|
+
return { guard, job, mixed };
|
|
23
|
+
}
|
|
24
|
+
(0, vitest_1.describe)("computeMixedSurfacePropagatedSet(调用方混合并集,第九刀)", () => {
|
|
25
|
+
(0, vitest_1.it)("混合调用方(已守卫 + 作业)⇒ 豁免", () => {
|
|
26
|
+
const fns = [
|
|
27
|
+
{ name: "GuardedController.create", file: "c/page.controller.ts", calls: [MG, "log"] },
|
|
28
|
+
{ name: "AuditService.log", file: "s/audit.service.ts", calls: ["fn"] },
|
|
29
|
+
{ name: "DigestJob.run", file: "q/digest.job.ts", calls: [MJ, "fn"] },
|
|
30
|
+
{ name: "MixedTarget.fn", file: "m/target.ts", calls: [] },
|
|
31
|
+
];
|
|
32
|
+
// AuditService.log:全部调用方已守卫 ⇒ 守卫集成员;
|
|
33
|
+
// MixedTarget.fn 调用方 = {AuditService.log[g], DigestJob.run[j]} ⇒ 混合豁免
|
|
34
|
+
const { guard, job, mixed } = sets(fns);
|
|
35
|
+
(0, vitest_1.expect)(guard.has("AuditService.log")).toBe(true);
|
|
36
|
+
(0, vitest_1.expect)(job.has("DigestJob.run")).toBe(true);
|
|
37
|
+
(0, vitest_1.expect)(mixed.has("MixedTarget.fn")).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
(0, vitest_1.it)("全部调用方已守卫但超过守卫传播深度(第 4 跳)⇒ 并集豁免", () => {
|
|
40
|
+
const fns = [
|
|
41
|
+
{ name: "AuthRoot", file: "f.ts", calls: [MG, "b"] },
|
|
42
|
+
{ name: "b", file: "f.ts", calls: ["c"] },
|
|
43
|
+
{ name: "c", file: "f.ts", calls: ["d"] },
|
|
44
|
+
{ name: "d", file: "f.ts", calls: ["e"] },
|
|
45
|
+
{ name: "e", file: "f.ts", calls: [] },
|
|
46
|
+
];
|
|
47
|
+
const { guard, mixed } = sets(fns);
|
|
48
|
+
(0, vitest_1.expect)(guard.has("d")).toBe(true); // 第 3 跳:在守卫集内
|
|
49
|
+
(0, vitest_1.expect)(guard.has("e")).toBe(false); // 第 4 跳:守卫传播边界外
|
|
50
|
+
(0, vitest_1.expect)(mixed.has("e")).toBe(true); // 并集判据罩住(调用方 d ∈ 守卫集)
|
|
51
|
+
});
|
|
52
|
+
(0, vitest_1.it)("作业调用方与作业面集族级保守的差异:按边解析只豁免作业链成员", () => {
|
|
53
|
+
// docmost 实测形态:作业处理器与被调方同「类根」(processExpiring
|
|
54
|
+
// → getPageContext),逐边消歧可解析。B 判据(第七刀)在裸名族含
|
|
55
|
+
// HTTP 调用方时全族不豁免;并集判据按边解析——只豁免作业链上
|
|
56
|
+
// 那个成员,HTTP 面成员照报。
|
|
57
|
+
const fns = [
|
|
58
|
+
{ name: "VerificationNotificationService.processExpiring", file: "s/verification.service.ts", calls: [MJ, "getPageContext"] },
|
|
59
|
+
{ name: "VerificationNotificationService.getPageContext", file: "s/verification.service.ts", calls: [] },
|
|
60
|
+
{ name: "PageNotificationService.getPageContext", file: "s/page.service.ts", calls: [] },
|
|
61
|
+
{ name: "PageNotificationService.notify", file: "s/page.service.ts", calls: ["getPageContext"] }, // 同文件 HTTP 面调用方
|
|
62
|
+
];
|
|
63
|
+
const { job, mixed } = sets(fns);
|
|
64
|
+
// 第七刀 B 判据:裸名族含非作业调用方 ⇒ 全族不豁免
|
|
65
|
+
(0, vitest_1.expect)(job.has("VerificationNotificationService.getPageContext")).toBe(false);
|
|
66
|
+
(0, vitest_1.expect)(job.has("PageNotificationService.getPageContext")).toBe(false);
|
|
67
|
+
// 第九刀并集:边解析逐条——Verification 成员只被作业调用 ⇒ 豁免;
|
|
68
|
+
// Page 成员被 notify(并集外)调用 ⇒ 不豁免
|
|
69
|
+
(0, vitest_1.expect)(mixed.has("VerificationNotificationService.getPageContext")).toBe(true);
|
|
70
|
+
(0, vitest_1.expect)(mixed.has("PageNotificationService.getPageContext")).toBe(false);
|
|
71
|
+
});
|
|
72
|
+
(0, vitest_1.it)("任一调用方在并集外 ⇒ 不豁免(fail-safe)", () => {
|
|
73
|
+
const fns = [
|
|
74
|
+
{ name: "GuardedController.create", file: "c.ts", calls: [MG, "write"] },
|
|
75
|
+
{ name: "PublicController.import", file: "c2.ts", calls: ["write"] },
|
|
76
|
+
{ name: "write", file: "r.ts", calls: [] },
|
|
77
|
+
];
|
|
78
|
+
const { mixed } = sets(fns);
|
|
79
|
+
(0, vitest_1.expect)(mixed.has("write")).toBe(false);
|
|
80
|
+
});
|
|
81
|
+
(0, vitest_1.it)("无调用方(入口/未连通)⇒ 不豁免(R1)", () => {
|
|
82
|
+
const fns = [{ name: "orphanEntry", file: "m.ts", calls: [] }];
|
|
83
|
+
const { mixed } = sets(fns);
|
|
84
|
+
(0, vitest_1.expect)(mixed.has("orphanEntry")).toBe(false);
|
|
85
|
+
});
|
|
86
|
+
(0, vitest_1.it)("歧义调用边 ⇒ 不豁免(R2 fail-safe)", () => {
|
|
87
|
+
// 同名裸名两个候选、调用方无法消歧(不同文件不同目录不同类根)
|
|
88
|
+
const fns = [
|
|
89
|
+
{ name: "GuardedController.create", file: "c/ctrl.ts", calls: [MG, "load"] },
|
|
90
|
+
{ name: "XxxService.load", file: "x/x.service.ts", calls: [] },
|
|
91
|
+
{ name: "YyyService.load", file: "y/y.service.ts", calls: [] },
|
|
92
|
+
];
|
|
93
|
+
const { mixed } = sets(fns);
|
|
94
|
+
(0, vitest_1.expect)(mixed.has("XxxService.load")).toBe(false);
|
|
95
|
+
(0, vitest_1.expect)(mixed.has("YyyService.load")).toBe(false);
|
|
96
|
+
});
|
|
97
|
+
(0, vitest_1.it)("默认 1 轮:并集下游的下游不豁免(传递闭包未开)", () => {
|
|
98
|
+
const fns = [
|
|
99
|
+
{ name: "Job.run", file: "q.ts", calls: [MJ, "b"] },
|
|
100
|
+
{ name: "b", file: "f.ts", calls: ["c"] },
|
|
101
|
+
{ name: "c", file: "f.ts", calls: ["d"] },
|
|
102
|
+
{ name: "d", file: "f.ts", calls: [] },
|
|
103
|
+
];
|
|
104
|
+
const { job, mixed } = sets(fns);
|
|
105
|
+
(0, vitest_1.expect)(job.has("b")).toBe(true); // b 已在作业面集(第七刀,第 1 跳)
|
|
106
|
+
(0, vitest_1.expect)(mixed.has("b")).toBe(false); // R5:既有成员不重复进 mixed
|
|
107
|
+
(0, vitest_1.expect)(mixed.has("c")).toBe(true); // c 调用方 {b[j]} ⊆ 并集 ⇒ 混合豁免
|
|
108
|
+
(0, vitest_1.expect)(mixed.has("d")).toBe(false); // d 调用方 {c} 不在并集(1 轮)⇒ 不豁免
|
|
109
|
+
});
|
|
110
|
+
(0, vitest_1.it)("maxRounds=2 ⇒ 传递两跳成立(快照轮次语义)", () => {
|
|
111
|
+
const fns = [
|
|
112
|
+
{ name: "Job.run", file: "q.ts", calls: [MJ, "b"] },
|
|
113
|
+
{ name: "b", file: "f.ts", calls: ["c"] },
|
|
114
|
+
{ name: "c", file: "f.ts", calls: ["d"] },
|
|
115
|
+
{ name: "d", file: "f.ts", calls: [] },
|
|
116
|
+
];
|
|
117
|
+
const { mixed } = sets(fns, { maxRounds: 2 });
|
|
118
|
+
(0, vitest_1.expect)(mixed.has("c")).toBe(true);
|
|
119
|
+
(0, vitest_1.expect)(mixed.has("d")).toBe(true); // 第 2 轮:d 调用方 {c} ⊆ 并集∪第 1 轮新增
|
|
120
|
+
});
|
|
121
|
+
(0, vitest_1.it)("返回集合只含并集新增成员(R5:种子/既有成员由各自刀处理)", () => {
|
|
122
|
+
const fns = [
|
|
123
|
+
{ name: "GuardedController.create", file: "c.ts", calls: [MG, "service"] },
|
|
124
|
+
{ name: "Job.run", file: "q.ts", calls: [MJ, "jobOnly"] },
|
|
125
|
+
{ name: "service", file: "s.ts", calls: ["target"] },
|
|
126
|
+
{ name: "jobOnly", file: "s.ts", calls: ["target"] },
|
|
127
|
+
{ name: "target", file: "s.ts", calls: [] },
|
|
128
|
+
];
|
|
129
|
+
const { guard, job, mixed } = sets(fns);
|
|
130
|
+
// 种子/既有成员(GuardedController.create、Job.run、service、jobOnly)不重复进 mixed
|
|
131
|
+
(0, vitest_1.expect)(guard.has("service")).toBe(true); // service 全部调用方已守卫 ⇒ 守卫集成员
|
|
132
|
+
(0, vitest_1.expect)(job.has("jobOnly")).toBe(true); // jobOnly 只被作业调用 ⇒ 作业面成员
|
|
133
|
+
(0, vitest_1.expect)(mixed.has("GuardedController.create")).toBe(false);
|
|
134
|
+
(0, vitest_1.expect)(mixed.has("Job.run")).toBe(false);
|
|
135
|
+
(0, vitest_1.expect)(mixed.has("service")).toBe(false);
|
|
136
|
+
(0, vitest_1.expect)(mixed.has("jobOnly")).toBe(false);
|
|
137
|
+
// 新增成员:target 调用方 = {service[g], jobOnly[j]} ⇒ 混合豁免
|
|
138
|
+
(0, vitest_1.expect)(mixed.has("target")).toBe(true);
|
|
139
|
+
});
|
|
140
|
+
(0, vitest_1.it)("自调不提供豁免证据", () => {
|
|
141
|
+
const fns = [{ name: "CleanupService.cleanup", file: "s.ts", calls: ["cleanup"] }];
|
|
142
|
+
const { mixed } = sets(fns);
|
|
143
|
+
(0, vitest_1.expect)(mixed.has("CleanupService.cleanup")).toBe(false);
|
|
144
|
+
});
|
|
145
|
+
(0, vitest_1.it)("直接标记函数不进入混合新增集合(自身已是守卫/作业种子)", () => {
|
|
146
|
+
const fns = [
|
|
147
|
+
{ name: "GuardedController.create", file: "c.ts", calls: [MG, "self2"] },
|
|
148
|
+
{ name: "self2", file: "c.ts", calls: [] },
|
|
149
|
+
];
|
|
150
|
+
const { mixed } = sets(fns);
|
|
151
|
+
(0, vitest_1.expect)(mixed.has("GuardedController.create")).toBe(false);
|
|
152
|
+
(0, vitest_1.expect)(mixed.has("self2")).toBe(false); // self2 已在守卫集(第 1 跳)
|
|
153
|
+
});
|
|
154
|
+
});
|
|
@@ -14,8 +14,10 @@ exports.isAuthFunctionName = isAuthFunctionName;
|
|
|
14
14
|
exports.classifyParamType = classifyParamType;
|
|
15
15
|
exports.detectSafeguardViolations = detectSafeguardViolations;
|
|
16
16
|
exports.buildCallerMap = buildCallerMap;
|
|
17
|
+
exports.computeResolvedCallerMap = computeResolvedCallerMap;
|
|
17
18
|
exports.computeGuardPropagatedSet = computeGuardPropagatedSet;
|
|
18
19
|
exports.computeJobSurfacePropagatedSet = computeJobSurfacePropagatedSet;
|
|
20
|
+
exports.computeMixedSurfacePropagatedSet = computeMixedSurfacePropagatedSet;
|
|
19
21
|
exports.detectSafeguardViolationsV7 = detectSafeguardViolationsV7;
|
|
20
22
|
exports.detectProtocolViolations = detectProtocolViolations;
|
|
21
23
|
exports.validateProtocolState = validateProtocolState;
|
|
@@ -451,6 +453,9 @@ const SAFEGUARD_RULES = [
|
|
|
451
453
|
/findBig|findKey|findPk/, // internal search (not API)
|
|
452
454
|
/get_cache\w*/, // cache machinery (get_cache_alias — not data access)
|
|
453
455
|
/get_context_data/, // Django CBV context assembly — framework verb
|
|
456
|
+
/\bgetConfig\b/, // 修冤枉第八刀(2026-10-12):存储驱动配置读取
|
|
457
|
+
// (S3/Azure/Local driver getConfig,无 DB 调用——
|
|
458
|
+
// 配置非用户数据;snake_case 形态已有 /get_config/)
|
|
454
459
|
],
|
|
455
460
|
},
|
|
456
461
|
// Mutations without any authentication. The Unauthenticated Access rule above
|
|
@@ -491,6 +496,15 @@ const SAFEGUARD_RULES = [
|
|
|
491
496
|
/set_config/, // configuration setter
|
|
492
497
|
/set_option/, // option setter
|
|
493
498
|
/\bsetup\b/, // framework/component setup — configuration, not data mutation
|
|
499
|
+
// 修冤枉第八刀(2026-10-12):词撞类——trigger 的 /i 让 [A-Z]\w* 退化为
|
|
500
|
+
// [A-Za-z]\w*,「for+Update」「create+Cipheriv」等非变更词段全数命中。
|
|
501
|
+
// 逐条核对(docmost 12 函数):forUpdate=Kysely 悲观锁 SELECT 非变更、
|
|
502
|
+
// createCipheriv/createDecipheriv=crypto AEAD 构造器、withLastUpdatedBy=
|
|
503
|
+
// select 子查询构建器。锁后写/加密后存形态在 docmost 与盲测语料均不存在
|
|
504
|
+
// (FN 风险记录在案:真出现时 excludePatterns 会整体豁免该函数)。
|
|
505
|
+
/\bforUpdate\b/, // Kysely/TypeORM SELECT … FOR UPDATE 锁子句
|
|
506
|
+
/\bcreateCipheriv\b|\bcreateDecipheriv\b/, // node crypto AEAD 构造器(加密流非数据变更)
|
|
507
|
+
/\bwith[A-Z]\w*UpdatedBy\b/, // select 子查询构建器名("LastUpdatedBy" 词段撞 Updated)
|
|
494
508
|
],
|
|
495
509
|
},
|
|
496
510
|
// ── Data Integrity (Foreign Key Validation) ──
|
|
@@ -1631,31 +1645,17 @@ function buildCallerMap(funcs) {
|
|
|
1631
1645
|
return callerMap;
|
|
1632
1646
|
}
|
|
1633
1647
|
/**
|
|
1634
|
-
*
|
|
1635
|
-
*
|
|
1636
|
-
*
|
|
1637
|
-
* 语义:「函数的**全部**直接调用方都已守卫 ⇒ 该函数视为守卫后」。
|
|
1638
|
-
* 证据:docmost 补标注 111 条授权族告警全 FP,其中 49 条(44%)是
|
|
1639
|
-
* controller 已 @UseGuards、service/repo 层函数被误报。保守量化
|
|
1640
|
-
* (blind-benchmark/auth-guard-propagation-probe.ts):
|
|
1641
|
-
* docmost 745 条授权族告警可压 47 条(6.3%),TS 盲测可压 0 条。
|
|
1642
|
-
*
|
|
1643
|
-
* 保守规则(R1-R4,fail-safe 方向):
|
|
1644
|
-
* R1 只有【全部】调用方已守卫才豁免;任一调用方无守卫证据 → 照报
|
|
1645
|
-
* R2 守卫沿调用链向下传播 ≤ maxDepth(默认 3)跳
|
|
1646
|
-
* R3 传播通道只认 `marker`(默认 __progmune_auth_machinery__)一个标记
|
|
1647
|
-
* R4 裸名调用解析分层消歧(唯一才解析,否则给全部候选挂未解析边):
|
|
1648
|
-
* ① 名字后缀唯一 ② 同文件唯一 ③ 同目录唯一
|
|
1649
|
-
* ④ 类根对齐唯一(调用方 XController/Service → 候选类根同为 X)
|
|
1650
|
-
* R6 豁免只给【非直接标记】函数——自身带标记的维持既有 E2 语义
|
|
1651
|
-
* (只接受标记的规则由 detector 自身处理;不接受标记的规则照报)
|
|
1648
|
+
* 反向调用图(分层消歧,R4):被调【全限定名】→ 调用方列表
|
|
1649
|
+
* (resolved=false = 歧义 fail-safe)。computeGuardPropagatedSet(第五刀)
|
|
1650
|
+
* 与 computeMixedSurfacePropagatedSet(第九刀)共享同一边解析。
|
|
1652
1651
|
*
|
|
1653
|
-
*
|
|
1654
|
-
*
|
|
1652
|
+
* 边解析分层消歧(R4):① 后缀唯一 ② 同文件唯一 ③ 同目录唯一
|
|
1653
|
+
* ④ 类根对齐唯一(XController→XService→XRepo 的 MVC 命名惯例);
|
|
1654
|
+
* 全部失败 ⇒ 给全部同名候选挂未解析边(fail-safe)。
|
|
1655
|
+
* 调用方自身排除:controller.create 调 "create" 时自己就是后缀候选
|
|
1656
|
+
* 之一,同文件/同目录层会错误自我捕获(递归/自调不提供守卫证据)。
|
|
1655
1657
|
*/
|
|
1656
|
-
function
|
|
1657
|
-
const marker = opts?.marker ?? "__progmune_auth_machinery__";
|
|
1658
|
-
const maxDepth = opts?.maxDepth ?? 3;
|
|
1658
|
+
function computeResolvedCallerMap(funcs) {
|
|
1659
1659
|
const byName = new Map();
|
|
1660
1660
|
for (const f of funcs)
|
|
1661
1661
|
if (!byName.has(f.name))
|
|
@@ -1668,10 +1668,6 @@ function computeGuardPropagatedSet(funcs, opts) {
|
|
|
1668
1668
|
bySuffix.set(bare, []);
|
|
1669
1669
|
bySuffix.get(bare).push(f.name);
|
|
1670
1670
|
}
|
|
1671
|
-
// 反向调用图:被调【全限定名】→ 调用方列表(resolved=false = 歧义 fail-safe)。
|
|
1672
|
-
// 边解析分层消歧(R4):① 后缀唯一 ② 同文件唯一 ③ 同目录唯一
|
|
1673
|
-
// ④ 类根对齐唯一(XController→XService→XRepo 的 MVC 命名惯例);
|
|
1674
|
-
// 全部失败 ⇒ 给全部同名候选挂未解析边(fail-safe)。
|
|
1675
1671
|
const dirOf = (p) => (p ? p.split("/").slice(0, -1).join("/") : "");
|
|
1676
1672
|
const rootOf = (n) => {
|
|
1677
1673
|
const cls = n.split(".")[0] || n;
|
|
@@ -1682,8 +1678,6 @@ function computeGuardPropagatedSet(funcs, opts) {
|
|
|
1682
1678
|
for (const c of f.calls || []) {
|
|
1683
1679
|
if (!bySuffix.has(c))
|
|
1684
1680
|
continue; // 非项目函数名(外部库调用)→ 无守卫语义
|
|
1685
|
-
// 排除调用方自身:controller.create 调 "create" 时,自己就是后缀
|
|
1686
|
-
// 候选之一,同文件/同目录层会错误自我捕获(递归/自调不提供守卫证据)
|
|
1687
1681
|
const cands = bySuffix.get(c).filter((n) => n !== f.name);
|
|
1688
1682
|
if (cands.length === 0)
|
|
1689
1683
|
continue;
|
|
@@ -1728,6 +1722,35 @@ function computeGuardPropagatedSet(funcs, opts) {
|
|
|
1728
1722
|
}
|
|
1729
1723
|
}
|
|
1730
1724
|
}
|
|
1725
|
+
return calleeToCallers;
|
|
1726
|
+
}
|
|
1727
|
+
/**
|
|
1728
|
+
* 修冤枉第五刀(2026-10-10):守卫沿调用链传播——
|
|
1729
|
+
* 授权族告警的「守卫在 controller 层、名字级看不见」根因的保守豁免。
|
|
1730
|
+
*
|
|
1731
|
+
* 语义:「函数的**全部**直接调用方都已守卫 ⇒ 该函数视为守卫后」。
|
|
1732
|
+
* 证据:docmost 补标注 111 条授权族告警全 FP,其中 49 条(44%)是
|
|
1733
|
+
* controller 已 @UseGuards、service/repo 层函数被误报。保守量化
|
|
1734
|
+
* (blind-benchmark/auth-guard-propagation-probe.ts):
|
|
1735
|
+
* docmost 745 条授权族告警可压 47 条(6.3%),TS 盲测可压 0 条。
|
|
1736
|
+
*
|
|
1737
|
+
* 保守规则(R1-R4,fail-safe 方向):
|
|
1738
|
+
* R1 只有【全部】调用方已守卫才豁免;任一调用方无守卫证据 → 照报
|
|
1739
|
+
* R2 守卫沿调用链向下传播 ≤ maxDepth(默认 3)跳
|
|
1740
|
+
* R3 传播通道只认 `marker`(默认 __progmune_auth_machinery__)一个标记
|
|
1741
|
+
* R4 裸名调用解析分层消歧(唯一才解析,否则给全部候选挂未解析边):
|
|
1742
|
+
* ① 名字后缀唯一 ② 同文件唯一 ③ 同目录唯一
|
|
1743
|
+
* ④ 类根对齐唯一(调用方 XController/Service → 候选类根同为 X)
|
|
1744
|
+
* R6 豁免只给【非直接标记】函数——自身带标记的维持既有 E2 语义
|
|
1745
|
+
* (只接受标记的规则由 detector 自身处理;不接受标记的规则照报)
|
|
1746
|
+
*
|
|
1747
|
+
* 返回集合含直接标记函数本身(消费方须自行应用 R6 过滤),
|
|
1748
|
+
* 因为传递闭包的种子就是它们。
|
|
1749
|
+
*/
|
|
1750
|
+
function computeGuardPropagatedSet(funcs, opts) {
|
|
1751
|
+
const marker = opts?.marker ?? "__progmune_auth_machinery__";
|
|
1752
|
+
const maxDepth = opts?.maxDepth ?? 3;
|
|
1753
|
+
const calleeToCallers = computeResolvedCallerMap(funcs);
|
|
1731
1754
|
// 种子:直接带标记的函数
|
|
1732
1755
|
const guarded = new Set();
|
|
1733
1756
|
for (const f of funcs)
|
|
@@ -1815,6 +1838,53 @@ function computeJobSurfacePropagatedSet(funcs, opts) {
|
|
|
1815
1838
|
}
|
|
1816
1839
|
return exempt;
|
|
1817
1840
|
}
|
|
1841
|
+
/**
|
|
1842
|
+
* 修冤枉第九刀(2026-10-16):调用方混合并集豁免——
|
|
1843
|
+
* 「函数的每个直接调用方都 ∈ {已守卫传播集} ∪ {作业面传播集}」。
|
|
1844
|
+
*
|
|
1845
|
+
* 第五刀要求【全部调用方已守卫】,第七刀要求【全部调用方是作业】;
|
|
1846
|
+
* 混合形态(部分调用方是已守卫 controller、部分是无请求面作业)两者都
|
|
1847
|
+
* 覆盖不到,但语义上同样无未鉴权请求面:每条到达 F 的路径要么已过守卫
|
|
1848
|
+
* (guardSet 成员自身及其调用链上方都已守卫),要么源起作业处理器
|
|
1849
|
+
* (jobSet 成员无请求面)。C4 repo 层 update* 语义类与 C5 getter 语义类
|
|
1850
|
+
* 的主力根因就是这种调用方混合(knife8-word-collision-probe 分类结论)。
|
|
1851
|
+
*
|
|
1852
|
+
* 保守规则(与第五刀共享边解析,见 computeResolvedCallerMap R4):
|
|
1853
|
+
* R1 调用方集合非空——入口函数(HTTP 面)无项目调用方,永不豁免
|
|
1854
|
+
* R2 每条调用边都必须可解析(resolved=true),歧义 fail-safe
|
|
1855
|
+
* R3 调用方必须 ∈ guardSet ∪ jobSet;任一调用方不在此并集 → 照报
|
|
1856
|
+
* R4 快照轮次语义(与第五刀一致);默认 1 轮(只豁免直接混合下游;
|
|
1857
|
+
* 传递闭包留数据驱动再开,与第七刀同决策)
|
|
1858
|
+
* R5 返回集合只含并集【新增】成员——guardSet/jobSet 成员由各自刀处理
|
|
1859
|
+
*
|
|
1860
|
+
* guardSet/jobSet 由调用方传入(engine 处与第五/七刀同源:
|
|
1861
|
+
* computeGuardPropagatedSet / computeJobSurfacePropagatedSet 的返回值)。
|
|
1862
|
+
*/
|
|
1863
|
+
function computeMixedSurfacePropagatedSet(funcs, guardSet, jobSet, opts) {
|
|
1864
|
+
const maxRounds = opts?.maxRounds ?? 1;
|
|
1865
|
+
const calleeToCallers = computeResolvedCallerMap(funcs);
|
|
1866
|
+
const base = new Set([...guardSet, ...jobSet]);
|
|
1867
|
+
const mixed = new Set(); // R5:只记本轮新增
|
|
1868
|
+
for (let round = 0; round < maxRounds; round++) {
|
|
1869
|
+
const toAdd = [];
|
|
1870
|
+
for (const f of funcs) {
|
|
1871
|
+
if (base.has(f.name))
|
|
1872
|
+
continue;
|
|
1873
|
+
const callers = calleeToCallers.get(f.name) || [];
|
|
1874
|
+
if (callers.length === 0)
|
|
1875
|
+
continue; // R1:无调用方(入口/未连通)→ 不豁免
|
|
1876
|
+
if (callers.every((c) => c.resolved && base.has(c.caller)))
|
|
1877
|
+
toAdd.push(f.name);
|
|
1878
|
+
}
|
|
1879
|
+
if (toAdd.length === 0)
|
|
1880
|
+
break;
|
|
1881
|
+
for (const n of toAdd) {
|
|
1882
|
+
base.add(n);
|
|
1883
|
+
mixed.add(n);
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
return mixed;
|
|
1887
|
+
}
|
|
1818
1888
|
/**
|
|
1819
1889
|
* v7: Build safeguard context — own → same-file → caller chain.
|
|
1820
1890
|
*
|
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.68";
|
|
24
24
|
function verify(filePath) {
|
|
25
25
|
const cert = (0, certify_1.certify)(filePath);
|
|
26
26
|
const kb = (0, protocol_knowledge_1.buildKnowledgeBase)();
|
package/dist/trust/engine.js
CHANGED
|
@@ -1512,6 +1512,14 @@ async function collectProtocolViolations(ctx, callGraph) {
|
|
|
1512
1512
|
const jobSurfacePropagated = process.env.PROGMUNE_JOB_SURFACE_TOGGLE === "off"
|
|
1513
1513
|
? new Set()
|
|
1514
1514
|
: (0, protocol_detector_1.computeJobSurfacePropagatedSet)(functions);
|
|
1515
|
+
// 修冤枉第九刀(2026-10-16):调用方混合并集豁免——「每个直接
|
|
1516
|
+
// 调用方都 ∈ {已守卫传播集} ∪ {作业面传播集}」⇒ 无未鉴权请求面。
|
|
1517
|
+
// 保守判据见 computeMixedSurfacePropagatedSet(R1-R5):调用方
|
|
1518
|
+
// 非空、边全解析、默认 1 轮。豁免仅作用于 authorization 类且
|
|
1519
|
+
// 函数自身不直接带标记(R5/R6)——与第五/七刀同:标记而非丢弃。
|
|
1520
|
+
const mixedSurfacePropagated = process.env.PROGMUNE_MIXED_SURFACE_TOGGLE === "off"
|
|
1521
|
+
? new Set()
|
|
1522
|
+
: (0, protocol_detector_1.computeMixedSurfacePropagatedSet)(functions, guardPropagated, jobSurfacePropagated);
|
|
1515
1523
|
for (const f of functions) {
|
|
1516
1524
|
const fCalls = f.calls || [];
|
|
1517
1525
|
let svs;
|
|
@@ -1528,6 +1536,7 @@ async function collectProtocolViolations(ctx, callGraph) {
|
|
|
1528
1536
|
const directlyJobMarked = fCalls.includes("__progmune_no_request_surface__");
|
|
1529
1537
|
const guardExempt = !directlyMarked && guardPropagated.has(f.name);
|
|
1530
1538
|
const jobExempt = !directlyJobMarked && jobSurfacePropagated.has(f.name);
|
|
1539
|
+
const mixedExempt = !directlyMarked && !directlyJobMarked && mixedSurfacePropagated.has(f.name);
|
|
1531
1540
|
for (const v of svs) {
|
|
1532
1541
|
const alert = {
|
|
1533
1542
|
rule: v.rule,
|
|
@@ -1547,6 +1556,10 @@ async function collectProtocolViolations(ctx, callGraph) {
|
|
|
1547
1556
|
alert.suppressedByJobSurfacePropagation = true;
|
|
1548
1557
|
suppressed = true;
|
|
1549
1558
|
}
|
|
1559
|
+
if (mixedExempt) {
|
|
1560
|
+
alert.suppressedByMixedSurfacePropagation = true;
|
|
1561
|
+
suppressed = true;
|
|
1562
|
+
}
|
|
1550
1563
|
if (suppressed) {
|
|
1551
1564
|
safeguardAlertsSuppressed.push(alert);
|
|
1552
1565
|
continue;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "progmune-runtime",
|
|
3
|
-
"version": "3.7.
|
|
3
|
+
"version": "3.7.68",
|
|
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/",
|