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 +29 -0
- package/dist/extract-ir.js +108 -180
- package/dist/sdk.js +1 -1
- package/dist/trust/engine.js +96 -0
- package/package.json +1 -1
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 驱动)
|
package/dist/extract-ir.js
CHANGED
|
@@ -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
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
if (text
|
|
628
|
-
|
|
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
|
|
677
|
-
const
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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.
|
|
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)();
|
package/dist/trust/engine.js
CHANGED
|
@@ -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.
|
|
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/",
|