progmune-runtime 3.7.24 → 3.7.26

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,37 @@
1
1
  # Changelog
2
2
 
3
+ ## [3.7.26] — 2026-09-13
4
+
5
+ ### 精度修复批次(AI 生成项目验证 V2 + 真实修复回归驱动的引擎改动)
6
+
7
+ - **FastAPI 认证词表补现代认证形态**(`tools/extract_framework_py.py`):`current_org`/`current_caller`/`caller_context`(skyvern 146 条误报的根因词族)+ `verified_user`/`admin_user`(open-webui 142 条误报的根因词族)——每个新真实语料都暴露一批框架层词汇缺口;fastapi-realworld 0 FP 保持 + 摘保护反证精确触发
8
+ - **词段匹配边界收紧**(`ssg-bridge.ts` Strategy 2):规则词必须按序出现且首词@词位 0、末词@末位(此前仅要求全部出现即命中)——skyvern 35 条 SSG 误报消灭 16 条、nginx 3 FP→0;**Python 盲测 64/64 零漂移、TS 795 零漂移(3086 flags LOST 0 / ADDED 0)、C 金标 F1=95.7% 不变、Go 盲测 P=R=100%**
9
+ - **一次性文件 API 原子化**(`protocols.json`):`read_text`/`write_text`/`read_bytes`/`write_bytes`/`pathlib.*` 入 `read_file_atomic`/`write_file_atomic` 别名——Path.read_text 族内部完成 open→close,不再报裸读
10
+ - **类方法提取修复**(`extract-ir.ts`):类方法循环此前误置于变量声明循环内——无顶层变量声明的文件(openhop store.ts 复现)方法整体漏提取,有变量声明的文件重复 push;移至文件层 + `cls.isExported()`
11
+ - **路径穿越检测 TS 化(A1,修复回归 fr-007 驱动)**:TS 提取器注入 `__progmune_path_traversal__`(request 污点→fs sink 单跳 + 跨函数一跳——调用点污点实参流入项目方法体内的文件 sink);**引擎 IR 层直接消费标记**(call-sequence 构建过滤 `__progmune_` 前缀,标记到不了 specific-check——Python 标记此前在引擎管线中是死的,本版同时点亮两侧);规则 `languages` 补 typescript/javascript;domain-validator 补 `PATH_TRAVERSAL` 检查
12
+
13
+ ### 真实安全修复回归 V1(决定性实验,回答「能力 vs 语料」)
14
+
15
+ - **语料**:8 条真实修复(open-webui 6 Python / openhop 1 TS / SimpleWebAuthn 1 TS,全部 GHSA/CVE + 修复 commit + 真值文件清单),注册表 `blind-benchmark/fix-regression-corpus.json`
16
+ - **基线 0/8 检出**——漏报根因七类逐条记录(查询语义 / 策略逻辑分支 / 凭据数据流 / 归属数据流 / 配置通道路由 / 语言面缺口 / 密码学协议语义),见 `blind-benchmark/REALWORLD_FIX_REGRESSION_V1.md`
17
+ - **A1 后 fr-007(openhop Flow ID 路径穿越)转 DETECTED**:PATH_TRAVERSAL ×4 命中真值位置(routes.ts flowRoutes + store.ts FlowStore.save/get/delete);fr-001~006/008 待对应能力扩展
18
+ - **AI 生成项目验证 V2**(`blind-benchmark/REALWORLD_AI_GENERATED_V2.md`):Skyvern ⭐22,971 生产管线扫描 181 条违规逐条人工标注全 FP——框架层认证词汇缺口 144 条 + 词段误报 35 条(其中 16 条把代码里的路径穿越防御/符号链接校验误报为协议违规);修复后 181→22(剩余 19 条夹心形态词法不可区分 + 2 条 HMAC webhook + 1 条精确名碰撞,见报告 §3.2 与 §5)
19
+
20
+ ### 回归
21
+
22
+ - 发布门:build ✓ / check 免疫正常 ✓ / Python 盲测 64 零漂移 ✓ / TS 795 零漂移 ✓ / C 金标 F1=95.7% ✓ / Go 盲测 P=R=100% ✓ / 定向 184 tests green(本地全量套件重负载 worker 崩溃为既有环境问题,以 CI 为准)
23
+
24
+ ## [3.7.25] — 2026-09-10
25
+
26
+ ### 失败语料上报链路修复(中央 hub 恢复数据流)
27
+
28
+ - **诊断**:中央 hub 自 2026-05-18 起零新增(totalCount=12),npm 周下载 1000-2000 但数据全丢——两个断点:① `immune-reporter` 读废弃路径 `failure_corpus/`(语料已统一到 `.progmune_corpus/`)② 端点默认 `localhost:3000`,安装态用户到不了中央 hub
29
+ - **修复**:数据源对齐 `.progmune_corpus/{date}/fail_*.json`(`PROGMUNE_PROJECT_DIR`/`PROGMUNE_CORPUS_DIR` 感知,损坏记录跳过);端点默认 `https://progmune-runtime.fly.dev/report`;`PROGMUNE_HUB=off` 可关闭上报
30
+ - **默认脱敏**:函数名 SHA-256 截 12 位(同名同哈希,模式聚合仍有效);`PROGMUNE_FINGERPRINT_DETAIL=1` 才发送原文函数名;源码与变量值永不上传
31
+ - **隐私口径如实披露**:README/落地页/privacy 页——「代码不上传」保持为真,「无遥测」表述改为如实披露脱敏指纹默认上报与关闭方式
32
+ - **中央 hub 防护层上线**:字段白名单 + 类型校验(SVL-1~4、时间戳 ±窗口、序列长度上限);请求体 512KB / 每请求 ≤500 条;实例+时间戳+模式指纹去重;每实例每 10 分钟 ≤5 次;每日 ≤10000 条;可选 `PROGMUNE_HUB_TOKEN` Bearer 认证(客户端 `PROGMUNE_HUB_TOKEN` 环境变量同步支持)
33
+ - 回归 12 green;hub 冒烟六场景(正常/畸形/去重/超大/限流/认证)全过
34
+
3
35
  ## [3.7.24] — 2026-09-06
