progmune-runtime 3.7.62 → 3.7.64

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,80 @@
1
1
  # Changelog
2
2
 
3
+ ## [3.7.64] — 2026-10-10
4
+
5
+ ### 守卫沿调用链传播(修冤枉第五刀:授权族告警的「守卫在 controller 层」根因)
6
+
7
+ **根因**:docmost 补标注 111 条授权族告警全 FP,其中 49 条(44%)是
8
+ service/repo 层函数被误报「未认证/无归属检查」——守卫其实写在
9
+ controller 层的 @UseGuards(JwtAuthGuard) 上,名字级检测器看不见。
10
+
11
+ - **机制**:`computeGuardPropagatedSet`(protocol-detector.ts)——
12
+ 「函数的**全部**调用方都已守卫 ⇒ 该函数视为守卫后」,守卫沿调用链
13
+ 向下传播 ≤3 跳,只认 `__progmune_auth_machinery__` 一个标记通道
14
+ (E2 已在带 @UseGuards 装饰器的函数上注入)
15
+ - **保守判据(R1-R4,fail-safe)**:全部调用方已守卫才豁免(任一
16
+ 未守卫→照报);裸名调用分层消歧——后缀唯一→同文件→同目录→类根
17
+ 对齐(XController→XService→XRepo 惯例),唯一才解析、歧义给全部
18
+ 候选挂未解析边;调用方自身排除(controller.create 调 "create" 时
19
+ 同文件层自我捕获的坑)
20
+ - **标记而非静默丢弃(R9)**:被压告警进 `overall.safeguardAlerts.
21
+ suppressed` 独立清单(带 suppressedByGuardPropagation 标记),
22
+ **不进 total/groups/topRanked 主计数**但完整保留可复核;
23
+ 豁免仅作用于 authorization 类规则、且不作用于自身直接带标记的
24
+ 函数(R5/R6,维持 E2 既有语义)
25
+ - **同条件 A/B(docmost 双扫描)**:主告警流 865→748(−117 =
26
+ Unauthenticated Access 46 + Ownership Check 36 + Unauthenticated
27
+ Mutation 31 + Resource Ownership 4),suppressed 清单 117 条全带
28
+ 标记,748+117=865 证据一条不少;**主路径 violations 9=9 /
29
+ score 85=85 完全一致**
30
+ - **可压名单交叉核对**:117 条中 25 条在 174 标注队列(14 同规则
31
+ FP + 11 同函数不同规则 FP);92 条未标注的逐条列调用链复核——全部
32
+ 由直接守卫 controller 或传播守卫 service 调用(附件
33
+ blind-benchmark/auth-guard-crosscheck.ts 可复跑)
34
+ - **回归锁定**:单元测试 12 例(protocol-detector-guard-propagation
35
+ .test.ts:全调用方守卫/歧义 fail-safe/同文件/同目录/类根消歧/深度
36
+ 限制/自定义标记/种子语义)+ 引擎产品路径 2 例(tests/trust/engine
37
+ .test.ts:守卫后进 suppressed 不进主计数 + 无守卫对照组照常上报);
38
+ 引擎套件 25/25;TS 盲测零漂移(注:batch-scan 不经过引擎告警循环,
39
+ 此门对本改动是空过——真实门=引擎测试+docmost A/B,如实记录)
40
+ - 工具入册:`blind-benchmark/auth-guard-propagation-probe.ts`(量化
41
+ 探针)+ `auth-guard-crosscheck.ts`(标注交叉核对)
42
+
43
+ ## [3.7.63] — 2026-10-10
44
+
45
+ ### No Input Sanitization 触发词收窄(修冤枉第四刀:删 insert/append/write 通用词)
46
+
47
+ **根因**:trigger 里三个「输出系」通用词系统性词撞,从没在真实语料命中过
48
+ 一条 XSS——`insert` 撞 Kysely `insertInto` 数据入库(docmost 补标注
49
+ 30/30 条 FP)、`append` 撞 Python `list.append` 内存数组(Python 盲测
50
+ 45 项目 270 条 FP)、`write` 撞 `writeFileSync` 文件写入(TS 盲测 8 项目
51
+ FP + docmost 告警流 6 条)。
52
+
53
+ - **修复**:trigger 收窄为 `render|display|output|innerHTML|
54
+ dangerouslySetInnerHTML|echo|printf|sprintf|insertAdjacentHTML`——
55
+ DOM sink 用原始调用名显式保留(`el.insertAdjacentHTML(...)` 的提取名
56
+ 就是 `insertAdjacentHTML`),XSS 本意完整保留
57
+ - **逐条归类(R30)**:TS 盲测 LOST 10(8 项目 writeFileSync 族 +
58
+ webshape_F insertInto/insert)、Python 盲测 LOST 45 项目(全部
59
+ list.append)、ADDED 0/0——**无一例外是存储 I/O 词撞,零 XSS sink**
60
+ - **头条数字零影响(如实)**:TS 795 gold 与 Python 729 gold 均不含
61
+ No Input Sanitization 条目(金标全为 protocol/auth/resource 类),
62
+ recall/precision 不变;docmost 告警流 942→865(No Input Sanitization
63
+ 83→6,残留 6 条为 render/output 词族:react-email 服务端渲染×2、
64
+ 函数名 generatePdfRenderToken 撞 render×1、存储驱动 outputFile×3)
65
+ - **A/B 隔离注记**:主路径 violations 扫描间 7↔10 波动为 LLM 语义映射
66
+ 抖动(已知类,见 skyvern CROSS_DOMAIN 记录),与本次改动无因果——
67
+ safeguard 规则不进 violations(§51 证据流与判定分离),同条件 A/B
68
+ 对照主路径一致
69
+ - **回归锁定**:`src/protocol-detector-nis-trigger.test.ts` 5 例
70
+ (insertInto/writeFileSync/list.append 不触发、insertAdjacentHTML 仍
71
+ 触发、render+sanitize 守卫仍抑制);taintpath 160/160;fr-007
72
+ pre5/post0、fr-016 pre7/post0 维持
73
+ - **已知代价(如实记录)**:`document.write` 的提取名是裸 `write`,
74
+ 与文件写入词法不可区分,本刀后不再命中——三语料里 0 条由
75
+ document.write 驱动;浏览器侧 DOM sink 证据通道(marker 化)留待
76
+ R27 方向
77
+
3
78
  ## [3.7.62] — 2026-10-09
