progmune-runtime 3.7.63 → 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,45 @@
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
+
3
43
  ## [3.7.63] — 2026-10-10
4
44
 
5
45
  ### No Input Sanitization 触发词收窄(修冤枉第四刀:删 insert/append/write 通用词)
@@ -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
+ });
@@ -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;
@@ -1623,6 +1624,130 @@ function buildCallerMap(funcs) {
1623
1624
  }
1624
1625
  return callerMap;
1625
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
+ }
1626
1751
  /**
1627
1752
  * v7: Build safeguard context — own → same-file → caller chain.
1628
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.63";
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.63",
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/",