4
36
 
5
37
  ### 审计复检三项修复
package/README.md CHANGED
@@ -6,6 +6,10 @@
6
6
  [![MCP](https://img.shields.io/badge/MCP-Compatible-blue)](https://modelcontextprotocol.io)
7
7
  [![TS Benchmark](https://img.shields.io/badge/TS%20R98.5%25%20P100%25-22c55e)]()
8
8
  [![Python Benchmark](https://img.shields.io/badge/Python%20R100%25%20P100%25-22c55e)]()
9
+ [![npm version](https://img.shields.io/npm/v/progmune-runtime?color=cb3837)](https://www.npmjs.com/package/progmune-runtime)
10
+ [![npm weekly downloads](https://img.shields.io/npm/dw/progmune-runtime?color=cb3837)](https://www.npmjs.com/package/progmune-runtime)
11
+ [![GitHub stars](https://img.shields.io/github/stars/shenlian19831109/progmune-runtime?color=gold)](https://github.com/shenlian19831109/progmune-runtime)
12
+ [![GitHub release](https://img.shields.io/github/v/release/shenlian19831109/progmune-runtime?color=gold)](https://github.com/shenlian19831109/progmune-runtime/releases)
9
13
 
10
14
  > [中文版](https://github.com/shenlian19831109/progmune-runtime/blob/main/README.zh-CN.md) · English Version
11
15
 
@@ -369,6 +369,235 @@ function extractDirectCalls(func) {
369
369
  }
370
370
  return [...new Set(calls)];
371
371
  }
372
+ // ═══════════════════════════════════════════════════════════════
373
+ // Path traversal marker(2026-09-11,REALWORLD_FIX_REGRESSION_V1 fr-007)
374
+ // 镜像 tools/extract_ir.py 的 request 污点 → 文件 sink 单跳追踪,另补
375
+ // 跨函数一跳:调用点把污点实参传给项目方法,该方法体内对应参数流入
376
+ // 文件 sink → 在调用方函数注入 __progmune_path_traversal__。
377
+ // 消费方:protocol-domain-validator 的 PATH_TRAVERSAL 检查(引擎路径)
378
+ // 与 protocol-detector 的同名规则(source-level benchmark 路径)。
379
+ // ═══════════════════════════════════════════════════════════════
380
+ const PATH_TRAVERSAL_MARKER = "__progmune_path_traversal__";
381
+ const TS_FILE_SINK_NAMES = [
382
+ "readFile", "readFileSync", "writeFile", "writeFileSync",
383
+ "appendFile", "appendFileSync", "unlink", "unlinkSync", "rm", "rmSync",
384
+ "mkdir", "mkdirSync", "open", "openSync", "createReadStream",
385
+ "createWriteStream", "existsSync", "readdir", "readdirSync",
386
+ "copyFile", "rename", "stat", "lstat", "rmdir", "truncate",
387
+ ];
388
+ function tsSinkCallRegex() {
389
+ return new RegExp(`(?:^|[^\\w.$])(?:await\\s+)?(?:[\\w$]+\\.)?(${TS_FILE_SINK_NAMES.join("|")})\\s*\\(([\\s\\S]{0,250}?)\\)`, "g");
390
+ }
391
+ /** request 根表达式:req.params / request.body / req.query['x'] … */
392
+ function hasRequestRootedExpr(text) {
393
+ return /\b(?:req|request)\.(?:params|query|body|headers|cookies)\b[.[]?/.test(text);
394
+ }
395
+ /** 函数体内被 request 污染的局部名(含解构与单跳赋值,深度 ≤2) */
396
+ function collectTaintedNames(text) {
397
+ const tainted = new Set();
398
+ let m;
399
+ const direct = /(?:const|let|var)\s+([\w$]+)\s*=\s*(?:await\s+)?(?:req|request)\.(?:params|query|body|headers|cookies)\b[.[]?/g;
400
+ while ((m = direct.exec(text)) !== null)
401
+ tainted.add(m[1]);
402
+ const destr = /(?:const|let|var)\s*\{\s*([^}=]*?)\s*\}\s*=\s*(?:await\s+)?(?:req|request)\.(?:params|query|body|headers|cookies)\b/g;
403
+ while ((m = destr.exec(text)) !== null) {
404
+ for (const part of m[1].split(",")) {
405
+ const name = part.trim().split(":")[0].trim();
406
+ if (/^[\w$]+$/.test(name))
407
+ tainted.add(name);
408
+ }
409
+ }
410
+ const bareAssign = /(?:^|[^\w$.])([\w$]+)\s*=\s*(?:req|request)\.(?:params|query|body|headers|cookies)\b[.[]?/g;
411
+ while ((m = bareAssign.exec(text)) !== null)
412
+ tainted.add(m[1]);
413
+ // 单跳传播(深度 ≤2)
414
+ for (let depth = 0; depth < 2; depth++) {
415
+ if (tainted.size === 0)
416
+ break;
417
+ const names = [...tainted].join("|");
418
+ const hop = new RegExp(`(?:const|let|var)\\s+([\\w$]+)\\s*=\\s*\\b(?:${names})\\b`, "g");
419
+ let added = false;
420
+ while ((m = hop.exec(text)) !== null) {
421
+ if (!tainted.has(m[1])) {
422
+ tainted.add(m[1]);
423
+ added = true;
424
+ }
425
+ }
426
+ if (!added)
427
+ break;
428
+ }
429
+ return tainted;
430
+ }
431
+ function taintPattern(tainted) {
432
+ const parts = [
433
+ /\b(?:req|request)\.(?:params|query|body|headers|cookies)\b[.[]?/.source,
434
+ ];
435
+ if (tainted.size > 0) {
436
+ parts.push(`\\b(?:${[...tainted].join("|")})\\b`);
437
+ }
438
+ return new RegExp(parts.join("|"));
439
+ }
440
+ /** 本函数体内:文件 sink 的实参窗口含 request 污点 → true */
441
+ function hasTaintedSinkCall(text, tainted) {
442
+ const taint = taintPattern(tainted);
443
+ if (!taint)
444
+ return false;
445
+ const sinkRe = tsSinkCallRegex();
446
+ let m;
447
+ while ((m = sinkRe.exec(text)) !== null) {
448
+ if (taint.test(m[2] || ""))
449
+ return true;
450
+ }
451
+ return false;
452
+ }
453
+ /** 深度 0 逗号切分实参窗口(截断窗口内近似;首个参数位置常用于路径) */
454
+ function splitArgWindow(win) {
455
+ const args = [];
456
+ let depth = 0;
457
+ let cur = "";
458
+ for (const ch of win) {
459
+ if (ch === "(" || ch === "[" || ch === "{")
460
+ depth++;
461
+ else if (ch === ")" || ch === "]" || ch === "}")
462
+ depth--;
463
+ else if (ch === "," && depth === 0) {
464
+ args.push(cur);
465
+ cur = "";
466
+ continue;
467
+ }
468
+ cur += ch;
469
+ }
470
+ if (cur.trim())
471
+ args.push(cur);
472
+ return args;
473
+ }
474
+ /** 项目方法名 → 方法体内流入文件 sink 的形参下标集合 */
475
+ function methodSinkParamMap(project, absRoot) {
476
+ const map = new Map();
477
+ for (const sf of project.getSourceFiles()) {
478
+ const relPath = path.relative(absRoot, sf.getFilePath());
479
+ for (const cls of sf.getClasses()) {
480
+ const cn = cls.getName();
481
+ if (!cn)
482
+ continue;
483
+ for (const m of cls.getMethods()) {
484
+ const params = m.getParameters().map((p) => p.getName());
485
+ if (params.length === 0)
486
+ continue;
487
+ const text = m.getText();
488
+ const sinkRe = tsSinkCallRegex();
489
+ let sm;
490
+ while ((sm = sinkRe.exec(text)) !== null) {
491
+ const win = sm[2] || "";
492
+ params.forEach((pname, idx) => {
493
+ if (new RegExp(`\\b${pname}\\b`).test(win)) {
494
+ if (!map.has(m.getName())) {
495
+ map.set(m.getName(), { idxs: new Set(), entries: [] });
496
+ }
497
+ const rec = map.get(m.getName());
498
+ rec.idxs.add(idx);
499
+ const fullName = `${cn}.${m.getName()}`;
500
+ if (!rec.entries.some((e) => e.name === fullName)) {
501
+ rec.entries.push({ name: fullName, file: relPath });
502
+ }
503
+ }
504
+ });
505
+ }
506
+ }
507
+ }
508
+ }
509
+ return map;
510
+ }
511
+ /**
512
+ * 注入路径穿越标记:
513
+ * 1) 本函数体内 sink 实参直接污点;
514
+ * 2) 跨函数一跳——调用项目方法且污点实参落在该方法的 sink 形参位置。
515
+ * 标记只进 funcs 中该函数的 calls(方法条目不单独标——违规归因到入口)。
516
+ */
517
+ function augmentPathTraversalMarkers(project, funcs, absRoot) {
518
+ const sinkParams = methodSinkParamMap(project, absRoot);
519
+ const methodCallRe = new RegExp(`\\.([\\w$]+)\\s*\\(([\\s\\S]{0,250}?)\\)`, "g");
520
+ const mark = (name, file) => {
521
+ const entry = funcs.find((x) => x.name === name && x.file === file);
522
+ if (!entry)
523
+ return;
524
+ entry.calls = entry.calls || [];
525
+ if (!entry.calls.includes(PATH_TRAVERSAL_MARKER)) {
526
+ entry.calls.push(PATH_TRAVERSAL_MARKER);
527
+ }
528
+ };
529
+ // 对每个函数式节点(函数声明/方法/箭头回调——fastify 路由回调是内联
530
+ // 箭头函数,不在 funcs 中)独立做污点分析。节点文本预过滤:
531
+ // 不含 request 访问也不含 sink 方法调用则跳过。
532
+ const sinkMethodNames = [...sinkParams.keys()];
533
+ const prefilter = new RegExp(`\\b(?:req|request)\\.(?:params|query|body|headers|cookies)\\b|` +
534
+ (sinkMethodNames.length > 0 ? `\\.(?:${sinkMethodNames.join("|")})\\s*\\(` : "a^"));
535
+ for (const sf of project.getSourceFiles()) {
536
+ if (sf.getFilePath().includes("node_modules"))
537
+ continue;
538
+ const relPath = path.relative(absRoot, sf.getFilePath());
539
+ sf.forEachDescendant((node) => {
540
+ if (!ts_morph_1.Node.isFunctionDeclaration(node) &&
541
+ !ts_morph_1.Node.isArrowFunction(node) &&
542
+ !ts_morph_1.Node.isMethodDeclaration(node)) {
543
+ return;
544
+ }
545
+ const text = node.getText();
546
+ if (!prefilter.test(text))
547
+ return;
548
+ const tainted = collectTaintedNames(text);
549
+ if (tainted.size === 0)
550
+ return;
551
+ const taint = taintPattern(tainted);
552
+ if (!taint)
553
+ return;
554
+ // 1) 节点体内 sink 实参直接污点 → 标记该节点归属的 funcs 条目
555
+ if (hasTaintedSinkCall(text, tainted)) {
556
+ const nodeName = ts_morph_1.Node.isArrowFunction(node) ? undefined : node.getName();
557
+ if (ts_morph_1.Node.isFunctionDeclaration(node) && nodeName) {
558
+ mark(nodeName, relPath);
559
+ }
560
+ else if (ts_morph_1.Node.isMethodDeclaration(node) && nodeName) {
561
+ const cls = node.getParent();
562
+ if (ts_morph_1.Node.isClassDeclaration(cls) && cls.getName()) {
563
+ mark(`${cls.getName()}.${nodeName}`, relPath);
564
+ }
565
+ }
566
+ return;
567
+ }
568
+ // 2) 跨函数一跳:污点实参传入项目方法体的文件 sink。
569
+ // 标记调用方(若可归属)+ 被调方法条目——路由回调不可归属时,
570
+ // 方法条目是唯一可归因位置(openhop 形态)。
571
+ methodCallRe.lastIndex = 0;
572
+ let m;
573
+ while ((m = methodCallRe.exec(text)) !== null) {
574
+ const callee = m[1];
575
+ const rec = sinkParams.get(callee);
576
+ if (!rec || rec.idxs.size === 0)
577
+ continue;
578
+ const win = m[2] || "";
579
+ const args = splitArgWindow(win);
580
+ const hit = args.some((arg, i) => rec.idxs.has(i) && taint.test(arg));
581
+ if (!hit)
582
+ continue;
583
+ const nodeName = ts_morph_1.Node.isArrowFunction(node) ? undefined : node.getName();
584
+ if (ts_morph_1.Node.isFunctionDeclaration(node) && nodeName) {
585
+ mark(nodeName, relPath);
586
+ }
587
+ else if (ts_morph_1.Node.isMethodDeclaration(node) && nodeName) {
588
+ const cls = node.getParent();
589
+ if (ts_morph_1.Node.isClassDeclaration(cls) && cls.getName()) {
590
+ mark(`${cls.getName()}.${nodeName}`, relPath);
591
+ }
592
+ }
593
+ for (const me of rec.entries) {
594
+ mark(me.name, me.file);
595
+ }
596
+ return;
597
+ }
598
+ });
599
+ }
600
+ }
372
601
  /**
373
602
  * 从 TypeScript 项目提取 IR(函数签名、参数、返回值、协议注解)。
374
603
  * @protocol namespace=dev_pipeline pre_states=[] post_states=["IR_EXTRACTED"] invalidate=["IR_STALE"]
@@ -519,30 +748,43 @@ function _extractSingleProject(absRoot, tsconfigPath) {
519
748
  }
520
749
  }
521
750
  }
522
- // Extract class methods: ts-morph getFunctions() excludes class members
523
- for (const cls of sf.getClasses()) {
524
- const cn = cls.getName();
525
- if (!cn)
751
+ }
752
+ // Extract class methods: ts-morph getFunctions() excludes class members
753
+ // (2026-09-12 修复:此循环此前误置于 VariableDeclaration 循环内——
754
+ // 无顶层变量声明的文件(如 openhop store.ts)方法整体漏提取,
755
+ // 且有变量声明的文件会重复 push。移至 sf 层 + 去重语义。)
756
+ for (const cls of sf.getClasses()) {
757
+ const cn = cls.getName();
758
+ if (!cn)
759
+ continue;
760
+ for (const m of cls.getMethods()) {
761
+ const mn = m.getName();
762
+ if (!mn)
526
763
  continue;
527
- for (const m of cls.getMethods()) {
528
- const mn = m.getName();
529
- if (!mn)
530
- continue;
531
- funcs.push({
532
- name: `${cn}.${mn}`,
533
- params: m.getParameters().map((p) => ({ name: p.getName(), type: getParamType(p), typeDetail: getParamTypeDetail(p) })),
534
- returnType: m.getReturnTypeNode?.()?.getText?.() || "any",
535
- returnTypeDetail: m.getReturnTypeNode?.()?.getText?.() || "any",
536
- file: relPath,
537
- exported: vd.isExported(), calls: [],
538
- ...parseCapabilityFromJSDoc(m),
539
- protocol: parseProtocolFromJSDoc(m),
540
- });
541
- }
764
+ funcs.push({
765
+ name: `${cn}.${mn}`,
766
+ params: m.getParameters().map((p) => ({ name: p.getName(), type: getParamType(p), typeDetail: getParamTypeDetail(p) })),
767
+ returnType: m.getReturnTypeNode?.()?.getText?.() || "any",
768
+ returnTypeDetail: m.getReturnTypeNode?.()?.getText?.() || "any",
769
+ file: relPath,
770
+ exported: cls.isExported(),
771
+ calls: [],
772
+ ...parseCapabilityFromJSDoc(m),
773
+ protocol: parseProtocolFromJSDoc(m),
774
+ });
542
775
  }
543
776
  }
544
777
  }
545
778
  // ═══════════════════════════════════════════════════════════════
779
+ // Phase 4.5: Path traversal marker(2026-09-11,fr-007 openhop)
780
+ // 镜像 Python 提取器的 request 污点 → 文件 sink 单跳追踪,并补
781
+ // 跨函数一跳:调用点把污点参数传给项目方法,该方法体内该参数
782
+ // 流入文件 sink → 调用方函数注入 __progmune_path_traversal__。
783
+ // 消费方:protocol-domain-validator PATH_TRAVERSAL 检查(引擎)
784
+ // 与 protocol-detector 同名规则(source-level benchmark 路径)。
785
+ // ═══════════════════════════════════════════════════════════════
786
+ augmentPathTraversalMarkers(project, funcs, absRoot);
787
+ // ═══════════════════════════════════════════════════════════════
546
788
  // Phase 5: Dynamic external function resolution
547
789
  // Replaces hardcoded knownExternals
548
790
  // includes: npm packages (.d.ts via ts.resolveModuleName), Node.js built-ins (@types/node)
@@ -33,6 +33,9 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.buildHeaders = buildHeaders;
37
+ exports.resolveEndpoint = resolveEndpoint;
38
+ exports.maskFunctionName = maskFunctionName;
36
39
  exports.extractFingerprints = extractFingerprints;
37
40
  exports.reportFingerprints = reportFingerprints;
38
41
  exports.previewFingerprints = previewFingerprints;
@@ -41,8 +44,34 @@ const path = __importStar(require("path"));
41
44
  const https = __importStar(require("https"));
42
45
  const http = __importStar(require("http"));
43
46
  const crypto = __importStar(require("crypto"));
44
- const REPORT_ENDPOINT = process.env.PROGMUNE_HUB || "http://localhost:3000/report";
45
- const CURSOR_FILE = path.resolve(__dirname, "../.progmune_memory/report_cursor.json");
47
+ // ── 数据源(与 failure-corpus.ts 统一路径,项目级、可写) ──
48
+ const PROJECT_DIR = process.env.PROGMUNE_PROJECT_DIR || process.cwd();
49
+ const CORPUS_DIR = process.env.PROGMUNE_CORPUS_DIR || path.resolve(PROJECT_DIR, ".progmune_corpus");
50
+ const CURSOR_FILE = path.join(CORPUS_DIR, ".report_cursor.json");
51
+ // ── 端点与开关 ──
52
+ const HUB_ENV = process.env.PROGMUNE_HUB;
53
+ const HUB_TOKEN = process.env.PROGMUNE_HUB_TOKEN;
54
+ const DETAIL_ENABLED = process.env.PROGMUNE_FINGERPRINT_DETAIL === "1";
55
+ const OFF_PATTERN = /^(off|0|false|no|disabled)$/i;
56
+ /** 上报请求头:hub 设了 PROGMUNE_HUB_TOKEN 时需带 Bearer 认证。 */
57
+ function buildHeaders(token = HUB_TOKEN) {
58
+ const headers = { "Content-Type": "application/json" };
59
+ if (token)
60
+ headers["Authorization"] = `Bearer ${token}`;
61
+ return headers;
62
+ }
63
+ /** 解析上报端点:显式关闭 → null;未设 → 中央 hub;其他 → 自定义 URL。 */
64
+ function resolveEndpoint(env = HUB_ENV) {
65
+ if (env && OFF_PATTERN.test(env))
66
+ return null;
67
+ return env || "https://progmune-runtime.fly.dev/report";
68
+ }
69
+ /** 脱敏:默认哈希函数名;DETAIL 开关打开时保留原文。 */
70
+ function maskFunctionName(name, detail = DETAIL_ENABLED) {
71
+ if (detail)
72
+ return name;
73
+ return "fn:" + crypto.createHash("sha256").update(name).digest("hex").substring(0, 12);
74
+ }
46
75
  function getInstanceId() {
47
76
  const host = require("os").hostname();
48
77
  const cwd = process.cwd();
@@ -62,8 +91,8 @@ function saveReportCursor(timestamp, count) {
62
91
  fs.mkdirSync(dir, { recursive: true });
63
92
  fs.writeFileSync(CURSOR_FILE, JSON.stringify({ lastTimestamp: timestamp, reportedCount: count, updatedAt: new Date().toISOString() }, null, 2));
64
93
  }
65
- function extractFingerprints(cursor) {
66
- const corpusDir = path.resolve(__dirname, "../failure_corpus");
94
+ /** 收集语料目录中的新失败指纹(含脱敏处理)。corpusDir 参数便于测试注入。 */
95
+ function extractFingerprints(cursor, corpusDir = CORPUS_DIR) {
67
96
  if (!fs.existsSync(corpusDir))
68
97
  return [];
69
98
  const fingerprints = [];
@@ -73,16 +102,22 @@ function extractFingerprints(cursor) {
73
102
  if (!fs.statSync(datePath).isDirectory())
74
103
  continue;
75
104
  for (const file of fs.readdirSync(datePath).sort()) {
76
- if (!file.endsWith(".json"))
105
+ if (!file.endsWith(".json") || !file.startsWith("fail_"))
77
106
  continue;
78
107
  const filePath = path.join(datePath, file);
79
- const record = JSON.parse(fs.readFileSync(filePath, "utf-8"));
80
- // 如果游标存在且该记录时间戳 <= 游标,跳过
81
- if (cursor && cursor.lastTimestamp && record.timestamp <= cursor.lastTimestamp)
108
+ let record;
109
+ try {
110
+ record = JSON.parse(fs.readFileSync(filePath, "utf-8"));
111
+ }
112
+ catch {
113
+ continue; /* 损坏记录跳过,best-effort */
114
+ }
115
+ // 游标存在且该记录时间戳 <= 游标,跳过
116
+ if (cursor && cursor.lastTimestamp && (record.timestamp || "") <= cursor.lastTimestamp)
82
117
  continue;
83
118
  const funcSeq = (record.actionSequence || [])
84
119
  .filter((a) => a.kind === "call")
85
- .map((a) => a.function);
120
+ .map((a) => maskFunctionName(String(a.function)));
86
121
  fingerprints.push({
87
122
  instance_id: instanceId,
88
123
  timestamp: record.timestamp,
@@ -99,6 +134,10 @@ function extractFingerprints(cursor) {
99
134
  }
100
135
  /** @requires CORPUS @produces FINGERPRINT_REPORT */
101
136
  async function reportFingerprints() {
137
+ const endpoint = resolveEndpoint();
138
+ if (endpoint === null) {
139
+ return { success: true, message: "上报已禁用(PROGMUNE_HUB=off),跳过" };
140
+ }
102
141
  const cursor = getReportCursor();
103
142
  const fingerprints = extractFingerprints(cursor);
104
143
  if (fingerprints.length === 0) {
@@ -109,11 +148,11 @@ async function reportFingerprints() {
109
148
  const maxTimestamp = fingerprints[fingerprints.length - 1].timestamp;
110
149
  const payload = JSON.stringify({ fingerprints });
111
150
  return new Promise((resolve) => {
112
- const url = new URL(REPORT_ENDPOINT);
151
+ const url = new URL(endpoint);
113
152
  const transport = url.protocol === "https:" ? https : http;
114
- const req = transport.request(REPORT_ENDPOINT, {
153
+ const req = transport.request(endpoint, {
115
154
  method: "POST",
116
- headers: { "Content-Type": "application/json" },
155
+ headers: buildHeaders(),
117
156
  }, (res) => {
118
157
  let data = "";
119
158
  res.on("data", (chunk) => data += chunk);
@@ -128,6 +167,7 @@ async function reportFingerprints() {
128
167
  }
129
168
  });
130
169
  });
170
+ req.setTimeout(8000, () => req.destroy(new Error("上报超时")));
131
171
  req.on("error", (e) => resolve({ success: false, message: `网络错误: ${e.message}` }));
132
172
  req.write(payload);
133
173
  req.end();
@@ -0,0 +1,149 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ const vitest_1 = require("vitest");
37
+ const fs = __importStar(require("fs"));
38
+ const os = __importStar(require("os"));
39
+ const path = __importStar(require("path"));
40
+ const immune_reporter_1 = require("./immune-reporter");
41
+ /**
42
+ * Immune Reporter 测试——2026-09 修复轮:
43
+ * 默认脱敏、PROGMUNE_HUB=off 开关、语料路径对齐 .progmune_corpus。
44
+ * 涉及文件系统的用例使用临时目录注入(corpusDir 参数),不触碰真实语料。
45
+ */
46
+ let tmpDir;
47
+ (0, vitest_1.beforeEach)(() => {
48
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "progmune-reporter-"));
49
+ });
50
+ (0, vitest_1.afterEach)(() => {
51
+ fs.rmSync(tmpDir, { recursive: true, force: true });
52
+ });
53
+ (0, vitest_1.describe)("resolveEndpoint", () => {
54
+ (0, vitest_1.it)("未设置 PROGMUNE_HUB 时默认指向中央 hub", () => {
55
+ (0, vitest_1.expect)((0, immune_reporter_1.resolveEndpoint)(undefined)).toBe("https://progmune-runtime.fly.dev/report");
56
+ });
57
+ (0, vitest_1.it)("PROGMUNE_HUB=off 及其变体(0/false/no/disabled)关闭上报", () => {
58
+ for (const v of ["off", "0", "false", "no", "disabled", "OFF"]) {
59
+ (0, vitest_1.expect)((0, immune_reporter_1.resolveEndpoint)(v)).toBeNull();
60
+ }
61
+ });
62
+ (0, vitest_1.it)("自定义 URL 原样生效", () => {
63
+ (0, vitest_1.expect)((0, immune_reporter_1.resolveEndpoint)("http://localhost:9999/report")).toBe("http://localhost:9999/report");
64
+ });
65
+ });
66
+ (0, vitest_1.describe)("maskFunctionName", () => {
67
+ (0, vitest_1.it)("默认脱敏:同名函数产生相同哈希(模式聚合保持有效)", () => {
68
+ const a = (0, immune_reporter_1.maskFunctionName)("generate_jwt", false);
69
+ const b = (0, immune_reporter_1.maskFunctionName)("generate_jwt", false);
70
+ (0, vitest_1.expect)(a).toBe(b);
71
+ (0, vitest_1.expect)(a).toMatch(/^fn:[0-9a-f]{12}$/);
72
+ (0, vitest_1.expect)(a).not.toContain("generate_jwt");
73
+ });
74
+ (0, vitest_1.it)("不同函数名产生不同哈希", () => {
75
+ (0, vitest_1.expect)((0, immune_reporter_1.maskFunctionName)("generate_jwt", false)).not.toBe((0, immune_reporter_1.maskFunctionName)("create_session", false));
76
+ });
77
+ (0, vitest_1.it)("PROGMUNE_FINGERPRINT_DETAIL=1 时保留原文", () => {
78
+ (0, vitest_1.expect)((0, immune_reporter_1.maskFunctionName)("generate_jwt", true)).toBe("generate_jwt");
79
+ });
80
+ });
81
+ (0, vitest_1.describe)("buildHeaders", () => {
82
+ (0, vitest_1.it)("无 token 时只带 Content-Type", () => {
83
+ (0, vitest_1.expect)((0, immune_reporter_1.buildHeaders)(undefined)).toEqual({ "Content-Type": "application/json" });
84
+ });
85
+ (0, vitest_1.it)("设置 PROGMUNE_HUB_TOKEN 时带 Bearer 认证头", () => {
86
+ const h = (0, immune_reporter_1.buildHeaders)("secret-token");
87
+ (0, vitest_1.expect)(h["Authorization"]).toBe("Bearer secret-token");
88
+ (0, vitest_1.expect)(h["Content-Type"]).toBe("application/json");
89
+ });
90
+ });
91
+ (0, vitest_1.describe)("extractFingerprints", () => {
92
+ function writeRecord(date, id, record) {
93
+ const dir = path.join(tmpDir, date);
94
+ fs.mkdirSync(dir, { recursive: true });
95
+ fs.writeFileSync(path.join(dir, `fail_${id}.json`), JSON.stringify(record));
96
+ }
97
+ (0, vitest_1.it)("从 .progmune_corpus 路径读取并默认脱敏函数名", () => {
98
+ writeRecord("2026-09-09", "1", {
99
+ timestamp: "2026-09-09T10:00:00.000Z",
100
+ violatedSVL: "SVL-4",
101
+ constraintType: "protocol",
102
+ actionSequence: [{ kind: "call", function: "generate_jwt" }],
103
+ ssgState: ["UNAUTHENTICATED"],
104
+ });
105
+ const fps = (0, immune_reporter_1.extractFingerprints)({ lastTimestamp: null }, tmpDir);
106
+ (0, vitest_1.expect)(fps.length).toBe(1);
107
+ (0, vitest_1.expect)(fps[0].functionSequence[0]).toMatch(/^fn:[0-9a-f]{12}$/);
108
+ (0, vitest_1.expect)(fps[0].preState).toEqual([["UNAUTHENTICATED"]]);
109
+ });
110
+ (0, vitest_1.it)("游标之后的新记录才会上报,旧记录跳过", () => {
111
+ writeRecord("2026-09-01", "old", {
112
+ timestamp: "2026-09-01T10:00:00.000Z",
113
+ violatedSVL: "SVL-4",
114
+ constraintType: "protocol",
115
+ actionSequence: [],
116
+ });
117
+ writeRecord("2026-09-09", "new", {
118
+ timestamp: "2026-09-09T10:00:00.000Z",
119
+ violatedSVL: "SVL-4",
120
+ constraintType: "protocol",
121
+ actionSequence: [],
122
+ });
123
+ const fps = (0, immune_reporter_1.extractFingerprints)({ lastTimestamp: "2026-09-05T00:00:00.000Z" }, tmpDir);
124
+ (0, vitest_1.expect)(fps.length).toBe(1);
125
+ (0, vitest_1.expect)(fps[0].timestamp).toBe("2026-09-09T10:00:00.000Z");
126
+ });
127
+ (0, vitest_1.it)("非 fail_ 前缀文件与损坏 JSON 跳过,不中断", () => {
128
+ writeRecord("2026-09-09", "ok", {
129
+ timestamp: "2026-09-09T10:00:00.000Z",
130
+ violatedSVL: "SVL-4",
131
+ constraintType: "protocol",
132
+ actionSequence: [],
133
+ });
134
+ fs.writeFileSync(path.join(tmpDir, "2026-09-09", "trajectory_x.json"), "{}");
135
+ fs.writeFileSync(path.join(tmpDir, "2026-09-09", "fail_broken.json"), "{corrupted");
136
+ const fps = (0, immune_reporter_1.extractFingerprints)({ lastTimestamp: null }, tmpDir);
137
+ (0, vitest_1.expect)(fps.length).toBe(1);
138
+ });
139
+ (0, vitest_1.it)("无调用序列时 functionSequence 为空数组而非报错", () => {
140
+ writeRecord("2026-09-09", "empty", {
141
+ timestamp: "2026-09-09T10:00:00.000Z",
142
+ violatedSVL: "SVL-2",
143
+ constraintType: "types",
144
+ });
145
+ const fps = (0, immune_reporter_1.extractFingerprints)({ lastTimestamp: null }, tmpDir);
146
+ (0, vitest_1.expect)(fps.length).toBe(1);
147
+ (0, vitest_1.expect)(fps[0].functionSequence).toEqual([]);
148
+ });
149
+ });
@@ -673,9 +673,12 @@ const SAFEGUARD_RULES = [
673
673
  {
674
674
  name: "Path Traversal (User-Controlled File Path)",
675
675
  category: "path_traversal",
676
- languages: ["python"],
677
- // Source-level detection: the Python extractor emits a synthetic marker
678
- // call when a file sink (open / io.open / os.open / Path(...).read_text)
676
+ // 2026-09-11(fr-007):TS 提取器(extract-ir.ts)自 2026-09-11 起同样
677
+ // 注入该标记(request 污点 → 文件 sink,含跨函数一跳)——语言面扩展。
678
+ languages: ["typescript", "javascript", "python"],
679
+ // Source-level detection: the language extractors emit a synthetic marker
680
+ // call when a file sink (open / io.open / os.open / Path(...).read_text;
681
+ // TS: readFile/writeFile/unlink/rm/mkdir/createReadStream 等 fs 族)
679
682
  // receives a path tainted by request-derived user input (directly or via
680
683
  // single-hop assignment — os.path.join chains resolve through assignment
681
684
  // tracking). No satisfier possible — the marker IS the violation evidence.
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.24";
23
+ exports.RUNTIME_VERSION = "3.7.26";
24
24
  function verify(filePath) {
25
25
  const cert = (0, certify_1.certify)(filePath);
26
26
  const kb = (0, protocol_knowledge_1.buildKnowledgeBase)();
@@ -1261,6 +1261,27 @@ async function collectProtocolViolations(ctx, callGraph) {
1261
1261
  const n = String(f.name);
1262
1262
  nameCounts.set(n, (nameCounts.get(n) || 0) + 1);
1263
1263
  }
1264
+ // 2026-09-12(REALWORLD_FIX_REGRESSION_V1 fr-007):路径穿越标记
1265
+ // 在 IR 层直接消费——call-sequence 构建会过滤 __progmune_ 前缀
1266
+ // (P4.5 历史),标记到不了 checkSpecificViolations;此前的 Python
1267
+ // 标记因此在引擎管线中一直是死的。此处同时点亮 Python 与 TS 两侧。
1268
+ for (const f of functions) {
1269
+ const calls = f.calls || [];
1270
+ if (calls.includes("__progmune_path_traversal__")) {
1271
+ violations.push({
1272
+ severity: "medium",
1273
+ rule_id: "PATH_TRAVERSAL",
1274
+ file: f.file,
1275
+ function: f.name || "unknown",
1276
+ message: "File operation receives a request-controlled path without visible validation — path traversal / arbitrary file access.",
1277
+ evidence: "__progmune_path_traversal__",
1278
+ why: "Extractor-verified taint flow: request-derived input reaches a file sink " +
1279
+ "(directly or via one-hop project method).",
1280
+ fix: "Validate/sanitize the path (allowlist, basename normalization) before file operations.",
1281
+ policy_ref: "protocol-safety.specific",
1282
+ });
1283
+ }
1284
+ }
1264
1285
  for (const f of functions) {
1265
1286
  if (!f.protocol)
1266
1287
  continue;
@@ -641,6 +641,24 @@ exports.SPECIFIC_VIOLATION_CHECKS = [
641
641
  // ═══════════════════════════════════════════════════════
642
642
  // QUIC-001: Active migration disabled
643
643
  // ═══════════════════════════════════════════════════════
644
+ // 2026-09-11(REALWORLD_FIX_REGRESSION_V1 fr-007):路径穿越引擎化。
645
+ // 标记由语言提取器注入(Python: tools/extract_ir.py 请求污点→文件
646
+ // sink 单跳追踪;TypeScript: src/extract-ir.ts 同款 + 跨函数一跳——
647
+ // 调用点污点参数流入项目方法体内的文件 sink)。此前 Python 标记在
648
+ // 引擎管线中无消费方(source-level 规则只活在 benchmark harness),
649
+ // 本检查同时点亮 Python 与 TS 两侧。
650
+ // ═══════════════════════════════════════════════════════
651
+ {
652
+ ruleId: "PATH_TRAVERSAL",
653
+ description: "A file operation receives a path derived from request-controlled input " +
654
+ "without visible validation — path traversal / arbitrary file access.",
655
+ languages: ["typescript", "javascript", "python"],
656
+ check: (steps) => {
657
+ return steps.some((s) => s.api === "__progmune_path_traversal__" ||
658
+ s.api.startsWith("__progmune_path_traversal"));
659
+ },
660
+ },
661
+ // ═══════════════════════════════════════════════════════
644
662
  {
645
663
  ruleId: "QUIC_DISABLE_ACTIVE_MIGRATION",
646
664
  description: "QUIC connection migration is disabled. This may impact privacy " +
@@ -287,14 +287,42 @@ function inferRuleName(apiName, domain, description, ruleNames, namespace, alias
287
287
  if (projectFunctions && isDotted)
288
288
  continue; // 带点调用只走限定精确匹配(+别名/域提示)
289
289
  const ruleWords = ruleName.split("_");
290
- const callWords = normalized.split("_");
291
290
  // Rule must have at least 2 words for this strategy (single-word rules
292
291
  // are too prone to false positives via substring)
293
292
  if (ruleWords.length < 2)
294
293
  continue;
295
- // Every rule word must appear as a complete call word segment
296
- const allWordsMatch = ruleWords.every((rw) => callWords.includes(rw));
297
- if (allWordsMatch) {
294
+ // 2026-09-11(REALWORLD_AI_GENERATED_V2 / skyvern):词段匹配加
295
+ // 「首词@首位 + 末词@末位」边界收紧。此前仅要求规则词全部出现——
296
+ // validate_local_file_path / _validate_file_stat / _session_create_data /
297
+ // _revoke_refresh_token_at_google / ngx_read_file 等真实语料 FP 均破坏
298
+ // 该边界;Python 盲测 S5 改名形态(verify_user_password /
299
+ // create_active_session / open_user_file)全部满足边界,TP 不受影响。
300
+ let callWords = normalized.split("_").filter((w) => w !== "");
301
+ if (callWords.length === 0)
302
+ callWords = normalized.split("_");
303
+ // Rule words must appear in order — first rule word at word index 0,
304
+ // last rule word at the final word index — not just anywhere in the set
305
+ let pos = 0;
306
+ const positions = [];
307
+ let ordered = true;
308
+ for (const rw of ruleWords) {
309
+ let found = -1;
310
+ for (let i = pos; i < callWords.length; i++) {
311
+ if (callWords[i] === rw) {
312
+ found = i;
313
+ break;
314
+ }
315
+ }
316
+ if (found === -1) {
317
+ ordered = false;
318
+ break;
319
+ }
320
+ positions.push(found);
321
+ pos = found + 1;
322
+ }
323
+ if (ordered &&
324
+ positions[0] === 0 &&
325
+ positions[positions.length - 1] === callWords.length - 1) {
298
326
  return ruleName;
299
327
  }
300
328
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "progmune-runtime",
3
- "version": "3.7.24",
3
+ "version": "3.7.26",
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/",
package/protocols.json CHANGED
@@ -2169,7 +2169,11 @@
2169
2169
  "fs.readFileSync",
2170
2170
  "fs.readFile",
2171
2171
  "fs.promises.readFile",
2172
- "fs.promises.readFileSync"
2172
+ "fs.promises.readFileSync",
2173
+ "read_text",
2174
+ "read_bytes",
2175
+ "pathlib.read_text",
2176
+ "pathlib.read_bytes"
2173
2177
  ],
2174
2178
  "status": "confirmed"
2175
2179
  },
@@ -2182,9 +2186,13 @@
2182
2186
  "fs.writeFileSync",
2183
2187
  "fs.writeFile",
2184
2188
  "fs.promises.writeFile",
2185
- "fs.promises.writeFileSync"
2189
+ "fs.promises.writeFileSync",
2190
+ "write_text",
2191
+ "write_bytes",
2192
+ "pathlib.write_text",
2193
+ "pathlib.write_bytes"
2186
2194
  ],
2187
2195
  "status": "confirmed"
2188
2196
  }
2189
2197
  }
2190
- }
2198
+ }
@@ -34,9 +34,20 @@ import sys
34
34
  # 依赖名含 user 会被误标认证(REALWORLD_STRUCTURAL_V2 假保护 FN)。
35
35
  # 真认证依赖用 current_user/authorizer 等强词识别(get_current_user
36
36
  # 含 current_user ✓)。
37
+ # 2026-09-11(REALWORLD_AI_GENERATED_V2 / skyvern):补 org-token 认证模型
38
+ # 词族——get_current_org / get_current_caller_context 是 23k★ 生产 SaaS
39
+ # 的主力认证依赖名,词表不认识 → 146 条 FASTAPI_ROUTE_NO_AUTH 误报。
40
+ # current_org/current_caller/caller_context 与 current_user 同强度
41
+ # (get_current_organization 误标风险与 get_current_user 同族,已接受)。
37
42
  AUTH_WORDS = (
38
43
  "auth", "login", "token", "credential", "session",
39
44
  "bearer", "permission", "current_user", "api_key", "oauth", "jwt",
45
+ "current_org", "current_caller", "caller_context",
46
+ # open-webui 真实语料(修复回归 fr-001 扫描,2026-09-11):get_verified_user /
47
+ # get_admin_user 是其全路由主力认证依赖(session/JWT 校验),词表不认识 →
48
+ # 142 条 FASTAPI_ROUTE_NO_AUTH 误报。复合词窄口:verified_user/admin_user
49
+ # 是强认证词(区别于裸 "user" 的 DB 查询误标风险)。
50
+ "verified_user", "admin_user",
40
51
  )
41
52
 
42
53
  SKIP_DIRS = {"tests", "test", "deps", "venv", "env", "node_modules", "vendor",