4
79
 
5
80
  ### 迁移脚本排除出应用面(修冤枉第三刀:§49.19 补标注后首个数据驱动修复)
@@ -0,0 +1,130 @@
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-10)单元测试。
7
+ *
8
+ * 保守判据(R1-R4,fail-safe 方向):全部调用方已守卫才豁免、
9
+ * ≤3 跳、只认 __progmune_auth_machinery__ 标记、歧义调用按未守卫处理。
10
+ * 消费方(engine)负责 R5/R6:只压 authorization 类、且不压直接标记函数。
11
+ */
12
+ const M = "__progmune_auth_machinery__";
13
+ (0, vitest_1.describe)("computeGuardPropagatedSet(守卫传播保守判据)", () => {
14
+ (0, vitest_1.it)("全部调用方已守卫 ⇒ 传播(controller→service)", () => {
15
+ const fns = [
16
+ { name: "PageController.create", file: "a.ts", calls: [M, "createPage"] },
17
+ { name: "PageService.createPage", file: "b.ts", calls: ["insertPage"] },
18
+ { name: "PageRepo.insertPage", file: "c.ts", calls: [] },
19
+ ];
20
+ const g = (0, protocol_detector_1.computeGuardPropagatedSet)(fns);
21
+ (0, vitest_1.expect)(g.has("PageService.createPage")).toBe(true);
22
+ });
23
+ (0, vitest_1.it)("传递两跳(controller→service→repo)", () => {
24
+ const fns = [
25
+ { name: "PageController.create", file: "a.ts", calls: [M, "createPage"] },
26
+ { name: "PageService.createPage", file: "b.ts", calls: ["insertPage"] },
27
+ { name: "PageRepo.insertPage", file: "c.ts", calls: [] },
28
+ ];
29
+ const g = (0, protocol_detector_1.computeGuardPropagatedSet)(fns);
30
+ (0, vitest_1.expect)(g.has("PageRepo.insertPage")).toBe(true);
31
+ });
32
+ (0, vitest_1.it)("任一调用方未守卫 ⇒ 不豁免(fail-safe)", () => {
33
+ const fns = [
34
+ { name: "PageController.create", file: "a.ts", calls: [M, "createPage"] },
35
+ { name: "ImportProcessor.run", file: "q.ts", calls: ["createPage"] },
36
+ { name: "PageService.createPage", file: "b.ts", calls: [] },
37
+ ];
38
+ const g = (0, protocol_detector_1.computeGuardPropagatedSet)(fns);
39
+ (0, vitest_1.expect)(g.has("PageService.createPage")).toBe(false);
40
+ });
41
+ (0, vitest_1.it)("无调用方(入口函数)⇒ 不豁免", () => {
42
+ const fns = [{ name: "bootstrap", file: "m.ts", calls: [] }];
43
+ const g = (0, protocol_detector_1.computeGuardPropagatedSet)(fns);
44
+ (0, vitest_1.expect)(g.has("bootstrap")).toBe(false);
45
+ });
46
+ (0, vitest_1.it)("同名多处定义 ⇒ 歧义 fail-safe ⇒ 不豁免", () => {
47
+ const fns = [
48
+ { name: "AController.do", file: "a.ts", calls: [M, "update"] },
49
+ { name: "UserRepo.update", file: "r1.ts", calls: [] },
50
+ { name: "SpaceRepo.update", file: "r2.ts", calls: [] },
51
+ ];
52
+ const g = (0, protocol_detector_1.computeGuardPropagatedSet)(fns);
53
+ // "update" 有两个候选,调用方 AController.do 的调用无法唯一解析
54
+ (0, vitest_1.expect)(g.has("UserRepo.update")).toBe(false);
55
+ (0, vitest_1.expect)(g.has("SpaceRepo.update")).toBe(false);
56
+ });
57
+ (0, vitest_1.it)("歧义时同文件唯一候选 ⇒ 消歧成立", () => {
58
+ const fns = [
59
+ { name: "AController.do", file: "a.ts", calls: [M, "update"] },
60
+ // UserRepo.update 与调用方同文件(helpers 场景)
61
+ { name: "UserRepo.update", file: "a.ts", calls: [] },
62
+ { name: "SpaceRepo.update", file: "r2.ts", calls: [] },
63
+ ];
64
+ const g = (0, protocol_detector_1.computeGuardPropagatedSet)(fns);
65
+ (0, vitest_1.expect)(g.has("UserRepo.update")).toBe(true);
66
+ (0, vitest_1.expect)(g.has("SpaceRepo.update")).toBe(false);
67
+ });
68
+ (0, vitest_1.it)("歧义时同目录唯一候选 ⇒ 消歧成立", () => {
69
+ const fns = [
70
+ { name: "AController.do", file: "d1/x.ts", calls: [M, "update"] },
71
+ { name: "UserRepo.update", file: "d1/y.ts", calls: [] },
72
+ { name: "SpaceRepo.update", file: "d2/z.ts", calls: [] },
73
+ ];
74
+ const g = (0, protocol_detector_1.computeGuardPropagatedSet)(fns);
75
+ (0, vitest_1.expect)(g.has("UserRepo.update")).toBe(true);
76
+ (0, vitest_1.expect)(g.has("SpaceRepo.update")).toBe(false);
77
+ });
78
+ (0, vitest_1.it)("歧义时类根对齐唯一候选 ⇒ 消歧成立(MVC 命名惯例)", () => {
79
+ const fns = [
80
+ { name: "PageController.create", file: "c/page.controller.ts", calls: [M, "create"] },
81
+ { name: "PageService.create", file: "s/page.service.ts", calls: [] },
82
+ { name: "SpaceService.create", file: "s/space.service.ts", calls: [] },
83
+ ];
84
+ const g = (0, protocol_detector_1.computeGuardPropagatedSet)(fns);
85
+ (0, vitest_1.expect)(g.has("PageService.create")).toBe(true);
86
+ (0, vitest_1.expect)(g.has("SpaceService.create")).toBe(false);
87
+ });
88
+ (0, vitest_1.it)("类根不唯一 ⇒ 保持 fail-safe", () => {
89
+ const fns = [
90
+ { name: "PageController.create", file: "c.ts", calls: [M, "create"] },
91
+ { name: "PageService.create", file: "s1.ts", calls: [] },
92
+ { name: "PageRepo.create", file: "r1.ts", calls: [] },
93
+ ];
94
+ // 类根 "Page" 命中两个候选(Service+Repo)⇒ 不唯一 ⇒ 不解析
95
+ const g = (0, protocol_detector_1.computeGuardPropagatedSet)(fns);
96
+ (0, vitest_1.expect)(g.has("PageService.create")).toBe(false);
97
+ (0, vitest_1.expect)(g.has("PageRepo.create")).toBe(false);
98
+ });
99
+ (0, vitest_1.it)("深度限制:第 4 跳不传播", () => {
100
+ const fns = [
101
+ { name: "C0", file: "f.ts", calls: [M, "C1"] },
102
+ { name: "C1", file: "f.ts", calls: ["C2"] },
103
+ { name: "C2", file: "f.ts", calls: ["C3"] },
104
+ { name: "C3", file: "f.ts", calls: ["C4"] },
105
+ { name: "C4", file: "f.ts", calls: [] },
106
+ ];
107
+ // C1 = 跳1, C2 = 跳2, C3 = 跳3, C4 = 跳4(maxDepth=3 传播 3 轮:C1,C2,C3)
108
+ const g = (0, protocol_detector_1.computeGuardPropagatedSet)(fns, { maxDepth: 3 });
109
+ (0, vitest_1.expect)(g.has("C1")).toBe(true);
110
+ (0, vitest_1.expect)(g.has("C2")).toBe(true);
111
+ (0, vitest_1.expect)(g.has("C3")).toBe(true);
112
+ (0, vitest_1.expect)(g.has("C4")).toBe(false);
113
+ });
114
+ (0, vitest_1.it)("自定义标记通道", () => {
115
+ const fns = [
116
+ { name: "A", file: "f.ts", calls: ["__custom_mark__", "B"] },
117
+ { name: "B", file: "f.ts", calls: [] },
118
+ ];
119
+ const g = (0, protocol_detector_1.computeGuardPropagatedSet)(fns, { marker: "__custom_mark__" });
120
+ (0, vitest_1.expect)(g.has("B")).toBe(true);
121
+ // 默认标记下不传播
122
+ const g2 = (0, protocol_detector_1.computeGuardPropagatedSet)(fns);
123
+ (0, vitest_1.expect)(g2.has("B")).toBe(false);
124
+ });
125
+ (0, vitest_1.it)("直接标记函数在返回集合中(种子语义,R6 由消费方过滤)", () => {
126
+ const fns = [{ name: "AController.do", file: "a.ts", calls: [M] }];
127
+ const g = (0, protocol_detector_1.computeGuardPropagatedSet)(fns);
128
+ (0, vitest_1.expect)(g.has("AController.do")).toBe(true);
129
+ });
130
+ });
@@ -0,0 +1,43 @@
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
+ * No Input Sanitization 触发词收窄(修冤枉第四刀,2026-10-09)锁定。
7
+ *
8
+ * 数据:docmost 补标注 30/30 FP(Kysely insertInto)、Python 盲测
9
+ * 270/270 FP(list.append)、TS 盲测 33/33 FP(writeFileSync 族)——
10
+ * 「输出系」通用词系统性词撞,本意是 XSS sink 却从没在真实语料
11
+ * 命中过一条 XSS。收窄后 trigger 只留真实 sink 语义:
12
+ * render/display/output/innerHTML/dangerouslySetInnerHTML/echo/
13
+ * printf/sprintf + 显式 DOM sink 原始名 insertAdjacentHTML。
14
+ *
15
+ * 已知代价:document.write 的提取名是裸 "write",与文件写入不可分,
16
+ * 不再命中(盲测/语料里 0 条由 document.write 驱动,如实记录)。
17
+ */
18
+ const RULE = "No Input Sanitization";
19
+ const has = (vs) => vs.some((v) => v.rule === RULE);
20
+ (0, vitest_1.describe)("No Input Sanitization:触发词收窄(修冤枉第四刀)", () => {
21
+ (0, vitest_1.it)("数据库写入 insertInto 不再触发(docmost 30/30 FP 根因)", () => {
22
+ const vs = (0, protocol_detector_1.detectSafeguardViolations)(["dbOrTx", "executeTakeFirst", "returningAll", "values", "insertInto", "__progmune_input_effect__"], "PageRepo.insertPage", "typescript");
23
+ (0, vitest_1.expect)(has(vs)).toBe(false);
24
+ });
25
+ (0, vitest_1.it)("文件写入 writeFileSync 不再触发(TS 盲测 8 项目 FP 根因)", () => {
26
+ const vs = (0, protocol_detector_1.detectSafeguardViolations)(["writeFileSync", "mkdirSync"], "uploadFile", "typescript");
27
+ (0, vitest_1.expect)(has(vs)).toBe(false);
28
+ });
29
+ (0, vitest_1.it)("Python list.append 不再触发(Python 盲测 45 项目 270 条 FP 根因)", () => {
30
+ const vs = (0, protocol_detector_1.detectSafeguardViolations)(["users", "append", "sessions", "append"], "register", "python");
31
+ (0, vitest_1.expect)(has(vs)).toBe(false);
32
+ });
33
+ (0, vitest_1.it)("真实 DOM sink insertAdjacentHTML 仍触发(XSS 本意保留)", () => {
34
+ const vs = (0, protocol_detector_1.detectSafeguardViolations)(["insertAdjacentHTML"], "renderComment", "typescript");
35
+ (0, vitest_1.expect)(has(vs)).toBe(true);
36
+ });
37
+ (0, vitest_1.it)("真实渲染 sink render 仍触发,带 sanitize 守卫仍抑制", () => {
38
+ const bare = (0, protocol_detector_1.detectSafeguardViolations)(["render"], "renderTemplate", "typescript");
39
+ (0, vitest_1.expect)(has(bare)).toBe(true);
40
+ const guarded = (0, protocol_detector_1.detectSafeguardViolations)(["render", "sanitizeHtml"], "renderTemplate", "typescript");
41
+ (0, vitest_1.expect)(has(guarded)).toBe(false);
42
+ });
43
+ });
@@ -14,6 +14,7 @@ exports.isAuthFunctionName = isAuthFunctionName;
14
14
  exports.classifyParamType = classifyParamType;
