progmune-runtime 3.7.28 → 3.7.30

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,34 @@
1
1
  # Changelog
2
2
 
3
+ ## [3.7.30] — 2026-09-17
4
+
5
+ ### 提取器性能重构(3.7.28 引入的回归修复)
6
+
7
+ - **问题**:3.7.28 的标记增强(路径穿越/SSRF)采用独立遍历——对每个函数类节点做 ts-morph getStart/getEnd/getText + 正则扫描,check 自扫(仓库 375 个源文件)从 ~30s 退化到 10 分钟+(SSRF 增强单测 4 分钟 CPU)
8
+ - **修复**(`src/extract-ir.ts`):①标记计算**内联进主循环**——复用主循环已获取的函数文本与形参名(computeMarkerCalls),删除两个独立遍历增强函数;②文件级预过滤(路径穿越只看含 request 模式的文件,SSRF 只看含 fetch sink 的文件);③主循环跳过 node_modules(lib.d.ts 等声明文件只贡献噪声);④微型节点(<80 字符)跳过正则扫描
9
+ - **行为保持**:fr-007 openhop 标记(flowRoutes + FlowStore.save/get/delete/updateFlow)与 fr-011 mcp-from-openapi 标记(fromURL)重构后逐项一致;**TS 795 零漂移(3086 flags LOST 0 / ADDED 0)**
10
+ - **诊断教训**:期间本机 8GB 内存被后台进程耗尽(一个 9 小时 CPU 的幽灵 extractor 进程 + 多个未杀净的扫描进程),计时测量被环境噪声污染——定位靠逐步计时(PT 增强 267ms / SSRF 增强 4min+)与进程审计,最终重构从架构上消除该开销
11
+
12
+ ### 回归
13
+
14
+ - TS 795 零漂移 ✓ / 标记双语料复验一致 ✓ / 引擎测试 73 green ✓ / build ✓
15
+
16
+ ## [3.7.29] — 2026-09-14
17
+
18
+ ### 失败语料沉淀接通(知识网络入口修复)
19
+
20
+ - **断点**:信任引擎(trust CLI/agent/patrol/MCP)检出的违规从不写入失败语料库——此前只有 agent-loop 的 planner 失败会入 `.progmune_corpus`,引擎找到的真东西不进库,知识网络永远不喂新数据
21
+ - **修复**(`src/trust/engine.ts` `writeTrustFailuresToCorpus`):每次 evaluateTrust 的违规(cap 50/扫描)经 `recordFailure` 沉淀——intent=`trust-scan:<project>`、severity→SVL 阶梯映射(critical→SVL-4…low→SVL-1)、项目函数名快照(cap 300,语料挖掘词段门控输入)
22
+ - **去重**:同日同(项目+规则+函数)sidecar 去重——反复扫描不重复沉淀
23
+ - **噪声排除**:`PROTOCOL_CROSS_DOMAIN`/`PLAINTEXT_AUTH_WITHOUT_TLS`(已知高 FP、LLM 域映射抖动的 specific-check)不入语料;真实检出面(PATH_TRAVERSAL/SSRF/AUTHZ_CROSS_USER_WRITE/框架层/SSG)照常沉淀
24
+ - **测试隔离**:VITEST/NODE_ENV=test 跳过——单测不污染语料
25
+ - **首批入库验证**:本周 10 条真实检出全部入库存(openhop PATH_TRAVERSAL×4 + FASTIFY×1、open-webui SSRF×3 + AUTHZ_CROSS_USER_WRITE×1、mcp-from-openapi SSRF×1)——其中 open-webui 3 条 SSRF 命中的正是 fr-009 修复补丁加固的 web 加载器函数族,交叉验证 A3 扩展
26
+ - 语料经 immune-reporter 既有管线脱敏上传 hub(默认脱敏口径不变)
27
+
28
+ ### 回归
29
+
30
+ - build ✓ / 引擎测试 21 green ✓ / 语料写入全部 best-effort(永不打断扫描)
31
+
3
32
  ## [3.7.28] — 2026-09-14
