progmune-runtime 3.7.25 → 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 +21 -0
- package/dist/extract-ir.js +261 -19
- package/dist/protocol-detector.js +6 -3
- package/dist/sdk.js +1 -1
- package/dist/trust/engine.js +21 -0
- package/dist/trust/protocol-domain-validator.js +18 -0
- package/dist/trust/ssg-bridge.js +32 -4
- package/package.json +1 -1
- package/protocols.json +11 -3
- package/tools/extract_framework_py.py +11 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
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
|
+
|
|
3
24
|
## [3.7.25] — 2026-09-10
|
|
4
25
|
|
|
5
26
|
### 失败语料上报链路修复(中央 hub 恢复数据流)
|
package/dist/extract-ir.js
CHANGED
|
@@ -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
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
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
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
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)
|
|
@@ -673,9 +673,12 @@ const SAFEGUARD_RULES = [
|
|
|
673
673
|
{
|
|
674
674
|
name: "Path Traversal (User-Controlled File Path)",
|
|
675
675
|
category: "path_traversal",
|
|
676
|
-
|
|
677
|
-
//
|
|
678
|
-
|
|
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.
|
|
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)();
|
package/dist/trust/engine.js
CHANGED
|
@@ -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 " +
|
package/dist/trust/ssg-bridge.js
CHANGED
|
@@ -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
|
-
//
|
|
296
|
-
|
|
297
|
-
|
|
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.
|
|
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",
|