15
15
  exports.detectSafeguardViolations = detectSafeguardViolations;
16
16
  exports.buildCallerMap = buildCallerMap;
17
+ exports.computeGuardPropagatedSet = computeGuardPropagatedSet;
17
18
  exports.detectSafeguardViolationsV7 = detectSafeguardViolationsV7;
18
19
  exports.detectProtocolViolations = detectProtocolViolations;
19
20
  exports.validateProtocolState = validateProtocolState;
@@ -1090,10 +1091,25 @@ const SAFEGUARD_RULES = [
1090
1091
  ],
1091
1092
  },
1092
1093
  // ── Resource: Input sanitization missing ──
1094
+ //
1095
+ // 修冤枉第四刀(2026-10-09,§49.19 补标注数据):trigger 里三个「输出系」
1096
+ // 通用词在真实与合成语料上系统性词撞,全量 FP——
1097
+ // insert:Kysely insertInto 数据入库(docmost 标注 30/30 FP;
1098
+ // 盲测 webshape_F 2/2 FP)
1099
+ // append:Python list.append 内存数组(Python 盲测 270/270 FP)
1100
+ // write: 文件写入 writeFileSync(TS 盲测 33/33 FP)
1101
+ // 本意是 XSS sink(DOM 插入/文档写入),逐条核实无一条真阳性。
1102
+ // 收窄方案:删通用词,DOM sink 用**原始调用名**显式保留
1103
+ // (extractDirectCalls 对属性访问发射 getName() 原样名:
1104
+ // el.insertAdjacentHTML(...) → "insertAdjacentHTML")。
1105
+ // 已知代价(如实记录):document.write 的原始发射名是 "write",
1106
+ // 与文件写入词法不可区分,本刀后不再命中——盲测/语料里 0 条由
1107
+ // document.write 驱动,风险=浏览器侧代码的 XSS sink 词,留待
1108
+ // marker 化证据通道(R27 方向)。
1093
1109
  {
1094
1110
  name: "No Input Sanitization",
1095
1111
  category: "resource",
1096
- trigger: /\b(render|display|write|output|append|insert|innerHTML|dangerouslySetInnerHTML|document\.write|echo|printf|sprintf)\b/i,
1112
+ trigger: /\b(render|display|output|innerHTML|dangerouslySetInnerHTML|echo|printf|sprintf|insertAdjacentHTML)\b/i,
1097
1113
  safeguards: [
1098
1114
  { pattern: /\b(sanitize|escape|encode|html\w*entities|htmlspecialchars|encodeURI|stripTags|purify|DOMPurify|clean|filter\w*html|escape\w*html)\b/i, label: "sanitize" },
1099
1115
  ],
@@ -1608,6 +1624,130 @@ function buildCallerMap(funcs) {
1608
1624
  }
1609
1625
  return callerMap;
1610
1626
  }
