progmune-runtime 3.7.51 → 3.7.52
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 +19 -0
- package/dist/extract-ir-taint-structural.test.js +78 -4
- package/dist/extract-ir.js +201 -0
- package/dist/protocol-detector.js +38 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [3.7.52] — 2026-09-23
|
|
4
|
+
|
|
5
|
+
### 语料补形 + 两处基础设施修复 + E4/E5 两个确定性通道(§26-§31 合并发布)
|
|
6
|
+
|
|
7
|
+
**语料补形(§26,R23 家族的处置而非第五条注记)**:`webshape_A..F` 六族钉进 generated/——NestJS 装饰器/DTO 校验/类方法入口/分发器/上传校验证据/副作用 sink 形状;`check-webshape.ts` 闸门(默认实时提取、语料缺失即硬失败);期望表由生成器落盘(R7);补形后 exposed 通道两变体的离线重放得以全量验证——增量召回 2 条真报 vs 70 条已知误报 ⇒ **exposed 通道按 R33 判不做**(做完也不划算,理由写死进设计稿 §25)。
|
|
8
|
+
|
|
9
|
+
**基础设施两修(§27)**:
|
|
10
|
+
- 死切片复活:solution 风格仓库的根 tsconfig 是容器 ⇒ 提取器 0 源文件静默通过(gothinkster 37 文件 0 函数污染所有分母)——修复=零源文件兜底回退 `**/*.ts` + stderr 告警(115 generated 项目 0 误触发)+ 抓取器保留 tsconfig 变体
|
|
11
|
+
- language 门:TS 扫描一直传 undefined ⇒ activeRules 退化「全部规则」,Python/C 专属规则套在 TS 上(池少 5 条 Context Manager Usage)——batch-scan/fp-pool-scan 补 "typescript"
|
|
12
|
+
- R36:产出 0 的环节必须自证扫到了东西;R37 判据冲突检测(当场逼出 L7 虚高 3 条真报);R38 口径可比性;R39 gold 分层+多数类基线;R40 外部服务先无效凭据验证
|
|
13
|
+
|
|
14
|
+
**E4(§30):判据瞎,不是判据不准**(R41 缺信息 vs 缺语义)——`inputGuardFunctionKeys` 预扫函数体校验证据(limits:{…}+HTTP 异常抛掷)⇒ `__progmune_input_guard__`(只进抑制位);webshape_E 5 条正反钉死;池 −5 条全部逐条确证(3 条在探针预测外——探针漏箭头常量形态)
|
|
15
|
+
|
|
16
|
+
**E5(§31):参数流向外部副作用的语义前置**(先量化再实现,R43)——`inputEffectFunctionKeys` + `requireMarkerLanguages`(R42:不限定语言 Python 侧该规则整体归零而四门看不见);真值集预演:TP 召回 100%(17/17)、FP 压制 77.2%,粗判据取舍(丢真报比留误报贵);池 409→364、IV 75→30、**verified TP 6/6 零丢失、verified FP 29/29 全压**;盲测 −216 全 IV=口径修正(generated 语料无真实 sink),webshape_F 补真实 sink 形状 3 条照常报
|
|
17
|
+
|
|
18
|
+
**方法学累计**:R34(反向验证期望须由期望表推导,本轮第三次同形踩坑——推导逻辑应工具化)、R41-R43;fp-gold.jsonl 真值集入库(verified 53 条/12.8%——启发式不能当 gold 的实证);jev 实验台(无 key,401 确认端点,ECE/多数类基线硬指标,绝不进扫描路径)
|
|
19
|
+
|
|
20
|
+
**门**:build ✓ / taintpath 160/160 / webshape 29/29 / fr-007 pre5·post0 + fr-016 pre7·post0 / 定向 212 全绿 / 盲测 LOST 391 IV+1 已知 G2 项、ADDED 全在新语料 / 池 8 片 573 文件 IV=30 ✓
|
|
21
|
+
|
|
3
22
|
## [3.7.51] — 2026-09-22
|
|
4
23
|
|
|
5
24
|
### Input Validation 的 trigger 只认函数名:不再被 callee 拖下水(F 轮)
|
|
@@ -2343,6 +2343,10 @@ export class Uploader {
|
|
|
2343
2343
|
// 标记不得顺带压掉别的规则(Data Integrity 曾因标记名含 validate 被误压,
|
|
2344
2344
|
// 见 extract-ir.ts 里标记命名的说明 ⇒ 这是本组专门要守的)。
|
|
2345
2345
|
// ═══════════════════════════════════════════════════════════════
|
|
2346
|
+
/** E5(2026-09-23):提取器在「参数流向外部副作用」时注入的语义前置标记。
|
|
2347
|
+
* Input Validation 规则据此 requireMarker —— 名字叫 create/add 但根本不接外部
|
|
2348
|
+
* 输入的函数(配置装配、工厂)不该被要求「必须校验输入」。 */
|
|
2349
|
+
const INPUT_EFFECT = "__progmune_input_effect__";
|
|
2346
2350
|
(0, vitest_1.describe)("E3:DTO 的 schema 校验必须对 Input Validation 可见", () => {
|
|
2347
2351
|
const MARK = "__progmune_input_schema__";
|
|
2348
2352
|
const DECLS = `
|
|
@@ -2500,14 +2504,17 @@ export class C {
|
|
|
2500
2504
|
}, 150000);
|
|
2501
2505
|
}
|
|
2502
2506
|
// ── 规则侧:标记必须真的压得住 Input Validation,且只压这一条 ──
|
|
2507
|
+
// E5(2026-09-23):Input Validation 现在带 requireMarker(须有副作用证据才触发),
|
|
2508
|
+
// 故这几条断言必须显式带上 INPUT_EFFECT —— 它们验证的是「在提取器已产出副作用
|
|
2509
|
+
// 证据的前提下,schema 标记仍能压住本条」,不是「裸名字就该报」。
|
|
2503
2510
|
(0, vitest_1.it)("规则侧·① 注入标记后 Input Validation 必须消失", () => {
|
|
2504
|
-
const before = (0, protocol_detector_1.detectSafeguardViolations)(["createPost"], "createPost");
|
|
2511
|
+
const before = (0, protocol_detector_1.detectSafeguardViolations)(["createPost", INPUT_EFFECT], "createPost");
|
|
2505
2512
|
(0, vitest_1.expect)(before.some(v => v.rule === "Input Validation")).toBe(true);
|
|
2506
|
-
const after = (0, protocol_detector_1.detectSafeguardViolations)(["createPost", MARK], "createPost");
|
|
2513
|
+
const after = (0, protocol_detector_1.detectSafeguardViolations)(["createPost", INPUT_EFFECT, MARK], "createPost");
|
|
2507
2514
|
(0, vitest_1.expect)(after.some(v => v.rule === "Input Validation")).toBe(false);
|
|
2508
2515
|
});
|
|
2509
2516
|
(0, vitest_1.it)("规则侧·② 标记不得压掉别的规则(Authorization 仍在)", () => {
|
|
2510
|
-
const after = (0, protocol_detector_1.detectSafeguardViolations)(["createPost", MARK], "createPost");
|
|
2517
|
+
const after = (0, protocol_detector_1.detectSafeguardViolations)(["createPost", INPUT_EFFECT, MARK], "createPost");
|
|
2511
2518
|
(0, vitest_1.expect)(after.some(v => v.category === "authorization")).toBe(true);
|
|
2512
2519
|
});
|
|
2513
2520
|
(0, vitest_1.it)("规则侧·③ 标记名不得撞上 Data Integrity 的 safeguard(命名事故回归)", () => {
|
|
@@ -2529,7 +2536,11 @@ export class C {
|
|
|
2529
2536
|
// (b) 理由错 —— login/register/authenticate 因调用 **createHash**(密码哈希)
|
|
2530
2537
|
// 被判成「内容创建函数未校验输入」(69/175)。
|
|
2531
2538
|
// 175 条已逐条归类,无一为真阳性;另有 6 条 list/get 查询函数。
|
|
2532
|
-
|
|
2539
|
+
// E5(2026-09-23):本规则现在带 requireMarker(须有「参数流向外部副作用」的证据
|
|
2540
|
+
// 才触发)。F 组守护的是「trigger 只认函数名、不被 callee 拖下水」——这个命题在
|
|
2541
|
+
// **已有副作用证据**的前提下才成立,故统一带上 INPUT_EFFECT。
|
|
2542
|
+
// 副作用证据本身的行为由下面 E5 组单独钉住(含正反两面与语言限定)。
|
|
2543
|
+
const IV = (calls, own) => (0, protocol_detector_1.detectSafeguardViolations)([...calls, INPUT_EFFECT], own).filter(v => v.rule === "Input Validation");
|
|
2533
2544
|
const hit = (calls, own) => IV(calls, own).length > 0;
|
|
2534
2545
|
// ── 正例:函数名自己就是创建函数 ⇒ 必须仍然报(本轮不得削弱真阳性) ──
|
|
2535
2546
|
(0, vitest_1.it)("正例·① createPost 自身仍报", () => {
|
|
@@ -2585,3 +2596,66 @@ export class C {
|
|
|
2585
2596
|
// real-world 池里亦未发现(27 条触发源全为 ORM/哈希/权限工厂/内部工具)。
|
|
2586
2597
|
// 若要补回,需要 `exposed`(web handler)通道 —— 按 R27 先查该通道通不通。
|
|
2587
2598
|
});
|
|
2599
|
+
// ═══════════════════════════════════════════════════════════════
|
|
2600
|
+
// E5(2026-09-23):Input Validation 的语义前置条件 —— 参数流向外部副作用
|
|
2601
|
+
//
|
|
2602
|
+
// 立项依据:本规则的 trigger 是纯函数名正则(create|add|post|upload),判据手里
|
|
2603
|
+
// **只有名字**。FP 池实测 75 条里 62 条(83%)完全没有请求入口迹象:
|
|
2604
|
+
// verdaccio ConfigBuilder.addStorage/addLogger(配置装配)、ducktors
|
|
2605
|
+
// createS3/createAzureBlobStorage(工厂)都被要求「必须校验输入」。
|
|
2606
|
+
//
|
|
2607
|
+
// 判据为什么不是「是否 HTTP 入口」:真值集里 4 条已核验真报(docmost
|
|
2608
|
+
// addContributors→redis.sadd、addLabelsToPage→labelRepo.findOrCreate、hedgedoc
|
|
2609
|
+
// ApiTokenService.createToken、hoppscotch AdminService.createATeam)都没有 req
|
|
2610
|
+
// 对象,但参数是从 Controller 透传的外部输入,且确实没校验 ⇒ Service 层不因
|
|
2611
|
+
// 组件边界豁免。区分点在**参数流向**,不在入口形态。
|
|
2612
|
+
//
|
|
2613
|
+
// 量化(probe-body-evidence.ts --from-gold,74 条已标注):
|
|
2614
|
+
// TP 召回 100%(17/17,丢 0)/FP 压制 77.2%(44/57)/precision 23.0%→56.7%
|
|
2615
|
+
// ═══════════════════════════════════════════════════════════════
|
|
2616
|
+
(0, vitest_1.describe)("E5:Input Validation 须有「参数流向外部副作用」的证据才触发", () => {
|
|
2617
|
+
const IV = (calls, own, language) => (0, protocol_detector_1.detectSafeguardViolations)(calls, own, language).filter(v => v.rule === "Input Validation");
|
|
2618
|
+
const hit = (calls, own, language) => IV(calls, own, language).length > 0;
|
|
2619
|
+
// ── 反面:名字像创建函数,但参数不流向任何 sink ⇒ 不该报 ──
|
|
2620
|
+
(0, vitest_1.it)("反例·① 纯内存装配(push 进数组)不再被要求校验输入", () => {
|
|
2621
|
+
(0, vitest_1.expect)(hit(["push", "sort"], "addWidgetToCache")).toBe(false);
|
|
2622
|
+
});
|
|
2623
|
+
(0, vitest_1.it)("反例·② 工厂形态(只构造对象)不再被要求校验输入", () => {
|
|
2624
|
+
// FP 池里 ducktors createS3 / createAzureBlobStorage 即此形态
|
|
2625
|
+
(0, vitest_1.expect)(hit(["Bucket", "Credentials"], "createS3")).toBe(false);
|
|
2626
|
+
});
|
|
2627
|
+
(0, vitest_1.it)("反例·③ 配置装配(写进内部 config 对象)不再被要求校验输入", () => {
|
|
2628
|
+
// FP 池里 verdaccio ConfigBuilder.addStorage / addLogger 即此形态
|
|
2629
|
+
(0, vitest_1.expect)(hit(["assign", "keys"], "addStorage")).toBe(false);
|
|
2630
|
+
});
|
|
2631
|
+
// ── 正面:有副作用证据 ⇒ 仍须报(防止 requireMarker 退化成无条件压制)──
|
|
2632
|
+
(0, vitest_1.it)("正例·① ORM 写入(repo.save)仍报", () => {
|
|
2633
|
+
(0, vitest_1.expect)(hit(["db.save", INPUT_EFFECT], "createWidget")).toBe(true);
|
|
2634
|
+
});
|
|
2635
|
+
(0, vitest_1.it)("正例·② 委托给 service 层(含 upload 动词)仍报", () => {
|
|
2636
|
+
// docmost uploadToDrive 只有 `storageService.upload(...)`,
|
|
2637
|
+
// 动词表漏掉 upload 就会丢这条真报
|
|
2638
|
+
(0, vitest_1.expect)(hit(["storageService.upload", INPUT_EFFECT], "uploadToDrive")).toBe(true);
|
|
2639
|
+
});
|
|
2640
|
+
(0, vitest_1.it)("正例·③ 缓存写入(redis.sadd)仍报 —— Service 层不因无 req 豁免", () => {
|
|
2641
|
+
// docmost CollabHistoryService.addContributors 是已核验真报,没有 req 对象
|
|
2642
|
+
(0, vitest_1.expect)(hit(["redis.sadd", INPUT_EFFECT], "addContributors")).toBe(true);
|
|
2643
|
+
});
|
|
2644
|
+
// ── 语言限定(R42):标记只由 TS 提取器产出,Python 侧不得因此静默归零 ──
|
|
2645
|
+
(0, vitest_1.it)("语言·① TypeScript 缺标记 ⇒ 不报(前置条件生效)", () => {
|
|
2646
|
+
(0, vitest_1.expect)(hit(["db.save"], "createWidget", "typescript")).toBe(false);
|
|
2647
|
+
});
|
|
2648
|
+
(0, vitest_1.it)("语言·② Python 无标记也必须照旧报(不得静默归零)", () => {
|
|
2649
|
+
(0, vitest_1.expect)(hit(["db.save"], "createWidget", "python")).toBe(true);
|
|
2650
|
+
});
|
|
2651
|
+
(0, vitest_1.it)("语言·③ 未传语言时按原语义处理(不引入新的静默失效)", () => {
|
|
2652
|
+
(0, vitest_1.expect)(hit(["db.save", INPUT_EFFECT], "createWidget")).toBe(true);
|
|
2653
|
+
});
|
|
2654
|
+
// ── 与既有抑制机制的关系:两个通道互不覆盖 ──
|
|
2655
|
+
(0, vitest_1.it)("关系·① 有副作用 + 有 schema 标记 ⇒ 仍不报(E3 通道未被本轮破坏)", () => {
|
|
2656
|
+
(0, vitest_1.expect)(hit(["db.save", INPUT_EFFECT, "__progmune_input_schema__"], "createWidget")).toBe(false);
|
|
2657
|
+
});
|
|
2658
|
+
(0, vitest_1.it)("关系·② 有副作用 + 有函数体校验证据 ⇒ 仍不报(E4 通道未被本轮破坏)", () => {
|
|
2659
|
+
(0, vitest_1.expect)(hit(["db.save", INPUT_EFFECT, "__progmune_input_guard__"], "createWidget")).toBe(false);
|
|
2660
|
+
});
|
|
2661
|
+
});
|
package/dist/extract-ir.js
CHANGED
|
@@ -509,6 +509,166 @@ function validatedDtoClassNames(project) {
|
|
|
509
509
|
}
|
|
510
510
|
return out;
|
|
511
511
|
}
|
|
512
|
+
// ═══════════════════════════════════════════════════════════════
|
|
513
|
+
// E4(2026-09-23):函数体里的输入校验证据 ⇒ 注入 __progmune_input_guard__
|
|
514
|
+
//
|
|
515
|
+
// 背景:Input Validation 规则的 safeguard 只看**函数名与调用名**(词形匹配),
|
|
516
|
+
// 看不见函数体。实测 docmost 的 AttachmentController.uploadFile 函数体里明明
|
|
517
|
+
// 写着 `limits:{ fileSize, fields, files }` 与
|
|
518
|
+
// `if (!pageId) throw new BadRequestException('PageId is required')`,
|
|
519
|
+
// 却仍被判「内容创建函数未校验输入」。判据不是不准,是**瞎** ——
|
|
520
|
+
// 缺的是信息,不是语义理解,所以这里是确定性规则而不是概率模型。
|
|
521
|
+
//
|
|
522
|
+
// 判据(逐条来自语料实测,不是拍脑袋词表):
|
|
523
|
+
// A. 上传/写入限制选项:`limits:{ … }` 且块后 140 字符内出现
|
|
524
|
+
// fileSize | maxFileSize | files | fields | parts
|
|
525
|
+
// B. 入参判空即拒:`if (!x) throw <HTTP/校验类异常>`
|
|
526
|
+
// —— 必须是 BadRequest/Validation/Unprocessable/Invalid/Forbidden/Http… 类。
|
|
527
|
+
// 实测两条反例因此被排除:createS3 抛裸 `Error('…required')` 是**环境配置**
|
|
528
|
+
// 校验;AttachmentService.uploadFile 抛 NotFoundException 是**存在性**校验。
|
|
529
|
+
// 两者都不是「对输入内容做校验」。
|
|
530
|
+
//
|
|
531
|
+
// 范围实测:在 FP 池 112 条 Input Validation 违规上,A 与 B 命中集合**完全重合**
|
|
532
|
+
// (均为 docmost 的两个 upload controller),**零命中 verified TP**;
|
|
533
|
+
// 候选 C(校验库调用 validateOrReject/schema.parse)与 D(mime 白名单)
|
|
534
|
+
// 命中数均为 0 ⇒ **不实现**(没有证据支撑的机制不进代码)。
|
|
535
|
+
//
|
|
536
|
+
// 与 E3 同构:AST 预扫 ⇒ key 集合 ⇒ 主循环后统一注入。只进 safeguard,不进 trigger。
|
|
537
|
+
// ═══════════════════════════════════════════════════════════════
|
|
538
|
+
const INPUT_GUARD_MARKER = "__progmune_input_guard__";
|
|
539
|
+
function bodyHasInputGuard(text) {
|
|
540
|
+
if (!text)
|
|
541
|
+
return false;
|
|
542
|
+
// 迭代用的正则必须带 g(非全局正则的 exec 恒返回首个匹配 ⇒ 死循环)。
|
|
543
|
+
// 每次调用新建字面量正则,不复用带 lastIndex 的共享对象。
|
|
544
|
+
for (const m of text.matchAll(/\blimits\s*:\s*\{/gi)) {
|
|
545
|
+
const tail = text.slice(m.index + m[0].length, m.index + m[0].length + 140);
|
|
546
|
+
if (/\b(?:fileSize|maxFileSize|files|fields|parts)\b/i.test(tail))
|
|
547
|
+
return true;
|
|
548
|
+
}
|
|
549
|
+
for (const m of text.matchAll(/\bif\s*\(\s*!\s*[A-Za-z_$][\w$.?]*\s*\)\s*\{?\s*throw\b/g)) {
|
|
550
|
+
const tail = text.slice(m.index + m[0].length, m.index + m[0].length + 120);
|
|
551
|
+
if (/BadRequest|Validation|Unprocessable|Invalid|Forbidden|Http|NotAcceptable|PayloadTooLarge/i.test(tail)) {
|
|
552
|
+
return true;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
return false;
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* 全项目扫描:函数体内存在输入校验证据的函数集合。
|
|
559
|
+
* key = `${相对路径}|${函数名}`(类方法为 `Class.method`,与 IR 的 f.name 一致)。
|
|
560
|
+
*/
|
|
561
|
+
function inputGuardFunctionKeys(project, absRoot) {
|
|
562
|
+
const out = new Set();
|
|
563
|
+
const consider = (name, node, fileRel) => {
|
|
564
|
+
if (!name || !node)
|
|
565
|
+
return;
|
|
566
|
+
if (bodyHasInputGuard(node.getText?.() ?? ""))
|
|
567
|
+
out.add(`${fileRel}|${name}`);
|
|
568
|
+
};
|
|
569
|
+
for (const sf of project.getSourceFiles()) {
|
|
570
|
+
if (sf.getFilePath().includes("node_modules"))
|
|
571
|
+
continue;
|
|
572
|
+
const fileRel = path.relative(absRoot, sf.getFilePath()).split(path.sep).join("/");
|
|
573
|
+
for (const fn of sf.getFunctions())
|
|
574
|
+
consider(fn.getName(), fn, fileRel);
|
|
575
|
+
for (const cls of sf.getClasses()) {
|
|
576
|
+
const cn = cls.getName();
|
|
577
|
+
for (const m of cls.getMethods()) {
|
|
578
|
+
const mn = m.getName();
|
|
579
|
+
consider(cn ? `${cn}.${mn}` : mn, m, fileRel);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
for (const vd of sf.getVariableDeclarations()) {
|
|
583
|
+
const init = vd.getInitializer();
|
|
584
|
+
if (init && (ts_morph_1.Node.isArrowFunction(init) || ts_morph_1.Node.isFunctionExpression(init))) {
|
|
585
|
+
consider(vd.getName(), init, fileRel);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
return out;
|
|
590
|
+
}
|
|
591
|
+
// ═══════════════════════════════════════════════════════════════
|
|
592
|
+
// E5(2026-09-23):参数流向外部副作用的证据 ⇒ 注入 __progmune_input_effect__
|
|
593
|
+
//
|
|
594
|
+
// 背景:Input Validation 规则的判据手里**只有函数名**(trigger 是
|
|
595
|
+
// create|add|post|upload 的名字正则),于是任何叫 add*/create* 的函数都被要求
|
|
596
|
+
// 「必须校验输入」:verdaccio ConfigBuilder.addStorage(配置装配)、ducktors
|
|
597
|
+
// createS3(工厂)都在报。FP 池 75 条里 62 条(83%)完全没有请求入口迹象。
|
|
598
|
+
//
|
|
599
|
+
// 判据来源:不是拍脑袋的词表,是先写探针 blind-benchmark/probe-body-evidence.ts
|
|
600
|
+
// 在**真值集**(fp-gold.jsonl,74 条已标注)上量化出来的:
|
|
601
|
+
// 粗判据 TP 召回 100%(17/17)/ FP 压制 77.2%(44/57)/ precision 23%→56.7%
|
|
602
|
+
// 精化判据(要求副作用调用吃到参数)FP 压制 91.2% 但丢 4 条真报 ⇒ 弃
|
|
603
|
+
// ⇒ 本文件里的正则与探针里的 EFFECT 表**逐字一致**,改一处必须改另一处
|
|
604
|
+
// (否则实测口径与代码口径不可比,R38)。
|
|
605
|
+
//
|
|
606
|
+
// 判据不是「是否 HTTP 入口」:见 protocol-detector.ts 侧注释,那条判据会压掉
|
|
607
|
+
// 4 条已核验真报。这里问的是「参数流向」,不是「入口形态」。
|
|
608
|
+
//
|
|
609
|
+
// 与 E3/E4 同构:AST 预扫 ⇒ key 集合 ⇒ 主循环后统一注入。
|
|
610
|
+
// ═══════════════════════════════════════════════════════════════
|
|
611
|
+
const INPUT_EFFECT_MARKER = "__progmune_input_effect__";
|
|
612
|
+
// 与 probe-body-evidence.ts 的 EFFECT 表一致(S1-S7)
|
|
613
|
+
const INPUT_EFFECT_PATTERNS = [
|
|
614
|
+
// S1 ORM/repository 写入
|
|
615
|
+
/\b(?:save|create|update|insert|upsert|persist|remove|deleteMany|delete|findOrCreate|upsertMany|createMany|updateMany)\s*\(/,
|
|
616
|
+
// S2 缓存/Redis 写入
|
|
617
|
+
/\b(?:sadd|hset|hmset|lpush|rpush|setex|zadd|incr|append)\s*\(/,
|
|
618
|
+
// S3 HTTP 出站 / 消息投递
|
|
619
|
+
/\b(?:fetch|axios|request|got|superagent)\s*[.(]|\bhttps?\.\s*(?:request|get|post)\s*\(|\b(?:publish|emit|send|dispatch)\s*\(/,
|
|
620
|
+
// S4 文件系统写入
|
|
621
|
+
/\b(?:writeFile|writeFileSync|appendFile|createWriteStream|mkdir|unlink|rm|rename|copyFile)\s*\(/,
|
|
622
|
+
// S5 委托给 repo/service/storage 层。**必须大小写不敏感**:webshape_E 语料写的是
|
|
623
|
+
// `repo.saveBanner(file)`(小写前缀 + 带后缀动词),大写前缀版两条件都不满足
|
|
624
|
+
// ⇒ 副作用漏判 ⇒ 该报的被压掉,闸门当场转红 3 条。
|
|
625
|
+
// 动词表含 upload:docmost uploadToDrive 只有 `this.storageService.upload(...)`,
|
|
626
|
+
// 漏一个动词就丢一条真报。
|
|
627
|
+
/\b\w*(?:repo|repository|service|storage|dao|manager|gateway|client|provider)\s*\.\s*(?:create|add|save|update|insert|upsert|write|store|persist|set|upload|put|send|publish|remove|delete)[A-Za-z0-9_]*\s*\(/i,
|
|
628
|
+
// S6 prisma / knex 写入
|
|
629
|
+
/\bprisma\s*\.\s*\w+\s*\.\s*(?:create|update|upsert|delete|createMany|updateMany)\s*\(|\b(?:knex|db|connection|tx|trx)\s*\(\s*['"]\w+['"]\s*\)\s*\.\s*(?:insert|update|del)\s*\(/,
|
|
630
|
+
// S7 Kysely 链式(trx.insertInto(...).values(...).execute())
|
|
631
|
+
/\.\s*(?:insertInto|updateTable|deleteFrom|insert|update|del|delete)\s*\(/,
|
|
632
|
+
];
|
|
633
|
+
function bodyHasExternalEffect(text) {
|
|
634
|
+
if (!text)
|
|
635
|
+
return false;
|
|
636
|
+
return INPUT_EFFECT_PATTERNS.some((re) => re.test(text));
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* 全项目扫描:函数体内存在「参数流向外部副作用」证据的函数集合。
|
|
640
|
+
* key = `${相对路径}|${函数名}`(类方法为 `Class.method`,与 IR 的 f.name 一致)。
|
|
641
|
+
*/
|
|
642
|
+
function inputEffectFunctionKeys(project, absRoot) {
|
|
643
|
+
const out = new Set();
|
|
644
|
+
const consider = (name, node, fileRel) => {
|
|
645
|
+
if (!name || !node)
|
|
646
|
+
return;
|
|
647
|
+
if (bodyHasExternalEffect(node.getText?.() ?? ""))
|
|
648
|
+
out.add(`${fileRel}|${name}`);
|
|
649
|
+
};
|
|
650
|
+
for (const sf of project.getSourceFiles()) {
|
|
651
|
+
if (sf.getFilePath().includes("node_modules"))
|
|
652
|
+
continue;
|
|
653
|
+
const fileRel = path.relative(absRoot, sf.getFilePath()).split(path.sep).join("/");
|
|
654
|
+
for (const fn of sf.getFunctions())
|
|
655
|
+
consider(fn.getName(), fn, fileRel);
|
|
656
|
+
for (const cls of sf.getClasses()) {
|
|
657
|
+
const cn = cls.getName();
|
|
658
|
+
for (const m of cls.getMethods()) {
|
|
659
|
+
const mn = m.getName();
|
|
660
|
+
consider(cn ? `${cn}.${mn}` : mn, m, fileRel);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
for (const vd of sf.getVariableDeclarations()) {
|
|
664
|
+
const init = vd.getInitializer();
|
|
665
|
+
if (init && (ts_morph_1.Node.isArrowFunction(init) || ts_morph_1.Node.isFunctionExpression(init))) {
|
|
666
|
+
consider(vd.getName(), init, fileRel);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
return out;
|
|
671
|
+
}
|
|
512
672
|
/**
|
|
513
673
|
* 标识符是否「看起来像基目录」—— G-A2 的定案依据。
|
|
514
674
|
*
|
|
@@ -2871,6 +3031,26 @@ function _extractSingleProject(absRoot, tsconfigPath) {
|
|
|
2871
3031
|
if (!fs.existsSync(tsconfigPath)) {
|
|
2872
3032
|
project.addSourceFilesAtPaths(path.join(absRoot, "**/*.ts"));
|
|
2873
3033
|
}
|
|
3034
|
+
// ═══════════════════════════════════════════════════════════════
|
|
3035
|
+
// 零源文件兜底(2026-09-22):solution 风格 tsconfig 会静默产出 0 函数
|
|
3036
|
+
//
|
|
3037
|
+
// 根 tsconfig 是「容器」时(`"files": []` + `"include": []` + `references`
|
|
3038
|
+
// 指向 tsconfig.app.json 等),ts-morph 按它加载会得到 **0 个源文件** ⇒
|
|
3039
|
+
// 0 个函数 ⇒ 0 条违规,**且不报错、不告警**。
|
|
3040
|
+
// 这比崩溃危险得多:调用方会把「0 函数」读成「这个仓库很干净」——
|
|
3041
|
+
// 实测踩到:FP 池 6 个切片里 gothinkster-node-express-realworld-example-app
|
|
3042
|
+
// 一直以 functions=0 参与统计(切片只保留了根 tsconfig.json,它引用的
|
|
3043
|
+
// tsconfig.app.json / tsconfig.spec.json 不在切片里,references 因此全被过滤掉)。
|
|
3044
|
+
//
|
|
3045
|
+
// 只在「确实一个源文件都没有」时才兜底 ⇒ 现有工程全部不受影响(它们是
|
|
3046
|
+
// `include: ["src"]` 的正常 tsconfig,走不到这里)。兜底同时打 stderr,
|
|
3047
|
+
// 让这类切片不可能再悄悄以「0 违规」混入统计。
|
|
3048
|
+
// ═══════════════════════════════════════════════════════════════
|
|
3049
|
+
if (!project.getSourceFiles().some((sf) => !sf.getFilePath().includes("node_modules"))) {
|
|
3050
|
+
console.error(`⚠️ [extract-ir] ${absRoot}:tsconfig 未纳入任何源文件(solution 风格 / 引用缺失)→ 回退为全目录 glob`);
|
|
3051
|
+
project.addSourceFilesAtPaths(path.join(absRoot, "**/*.ts"));
|
|
3052
|
+
project.addSourceFilesAtPaths(path.join(absRoot, "**/*.tsx"));
|
|
3053
|
+
}
|
|
2874
3054
|
const funcs = [];
|
|
2875
3055
|
// 标记增强的共享状态(2026-09-15 内联重构):
|
|
2876
3056
|
// sinkParams = 项目方法名 → 方法体内流入文件 sink 的形参下标 + 方法条目;
|
|
@@ -3051,6 +3231,27 @@ function _extractSingleProject(absRoot, tsconfigPath) {
|
|
|
3051
3231
|
if (!f.calls.includes(INPUT_SCHEMA_MARKER))
|
|
3052
3232
|
f.calls.push(INPUT_SCHEMA_MARKER);
|
|
3053
3233
|
}
|
|
3234
|
+
// ── E4(2026-09-23):函数体里有输入校验证据 ⇒ 注入 __progmune_input_guard__
|
|
3235
|
+
// 与 E3 同一处收口、同一套形态(函数声明 / 类方法 / 变量箭头)。
|
|
3236
|
+
const inputGuardKeys = inputGuardFunctionKeys(project, absRoot);
|
|
3237
|
+
for (const f of funcs) {
|
|
3238
|
+
if (!inputGuardKeys.has(`${f.file}|${f.name}`))
|
|
3239
|
+
continue;
|
|
3240
|
+
f.calls = f.calls || [];
|
|
3241
|
+
if (!f.calls.includes(INPUT_GUARD_MARKER))
|
|
3242
|
+
f.calls.push(INPUT_GUARD_MARKER);
|
|
3243
|
+
}
|
|
3244
|
+
// ── E5(2026-09-23):参数流向外部副作用的证据 ⇒ 注入 __progmune_input_effect__
|
|
3245
|
+
// 与 E3/E4 同一处收口、同一套形态(函数声明 / 类方法 / 变量箭头)。
|
|
3246
|
+
// Input Validation 规则据此做 requireMarker(无副作用证据 ⇒ 不要求输入校验)。
|
|
3247
|
+
const inputEffectKeys = inputEffectFunctionKeys(project, absRoot);
|
|
3248
|
+
for (const f of funcs) {
|
|
3249
|
+
if (!inputEffectKeys.has(`${f.file}|${f.name}`))
|
|
3250
|
+
continue;
|
|
3251
|
+
f.calls = f.calls || [];
|
|
3252
|
+
if (!f.calls.includes(INPUT_EFFECT_MARKER))
|
|
3253
|
+
f.calls.push(INPUT_EFFECT_MARKER);
|
|
3254
|
+
}
|
|
3054
3255
|
// ═══════════════════════════════════════════════════════════════
|
|
3055
3256
|
// Phase 5: Dynamic external function resolution
|
|
3056
3257
|
// Replaces hardcoded knownExternals
|
|
@@ -437,12 +437,38 @@ const SAFEGUARD_RULES = [
|
|
|
437
437
|
// 175 条损失已逐条归类,无一为真阳性。⇒ trigger 只看函数名。
|
|
438
438
|
triggerOwnNameOnly: true,
|
|
439
439
|
trigger: /\b(create|add|post|upload)(?:[A-Z]\w*|_\w+)|(?:[A-Z]\w*|_\w+)(Create|Add|Post|Upload)\b/i,
|
|
440
|
+
// E5(2026-09-23):语义前置条件 —— 只有「参数确实流向外部副作用」才谈得上
|
|
441
|
+
// 该不该校验。此前 trigger 是纯函数名正则(create|add|post|upload),判据手里
|
|
442
|
+
// **只有名字**:verdaccio 的 ConfigBuilder.addStorage/addLogger(纯配置装配)、
|
|
443
|
+
// ducktors 的 createS3/createAzureBlobStorage(工厂)都被要求「必须校验输入」,
|
|
444
|
+
// 而它们根本不接外部输入。FP 池实测:75 条里 62 条(83%)完全没有请求入口迹象。
|
|
445
|
+
//
|
|
446
|
+
// 为什么是「副作用」而不是「是否 HTTP 入口」(R41:先分缺信息 vs 缺语义):
|
|
447
|
+
// 「非入口 ⇒ 不报」这个更直觉的判据**有毒** —— 真值集里 4 条已核验 TP
|
|
448
|
+
// (docmost addContributors→redis.sadd、addLabelsToPage→labelRepo.findOrCreate、
|
|
449
|
+
// hedgedoc ApiTokenService.createToken、hoppscotch AdminService.createATeam)
|
|
450
|
+
// 都没有 req 对象,但参数是从 Controller 透传的外部输入,且确实没有校验。
|
|
451
|
+
// ⇒ Service 层方法不因「组件边界」获得豁免。区分点在**参数流向**,不在入口形态。
|
|
452
|
+
//
|
|
453
|
+
// 量化(blind-benchmark/probe-body-evidence.ts --from-gold,74 条已标注真值):
|
|
454
|
+
// TP 召回 100%(17/17,丢 0 条真报)/FP 压制 77.2%(44/57)/
|
|
455
|
+
// precision 23.0% → 56.7%/净收益 44 条。多数类基线 77.0%。
|
|
456
|
+
// 「要求副作用调用吃到参数」的精化版 FP 压制虽达 91.2%,但会丢 4 条真报,
|
|
457
|
+
// 漏报比误报贵 ⇒ 取粗判据。
|
|
458
|
+
requireMarker: "__progmune_input_effect__",
|
|
459
|
+
// 该标记只由 TS 提取器产出(Python 提取器不产出任何 __progmune_* 标记),
|
|
460
|
+
// 故只在 TS/JS 上强制,Python 侧保持原有行为,避免静默归零(R42)。
|
|
461
|
+
requireMarkerLanguages: ["typescript", "javascript"],
|
|
440
462
|
safeguards: [
|
|
441
463
|
// E3(2026-09-22):__progmune_input_schema__ —— 提取器在「入参类型是带校验
|
|
442
464
|
// 装饰器的 DTO 类」时产出(NestJS 把校验写在 DTO 属性装饰器上,词形匹配永远
|
|
443
465
|
// 看不到)。本规则此前不接受任何 __progmune_* 标记 ⇒ 该证据通道是断的。
|
|
444
466
|
// 注意只加在**抑制位**(safeguard),不进 trigger:新通道只能做减法。
|
|
445
|
-
|
|
467
|
+
// E4(2026-09-23):__progmune_input_guard__ —— 提取器在函数体内找到输入校验
|
|
468
|
+
// 证据时产出(limits:{fileSize…} / if(!x) throw BadRequest…)。此前规则只看
|
|
469
|
+
// 函数名与调用名,函数体里的校验看不见 ⇒ docmost 两个 upload controller
|
|
470
|
+
// 明明有校验仍被判「未校验」。与 E3 同源:只加在抑制位,不进 trigger。
|
|
471
|
+
{ pattern: /\b(validate|sanitize|check|verify)(?:[A-Z]\w*|_\w*)(Content|Input|Length|Title|Body|Type|Size|File|Data|Param|Arg|Field|Value)\b|\b(validateContent|sanitizeInput|checkLength|verifyType|checkSize)\b|__progmune_input_schema__|__progmune_input_guard__/i, label: "input_validation" },
|
|
446
472
|
],
|
|
447
473
|
violationMessage: "Content creation function does not validate or sanitize input. XSS, injection, and oversized content possible.",
|
|
448
474
|
conceptMissing: ["InputSanitization", "ContentValidation", "SizeLimit"],
|
|
@@ -1104,7 +1130,7 @@ function identifierParse(name) {
|
|
|
1104
1130
|
const words = [];
|
|
1105
1131
|
for (const part of parts) {
|
|
1106
1132
|
// Split camelCase/PascalCase: "registerNewUser" → ["register", "New", "User"]
|
|
1107
|
-
const camelWords = part.replace(/([a-z])([A-Z])/g, "$1
|
|
1133
|
+
const camelWords = part.replace(/([a-z])([A-Z])/g, "$1\x00$2").split("\x00");
|
|
1108
1134
|
for (const w of camelWords) {
|
|
1109
1135
|
if (w.length > 0)
|
|
1110
1136
|
words.push(w);
|
|
@@ -1148,9 +1174,16 @@ function detectSafeguardViolations(calls, enclosingFuncName, language, params, e
|
|
|
1148
1174
|
const triggerMatch = triggerCalls.some(c => rule.trigger.test(c));
|
|
1149
1175
|
if (!triggerMatch)
|
|
1150
1176
|
continue;
|
|
1151
|
-
// Semantic precondition marker (extractor-emitted)
|
|
1152
|
-
|
|
1153
|
-
|
|
1177
|
+
// Semantic precondition marker (extractor-emitted).
|
|
1178
|
+
// E5(2026-09-23):只在能产出该标记的语言上强制。不限定语言的话,
|
|
1179
|
+
// Python 侧(提取器不产出任何 __progmune_* 标记)会静默归零——
|
|
1180
|
+
// 而 TS 侧所有闸门都看不见这种退化(R42)。
|
|
1181
|
+
if (rule.requireMarker) {
|
|
1182
|
+
const enforce = !rule.requireMarkerLanguages
|
|
1183
|
+
|| (language ? rule.requireMarkerLanguages.includes(language) : true);
|
|
1184
|
+
if (enforce && !effectiveCalls.includes(rule.requireMarker))
|
|
1185
|
+
continue;
|
|
1186
|
+
}
|
|
1154
1187
|
// Skip authorization rules for auth functions — they ARE the auth
|
|
1155
1188
|
if (isAuthFunction && rule.category === "authorization")
|
|
1156
1189
|
continue;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "progmune-runtime",
|
|
3
|
-
"version": "3.7.
|
|
3
|
+
"version": "3.7.52",
|
|
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/",
|