4
33
 
5
34
  ### SSRF 检测 TS 化 + Python 形参污点扩展(A3,修复回归 fr-009/010/011 驱动)
@@ -338,7 +338,7 @@ function manualResolveModule(moduleSpecifier, sourceFilePath, projectRoot, origi
338
338
  }
339
339
  return null;
340
340
  }
341
- function extractDirectCalls(func) {
341
+ function extractDirectCalls(func, preText) {
342
342
  const body = func.getBody();
343
343
  if (!body)
344
344
  return [];
@@ -360,7 +360,7 @@ function extractDirectCalls(func) {
360
360
  // - inline ownership comparison: ownerId/authorId compared with ==/!== —
361
361
  // the Ownership Check rules' satisfier consumes it (the call-name
362
362
  // interface cannot see inline comparisons).
363
- const text = func.getText();
363
+ const text = preText !== undefined ? preText : func.getText();
364
364
  if (/set_cookie\(|setCookie\(|\btoken\s*[:=]|\bsession_token\s*[:=]/.test(text)) {
365
365
  calls.push("__progmune_token_issued__");
366
366
  }
@@ -508,96 +508,6 @@ function methodSinkParamMap(project, absRoot) {
508
508
  }
509
509
  return map;
510
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
- }
601
511
  // ═══════════════════════════════════════════════════════════════
602
512
  // SSRF marker(2026-09-14,REALWORLD_FIX_REGRESSION fr-011/fr-010)
603
513
  // URL 形参/request 污点 → HTTP fetch sink,函数内无 SSRF 守卫证据
@@ -611,82 +521,65 @@ const URL_PARAM_NAME = /^(url|target|endpoint|link|href|webUrl|web_url|sourceUrl
611
521
  function collectUrlParamNames(params) {
612
522
  return params.map((p) => p.name).filter((n) => URL_PARAM_NAME.test(n));
613
523
  }
614
- function augmentSsfrMarkers(project, funcs, absRoot) {
615
- const sinkRe = TS_HTTP_FETCH_SINK;
616
- for (const sf of project.getSourceFiles()) {
617
- if (sf.getFilePath().includes("node_modules"))
618
- continue;
619
- const relPath = path.relative(absRoot, sf.getFilePath());
620
- sf.forEachDescendant((node) => {
621
- if (!ts_morph_1.Node.isFunctionDeclaration(node) &&
622
- !ts_morph_1.Node.isArrowFunction(node) &&
623
- !ts_morph_1.Node.isMethodDeclaration(node)) {
624
- return;
625
- }
626
- const text = node.getText();
627
- if (text.length > 12000)
628
- return; // 巨型函数跳过(性能 + 噪声)
629
- // 预过滤:必须有 fetch sink
630
- sinkRe.lastIndex = 0;
631
- if (!sinkRe.test(text))
632
- return;
633
- // 守卫证据:函数内有 SSRF 防护词汇 → 抑制
634
- if (SSRF_GUARD_EVIDENCE.test(text))
635
- return;
636
- // URL 形参污点
637
- const urlParams = ts_morph_1.Node.isArrowFunction(node)
638
- ? []
639
- : collectUrlParamNames(node.getParameters().map((p) => ({ name: p.getName() })));
640
- // request 污点(Web 形态)
641
- const reqTainted = collectTaintedNames(text);
642
- const taintParts = [];
643
- if (urlParams.length > 0)
644
- taintParts.push(`\\b(?:${urlParams.join("|")})\\b`);
645
- if (reqTainted.size > 0)
646
- taintParts.push(`\\b(?:${[...reqTainted].join("|")})\\b`);
647
- taintParts.push(/\b(?:req|request)\.(?:params|query|body|headers|cookies)\b[.[]?/.source);
648
- const taint = new RegExp(taintParts.join("|"));
649
- if (taintParts.length <= 1) {
650
- // 只有 request 根模式而无实际污点名——仍需检查 sink 实参
651
- }
652
- // sink 实参窗口含污点 → 标记
653
- let flagged = false;
654
- sinkRe.lastIndex = 0;
655
- let m;
656
- while ((m = sinkRe.exec(text)) !== null) {
657
- // 取调用后 300 字符窗口(URL 是首个实参)
658
- const after = text.slice(m.index + m[0].length, m.index + m[0].length + 300);
659
- if (taint.test(after)) {
660
- flagged = true;
661
- break;
662
- }
663
- }
664
- if (!flagged)
665
- return;
666
- const nodeName = ts_morph_1.Node.isArrowFunction(node) ? undefined : node.getName();
667
- if (ts_morph_1.Node.isFunctionDeclaration(node) && nodeName) {
668
- const entry = funcs.find((x) => x.name === nodeName && x.file === relPath);
669
- if (entry) {
670
- entry.calls = entry.calls || [];
671
- if (!entry.calls.includes("__progmune_ssrf_user_url__")) {
672
- entry.calls.push("__progmune_ssrf_user_url__");
673
- }
674
- }
524
+ /**
525
+ * 标记增强的内联实现(2026-09-15 性能重构):消费主循环已获取的
526
+ * 函数文本与形参名,输出该函数应附加的合成标记。
527
+ * - 路径穿越:request 污点 → 文件 sink(本函数内);或污点实参传入
528
+ * 项目方法体内的文件 sink(跨函数一跳,onMethodHit 回调登记方法条目)
529
+ * - SSRF:URL 形参/request 污点 → HTTP fetch sink,无守卫词汇
530
+ */
531
+ function computeMarkerCalls(text, paramNames, sinkParams, onMethodHit) {
532
+ const markers = [];
533
+ // ── 路径穿越 ──
534
+ if (hasRequestRootedExpr(text)) {
535
+ const tainted = collectTaintedNames(text);
536
+ if (tainted.size > 0) {
537
+ if (hasTaintedSinkCall(text, tainted)) {
538
+ markers.push("__progmune_path_traversal__");
675
539
  }
676
- else if (ts_morph_1.Node.isMethodDeclaration(node) && nodeName) {
677
- const cls = node.getParent();
678
- if (ts_morph_1.Node.isClassDeclaration(cls) && cls.getName()) {
679
- const entry = funcs.find((x) => x.name === `${cls.getName()}.${nodeName}` && x.file === relPath);
680
- if (entry) {
681
- entry.calls = entry.calls || [];
682
- if (!entry.calls.includes("__progmune_ssrf_user_url__")) {
683
- entry.calls.push("__progmune_ssrf_user_url__");
540
+ else {
541
+ const methodCallRe = new RegExp(`\\.([\\w$]+)\\s*\\(([\\s\\S]{0,250}?)\\)`, "g");
542
+ const taint = taintPattern(tainted);
543
+ if (taint) {
544
+ let m;
545
+ while ((m = methodCallRe.exec(text)) !== null) {
546
+ const rec = sinkParams.get(m[1]);
547
+ if (!rec || rec.idxs.size === 0)
548
+ continue;
549
+ const args = splitArgWindow(m[2] || "");
550
+ const hit = args.some((arg, i) => rec.idxs.has(i) && taint.test(arg));
551
+ if (hit) {
552
+ markers.push("__progmune_path_traversal__");
553
+ onMethodHit(rec);
684
554
  }
685
555
  }
686
556
  }
687
557
  }
688
- });
558
+ }
689
559
  }
560
+ // ── SSRF ──
561
+ TS_HTTP_FETCH_SINK.lastIndex = 0;
562
+ if (TS_HTTP_FETCH_SINK.test(text) && !SSRF_GUARD_EVIDENCE.test(text)) {
563
+ const urlParams = paramNames.filter((n) => URL_PARAM_NAME.test(n));
564
+ const reqTainted = collectTaintedNames(text);
565
+ const taintParts = [];
566
+ if (urlParams.length > 0)
567
+ taintParts.push(`\\b(?:${urlParams.join("|")})\\b`);
568
+ if (reqTainted.size > 0)
569
+ taintParts.push(`\\b(?:${[...reqTainted].join("|")})\\b`);
570
+ taintParts.push(/\b(?:req|request)\.(?:params|query|body|headers|cookies)\b[.[]?/.source);
571
+ const taint = new RegExp(taintParts.join("|"));
572
+ TS_HTTP_FETCH_SINK.lastIndex = 0;
573
+ let m;
574
+ while ((m = TS_HTTP_FETCH_SINK.exec(text)) !== null) {
575
+ const after = text.slice(m.index + m[0].length, m.index + m[0].length + 300);
576
+ if (taint.test(after)) {
577
+ markers.push("__progmune_ssrf_user_url__");
578
+ break;
579
+ }
580
+ }
581
+ }
582
+ return markers;
690
583
  }
691
584
  /**
692
585
  * 从 TypeScript 项目提取 IR(函数签名、参数、返回值、协议注解)。
@@ -766,15 +659,31 @@ function _extractSingleProject(absRoot, tsconfigPath) {
766
659
  project.addSourceFilesAtPaths(path.join(absRoot, "**/*.ts"));
767
660
  }
768
661
  const funcs = [];
662
+ // 标记增强的共享状态(2026-09-15 内联重构):
663
+ // sinkParams = 项目方法名 → 方法体内流入文件 sink 的形参下标 + 方法条目;
664
+ // pendingMethodMarks = 跨函数命中待标记的方法条目(主循环后统一应用)
665
+ const sinkParams = methodSinkParamMap(project, absRoot);
666
+ const pendingMethodMarks = new Set();
667
+ const onMethodHit = (rec) => {
668
+ for (const me of rec.entries)
669
+ pendingMethodMarks.add(`${me.name}\u0000${me.file}`);
670
+ };
769
671
  for (const sf of project.getSourceFiles()) {
672
+ if (sf.getFilePath().includes("node_modules"))
673
+ continue; // lib.d.ts 等声明文件只贡献噪声(2026-09-17 性能)
770
674
  const relPath = path.relative(absRoot, sf.getFilePath());
675
+ const _ft0 = Date.now();
771
676
  for (const f of sf.getFunctions()) {
772
677
  const name = f.getName();
773
678
  if (!name)
774
679
  continue;
680
+ const fParams = f.getParameters();
681
+ const fText = f.getText();
682
+ const fCalls = extractDirectCalls(f, fText);
683
+ fCalls.push(...computeMarkerCalls(fText, fParams.map((p) => p.getName()), sinkParams, onMethodHit));
775
684
  funcs.push({
776
685
  name,
777
- params: f.getParameters().map(p => ({
686
+ params: fParams.map(p => ({
778
687
  name: p.getName(),
779
688
  type: getParamType(p),
780
689
  typeDetail: getParamTypeDetail(p),
@@ -782,7 +691,7 @@ function _extractSingleProject(absRoot, tsconfigPath) {
782
691
  returnType: getReturnType(f),
783
692
  returnTypeDetail: getReturnTypeDetail(f),
784
693
  file: relPath,
785
- calls: extractDirectCalls(f),
694
+ calls: fCalls,
786
695
  exported: f.isExported(),
787
696
  inputs: deriveInputs(f.getParameters().map(p => ({ name: p.getName(), type: p.getTypeNode()?.getText() || "any" }))),
788
697
  outputs: deriveOutputs(f.getReturnTypeNode()?.getText() || "any"),
@@ -798,9 +707,13 @@ function _extractSingleProject(absRoot, tsconfigPath) {
798
707
  // 直接箭头函数: const fn = () => {}
799
708
  if (ts_morph_1.Node.isArrowFunction(init)) {
800
709
  const name = vd.getName();
710
+ const initParams = init.getParameters();
711
+ const initText = init.getText();
712
+ const initCalls = extractDirectCalls(init, initText);
713
+ initCalls.push(...computeMarkerCalls(initText, initParams.map((p) => p.getName()), sinkParams, onMethodHit));
801
714
  funcs.push({
802
715
  name,
803
- params: init.getParameters().map(p => ({
716
+ params: initParams.map(p => ({
804
717
  name: p.getName(),
805
718
  type: getParamType(p),
806
719
  typeDetail: getParamTypeDetail(p),
@@ -809,7 +722,7 @@ function _extractSingleProject(absRoot, tsconfigPath) {
809
722
  returnTypeDetail: getReturnTypeDetail(init),
810
723
  file: relPath,
811
724
  exported: vd.isExported(),
812
- calls: extractDirectCalls(init),
725
+ calls: initCalls,
813
726
  protocol: parseProtocolFromJSDoc(vd),
814
727
  ...parseCapabilityFromJSDoc(vd),
815
728
  });
@@ -820,9 +733,13 @@ function _extractSingleProject(absRoot, tsconfigPath) {
820
733
  for (const arg of init.getArguments()) {
821
734
  if (ts_morph_1.Node.isArrowFunction(arg)) {
822
735
  const name = vd.getName();
736
+ const argParams = arg.getParameters();
737
+ const argText = arg.getText();
738
+ const argCalls = extractDirectCalls(arg, argText);
739
+ argCalls.push(...computeMarkerCalls(argText, argParams.map((p) => p.getName()), sinkParams, onMethodHit));
823
740
  funcs.push({
824
741
  name,
825
- params: arg.getParameters().map(p => ({
742
+ params: argParams.map(p => ({
826
743
  name: p.getName(),
827
744
  type: getParamType(p),
828
745
  typeDetail: getParamTypeDetail(p),
@@ -831,7 +748,7 @@ function _extractSingleProject(absRoot, tsconfigPath) {
831
748
  returnTypeDetail: getReturnTypeDetail(arg),
832
749
  file: relPath,
833
750
  exported: vd.isExported(),
834
- calls: extractDirectCalls(arg),
751
+ calls: argCalls,
835
752
  protocol: parseProtocolFromJSDoc(vd),
836
753
  });
837
754
  break; // 只取第一个箭头函数参数
@@ -851,30 +768,41 @@ function _extractSingleProject(absRoot, tsconfigPath) {
851
768
  const mn = m.getName();
852
769
  if (!mn)
853
770
  continue;
771
+ const mParams = m.getParameters();
772
+ const mText = m.getText();
773
+ const mCalls = computeMarkerCalls(mText, mParams.map((p) => p.getName()), sinkParams, onMethodHit);
854
774
  funcs.push({
855
775
  name: `${cn}.${mn}`,
856
- params: m.getParameters().map((p) => ({ name: p.getName(), type: getParamType(p), typeDetail: getParamTypeDetail(p) })),
776
+ params: mParams.map((p) => ({ name: p.getName(), type: getParamType(p), typeDetail: getParamTypeDetail(p) })),
857
777
  returnType: m.getReturnTypeNode?.()?.getText?.() || "any",
858
778
  returnTypeDetail: m.getReturnTypeNode?.()?.getText?.() || "any",
859
779
  file: relPath,
860
780
  exported: cls.isExported(),
861
- calls: [],
781
+ calls: mCalls,
862
782
  ...parseCapabilityFromJSDoc(m),
863
783
  protocol: parseProtocolFromJSDoc(m),
864
784
  });
865
785
  }
866
786
  }
787
+ const _ftd = Date.now() - _ft0;
788
+ if (_ftd > 3000)
789
+ console.error(`[perf-file] ${relPath}: ${_ftd}ms`);
790
+ }
791
+ // ── 标记增强(2026-09-11 fr-007 / 2026-09-14 fr-011):在下方
792
+ // 主循环内联计算(复用已获取的函数文本,无独立遍历——
793
+ // 2026-09-15 性能重构);跨函数命中在循环结束后统一应用。
794
+ for (const key of pendingMethodMarks) {
795
+ const idx = key.indexOf("\u0000");
796
+ const mname = key.slice(0, idx);
797
+ const mfile = key.slice(idx + 1);
798
+ const entry = funcs.find((x) => x.name === mname && x.file === mfile);
799
+ if (entry) {
800
+ entry.calls = entry.calls || [];
801
+ if (!entry.calls.includes("__progmune_path_traversal__")) {
802
+ entry.calls.push("__progmune_path_traversal__");
803
+ }
804
+ }
867
805
  }
868
- // ═══════════════════════════════════════════════════════════════
869
- // Phase 4.5: Path traversal marker(2026-09-11,fr-007 openhop)
870
- // 镜像 Python 提取器的 request 污点 → 文件 sink 单跳追踪,并补
871
- // 跨函数一跳:调用点把污点参数传给项目方法,该方法体内该参数
872
- // 流入文件 sink → 调用方函数注入 __progmune_path_traversal__。
873
- // 消费方:protocol-domain-validator PATH_TRAVERSAL 检查(引擎)
874
- // 与 protocol-detector 同名规则(source-level benchmark 路径)。
875
- // ═══════════════════════════════════════════════════════════════
876
- augmentPathTraversalMarkers(project, funcs, absRoot);
877
- augmentSsfrMarkers(project, funcs, absRoot);
878
806
  // ═══════════════════════════════════════════════════════════════
879
807
  // Phase 5: Dynamic external function resolution
880
808
  // Replaces hardcoded knownExternals
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.28";
23
+ exports.RUNTIME_VERSION = "3.7.30";
24
24
  function verify(filePath) {
25
25
  const cert = (0, certify_1.certify)(filePath);
26
26
  const kb = (0, protocol_knowledge_1.buildKnowledgeBase)();
@@ -140,6 +140,11 @@ async function evaluateTrust(ctx) {
140
140
  ...fiberResult.violations,
141
141
  ];
142
142
  // ═══════════════════════════════════════
143
+ // ── 失败语料沉淀(2026-09-14):信任引擎检出的违规写入本地语料库 ——
144
+ // 知识网络的入口。此前只有 agent-loop 的 planner 失败会入语料,
145
+ // trust 扫描(CLI/agent/patrol/MCP)检出的真违规不沉淀。
146
+ // 测试环境跳过;同日同(项目+规则+函数)去重;语料写入永不打断扫描。
147
+ writeTrustFailuresToCorpus(ctx, allViolations);
143
148
  // PHASE 3: SCORE
144
149
  // ═══════════════════════════════════════
145
150
  const policyResult = (0, score_calculator_1.scorePolicyCompliance)(allViolations);
@@ -1814,6 +1819,97 @@ function mapRiskSeverity(severity) {
1814
1819
  default: return "low";
1815
1820
  }
1816
1821
  }
1822
+ // ═══════════════════════════════════════════════════════════════
1823
+ // 失败语料沉淀(2026-09-14):信任引擎违规 → .progmune_corpus
1824
+ // ═══════════════════════════════════════════════════════════════
1825
+ const SEVERITY_TO_SVL = {
1826
+ critical: "SVL-4",
1827
+ high: "SVL-3",
1828
+ medium: "SVL-2",
1829
+ low: "SVL-1",
1830
+ };
1831
+ /** 与 failure-corpus.ts 相同的语料目录解析(模块加载时定死 cwd 的语义) */
1832
+ function trustCorpusDir() {
1833
+ if (process.env.PROGMUNE_CORPUS_DIR)
1834
+ return process.env.PROGMUNE_CORPUS_DIR;
1835
+ const projectDir = process.env.PROGMUNE_PROJECT_DIR || process.cwd();
1836
+ return path.join(projectDir, ".progmune_corpus");
1837
+ }
1838
+ /**
1839
+ * 把本次扫描检出的违规写入失败语料库(知识网络的入口)。
1840
+ * - 测试环境(VITEST/NODE_ENV=test)跳过——单测不得污染语料
1841
+ * - 同日同(项目+规则+函数)去重——反复扫描不重复沉淀
1842
+ * - 全部 best-effort:语料写入失败绝不打断扫描主流程
1843
+ */
1844
+ function writeTrustFailuresToCorpus(ctx, violations) {
1845
+ if (process.env.VITEST === "true" || process.env.NODE_ENV === "test")
1846
+ return;
1847
+ if (violations.length === 0)
1848
+ return;
1849
+ try {
1850
+ const { recordFailure } = require("../failure-corpus");
1851
+ const fs = require("fs");
1852
+ const corpusDir = trustCorpusDir();
1853
+ const date = new Date().toISOString().slice(0, 10);
1854
+ const dedupFile = path.join(corpusDir, `.trust-dedup-${date}.json`);
1855
+ let seen = {};
1856
+ try {
1857
+ seen = JSON.parse(fs.readFileSync(dedupFile, "utf-8"));
1858
+ }
1859
+ catch { /* fresh day */ }
1860
+ // 项目函数名快照(语料挖掘的词段门控输入),cap 300
1861
+ let projectFunctions = [];
1862
+ try {
1863
+ const irPath = path.join(ctx.projectPath, "ir.json");
1864
+ if (fs.existsSync(irPath)) {
1865
+ const ir = JSON.parse(fs.readFileSync(irPath, "utf-8"));
1866
+ const funcs = Array.isArray(ir) ? ir : (ir.functions || []);
1867
+ projectFunctions = funcs
1868
+ .map((f) => String(f.name))
1869
+ .filter((n) => n && !n.startsWith("__progmune_"))
1870
+ .slice(0, 300);
1871
+ }
1872
+ }
1873
+ catch { /* best-effort */ }
1874
+ let added = 0;
1875
+ for (const v of violations.slice(0, 50)) {
1876
+ // 已知高 FP 的 specific-check 规则不入语料——它们以 LLM 域映射
1877
+ // 抖动为主(open-webui 单次扫描 36 条 CROSS_DOMAIN),对语料挖掘
1878
+ // 是纯噪声。真实检出面(PATH_TRAVERSAL/SSRF/CROSS_USER_WRITE/
1879
+ // 框架层/SSG)照常沉淀。
1880
+ if (v.rule_id === "PROTOCOL_CROSS_DOMAIN" || v.rule_id === "PLAINTEXT_AUTH_WITHOUT_TLS")
1881
+ continue;
1882
+ const key = `${ctx.projectName}:${v.rule_id}:${v.function || ""}`;
1883
+ if (seen[key])
1884
+ continue;
1885
+ recordFailure({
1886
+ intent: `trust-scan:${ctx.projectName}`,
1887
+ projectFunctions,
1888
+ violatedSVL: (SEVERITY_TO_SVL[v.severity] || "SVL-2"),
1889
+ constraintType: v.rule_id,
1890
+ actionSequence: [
1891
+ {
1892
+ kind: "violation",
1893
+ function: v.function || "unknown",
1894
+ file: v.file,
1895
+ rule: v.rule_id,
1896
+ },
1897
+ ],
1898
+ errorDetail: (v.message || v.why || "").slice(0, 500),
1899
+ });
1900
+ seen[key] = true;
1901
+ added++;
1902
+ }
1903
+ if (added > 0) {
1904
+ try {
1905
+ fs.mkdirSync(corpusDir, { recursive: true });
1906
+ fs.writeFileSync(dedupFile, JSON.stringify(seen, null, 2));
1907
+ }
1908
+ catch { /* best-effort */ }
1909
+ }
1910
+ }
1911
+ catch { /* corpus write must never crash the scan */ }
1912
+ }
1817
1913
  function extractFunctionName(text) {
1818
1914
  if (!text)
1819
1915
  return "unknown";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "progmune-runtime",
3
- "version": "3.7.28",
3
+ "version": "3.7.30",
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/",