1627
+ /**
1628
+ * 修冤枉第五刀(2026-10-10):守卫沿调用链传播——
1629
+ * 授权族告警的「守卫在 controller 层、名字级看不见」根因的保守豁免。
1630
+ *
1631
+ * 语义:「函数的**全部**直接调用方都已守卫 ⇒ 该函数视为守卫后」。
1632
+ * 证据:docmost 补标注 111 条授权族告警全 FP,其中 49 条(44%)是
1633
+ * controller 已 @UseGuards、service/repo 层函数被误报。保守量化
1634
+ * (blind-benchmark/auth-guard-propagation-probe.ts):
1635
+ * docmost 745 条授权族告警可压 47 条(6.3%),TS 盲测可压 0 条。
1636
+ *
1637
+ * 保守规则(R1-R4,fail-safe 方向):
1638
+ * R1 只有【全部】调用方已守卫才豁免;任一调用方无守卫证据 → 照报
1639
+ * R2 守卫沿调用链向下传播 ≤ maxDepth(默认 3)跳
1640
+ * R3 传播通道只认 `marker`(默认 __progmune_auth_machinery__)一个标记
1641
+ * R4 裸名调用解析分层消歧(唯一才解析,否则给全部候选挂未解析边):
1642
+ * ① 名字后缀唯一 ② 同文件唯一 ③ 同目录唯一
1643
+ * ④ 类根对齐唯一(调用方 XController/Service → 候选类根同为 X)
1644
+ * R6 豁免只给【非直接标记】函数——自身带标记的维持既有 E2 语义
1645
+ * (只接受标记的规则由 detector 自身处理;不接受标记的规则照报)
1646
+ *
1647
+ * 返回集合含直接标记函数本身(消费方须自行应用 R6 过滤),
1648
+ * 因为传递闭包的种子就是它们。
1649
+ */
1650
+ function computeGuardPropagatedSet(funcs, opts) {
1651
+ const marker = opts?.marker ?? "__progmune_auth_machinery__";
1652
+ const maxDepth = opts?.maxDepth ?? 3;
1653
+ const byName = new Map();
1654
+ for (const f of funcs)
1655
+ if (!byName.has(f.name))
1656
+ byName.set(f.name, f);
1657
+ // 后缀索引:裸名 → 候选全限定名(>1 ⇒ 歧义)
1658
+ const bySuffix = new Map();
1659
+ for (const f of funcs) {
1660
+ const bare = f.name.split(".").pop() || f.name;
1661
+ if (!bySuffix.has(bare))
1662
+ bySuffix.set(bare, []);
1663
+ bySuffix.get(bare).push(f.name);
1664
+ }
1665
+ // 反向调用图:被调【全限定名】→ 调用方列表(resolved=false = 歧义 fail-safe)。
1666
+ // 边解析分层消歧(R4):① 后缀唯一 ② 同文件唯一 ③ 同目录唯一
1667
+ // ④ 类根对齐唯一(XController→XService→XRepo 的 MVC 命名惯例);
1668
+ // 全部失败 ⇒ 给全部同名候选挂未解析边(fail-safe)。
1669
+ const dirOf = (p) => (p ? p.split("/").slice(0, -1).join("/") : "");
1670
+ const rootOf = (n) => {
1671
+ const cls = n.split(".")[0] || n;
1672
+ return cls.replace(/(Controller|Service|Repo|Repository|Listener|Processor|Handler|Extension|Util|Utils|Helper|Driver|Provider)$/, "");
1673
+ };
1674
+ const calleeToCallers = new Map();
1675
+ for (const f of funcs) {
1676
+ for (const c of f.calls || []) {
1677
+ if (!bySuffix.has(c))
1678
+ continue; // 非项目函数名(外部库调用)→ 无守卫语义
1679
+ // 排除调用方自身:controller.create 调 "create" 时,自己就是后缀
1680
+ // 候选之一,同文件/同目录层会错误自我捕获(递归/自调不提供守卫证据)
1681
+ const cands = bySuffix.get(c).filter((n) => n !== f.name);
1682
+ if (cands.length === 0)
1683
+ continue;
1684
+ let targets = null;
1685
+ if (cands.length === 1) {
1686
+ targets = cands;
1687
+ }
1688
+ else {
1689
+ const sameFile = cands.filter((n) => {
1690
+ const g = byName.get(n);
1691
+ return g && g.file && f.file && g.file === f.file;
1692
+ });
1693
+ if (sameFile.length === 1)
1694
+ targets = sameFile;
1695
+ else {
1696
+ const sameDir = cands.filter((n) => {
1697
+ const g = byName.get(n);
1698
+ return g && g.file && f.file && dirOf(g.file) === dirOf(f.file);
1699
+ });
1700
+ if (sameDir.length === 1)
1701
+ targets = sameDir;
1702
+ else {
1703
+ const rootMatch = cands.filter((n) => rootOf(n) === rootOf(f.name));
1704
+ if (rootMatch.length === 1)
1705
+ targets = rootMatch;
1706
+ }
1707
+ }
1708
+ }
1709
+ if (targets === null) {
1710
+ for (const n of cands) {
1711
+ if (!calleeToCallers.has(n))
1712
+ calleeToCallers.set(n, []);
1713
+ calleeToCallers.get(n).push({ caller: f.name, resolved: false });
1714
+ }
1715
+ }
1716
+ else {
1717
+ for (const n of targets) {
1718
+ if (!calleeToCallers.has(n))
1719
+ calleeToCallers.set(n, []);
1720
+ calleeToCallers.get(n).push({ caller: f.name, resolved: true });
1721
+ }
1722
+ }
1723
+ }
1724
+ }
1725
+ // 种子:直接带标记的函数
1726
+ const guarded = new Set();
1727
+ for (const f of funcs)
1728
+ if ((f.calls || []).includes(marker))
1729
+ guarded.add(f.name);
1730
+ // 不动点传播(≤ maxDepth 轮):被调方全部调用方已守卫 ⇒ 已守卫。
1731
+ // 快照语义:每轮基于【轮初】守卫集计算、轮末统一应用——
1732
+ // 轮内级联会让深度失去意义(同轮后半段函数借前半段的新增一步到位)。
1733
+ for (let round = 0; round < maxDepth; round++) {
1734
+ const toAdd = [];
1735
+ for (const f of funcs) {
1736
+ if (guarded.has(f.name))
1737
+ continue;
1738
+ const callers = calleeToCallers.get(f.name) || [];
1739
+ if (callers.length === 0)
1740
+ continue; // 无调用方(入口/未连通)→ 不豁免
1741
+ if (callers.every((c) => c.resolved && guarded.has(c.caller)))
1742
+ toAdd.push(f.name);
1743
+ }
1744
+ if (toAdd.length === 0)
1745
+ break;
1746
+ for (const n of toAdd)
1747
+ guarded.add(n);
1748
+ }
1749
+ return guarded;
1750
+ }
1611
1751
  /**
1612
1752
  * v7: Build safeguard context — own → same-file → caller chain.
1613
1753
  *
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.62";
23
+ exports.RUNTIME_VERSION = "3.7.64";
24
24
  function verify(filePath) {
25
25
  const cert = (0, certify_1.certify)(filePath);
26
26
  const kb = (0, protocol_knowledge_1.buildKnowledgeBase)();
@@ -1496,7 +1496,14 @@ async function collectProtocolViolations(ctx, callGraph) {
1496
1496
  // 不改 decision/score——排序告警是给人工审查的入口(§49.16:
1497
1497
  // 排序不删告警,一条不少;groupAlerts 分组防整族沉底)。
1498
1498
  const safeguardAlertsRaw = [];
1499
+ const safeguardAlertsSuppressed = [];
1499
1500
  const svLang = ctx.language || "typescript";
1501
+ // 修冤枉第五刀(2026-10-10):守卫传播豁免——授权族告警的
1502
+ // 「守卫在 controller 层」根因。保守判据见
1503
+ // computeGuardPropagatedSet(R1-R4):全部调用方已守卫才豁免、
1504
+ // ≤3 跳、歧义 fail-safe。豁免仅作用于 authorization 类规则
1505
+ // 且函数自身不直接带标记(R5/R6)——标记而非静默丢弃。
1506
+ const guardPropagated = (0, protocol_detector_1.computeGuardPropagatedSet)(functions);
1500
1507
  for (const f of functions) {
1501
1508
  const fCalls = f.calls || [];
1502
1509
  let svs;
@@ -1509,21 +1516,31 @@ async function collectProtocolViolations(ctx, callGraph) {
1509
1516
  if (svs.length === 0)
1510
1517
  continue;
1511
1518
  const params = (f.params || []).map((p) => ({ n: p.name, t: p.type || "" }));
1519
+ const directlyMarked = fCalls.includes("__progmune_auth_machinery__");
1520
+ const guardExempt = !directlyMarked && guardPropagated.has(f.name);
1512
1521
  for (const v of svs) {
1513
- safeguardAlertsRaw.push({
1522
+ const alert = {
1514
1523
  rule: v.rule,
1515
1524
  file: f.file,
1516
1525
  function: f.name,
1517
1526
  calls: fCalls,
1518
1527
  nRules: svs.length,
1519
1528
  params,
1520
- });
1529
+ };
1530
+ if (guardExempt && v.category === "authorization") {
1531
+ alert.suppressedByGuardPropagation = true;
1532
+ safeguardAlertsSuppressed.push(alert);
1533
+ }
1534
+ else {
1535
+ safeguardAlertsRaw.push(alert);
1536
+ }
1521
1537
  }
1522
1538
  }
1523
1539
  safeguardAlerts = {
1524
1540
  total: safeguardAlertsRaw.length,
1525
1541
  groups: (0, alert_ranker_1.groupAlerts)(safeguardAlertsRaw),
1526
1542
  topRanked: (0, alert_ranker_1.rankAlerts)(safeguardAlertsRaw, { minPerRule: 1 }),
1543
+ suppressed: safeguardAlertsSuppressed,
1527
1544
  };
1528
1545
  }
1529
1546
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "progmune-runtime",
3
- "version": "3.7.62",
3
+ "version": "3.7.64",
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/",