progmune-runtime 3.7.30 → 3.7.32
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 +34 -0
- package/README.md +6 -3
- package/dist/extract-ir-ssrf-loop.test.js +123 -0
- package/dist/extract-ir.js +107 -43
- package/dist/ir-staleness.js +233 -0
- package/dist/ir-staleness.test.js +228 -0
- package/dist/sdk.js +1 -1
- package/dist/trust/engine.js +28 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [3.7.32] — 2026-09-19
|
|
4
|
+
|
|
5
|
+
### IR 陈旧性判定(两次会话级误判的根因修复)
|
|
6
|
+
|
|
7
|
+
- **断点**:trust 引擎自动提取是「缺才提取」——项目已有 ir.json 就以文件为准不重生成。源码改了、ir.json 没更新时静默吃旧 IR:fr-005 首扫报 0(`rm ir.json` 后才报出)、污点试点「标记注入但 trust 报 0」同源
|
|
8
|
+
- **修复**(`src/ir-staleness.ts` + `src/trust/engine.ts` 集成):mtime 比对——仅当有源码新于 ir.json 才重提,不做每次全量重提(skyvern 级项目 5-10 分钟成本)
|
|
9
|
+
- **三档策略 `PROGMUNE_IR_REEXTRACT`**:auto(陈旧 + 遍历完整 + 源码 ≤5000 文件才自动重提,否则**只警告不重提**)/always(语料复测自担成本)/never(旧语义,但必须打「IR 可能陈旧」警告——**不接受静默**)
|
|
10
|
+
- **双闸设计**:完整遍历(不做「找到就早退」——早退会抹掉 truncated 信号,本仓库 22k 源文件实测会把单测推过 30s 超时)+ evidenceComplete + 规模预算闸;stat 一次遍历的常量成本换可靠的 fresh 判定
|
|
11
|
+
- **验收**:`src/ir-staleness.test.ts` 12 green + DSH 陷阱回归 + SSRF 死循环锁 35 passed;**TS 795 零漂移(3086 LOST 0/ADDED 0)**;build ✓
|
|
12
|
+
|
|
13
|
+
## [3.7.31] — 2026-09-18
|
|
14
|
+
|
|
15
|
+
### 提取器 SSRF 标记正则死循环修复(P0,真实工程可用性故障)
|
|
16
|
+
|
|
17
|
+
- **现象**:IR 提取阶段对含 `fetch(` 的 TS 文件 100% CPU 挂死——一个 55 行的切片 >480s 未完成,而 108 KB / 5 文件的语料只需 35s。实测 **nuxt-modules/og-image、tinacms/tinacms、zereight/gitlab-mcp 三个独立仓库全部触发**
|
|
18
|
+
- **定位**:对挂住进程做 CPU 采样(`sample`),**2193/2193 采样全部落在 `Builtins_RegExpPrototypeExec`**——确定为正则死循环,而非此前怀疑的 OOM / ts-morph 依赖解析
|
|
19
|
+
- **根因**(`src/extract-ir.ts` SSRF 分支,3.7.28 引入):`TS_HTTP_FETCH_SINK` 是**无 `g` 标志**的正则,却用于 `while ((m = TS_HTTP_FETCH_SINK.exec(text)) !== null)` 迭代。非全局正则的 `exec()` 忽略 `lastIndex` 并恒返回首个匹配;当首个 `fetch(` 后的 300 字符窗口不含污点时(真实代码里最常见的「良性 fetch」形态),循环无出口。同文件另 7 处 `while-exec` 循环均带 `g`,仅此一处遗漏
|
|
20
|
+
- **修复**:新增 `tsHttpFetchSinkIter()`(同模式 `"g"` 版)专供迭代;布尔探测仍用无标志版本(带 `g` 的 `.test()` 会污染 `lastIndex`)。模式至少匹配 5 字符,不可能零宽匹配
|
|
21
|
+
- **影响面**:任何含 `fetch(` 且函数体内无 SSRF 守卫词汇(`localhost|ssrf|hostname|…`)的 TS 工程都会挂死提取阶段——这是面向真实代码的普遍故障,不是基准内部的边缘情况
|
|
22
|
+
- **验收**:og-image 480s+ → **48s**;tinacms 300s+ → **57s**;gitlab-mcp 切片 480s+ → **80s**;最小复现 70s+ → **18s**
|
|
23
|
+
|
|
24
|
+
### 修复回归语料扩样至 17 条(fr-012 ~ fr-017)
|
|
25
|
+
|
|
26
|
+
- **样本结构**:open-webui 占比 75% → **41%**,独立仓库 3 → **10**(gitlab-mcp ×2、mockoon、og-image、redocly-cli、tinacms)
|
|
27
|
+
- **结果**:6 条新语料全部 MISS(pre/post 均 0 违规),全库 **DETECTED 4/17、MISS 13/17、PENDING 0**
|
|
28
|
+
- **正向对照**:为排除「0 违规是哑值」,scan `demo-project/` 报出 1 条真实违规(`SSG_AUTH_STATE_VIOLATION`,`token_lifecycle_flow.ts::main`)——引擎是活的
|
|
29
|
+
- **注册数据修正**:fr-013 的 parent SHA 末 4 位转置(codeload 404)且原 fix_commit 是纯 CHANGELOG 文档提交,经 PR #624 正文 link 定位到真修复 PR #571;fr-012 锚点错位(其「文件读取」半边由更早的 PR #482 修复)
|
|
30
|
+
- **新根因类别**:fr-012(MCP 工具实参 `args.file_path`)、fr-015(`decodeURIComponent` 解码后的对象字段 `font.path`)暴露**污点源词汇表缺口**——与 skyvern/open-webui 已暴露的认证词表缺口同族,共同病灶是「标记管线靠名字识源,不靠数据流语义」
|
|
31
|
+
- 详见 `blind-benchmark/REALWORLD_FIX_REGRESSION_V2.md`
|
|
32
|
+
|
|
33
|
+
### 回归
|
|
34
|
+
|
|
35
|
+
- 新增 `src/extract-ir-ssrf-loop.test.ts` 2 green ✓(①良性 fetch 必须正常返回——修复前永不返回;②URL 形参流入 fetch 仍必须注入 `__progmune_ssrf_user_url__`)/ build ✓
|
|
36
|
+
|
|
3
37
|
## [3.7.30] — 2026-09-17
|
|
4
38
|
|
|
5
39
|
### 提取器性能重构(3.7.28 引入的回归修复)
|
package/README.md
CHANGED
|
@@ -109,15 +109,18 @@ Progmune is honest about what it can and cannot verify.
|
|
|
109
109
|
| **Python** | ✅ Production | Blind benchmark: **recall 100% / precision 100%** (729 gold findings, 90 projects); real-world validation: PyGoat (OWASP vulnerable-by-design Django app) **67 TP / 0 FP, 100% labeled precision**; three well-written apps (django/fastapi realworld, django-unicorn) with 0 false-positive true findings |
|
|
110
110
|
| **C** | ✅ Annotation-driven (Beta) | IR extraction via the registry + SSG state machine; **annotate ~2-3 protocol primitives per protocol to get trusted verification** (gold 5/5 on real modules: redis ACL / libssh client / libssh server / libssh callback-dispatch / uftpd transfer-auth — all 0 FP with precise violation localization; app-level gold v2: **P=91.7% / R=100% / F1=95.7%**). Unannotated auto-detection stays out of scope (0 TP on real corpus — positioning decision recorded in the C Language Status doc); TLS-level coverage still absent. See [C Language Status](https://github.com/shenlian19831109/progmune-runtime/blob/main/docs/c-language-status.md). |
|
|
111
111
|
| **Go** | ✅ Annotation-driven (Beta) | IR extraction via registry + SSG state machine; synthetic gold v1: **P=100% / R=100%** (clean×3 + violations×3); zero external toolchain (pure-TS lexical extractor — works from npm installs) |
|
|
112
|
-
| **Java** |
|
|
112
|
+
| **Java** | 🔶 Annotation-driven (Beta) | Two layers, verified on real corpora. **Framework layer ✅**: Spring adapter (`spring-detector.ts` — security rule ordering + ant patterns + controller annotation routes) on gothinkster/spring-boot-realworld (1581★): 19 routes / 12 mutations fully parsed, 0 issues, catch-all flip counter-proof ×10; Spring Boot 3 modern dialect re-verified (SecurityFilterChain bean + String[] allowlist expansion): 15 routes, 0 issues. **Core layer**: pure-TS lexical extractor (zero toolchain deps) at **100%/100% recovery vs a tree-sitter AST baseline (1216/1216)**; SSG annotation-driven protocol rows (token / auth-register / resource) engine-locked with a real-corpus closed loop (**1 real TP / 0 FP**). Unannotated auto-detection stays out of scope (same decision as C/Go). Production label pending enterprise PoC. See [Java Language Status](https://github.com/shenlian19831109/progmune-runtime/blob/main/docs/java-language-status.md). |
|
|
113
113
|
|
|
114
114
|
**Multi-language IR (registry-based).** TypeScript (ts-morph) and Python (AST) extractors are registered entries in one registry (`src/extract-project-ir.ts`); `extractProjectIR` merges every detected language into a single function IR shared by the agent loop, `execute()`, and the MCP server — the agent composes function-protocol chains across both languages. Adding a language (Go, Java, …) = register one extractor entry; callers don't change.
|
|
115
115
|
|
|
116
|
-
**Framework adapters:
|
|
116
|
+
**Framework adapters: 13 dedicated detectors — 4 structural (AST-based) + 8 heuristic (code-string) + Spring Boot (security rule-ordering model); 13/13 now validated on real corpora.** Express ✅, tRPC ✅, FastAPI ✅, Django ✅ (structural route-level auth analysis — every mutation route checked for auth), Flask ✅ (route/before_request auth guard analysis), Fastify ✅ (route preHandler/hook auth analysis) and Next.js ✅ (App Router route-handler auth analysis), NestJS ✅ (decorator route parsing + global APP_GUARD recognition + @Public exemption), Koa ✅, Hapi ✅ (route-config auth strategy analysis), Gin ✅ and Fiber ✅ (Go route/auth-middleware analysis) and Spring Boot ✅ (Spring Security rule ordering; Boot 2 + Boot 3 dialects) have dedicated detectors. Next.js version-aware governance also exists.
|
|
117
|
+
|
|
118
|
+
**Evidence tiers** (adopted 2026-09-02): 🟡 synthetic → ✅ **real-corpus validated** (≥1 real production corpus at 0 protocol-level FP + guard-removal counter-proof) → 🏭 production (enterprise PoC/adoption — none yet). **All 13/13 are ✅.** Documented boundaries: Express `main.ts` retains a NO_HELMET hardening gap (not protocol-level); the NestJS corpus yields 1 real TP (`DELETE /users/:slug`); jiotv_go's 2 flags are capability-token shapes; gofiber recipes flags are demo-context noise (no auth intent). Remaining gap: a few TS frameworks and Go frameworks beyond Gin/Fiber.
|
|
117
119
|
|
|
118
120
|
### What Progmune does NOT cover (honest boundaries)
|
|
119
121
|
|
|
120
|
-
- **TS-side taint-based injection flaws
|
|
122
|
+
- **TS-side taint-based injection flaws — partial.** Python ships source-level SQLi/XSS/SSRF. On the TypeScript side, **path traversal (3.7.26)** and **SSRF (3.7.28)** are covered: the extractor emits markers (request/param taint → fs or HTTP sink, one hop + one cross-function hop) and the engine consumes them at the IR layer — validated against real security fixes at their ground-truth sites (fr-007 openhop, fr-011 mcp-from-openapi). **SQLi and XSS remain uncovered**: the TS extractor is name/call-based, not a dataflow taint engine, so those classes stay out of scope (documented, not hidden).
|
|
123
|
+
- **Missing-check (absence) vulnerabilities** — the dominant real-world fix shape. A protocol state machine proves "these calls happened in the wrong order"; it does not prove "a required check was never called." Authorisation-policy bypasses, allowlist-path omissions, and ownership checks missing on one route are the largest uncovered class — measured, not guessed: see the [real-fix regression corpus](https://github.com/shenlian19831109/progmune-runtime/blob/main/blind-benchmark/fix-regression-corpus.json).
|
|
121
124
|
- **SCA / dependency vulnerabilities** — hallucinated package names, supply-chain issues. Separate tooling exists for this.
|
|
122
125
|
- **Runtime behavior** — Progmune is static analysis only; no DAST/sandbox execution.
|
|
123
126
|
- **Framework internals** — well-known framework dispatch/cache machinery (e.g. django-unicorn internals) can produce a small number of boundary false positives; they are documented per-corpus in the benchmark gold files.
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* SSRF 标记提取的死循环回归(2026-09-18,P0)。
|
|
4
|
+
*
|
|
5
|
+
* 根因:computeMarkerCalls 的 SSRF 分支对一个**无 `g` 标志**的正则
|
|
6
|
+
* TS_HTTP_FETCH_SINK 做 while-exec 迭代。非全局正则的 exec() 忽略
|
|
7
|
+
* lastIndex 并恒返回首个匹配,因此只要首个 fetch 调用后的 300 字符窗口
|
|
8
|
+
* 内不含污点——真实代码里最常见的「良性 fetch」形态——循环就没有出口,
|
|
9
|
+
* 提取阶段 100% CPU 死循环。CPU 采样佐证:2193/2193 采样全部落在
|
|
10
|
+
* Builtins_RegExpPrototypeExec。
|
|
11
|
+
*
|
|
12
|
+
* 影响面:任何含 `fetch(` 且函数体内无 SSRF 守卫词汇的 TS 工程都会挂死
|
|
13
|
+
* 提取阶段(实测 nuxt og-image、tinacms、gitlab-mcp 三个独立仓库均触发)。
|
|
14
|
+
*
|
|
15
|
+
* 本文件锁定两端:
|
|
16
|
+
* 1. 良性 fetch 必须正常返回(修复前永不返回);
|
|
17
|
+
* 2. 真正的 SSRF 污点仍必须注入 __progmune_ssrf_user_url__(修复不得削弱检出)。
|
|
18
|
+
*/
|
|
19
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
20
|
+
if (k2 === undefined) k2 = k;
|
|
21
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
22
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
23
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
24
|
+
}
|
|
25
|
+
Object.defineProperty(o, k2, desc);
|
|
26
|
+
}) : (function(o, m, k, k2) {
|
|
27
|
+
if (k2 === undefined) k2 = k;
|
|
28
|
+
o[k2] = m[k];
|
|
29
|
+
}));
|
|
30
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
31
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
32
|
+
}) : function(o, v) {
|
|
33
|
+
o["default"] = v;
|
|
34
|
+
});
|
|
35
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
36
|
+
var ownKeys = function(o) {
|
|
37
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
38
|
+
var ar = [];
|
|
39
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
40
|
+
return ar;
|
|
41
|
+
};
|
|
42
|
+
return ownKeys(o);
|
|
43
|
+
};
|
|
44
|
+
return function (mod) {
|
|
45
|
+
if (mod && mod.__esModule) return mod;
|
|
46
|
+
var result = {};
|
|
47
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
48
|
+
__setModuleDefault(result, mod);
|
|
49
|
+
return result;
|
|
50
|
+
};
|
|
51
|
+
})();
|
|
52
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
53
|
+
const vitest_1 = require("vitest");
|
|
54
|
+
const fs = __importStar(require("fs"));
|
|
55
|
+
const os = __importStar(require("os"));
|
|
56
|
+
const path = __importStar(require("path"));
|
|
57
|
+
const extract_ir_1 = require("./extract-ir");
|
|
58
|
+
const TSCONFIG = JSON.stringify({
|
|
59
|
+
compilerOptions: {
|
|
60
|
+
target: "ES2020",
|
|
61
|
+
module: "commonjs",
|
|
62
|
+
moduleResolution: "node",
|
|
63
|
+
strict: false,
|
|
64
|
+
skipLibCheck: true,
|
|
65
|
+
noEmit: true,
|
|
66
|
+
},
|
|
67
|
+
include: ["**/*.ts"],
|
|
68
|
+
});
|
|
69
|
+
function makeProject(files) {
|
|
70
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pm-ssrf-loop-"));
|
|
71
|
+
fs.writeFileSync(path.join(dir, "tsconfig.json"), TSCONFIG);
|
|
72
|
+
fs.writeFileSync(path.join(dir, "package.json"), JSON.stringify({ name: "fixture", version: "0.0.0", private: true }));
|
|
73
|
+
for (const [rel, body] of Object.entries(files)) {
|
|
74
|
+
const abs = path.join(dir, rel);
|
|
75
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
76
|
+
fs.writeFileSync(abs, body);
|
|
77
|
+
}
|
|
78
|
+
return dir;
|
|
79
|
+
}
|
|
80
|
+
(0, vitest_1.describe)("SSRF 标记提取:良性 fetch 不得使提取器死循环", () => {
|
|
81
|
+
(0, vitest_1.it)("无守卫词汇、无污点的 fetch 调用应正常完成提取", () => {
|
|
82
|
+
const dir = makeProject({
|
|
83
|
+
"api.ts": `
|
|
84
|
+
export async function ping(): Promise<any> {
|
|
85
|
+
const response = await fetch("https://x.example/uploads", { method: "POST" });
|
|
86
|
+
return response.json();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function pong(): Promise<any> {
|
|
90
|
+
return fetch("https://x.example/other");
|
|
91
|
+
}
|
|
92
|
+
`,
|
|
93
|
+
});
|
|
94
|
+
try {
|
|
95
|
+
const fns = (0, extract_ir_1.extractIR)(dir);
|
|
96
|
+
const names = fns.map((f) => f.name);
|
|
97
|
+
(0, vitest_1.expect)(names).toContain("ping");
|
|
98
|
+
(0, vitest_1.expect)(names).toContain("pong");
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
102
|
+
}
|
|
103
|
+
}, 150000);
|
|
104
|
+
(0, vitest_1.it)("URL 形参直接流入 fetch 仍应注入 __progmune_ssrf_user_url__", () => {
|
|
105
|
+
const dir = makeProject({
|
|
106
|
+
"proxy.ts": `
|
|
107
|
+
export async function proxy(target: string): Promise<any> {
|
|
108
|
+
const response = await fetch(target);
|
|
109
|
+
return response;
|
|
110
|
+
}
|
|
111
|
+
`,
|
|
112
|
+
});
|
|
113
|
+
try {
|
|
114
|
+
const fns = (0, extract_ir_1.extractIR)(dir);
|
|
115
|
+
const proxy = fns.find((f) => f.name === "proxy");
|
|
116
|
+
(0, vitest_1.expect)(proxy).toBeDefined();
|
|
117
|
+
(0, vitest_1.expect)(proxy.calls ?? []).toContain("__progmune_ssrf_user_url__");
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
121
|
+
}
|
|
122
|
+
}, 150000);
|
|
123
|
+
});
|
package/dist/extract-ir.js
CHANGED
|
@@ -385,21 +385,49 @@ const TS_FILE_SINK_NAMES = [
|
|
|
385
385
|
"createWriteStream", "existsSync", "readdir", "readdirSync",
|
|
386
386
|
"copyFile", "rename", "stat", "lstat", "rmdir", "truncate",
|
|
387
387
|
];
|
|
388
|
+
// ═══════════════════════════════════════════════════════════════
|
|
389
|
+
// 不可信入口根表 —— 污点数据流试点(2026-09-18)
|
|
390
|
+
//
|
|
391
|
+
// 此前整条污点链路只锚定 Express 形态的
|
|
392
|
+
// `(?:req|request)\.(?:params|query|body|headers|cookies)`,
|
|
393
|
+
// 即**根集合只有一种来源**。后果是 MCP 工具实参、CLI 参数、URL 解码产物
|
|
394
|
+
// 等来源天然不可见:fr-012 的 `args.file_path`(← params.arguments)与
|
|
395
|
+
// fr-015 的 `font.path`(← URL 解码)都因此 MISS。往 URL_PARAM_NAME 里加
|
|
396
|
+
// 名字解决不了——那只是在已经认错了根的前提下扩充变量名清单(打地鼠)。
|
|
397
|
+
//
|
|
398
|
+
// 本表改为按**传输面**声明根:判定依据是值的来源性质,而不是变量叫什么。
|
|
399
|
+
// 每条根必须窄且可解释,新增需附 `why`(closes #taint-pilot)。
|
|
400
|
+
// ═══════════════════════════════════════════════════════════════
|
|
401
|
+
const UNTRUSTED_ROOTS = [
|
|
402
|
+
{
|
|
403
|
+
id: "http_request",
|
|
404
|
+
expr: String.raw `\b(?:req|request)\.(?:params|query|body|headers|cookies)\b[.[]?`,
|
|
405
|
+
why: "HTTP 请求对象字段,Express/Koa/Fastify/Nest 等通用形态",
|
|
406
|
+
},
|
|
407
|
+
{
|
|
408
|
+
id: "mcp_tool_args",
|
|
409
|
+
expr: String.raw `\bparams\.arguments\b[.[]?`,
|
|
410
|
+
why: "MCP CallToolRequest.params.arguments —— 工具调用实参,调用方可控(fr-012)",
|
|
411
|
+
},
|
|
412
|
+
];
|
|
413
|
+
/** 所有根的来源片段,供 **无标志** 的布尔探测正则使用 */
|
|
414
|
+
const UNTRUSTED_ROOT_SRC = UNTRUSTED_ROOTS.map((r) => `(?:${r.expr})`).join("|");
|
|
415
|
+
/** 既有的 request 根语义,改由根表统一提供 */
|
|
416
|
+
function hasRequestRootedExpr(text) {
|
|
417
|
+
return new RegExp(UNTRUSTED_ROOT_SRC).test(text);
|
|
418
|
+
}
|
|
388
419
|
function tsSinkCallRegex() {
|
|
389
420
|
return new RegExp(`(?:^|[^\\w.$])(?:await\\s+)?(?:[\\w$]+\\.)?(${TS_FILE_SINK_NAMES.join("|")})\\s*\\(([\\s\\S]{0,250}?)\\)`, "g");
|
|
390
421
|
}
|
|
391
|
-
/**
|
|
392
|
-
function hasRequestRootedExpr(text) {
|
|
393
|
-
return /\b(?:req|request)\.(?:params|query|body|headers|cookies)\b[.[]?/.test(text);
|
|
394
|
-
}
|
|
395
|
-
/** 函数体内被 request 污染的局部名(含解构与单跳赋值,深度 ≤2) */
|
|
422
|
+
/** 函数体内被不可信根污染的局部名(含解构与单跳赋值,深度 ≤2) */
|
|
396
423
|
function collectTaintedNames(text) {
|
|
397
424
|
const tainted = new Set();
|
|
398
425
|
let m;
|
|
399
|
-
const
|
|
426
|
+
const root = UNTRUSTED_ROOT_SRC;
|
|
427
|
+
const direct = new RegExp(`(?:const|let|var)\\s+([\\w$]+)\\s*=\\s*(?:await\\s+)?(?:${root})`, "g");
|
|
400
428
|
while ((m = direct.exec(text)) !== null)
|
|
401
429
|
tainted.add(m[1]);
|
|
402
|
-
const destr =
|
|
430
|
+
const destr = new RegExp(`(?:const|let|var)\\s*\\{\\s*([^}=]*?)\\s*\\}\\s*=\\s*(?:await\\s+)?(?:${root})`, "g");
|
|
403
431
|
while ((m = destr.exec(text)) !== null) {
|
|
404
432
|
for (const part of m[1].split(",")) {
|
|
405
433
|
const name = part.trim().split(":")[0].trim();
|
|
@@ -407,7 +435,7 @@ function collectTaintedNames(text) {
|
|
|
407
435
|
tainted.add(name);
|
|
408
436
|
}
|
|
409
437
|
}
|
|
410
|
-
const bareAssign =
|
|
438
|
+
const bareAssign = new RegExp(`(?:^|[^\\w$.])([\\w$]+)\\s*=\\s*(?:await\\s+)?(?:${root})`, "g");
|
|
411
439
|
while ((m = bareAssign.exec(text)) !== null)
|
|
412
440
|
tainted.add(m[1]);
|
|
413
441
|
// 单跳传播(深度 ≤2)
|
|
@@ -429,9 +457,7 @@ function collectTaintedNames(text) {
|
|
|
429
457
|
return tainted;
|
|
430
458
|
}
|
|
431
459
|
function taintPattern(tainted) {
|
|
432
|
-
const parts = [
|
|
433
|
-
/\b(?:req|request)\.(?:params|query|body|headers|cookies)\b[.[]?/.source,
|
|
434
|
-
];
|
|
460
|
+
const parts = [UNTRUSTED_ROOT_SRC];
|
|
435
461
|
if (tainted.size > 0) {
|
|
436
462
|
parts.push(`\\b(?:${[...tainted].join("|")})\\b`);
|
|
437
463
|
}
|
|
@@ -474,6 +500,27 @@ function splitArgWindow(win) {
|
|
|
474
500
|
/** 项目方法名 → 方法体内流入文件 sink 的形参下标集合 */
|
|
475
501
|
function methodSinkParamMap(project, absRoot) {
|
|
476
502
|
const map = new Map();
|
|
503
|
+
const register = (keyName, fullName, text, relPath, params) => {
|
|
504
|
+
const sinkRe = tsSinkCallRegex();
|
|
505
|
+
let sm;
|
|
506
|
+
while ((sm = sinkRe.exec(text)) !== null) {
|
|
507
|
+
const win = sm[2] || "";
|
|
508
|
+
params.forEach((pname, idx) => {
|
|
509
|
+
if (!pname)
|
|
510
|
+
return;
|
|
511
|
+
if (!new RegExp(`\\b${pname}\\b`).test(win))
|
|
512
|
+
return;
|
|
513
|
+
if (!map.has(keyName)) {
|
|
514
|
+
map.set(keyName, { idxs: new Set(), entries: [] });
|
|
515
|
+
}
|
|
516
|
+
const rec = map.get(keyName);
|
|
517
|
+
rec.idxs.add(idx);
|
|
518
|
+
if (!rec.entries.some((e) => e.name === fullName)) {
|
|
519
|
+
rec.entries.push({ name: fullName, file: relPath });
|
|
520
|
+
}
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
};
|
|
477
524
|
for (const sf of project.getSourceFiles()) {
|
|
478
525
|
const relPath = path.relative(absRoot, sf.getFilePath());
|
|
479
526
|
for (const cls of sf.getClasses()) {
|
|
@@ -484,27 +531,23 @@ function methodSinkParamMap(project, absRoot) {
|
|
|
484
531
|
const params = m.getParameters().map((p) => p.getName());
|
|
485
532
|
if (params.length === 0)
|
|
486
533
|
continue;
|
|
487
|
-
|
|
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
|
-
}
|
|
534
|
+
register(m.getName(), `${cn}.${m.getName()}`, m.getText(), relPath, params);
|
|
506
535
|
}
|
|
507
536
|
}
|
|
537
|
+
// ── 污点数据流试点(2026-09-18)──
|
|
538
|
+
// 此前只登记**类方法**。真实 TS 工程大量使用顶层函数声明(MCP handler、
|
|
539
|
+
// Nuxt runtime binding、工具函数),跨函数一跳对它们完全不生效——fr-012 的
|
|
540
|
+
// `markdownUpload` 正是顶层函数,因此从未进入本表,即使上游 taint 已识别,
|
|
541
|
+
// 也无法传导到 sink。这里按同样的「形参 → 文件 sink」规则纳入函数声明。
|
|
542
|
+
for (const fn of sf.getFunctions()) {
|
|
543
|
+
const fnName = fn.getName();
|
|
544
|
+
if (!fnName)
|
|
545
|
+
continue;
|
|
546
|
+
const params = fn.getParameters().map((p) => p.getName());
|
|
547
|
+
if (params.length === 0)
|
|
548
|
+
continue;
|
|
549
|
+
register(fnName, fnName, fn.getText(), relPath, params);
|
|
550
|
+
}
|
|
508
551
|
}
|
|
509
552
|
return map;
|
|
510
553
|
}
|
|
@@ -516,6 +559,19 @@ function methodSinkParamMap(project, absRoot) {
|
|
|
516
559
|
// Web 形态(req.params)共用同一标记;引擎 IR 层消费。
|
|
517
560
|
// ═══════════════════════════════════════════════════════════════
|
|
518
561
|
const TS_HTTP_FETCH_SINK = /\b(fetch|axios\.(?:get|post|put|delete|head|patch)|http\.request|https\.request|ky\.(?:get|post|put|delete|head|patch)|undici\.request|nodeFetch|got)\s*\(/;
|
|
562
|
+
/**
|
|
563
|
+
* 同模式的**全局**版本,仅用于 while-exec 迭代。
|
|
564
|
+
*
|
|
565
|
+
* 2026-09-18 P0 修复:此前 while 循环直接对无 `g` 标志的 TS_HTTP_FETCH_SINK
|
|
566
|
+
* 反复调用 exec()。非全局正则的 exec() 会忽略 lastIndex 并恒返回首个匹配,
|
|
567
|
+
* 一旦首个匹配的 300 字符窗口内不含污点(真实代码里最常见的良性 fetch
|
|
568
|
+
* 形态),循环无出口 → 提取器 100% CPU 死循环。CPU 采样证实:
|
|
569
|
+
* 2193/2193 采样全部落在 Builtins_RegExpPrototypeExec。
|
|
570
|
+
* 该缺陷会使任何含 `fetch(` 且无 SSRF 守卫词汇的 TS 工程挂死提取阶段。
|
|
571
|
+
*/
|
|
572
|
+
function tsHttpFetchSinkIter() {
|
|
573
|
+
return new RegExp(TS_HTTP_FETCH_SINK.source, "g");
|
|
574
|
+
}
|
|
519
575
|
const SSRF_GUARD_EVIDENCE = /127\.0\.0\.1|0\.0\.0\.0|169\.254\.|::1|localhost|isPrivate|isLoopback|hostname|denylist|blocklist|ssrf|validateUrl|isSafeUrl|getAddresses|ipaddress|forbidden_host|private_ip/i;
|
|
520
576
|
const URL_PARAM_NAME = /^(url|target|endpoint|link|href|webUrl|web_url|sourceUrl|source_url|remoteUrl|remote_url|fetchUrl|fetch_url|specUrl|spec_url|apiUrl|api_url|origin|baseUrl|base_url)$/i;
|
|
521
577
|
function collectUrlParamNames(params) {
|
|
@@ -539,18 +595,26 @@ function computeMarkerCalls(text, paramNames, sinkParams, onMethodHit) {
|
|
|
539
595
|
}
|
|
540
596
|
else {
|
|
541
597
|
const methodCallRe = new RegExp(`\\.([\\w$]+)\\s*\\(([\\s\\S]{0,250}?)\\)`, "g");
|
|
598
|
+
// ── 污点数据流试点(2026-09-18)──
|
|
599
|
+
// 此前只识别成员调用(`obj.sink(...)`)。顶层/模块内函数是真实 TS 工程
|
|
600
|
+
// 的主要形态(fr-012 的 `markdownUpload(...)` 就是裸调用),缺这一则
|
|
601
|
+
// 即使该函数的形参已登记为 sink 形参,taint 也无法传导过去。
|
|
602
|
+
const directCallRe = new RegExp(`(?:^|[^\\w$.])([\\w$]+)\\s*\\(([\\s\\S]{0,250}?)\\)`, "g");
|
|
542
603
|
const taint = taintPattern(tainted);
|
|
543
604
|
if (taint) {
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
605
|
+
for (const callRe of [methodCallRe, directCallRe]) {
|
|
606
|
+
callRe.lastIndex = 0;
|
|
607
|
+
let m;
|
|
608
|
+
while ((m = callRe.exec(text)) !== null) {
|
|
609
|
+
const rec = sinkParams.get(m[1]);
|
|
610
|
+
if (!rec || rec.idxs.size === 0)
|
|
611
|
+
continue;
|
|
612
|
+
const args = splitArgWindow(m[2] || "");
|
|
613
|
+
const hit = args.some((arg, i) => rec.idxs.has(i) && taint.test(arg));
|
|
614
|
+
if (hit) {
|
|
615
|
+
markers.push("__progmune_path_traversal__");
|
|
616
|
+
onMethodHit(rec);
|
|
617
|
+
}
|
|
554
618
|
}
|
|
555
619
|
}
|
|
556
620
|
}
|
|
@@ -567,11 +631,11 @@ function computeMarkerCalls(text, paramNames, sinkParams, onMethodHit) {
|
|
|
567
631
|
taintParts.push(`\\b(?:${urlParams.join("|")})\\b`);
|
|
568
632
|
if (reqTainted.size > 0)
|
|
569
633
|
taintParts.push(`\\b(?:${[...reqTainted].join("|")})\\b`);
|
|
570
|
-
taintParts.push(
|
|
634
|
+
taintParts.push(UNTRUSTED_ROOT_SRC);
|
|
571
635
|
const taint = new RegExp(taintParts.join("|"));
|
|
572
|
-
|
|
636
|
+
const fetchSinkIter = tsHttpFetchSinkIter();
|
|
573
637
|
let m;
|
|
574
|
-
while ((m =
|
|
638
|
+
while ((m = fetchSinkIter.exec(text)) !== null) {
|
|
575
639
|
const after = text.slice(m.index + m[0].length, m.index + m[0].length + 300);
|
|
576
640
|
if (taint.test(after)) {
|
|
577
641
|
markers.push("__progmune_ssrf_user_url__");
|
|
@@ -0,0 +1,233 @@
|
|
|
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
|
+
exports.inspectIrFreshness = inspectIrFreshness;
|
|
37
|
+
exports.reextractMode = reextractMode;
|
|
38
|
+
exports.reextractEnabled = reextractEnabled;
|
|
39
|
+
const fs = __importStar(require("fs"));
|
|
40
|
+
const path = __importStar(require("path"));
|
|
41
|
+
/**
|
|
42
|
+
* ir.json 陈旧性判定(2026-09-19,独立于污点试点立项)。
|
|
43
|
+
*
|
|
44
|
+
* 背景(真实事故,两次会话级误判):
|
|
45
|
+
* - trust 引擎的自动提取是「缺才提取」:项目已有 ir.json 就以文件为准、不重生成。
|
|
46
|
+
* - 该语义在【源码变了但 ir.json 没变】的场景下静默过期——
|
|
47
|
+
* ① 分析器读的是旧调用序列(fix-regression 复测结论可能是哑值);
|
|
48
|
+
* ② P4.5 注解合并读同一个 ir.json,旧文件让新加的 @protocol 注解不生效。
|
|
49
|
+
* - fr-005 首扫报 0(`rm ir.json` 后才报出)、污点试点 V1 的「标记注入但 trust 报 0」
|
|
50
|
+
* 根因都是同一处。
|
|
51
|
+
*
|
|
52
|
+
* 政策:mtime 比对,不做「每次全量重提」。
|
|
53
|
+
* - 全量重提对 skyvern 级项目是 5–10 分钟量级成本,不能默认付;
|
|
54
|
+
* - 只在【存在源文件 mtime > ir.json mtime】时才重提,常量级开销(一次 stat 遍历)。
|
|
55
|
+
* 政策:mtime 比对,不做「每次全量重提」。
|
|
56
|
+
* - 全量重提对 skyvern 级项目是 5–10 分钟量级成本,不能默认付;对本仓库
|
|
57
|
+
* (22k 源文件)足以触发单测 30s 超时——实测教训,见 tests/trust/engine.test.ts。
|
|
58
|
+
* - 只在【存在源文件 mtime > ir.json mtime】时才重提(一次 stat 遍历,恒定成本)。
|
|
59
|
+
* - 超大仓库(遍历被 MAX_STATS 截断)默认【只警告不重提】:此时 freshness 证据
|
|
60
|
+
* 不完整,自动重提会把 CLI 拖到分钟级。确需重提时显式 PROGMUNE_IR_REEXTRACT=always。
|
|
61
|
+
*
|
|
62
|
+
* 策略阀 `PROGMUNE_IR_REEXTRACT`:
|
|
63
|
+
* - 未设(auto):遍历完整且陈旧 → 重提;遍历被截断 → 只警告
|
|
64
|
+
* - always :只要陈旧就重提(自担成本,用于批量语料复测)
|
|
65
|
+
* - never :回到旧语义(永不重提),但会打「IR 可能陈旧」警告——不允许静默。
|
|
66
|
+
*/
|
|
67
|
+
/** 会被提取器读取的源码后缀(跨语言;不在表内的文件不影响 IR 内容)。 */
|
|
68
|
+
const SOURCE_EXT = new Set([
|
|
69
|
+
".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs",
|
|
70
|
+
".py", ".c", ".h", ".cc", ".cpp", ".hpp",
|
|
71
|
+
".go", ".java", ".rs", ".rb", ".php", ".cs", ".swift", ".kt", ".scala",
|
|
72
|
+
]);
|
|
73
|
+
/** 不可能是业务源码、且体量足以毁掉遍历成本的目录。 */
|
|
74
|
+
const SKIP_DIRS = new Set([
|
|
75
|
+
"node_modules", ".git", "dist", "build", "out", "coverage", ".next",
|
|
76
|
+
".nuxt", ".turbo", ".cache", "__pycache__", ".venv", "venv", "vendor",
|
|
77
|
+
"target", "bin", "obj", ".gradle", ".idea", ".vscode", ".workbuddy",
|
|
78
|
+
]);
|
|
79
|
+
/** 防御性上限:超大仓库不做无界遍历(宁可漏判也不卡死)。 */
|
|
80
|
+
const MAX_STATS = 20000;
|
|
81
|
+
const MAX_DEPTH = 12;
|
|
82
|
+
/**
|
|
83
|
+
* auto 模式下愿意为「一次 IR 重提」买单的项目规模上限(源码文件数)。
|
|
84
|
+
* 超过则只警告不自动重提——重提成本随文件数超线性增长,本仓库(万级源文件)
|
|
85
|
+
* 实测足以把单测推过 30s 超时。
|
|
86
|
+
*/
|
|
87
|
+
const AUTO_REEXTRACT_FILE_BUDGET = 5000;
|
|
88
|
+
/**
|
|
89
|
+
* 扫描项目目录,找出最新修改的源码文件。
|
|
90
|
+
*
|
|
91
|
+
* 必须【完整遍历】(不做「找到第一个更新的就早退」):早退会在超大仓库上
|
|
92
|
+
* 把 truncated 信号抹掉——本仓库(22k 源文件)就是活例:早退后 evidenceComplete
|
|
93
|
+
* 误为真 → 自动全量重提 → 单测 30s 超时。stat 扫一遍的成本远低于一次 IR 提取,
|
|
94
|
+
* 换来的是可靠的 fresh 判定与可靠的大仓信号。
|
|
95
|
+
*
|
|
96
|
+
* @requires PROJECT_PATH @produces NEWEST_SOURCE
|
|
97
|
+
* @tags ir, staleness, mtime
|
|
98
|
+
*/
|
|
99
|
+
function newestSourceUnder(root, opts) {
|
|
100
|
+
const maxStats = opts.maxStats ?? MAX_STATS;
|
|
101
|
+
const maxDepth = opts.maxDepth ?? MAX_DEPTH;
|
|
102
|
+
let newestMs = null;
|
|
103
|
+
let newestFile = null;
|
|
104
|
+
let scanned = 0;
|
|
105
|
+
let truncated = false;
|
|
106
|
+
const walk = (dir, depth) => {
|
|
107
|
+
if (depth > maxDepth || truncated)
|
|
108
|
+
return;
|
|
109
|
+
let entries;
|
|
110
|
+
try {
|
|
111
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return; // 权限 / 竞态删除:跳过
|
|
115
|
+
}
|
|
116
|
+
for (const ent of entries) {
|
|
117
|
+
if (truncated)
|
|
118
|
+
return;
|
|
119
|
+
if (ent.isDirectory()) {
|
|
120
|
+
if (SKIP_DIRS.has(ent.name))
|
|
121
|
+
continue;
|
|
122
|
+
if (ent.name.startsWith("."))
|
|
123
|
+
continue; // 隐藏目录多为工具产物
|
|
124
|
+
walk(path.join(dir, ent.name), depth + 1);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (!ent.isFile())
|
|
128
|
+
continue;
|
|
129
|
+
if (!SOURCE_EXT.has(path.extname(ent.name).toLowerCase()))
|
|
130
|
+
continue;
|
|
131
|
+
if (scanned >= maxStats) {
|
|
132
|
+
truncated = true;
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const full = path.join(dir, ent.name);
|
|
136
|
+
let st;
|
|
137
|
+
try {
|
|
138
|
+
st = fs.statSync(full, { throwIfNoEntry: false }) ?? undefined;
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
st = undefined;
|
|
142
|
+
}
|
|
143
|
+
if (!st)
|
|
144
|
+
continue;
|
|
145
|
+
scanned++;
|
|
146
|
+
if (newestMs === null || st.mtimeMs > newestMs) {
|
|
147
|
+
newestMs = st.mtimeMs;
|
|
148
|
+
newestFile = full;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
walk(root, 0);
|
|
153
|
+
return { mtimeMs: newestMs, file: newestFile, scannedFiles: scanned, truncated };
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* 判定 ir.json 是否需要重新生成。
|
|
157
|
+
*
|
|
158
|
+
* - ir.json 不存在 → missing(需提取)
|
|
159
|
+
* - 存在源码 mtime > ir.json mtime → source-newer(需重提)
|
|
160
|
+
* - 其余 → fresh / no-sources(沿用磁盘文件)
|
|
161
|
+
*
|
|
162
|
+
* @requires PROJECT_PATH @produces IR_FRESHNESS
|
|
163
|
+
* @tags ir, staleness, mtime
|
|
164
|
+
* @useWhen evaluateTrust 决定是否自动提取 IR
|
|
165
|
+
*/
|
|
166
|
+
function inspectIrFreshness(projectPath, irPathOverride, opts = {}) {
|
|
167
|
+
const irPath = irPathOverride || path.join(projectPath, "ir.json");
|
|
168
|
+
let irStat;
|
|
169
|
+
try {
|
|
170
|
+
irStat = fs.statSync(irPath, { throwIfNoEntry: false }) ?? undefined;
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
irStat = undefined;
|
|
174
|
+
}
|
|
175
|
+
const irMtimeMs = irStat ? irStat.mtimeMs : null;
|
|
176
|
+
const src = newestSourceUnder(projectPath, opts);
|
|
177
|
+
const exceedsAutoBudget = src.scannedFiles > (opts.autoBudget ?? AUTO_REEXTRACT_FILE_BUDGET);
|
|
178
|
+
if (!irStat) {
|
|
179
|
+
return {
|
|
180
|
+
exists: false,
|
|
181
|
+
stale: true,
|
|
182
|
+
reason: "missing",
|
|
183
|
+
irMtimeMs: null,
|
|
184
|
+
newestSourceMtimeMs: src.mtimeMs,
|
|
185
|
+
newestSourcePath: src.file,
|
|
186
|
+
scannedFiles: src.scannedFiles,
|
|
187
|
+
truncated: src.truncated,
|
|
188
|
+
evidenceComplete: false, // 是否有证据都不影响:缺就必须提
|
|
189
|
+
exceedsAutoBudget,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
if (src.mtimeMs === null) {
|
|
193
|
+
// 扫不到源码(目标目录不是真源码根,或全是跳过后缀):无证据 → 保守沿用。
|
|
194
|
+
return {
|
|
195
|
+
exists: true,
|
|
196
|
+
stale: false,
|
|
197
|
+
reason: "no-sources",
|
|
198
|
+
irMtimeMs,
|
|
199
|
+
newestSourceMtimeMs: null,
|
|
200
|
+
newestSourcePath: null,
|
|
201
|
+
scannedFiles: 0,
|
|
202
|
+
truncated: src.truncated,
|
|
203
|
+
evidenceComplete: !src.truncated,
|
|
204
|
+
exceedsAutoBudget,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
// 严格大于:ir.json 与源码同刻写出(写源码→随后写 ir.json 的常见顺序)
|
|
208
|
+
// 判为 fresh,避免把正常管线推入全量重提。
|
|
209
|
+
const stale = irMtimeMs !== null && src.mtimeMs > irMtimeMs;
|
|
210
|
+
return {
|
|
211
|
+
exists: true,
|
|
212
|
+
stale,
|
|
213
|
+
reason: stale ? "source-newer" : "fresh",
|
|
214
|
+
irMtimeMs,
|
|
215
|
+
newestSourceMtimeMs: src.mtimeMs,
|
|
216
|
+
newestSourcePath: src.file,
|
|
217
|
+
scannedFiles: src.scannedFiles,
|
|
218
|
+
truncated: src.truncated,
|
|
219
|
+
evidenceComplete: !src.truncated,
|
|
220
|
+
exceedsAutoBudget,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
function reextractMode() {
|
|
224
|
+
const v = (process.env.PROGMUNE_IR_REEXTRACT || "").toLowerCase();
|
|
225
|
+
if (v === "always")
|
|
226
|
+
return "always";
|
|
227
|
+
if (v === "never" || v === "off" || v === "0" || v === "false")
|
|
228
|
+
return "never";
|
|
229
|
+
return "auto";
|
|
230
|
+
}
|
|
231
|
+
function reextractEnabled() {
|
|
232
|
+
return reextractMode() !== "never";
|
|
233
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
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 ir_staleness_1 = require("./ir-staleness");
|
|
41
|
+
/**
|
|
42
|
+
* ir.json 陈旧性判定的回归测试(2026-09-19)。
|
|
43
|
+
*
|
|
44
|
+
* 锁三件事:
|
|
45
|
+
* 1. ir.json 缺失 → missing(需提取)——保住原有的自动提取语义;
|
|
46
|
+
* 2. 源码比 ir.json 新 → source-newer(需重提)——本次新增的判别力;
|
|
47
|
+
* 3. ir.json 不早于源码 → fresh(沿用磁盘文件)——不做无谓的全量重提,
|
|
48
|
+
* 否则 skyvern 级项目每次扫描要多付分钟级成本。
|
|
49
|
+
*/
|
|
50
|
+
function tmpdir(tag) {
|
|
51
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), `pm-ir-stale-${tag}-`));
|
|
52
|
+
}
|
|
53
|
+
/** 把文件的 mtime 显式钉到某个 epoch ms(避免同一毫秒内写完两文件的歧义)。 */
|
|
54
|
+
function touch(file, content, mtimeMs) {
|
|
55
|
+
fs.writeFileSync(file, content);
|
|
56
|
+
fs.utimesSync(file, mtimeMs / 1000, mtimeMs / 1000);
|
|
57
|
+
}
|
|
58
|
+
(0, vitest_1.describe)("inspectIrFreshness", () => {
|
|
59
|
+
(0, vitest_1.it)("ir.json 缺失 → missing,stale=true(触发自动提取)", () => {
|
|
60
|
+
const dir = tmpdir("missing");
|
|
61
|
+
try {
|
|
62
|
+
touch(path.join(dir, "app.ts"), "export const x = 1;", 1700000000000);
|
|
63
|
+
const r = (0, ir_staleness_1.inspectIrFreshness)(dir);
|
|
64
|
+
(0, vitest_1.expect)(r.exists).toBe(false);
|
|
65
|
+
(0, vitest_1.expect)(r.stale).toBe(true);
|
|
66
|
+
(0, vitest_1.expect)(r.reason).toBe("missing");
|
|
67
|
+
(0, vitest_1.expect)(r.irMtimeMs).toBeNull();
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
(0, vitest_1.it)("源码比 ir.json 新 → source-newer(本次修复的核心:原先会被静默沿用)", () => {
|
|
74
|
+
const dir = tmpdir("newer");
|
|
75
|
+
try {
|
|
76
|
+
const irFile = path.join(dir, "ir.json");
|
|
77
|
+
const srcFile = path.join(dir, "app.ts");
|
|
78
|
+
touch(irFile, "[]", 1700000000000);
|
|
79
|
+
touch(srcFile, "export const x = 1;", 1700000100000); // 晚 100s
|
|
80
|
+
const r = (0, ir_staleness_1.inspectIrFreshness)(dir);
|
|
81
|
+
(0, vitest_1.expect)(r.exists).toBe(true);
|
|
82
|
+
(0, vitest_1.expect)(r.stale).toBe(true);
|
|
83
|
+
(0, vitest_1.expect)(r.reason).toBe("source-newer");
|
|
84
|
+
(0, vitest_1.expect)(r.newestSourcePath).toBe(srcFile);
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
(0, vitest_1.it)("ir.json 比源码新 → fresh(不重提,避免分钟级全量重提)", () => {
|
|
91
|
+
const dir = tmpdir("fresh");
|
|
92
|
+
try {
|
|
93
|
+
touch(path.join(dir, "app.ts"), "export const x = 1;", 1700000000000);
|
|
94
|
+
touch(path.join(dir, "ir.json"), "[]", 1700000100000);
|
|
95
|
+
const r = (0, ir_staleness_1.inspectIrFreshness)(dir);
|
|
96
|
+
(0, vitest_1.expect)(r.stale).toBe(false);
|
|
97
|
+
(0, vitest_1.expect)(r.reason).toBe("fresh");
|
|
98
|
+
(0, vitest_1.expect)(r.scannedFiles).toBe(1);
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
(0, vitest_1.it)("同刻写出(源码→ir.json 的常见顺序)→ fresh(不得推入全量重提)", () => {
|
|
105
|
+
const dir = tmpdir("same");
|
|
106
|
+
try {
|
|
107
|
+
touch(path.join(dir, "app.ts"), "export const x = 1;", 1700000000000);
|
|
108
|
+
touch(path.join(dir, "ir.json"), "[]", 1700000000000);
|
|
109
|
+
const r = (0, ir_staleness_1.inspectIrFreshness)(dir);
|
|
110
|
+
(0, vitest_1.expect)(r.stale).toBe(false);
|
|
111
|
+
(0, vitest_1.expect)(r.reason).toBe("fresh");
|
|
112
|
+
}
|
|
113
|
+
finally {
|
|
114
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
(0, vitest_1.it)("子目录源码也算源码(只看根目录下的话会漏判)", () => {
|
|
118
|
+
const dir = tmpdir("nested");
|
|
119
|
+
try {
|
|
120
|
+
touch(path.join(dir, "ir.json"), "[]", 1700000000000);
|
|
121
|
+
const nested = path.join(dir, "src", "deep");
|
|
122
|
+
fs.mkdirSync(nested, { recursive: true });
|
|
123
|
+
touch(path.join(nested, "handler.ts"), "export function handle() {}", 1700000500000);
|
|
124
|
+
const r = (0, ir_staleness_1.inspectIrFreshness)(dir);
|
|
125
|
+
(0, vitest_1.expect)(r.stale).toBe(true);
|
|
126
|
+
(0, vitest_1.expect)(r.newestSourcePath).toContain(path.join("src", "deep", "handler.ts"));
|
|
127
|
+
}
|
|
128
|
+
finally {
|
|
129
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
(0, vitest_1.it)("node_modules 等非源码目录被跳过(成本约束:不得遍历依赖树)", () => {
|
|
133
|
+
const dir = tmpdir("skip");
|
|
134
|
+
try {
|
|
135
|
+
touch(path.join(dir, "ir.json"), "[]", 1700000500000);
|
|
136
|
+
const nm = path.join(dir, "node_modules", "pkg");
|
|
137
|
+
fs.mkdirSync(nm, { recursive: true });
|
|
138
|
+
touch(path.join(nm, "index.js"), "module.exports = 1;", 1700000900000);
|
|
139
|
+
const r = (0, ir_staleness_1.inspectIrFreshness)(dir);
|
|
140
|
+
(0, vitest_1.expect)(r.stale).toBe(false);
|
|
141
|
+
(0, vitest_1.expect)(r.reason).toBe("no-sources");
|
|
142
|
+
(0, vitest_1.expect)(r.scannedFiles).toBe(0);
|
|
143
|
+
}
|
|
144
|
+
finally {
|
|
145
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
(0, vitest_1.it)("非源码后缀(png/md/json)不影响判定", () => {
|
|
149
|
+
const dir = tmpdir("ext");
|
|
150
|
+
try {
|
|
151
|
+
touch(path.join(dir, "ir.json"), "[]", 1700000500000);
|
|
152
|
+
touch(path.join(dir, "README.md"), "hello", 1700000900000);
|
|
153
|
+
touch(path.join(dir, "logo.png"), "x", 1700000900000);
|
|
154
|
+
const r = (0, ir_staleness_1.inspectIrFreshness)(dir);
|
|
155
|
+
(0, vitest_1.expect)(r.stale).toBe(false);
|
|
156
|
+
(0, vitest_1.expect)(r.reason).toBe("no-sources");
|
|
157
|
+
}
|
|
158
|
+
finally {
|
|
159
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
(0, vitest_1.it)("源码规模超预算 → exceedsAutoBudget(auto 模式只警告,不自动全量重提)", () => {
|
|
163
|
+
const dir = tmpdir("budget");
|
|
164
|
+
try {
|
|
165
|
+
touch(path.join(dir, "ir.json"), "[]", 1700000000000);
|
|
166
|
+
for (let i = 0; i < 5; i++) {
|
|
167
|
+
touch(path.join(dir, `f${i}.ts`), "export const x = 1;", 1700000100000);
|
|
168
|
+
}
|
|
169
|
+
const r = (0, ir_staleness_1.inspectIrFreshness)(dir, undefined, { autoBudget: 3 });
|
|
170
|
+
(0, vitest_1.expect)(r.stale).toBe(true); // 陈旧判定不受预算影响
|
|
171
|
+
(0, vitest_1.expect)(r.exceedsAutoBudget).toBe(true);
|
|
172
|
+
(0, vitest_1.expect)(r.scannedFiles).toBe(5);
|
|
173
|
+
}
|
|
174
|
+
finally {
|
|
175
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
(0, vitest_1.it)("遍历被上限截断时标记 evidenceComplete=false(超大仓库不得静默全量重提)", () => {
|
|
179
|
+
const dir = tmpdir("trunc");
|
|
180
|
+
try {
|
|
181
|
+
touch(path.join(dir, "ir.json"), "[]", 1700000000000);
|
|
182
|
+
for (let i = 0; i < 5; i++) {
|
|
183
|
+
touch(path.join(dir, `f${i}.ts`), "export const x = 1;", 1700000100000);
|
|
184
|
+
}
|
|
185
|
+
// maxStats=2:扫到 2 个就停 —— stale 仍为真(证据已经够),但 fresh 不可信
|
|
186
|
+
const r = (0, ir_staleness_1.inspectIrFreshness)(dir, undefined, { maxStats: 2 });
|
|
187
|
+
(0, vitest_1.expect)(r.stale).toBe(true);
|
|
188
|
+
(0, vitest_1.expect)(r.truncated).toBe(true);
|
|
189
|
+
(0, vitest_1.expect)(r.evidenceComplete).toBe(false);
|
|
190
|
+
}
|
|
191
|
+
finally {
|
|
192
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
(0, vitest_1.it)("正常仓库(未截断)fresh 判定证据完整", () => {
|
|
196
|
+
const dir = tmpdir("complete");
|
|
197
|
+
try {
|
|
198
|
+
touch(path.join(dir, "app.ts"), "export const x = 1;", 1700000000000);
|
|
199
|
+
touch(path.join(dir, "ir.json"), "[]", 1700000100000);
|
|
200
|
+
const r = (0, ir_staleness_1.inspectIrFreshness)(dir);
|
|
201
|
+
(0, vitest_1.expect)(r.evidenceComplete).toBe(true);
|
|
202
|
+
(0, vitest_1.expect)(r.reason).toBe("fresh");
|
|
203
|
+
}
|
|
204
|
+
finally {
|
|
205
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
(0, vitest_1.it)("逃逸阀:默认 auto", () => {
|
|
209
|
+
const prev = process.env.PROGMUNE_IR_REEXTRACT;
|
|
210
|
+
delete process.env.PROGMUNE_IR_REEXTRACT;
|
|
211
|
+
(0, vitest_1.expect)((0, ir_staleness_1.reextractMode)()).toBe("auto");
|
|
212
|
+
(0, vitest_1.expect)((0, ir_staleness_1.reextractEnabled)()).toBe(true);
|
|
213
|
+
if (prev !== undefined)
|
|
214
|
+
process.env.PROGMUNE_IR_REEXTRACT = prev;
|
|
215
|
+
});
|
|
216
|
+
(0, vitest_1.it)("逃生阀:always / never 语义", () => {
|
|
217
|
+
const prev = process.env.PROGMUNE_IR_REEXTRACT;
|
|
218
|
+
process.env.PROGMUNE_IR_REEXTRACT = "always";
|
|
219
|
+
(0, vitest_1.expect)((0, ir_staleness_1.reextractMode)()).toBe("always");
|
|
220
|
+
process.env.PROGMUNE_IR_REEXTRACT = "never";
|
|
221
|
+
(0, vitest_1.expect)((0, ir_staleness_1.reextractMode)()).toBe("never");
|
|
222
|
+
(0, vitest_1.expect)((0, ir_staleness_1.reextractEnabled)()).toBe(false);
|
|
223
|
+
if (prev === undefined)
|
|
224
|
+
delete process.env.PROGMUNE_IR_REEXTRACT;
|
|
225
|
+
else
|
|
226
|
+
process.env.PROGMUNE_IR_REEXTRACT = prev;
|
|
227
|
+
});
|
|
228
|
+
});
|
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.32";
|
|
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
|
@@ -62,6 +62,7 @@ const api_semantic_mapper_1 = require("./api-semantic-mapper");
|
|
|
62
62
|
const protocol_domain_validator_1 = require("./protocol-domain-validator");
|
|
63
63
|
const annotation_suggest_1 = require("../annotation-suggest");
|
|
64
64
|
const call_graph_propagator_1 = require("./call-graph-propagator");
|
|
65
|
+
const ir_staleness_1 = require("../ir-staleness");
|
|
65
66
|
const fastapi_detector_1 = require("../frameworks/fastapi-detector");
|
|
66
67
|
const django_detector_1 = require("../frameworks/django-detector");
|
|
67
68
|
const flask_detector_1 = require("../frameworks/flask-detector");
|
|
@@ -87,7 +88,10 @@ const extract_ir_java_1 = require("../extract-ir-java");
|
|
|
87
88
|
* C → extractIRC 合并形态 { typeMap, functions })——注解合并(P4.5)依赖
|
|
88
89
|
* ir.json,调用方【无需】手动 extractProjectIR/写盘(曾是对 C 注解静默
|
|
89
90
|
* 失效的文档/API 陷阱,回归测试见 tests/trust/engine.test.ts「DSH 陷阱」)。
|
|
90
|
-
* 若项目已有 ir.json
|
|
91
|
+
* 若项目已有 ir.json:原语义是「以现有文件为准(不覆盖)」——该语义会让
|
|
92
|
+
* 【源码已改、ir.json 未更新】的场景静默读旧 IR(2026-09-19 修正):
|
|
93
|
+
* mtime 比对发现有源码新于 ir.json 时重提;否则沿用磁盘文件(不做无谓的全量重提)。
|
|
94
|
+
* 逃生阀 PROGMUNE_IR_REEXTRACT=never 可回到旧语义(会打「IR 可能陈旧」警告)。
|
|
91
95
|
*/
|
|
92
96
|
async function evaluateTrust(ctx) {
|
|
93
97
|
const engineVersion = "trust-runtime-v1.0.0";
|
|
@@ -1228,7 +1232,29 @@ async function collectProtocolViolations(ctx, callGraph) {
|
|
|
1228
1232
|
if (extractFn) {
|
|
1229
1233
|
try {
|
|
1230
1234
|
const fs = require("fs");
|
|
1231
|
-
|
|
1235
|
+
// 陈旧性判定(2026-09-19):原来只看 existsSync——源码改了、ir.json 没更新时
|
|
1236
|
+
// 引擎静默吃旧 IR(fr-005 首扫报 0、污点试点「标记注入但 trust 报 0」同源)。
|
|
1237
|
+
// 改为 mtime 比对:仅当有源码比 ir.json 新时才重提,避免全量重提的分钟级成本。
|
|
1238
|
+
const freshness = (0, ir_staleness_1.inspectIrFreshness)(ctx.projectPath);
|
|
1239
|
+
const mode = (0, ir_staleness_1.reextractMode)();
|
|
1240
|
+
// 陈旧的判定由 mtime 给出;是否【自动】重提还要过成本闸:
|
|
1241
|
+
// ① evidenceComplete 为假 → 遍历被截断,证据不可信;
|
|
1242
|
+
// ② exceedsAutoBudget → 源码规模超预算(万级源文件的本仓库实测把
|
|
1243
|
+
// 单次 evaluateTrust 推过 30s 超时),重提成本不该由默认路径承担。
|
|
1244
|
+
// 两种情况都改为显式警告;语料复测等场景用 PROGMUNE_IR_REEXTRACT=always 承担。
|
|
1245
|
+
const shouldExtract = !freshness.exists ||
|
|
1246
|
+
(freshness.stale &&
|
|
1247
|
+
(mode === "always" ||
|
|
1248
|
+
(mode === "auto" &&
|
|
1249
|
+
freshness.evidenceComplete &&
|
|
1250
|
+
!freshness.exceedsAutoBudget)));
|
|
1251
|
+
if (freshness.exists && freshness.stale && !shouldExtract) {
|
|
1252
|
+
console.warn(`[progmune] ⚠️ IR 可能陈旧:${freshness.newestSourcePath} 新于 ir.json` +
|
|
1253
|
+
`(mode=${mode}${freshness.exceedsAutoBudget ? ",源码超出重提预算" : ""}` +
|
|
1254
|
+
`${freshness.truncated ? ",遍历被截断" : ""};` +
|
|
1255
|
+
`需重提取运行 PROGMUNE_IR_REEXTRACT=always 或删除 ir.json)`);
|
|
1256
|
+
}
|
|
1257
|
+
if (shouldExtract) {
|
|
1232
1258
|
const ir = extractFn();
|
|
1233
1259
|
// C 走合并形态(与 execute/MCP 写盘一致);TS/Python 保持裸数组形态
|
|
1234
1260
|
const payload = lang === "c" ? { typeMap: {}, functions: ir } : ir;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "progmune-runtime",
|
|
3
|
-
"version": "3.7.
|
|
3
|
+
"version": "3.7.32",
|
|
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/",
|