progmune-runtime 3.7.12 → 3.7.14
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 +25 -0
- package/README.md +4 -3
- package/README.zh-CN.md +4 -3
- package/dist/extract-ir-go.js +419 -0
- package/dist/extract-ir-go.test.js +196 -0
- package/dist/extract-project-ir.js +6 -0
- package/dist/frameworks/fiber-detector.js +125 -0
- package/dist/frameworks/fiber-detector.test.js +57 -0
- package/dist/frameworks/gin-detector.js +133 -0
- package/dist/frameworks/gin-detector.test.js +64 -0
- package/dist/frameworks/index.js +7 -1
- package/dist/sdk.js +1 -1
- package/dist/trust/engine.js +90 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [3.7.14] — 2026-09-02
|
|
4
|
+
|
|
5
|
+
### Go 提取器修复 + 恢复率裁决 + 框架诚实分层 + 三语料实验
|
|
6
|
+
|
|
7
|
+
- **提取器两处修复**:①无函数体声明行(`//go:linkname` 类)吞并下一个函数(Getuid/RoundTrip 漏检根因——与 C 3.7.4「# 行吞函数」同族);②接收者方法 receiver 括号组误当参数组(returnType 取到整签名)——回归测试锁定
|
|
8
|
+
- **恢复率裁决**(评审第 3 条落地):tools/go-fn-list.go(go/parser 金标,同一文件集+只计有体函数)+ scan-go-recovery.ts——stdlib 四包 **2434/2434 = 100.0%** → **维持纯词法,go/parser 桥推迟**
|
|
9
|
+
- **命名鸿沟测量**(孵化器理论核心数据):词段可桥接 0-1.9%——Go stdlib 惯用命名无法桥接内置规则词汇(与 C 命名鸿沟同构)
|
|
10
|
+
- **框架诚实分层**(评审第 5 条落地):「12/13 结构级」→「**12 专用检测器 = 4 结构级(AST:NestJS/FastAPI/Django/Flask)+ 8 启发式(代码串:Express/tRPC/Fastify/Next.js/Koa/Hapi/Gin/Fiber)**」;转正门槛 = 真实项目 FP 数据点;CLAUDE.md 矛盾行修复;落地页/矩阵/README 双语同步
|
|
11
|
+
- **Go 三语料实验**(REALWORLD_GO_V1.md,评审第 4 条落地):stdlib 4 FP 全 FP(readColonFile 词段桥接跨窗口类);govwa(vendored benchmarks/go-apps/)B 路径 0 flags;**A 路径 demo-real-go-govwa**:真实代码 3 注解(loginAction=verify/SetSession=establish/AuthCheck=guard)→ 0 FP + 植入违规精确定位 + 3 注解/协议——**Go 定位维持注解驱动(Beta)**
|
|
12
|
+
- **TS 795 完整复跑**(评审第 6 条落地):recall 98.5%(有效 100%)/ precision 100% / FP 0——12 个框架探测器全上线后与历史基线一致
|
|
13
|
+
- 验证:35 定向测试;Python 盲测 v1.2 64 零漂移;check 免疫正常
|
|
14
|
+
|
|
15
|
+
## [3.7.13] — 2026-09-02
|
|
16
|
+
|
|
17
|
+
### Go 语言支持 + Gin/Fiber 框架适配——12/13
|
|
18
|
+
|
|
19
|
+
- **Go IR 提取器**(`src/extract-ir-go.ts`,纯 TS 词法——与 C 提取器同哲学:零外部工具链,npm 安装态可用,不重蹈 Python 桥的安装态覆辙):func 签名(多行/接收者方法)、调用提取(obj.Method() 取 Method)、注释注解 `// @progmune(...)` + 文档标签、exported=首字母大写、vendor/testdata/*_test.go 表面过滤、Go 关键字排除、反引号 raw string 掩码;8 个单测
|
|
20
|
+
- **注册表接入**:LANGUAGE_EXTRACTORS 第 4 语言(detect .go → extractIRGo)→ extractProjectIR/evaluateTrust 全链路自动生效;**引擎 autoExtractor 补 go**(修复 Go 项目静默走正则回退的同类陷阱——首轮金标全错暴露)
|
|
21
|
+
- **Go 协议盲测 v1**(`scan-protocol-golang.ts`,自包含生成+扫描):3 干净(直连/方法/helper)× 3 植入违规(missing_auth/read_without_open/leak)——**TP 5 / FP 0 / FN 0 → P=100% / R=100%**
|
|
22
|
+
- **Gin(第 11 个)**:`gin-detector.ts`——GIN_ROUTE_NO_AUTH(r.POST 中间件链 + Use/Group 组级认证);Go 方法名大写(r.POST)正则 i 标志修复;"middleware" 词误撞修复(loggerMiddleware 类工具中间件≠认证)
|
|
23
|
+
- **Fiber(第 12 个)**:`fiber-detector.ts`——FIBER_ROUTE_NO_AUTH(app.Post 中间件链 + Use);同款修复
|
|
24
|
+
- **引擎**:collectGinViolations/collectFiberViolations(Go 目录结构特殊:cmd/internal/pkg + 根目录 main.go)+ ginCoverage/fiberCoverage
|
|
25
|
+
- **文档**:Go 语言行 ❌→✅ 注解驱动(Beta)(README 双语/矩阵双语/CLAUDE.md/落地页 + i18n);框架 **12/13** 口径(剩余 Spring Boot 需 Java 支持先行)
|
|
26
|
+
- **验证**:23 个新单测(Go 提取器 8 + Gin 8 + Fiber 7);全套件 **204/204**;Python 盲测 v1.2 64 零漂移;C 演示不变;check 免疫正常
|
|
27
|
+
|
|
3
28
|
## [3.7.12] — 2026-09-01
|
|
4
29
|
|
|
5
30
|
### Koa + Hapi 框架结构适配——10/13
|
package/README.md
CHANGED
|
@@ -104,11 +104,12 @@ Progmune is honest about what it can and cannot verify.
|
|
|
104
104
|
| **TypeScript / JavaScript** | ✅ Production | Blind benchmark: **recall 98.5% / precision 100%** (795 gold findings, 100 projects) |
|
|
105
105
|
| **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 |
|
|
106
106
|
| **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). |
|
|
107
|
-
| **Go
|
|
107
|
+
| **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) |
|
|
108
|
+
| **Java** | ❌ None | Planned |
|
|
108
109
|
|
|
109
110
|
**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.
|
|
110
111
|
|
|
111
|
-
**Framework adapters:
|
|
112
|
+
**Framework adapters: 12 dedicated detectors (4 structural + 8 heuristic).** 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) have dedicated detectors. Next.js version-aware governance also exists. Spring Boot/Go frameworks and a few TS frameworks remain — framework adaptation is the #1 product gap.
|
|
112
113
|
|
|
113
114
|
### What Progmune does NOT cover (honest boundaries)
|
|
114
115
|
|
|
@@ -234,7 +235,7 @@ High-impact contribution areas:
|
|
|
234
235
|
- **Runtime Pipeline:** Detect → Explain → Repair → Validate (L1–L4)
|
|
235
236
|
- **Trust Engine:** 4-dimension scoring with binary explainability gate
|
|
236
237
|
- **MCP Tools:** 19 — `progmune_trust_check`, `progmune_score`, `progmune_policy_check`, `progmune_certify`, and more
|
|
237
|
-
- **Framework Adapters:**
|
|
238
|
+
- **Framework Adapters:** 12 dedicated detectors — 4 structural (NestJS/FastAPI/Django/Flask) + 8 heuristic (Express/tRPC/Fastify/Next.js/Koa/Hapi/Gin/Fiber)
|
|
238
239
|
- **Knowledge Base:** 31 domains, 148 protocol rules, 22 detectors, 26 safeguards, PLSB 13/13 categories — plus 15 source-level detection rules (Python)
|
|
239
240
|
- **Corpus:** 2,500+ trajectories across 6+ repositories; blind benchmarks 100 (TS) + 90 (Python) projects; real-world validation on 4 application repos
|
|
240
241
|
- **Current focus:** Enterprise PoC validation + remaining framework-internal boundary FPs
|
package/README.zh-CN.md
CHANGED
|
@@ -104,11 +104,12 @@ Progmune 对能验证什么、不能验证什么保持诚实。
|
|
|
104
104
|
| **TypeScript / JavaScript** | ✅ 生产 | 盲测基准:**召回 98.5% / 精确率 100%**(795 条 gold finding,100 个项目) |
|
|
105
105
|
| **Python** | ✅ 生产 | 盲测基准:**召回 100% / 精确率 100%**(729 条 gold finding,90 个项目);真实应用验证:PyGoat(OWASP 故意脆弱 Django 应用)**67 TP / 0 FP,标记精确率 100%**;三个良构应用(django/fastapi realworld、django-unicorn)0 误报真阳性 |
|
|
106
106
|
| **C** | ✅ 注解驱动(Beta) | IR 提取接入注册表 + SSG 状态机;**每协议标注 ~2-3 个原语即获得可信验证**(真实模块金标 5/5:redis ACL / libssh 客户端 / libssh 服务端 / libssh 回调分发 / uftpd 传送授权——全部 0 误报 + 违规精确定位;应用级金标 v2:**P=91.7% / R=100% / F1=95.7%**)。未注解自动检测不在范围(真实语料 0 TP——定位决议见 C 语言状态文档);TLS 级覆盖仍无。见 [C 语言状态](https://github.com/shenlian19831109/progmune-runtime/blob/main/docs/c-language-status.md)。 |
|
|
107
|
-
| **Go
|
|
107
|
+
| **Go** | ✅ 注解驱动(Beta) | IR 提取接入注册表 + SSG 状态机;合成金标 v1:**P=100% / R=100%**(3 干净 × 3 植入违规);零外部工具链(纯 TS 词法提取,npm 安装态可用) |
|
|
108
|
+
| **Java** | ❌ 无 | 规划中 |
|
|
108
109
|
|
|
109
110
|
**多语言 IR(注册表式)。** TypeScript(ts-morph)与 Python(AST)提取器是同一注册表(`src/extract-project-ir.ts`)中的注册项;`extractProjectIR` 把检测到的所有语言合并为一份函数 IR,由 agent loop、`execute()` 与 MCP server 共享——agent 可在两种语言上编排函数协议链。新增语言(Go、Java……)= 注册一条提取器,调用方零改动。
|
|
110
111
|
|
|
111
|
-
**框架适配器:
|
|
112
|
+
**框架适配器:12 专用检测器(4 结构级 + 8 启发式)。** Express ✅、tRPC ✅、FastAPI ✅、Django ✅(结构级路由认证分析——逐条检查写操作入口的认证)、Flask ✅(路由/before_request 认证守卫分析)、Fastify ✅(路由 preHandler/钩子认证分析)与 Next.js ✅(App Router 路由处理器认证分析)、NestJS ✅(装饰器路由解析 + 全局 APP_GUARD 守卫识别 + @Public 豁免)、Koa ✅、Hapi ✅(路由配置认证策略分析)、Gin ✅ 与 Fiber ✅(Go 路由/认证中间件分析)有专用检测器;Next.js 另有版本感知治理。结构级 = AST 解析(NestJS/FastAPI/Django/Flask——合成金标 P=R=100%);启发式 = 代码串模式(Express/tRPC/Fastify/Next.js/Koa/Hapi/Gin/Fiber——单测覆盖,真实项目 FP 数据待补)。Spring Boot 待适配——框架适配是 #1 产品缺口。
|
|
112
113
|
|
|
113
114
|
### Progmune 不覆盖什么(诚实边界)
|
|
114
115
|
|
|
@@ -233,7 +234,7 @@ Progmune 建立在"**LLM 输出是统计表演而非推理**"这一前提上—
|
|
|
233
234
|
- **运行时管线:** 检测 → 解释 → 修复 → 验证(L1–L4)
|
|
234
235
|
- **信任引擎:** 四维评分 + 二元可解释性门
|
|
235
236
|
- **MCP 工具:** 19 个——`progmune_trust_check`、`progmune_score`、`progmune_policy_check`、`progmune_certify` 等
|
|
236
|
-
- **框架适配器:**
|
|
237
|
+
- **框架适配器:** 12 专用检测器——4 结构级(NestJS/FastAPI/Django/Flask)+ 8 启发式(Express/tRPC/Fastify/Next.js/Koa/Hapi/Gin/Fiber)
|
|
237
238
|
- **知识库:** 31 个域、148 条协议规则、22 个检测器、26 条防护规则、PLSB 13/13 类别——另加 15 条源码级检测规则(Python)
|
|
238
239
|
- **语料:** 6+ 仓库 2,500+ 轨迹;盲测基准 100(TS)+ 90(Python)个项目;4 个应用仓库真实验证
|
|
239
240
|
- **当前重点:** 企业 PoC 验证 + 剩余框架内部边界误报
|
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Go IR Extractor — pure-TS function extraction for the merged multi-language
|
|
4
|
+
* registry (extract-project-ir.ts LANGUAGE_EXTRACTORS)。无子进程、无 Go
|
|
5
|
+
* 工具链依赖(npm 安装态可用——与 C 提取器同哲学,区别于 Python 桥)。
|
|
6
|
+
*
|
|
7
|
+
* Route: the registry "go" entry calls extractIRGo; the result merges into
|
|
8
|
+
* the same FunctionInfo list as TypeScript/Python/C and flows into
|
|
9
|
+
* call-sequence (P4.6), SSG validation and the agent loop with no rewiring.
|
|
10
|
+
*
|
|
11
|
+
* Extraction is a best-effort lexical parse of Go definitions:
|
|
12
|
+
* - func Name(params) returns { ... } 普通函数
|
|
13
|
+
* - func (r *Repo) Name(params) returns { } 接收者方法(方法名=Name)
|
|
14
|
+
* - 多行签名(平衡括号)、返回值类型 best-effort 记录
|
|
15
|
+
* - 直接调用提取(obj.Method() 取 Method——与 C 成员调用同规则)
|
|
16
|
+
* - 注释注解:// @progmune(namespace=..., pre=[...], post=[...])
|
|
17
|
+
* + @purpose/@tags/@requires/@produces/@useWhen/@inputs/@outputs 文档标签
|
|
18
|
+
* - exported = 首字母大写(Go 语言约定)
|
|
19
|
+
*
|
|
20
|
+
* Known limits(如实):
|
|
21
|
+
* - 接口方法/函数值分发静态不可见(interface 与 func 变量同 L3 边界)
|
|
22
|
+
* - 泛型类型参数(func F[T any](...))best-effort 剥除,不做完整解析
|
|
23
|
+
* - 无数据流/指针分析——与 C 的 L3/L4 结论同源
|
|
24
|
+
*/
|
|
25
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
26
|
+
if (k2 === undefined) k2 = k;
|
|
27
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
28
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
29
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
30
|
+
}
|
|
31
|
+
Object.defineProperty(o, k2, desc);
|
|
32
|
+
}) : (function(o, m, k, k2) {
|
|
33
|
+
if (k2 === undefined) k2 = k;
|
|
34
|
+
o[k2] = m[k];
|
|
35
|
+
}));
|
|
36
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
37
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
38
|
+
}) : function(o, v) {
|
|
39
|
+
o["default"] = v;
|
|
40
|
+
});
|
|
41
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
42
|
+
var ownKeys = function(o) {
|
|
43
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
44
|
+
var ar = [];
|
|
45
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
46
|
+
return ar;
|
|
47
|
+
};
|
|
48
|
+
return ownKeys(o);
|
|
49
|
+
};
|
|
50
|
+
return function (mod) {
|
|
51
|
+
if (mod && mod.__esModule) return mod;
|
|
52
|
+
var result = {};
|
|
53
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
54
|
+
__setModuleDefault(result, mod);
|
|
55
|
+
return result;
|
|
56
|
+
};
|
|
57
|
+
})();
|
|
58
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
59
|
+
exports.extractIRGo = extractIRGo;
|
|
60
|
+
const fs = __importStar(require("fs"));
|
|
61
|
+
const path = __importStar(require("path"));
|
|
62
|
+
// ── Constants ──
|
|
63
|
+
const GO_EXTENSIONS = new Set([".go"]);
|
|
64
|
+
/** Walk skip set(生产表面守卫,与 C/Python 提取器口径一致)。 */
|
|
65
|
+
const SKIP_DIRS = new Set([
|
|
66
|
+
"vendor", "testdata", "test", "tests", "examples", "example", "docs",
|
|
67
|
+
"doc", "third_party", "node_modules", ".git", "scripts", "tools",
|
|
68
|
+
]);
|
|
69
|
+
/** Go 关键字/内置(调用提取时排除)。 */
|
|
70
|
+
const GO_KEYWORDS = new Set([
|
|
71
|
+
"break", "case", "chan", "const", "continue", "default", "defer", "else",
|
|
72
|
+
"fallthrough", "for", "func", "go", "goto", "if", "import", "interface",
|
|
73
|
+
"map", "package", "range", "return", "select", "struct", "switch", "type",
|
|
74
|
+
"var", "append", "cap", "close", "complex", "copy", "delete", "imag",
|
|
75
|
+
"len", "make", "new", "panic", "print", "println", "real", "recover",
|
|
76
|
+
]);
|
|
77
|
+
function isTestFilename(name) {
|
|
78
|
+
return name.endsWith("_test.go");
|
|
79
|
+
}
|
|
80
|
+
function maskLine(line, state) {
|
|
81
|
+
let out = "";
|
|
82
|
+
for (let i = 0; i < line.length; i++) {
|
|
83
|
+
const ch = line[i];
|
|
84
|
+
const next = line[i + 1];
|
|
85
|
+
if (state.inBlock) {
|
|
86
|
+
if (ch === "*" && next === "/") {
|
|
87
|
+
state.inBlock = false;
|
|
88
|
+
i++;
|
|
89
|
+
}
|
|
90
|
+
out += " ";
|
|
91
|
+
}
|
|
92
|
+
else if (state.inString) {
|
|
93
|
+
if (ch === "\\") {
|
|
94
|
+
out += " ";
|
|
95
|
+
i++;
|
|
96
|
+
}
|
|
97
|
+
else if (ch === '"') {
|
|
98
|
+
state.inString = false;
|
|
99
|
+
out += " ";
|
|
100
|
+
}
|
|
101
|
+
else
|
|
102
|
+
out += " ";
|
|
103
|
+
}
|
|
104
|
+
else if (state.inRaw) {
|
|
105
|
+
if (ch === "`") {
|
|
106
|
+
state.inRaw = false;
|
|
107
|
+
out += " ";
|
|
108
|
+
}
|
|
109
|
+
else
|
|
110
|
+
out += " ";
|
|
111
|
+
}
|
|
112
|
+
else if (state.inRune) {
|
|
113
|
+
if (ch === "\\") {
|
|
114
|
+
out += " ";
|
|
115
|
+
i++;
|
|
116
|
+
}
|
|
117
|
+
else if (ch === "'") {
|
|
118
|
+
state.inRune = false;
|
|
119
|
+
out += " ";
|
|
120
|
+
}
|
|
121
|
+
else
|
|
122
|
+
out += " ";
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
if (ch === "/" && next === "*") {
|
|
126
|
+
state.inBlock = true;
|
|
127
|
+
i++;
|
|
128
|
+
out += " ";
|
|
129
|
+
}
|
|
130
|
+
else if (ch === "/" && next === "/") {
|
|
131
|
+
out += " ";
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
else if (ch === '"') {
|
|
135
|
+
state.inString = true;
|
|
136
|
+
out += " ";
|
|
137
|
+
}
|
|
138
|
+
else if (ch === "`") {
|
|
139
|
+
state.inRaw = true;
|
|
140
|
+
out += " ";
|
|
141
|
+
}
|
|
142
|
+
else if (ch === "'") {
|
|
143
|
+
state.inRune = true;
|
|
144
|
+
out += " ";
|
|
145
|
+
}
|
|
146
|
+
else
|
|
147
|
+
out += ch;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return out;
|
|
151
|
+
}
|
|
152
|
+
function parseGoAnnotation(text) {
|
|
153
|
+
const m = text.match(/@progmune\s*\(\s*([^)]*)\)/);
|
|
154
|
+
if (!m)
|
|
155
|
+
return null;
|
|
156
|
+
const body = m[1];
|
|
157
|
+
const ann = { pre_states: [], post_states: [] };
|
|
158
|
+
const ns = body.match(/namespace\s*=\s*"([^"]+)"/);
|
|
159
|
+
if (ns)
|
|
160
|
+
ann.namespace = ns[1];
|
|
161
|
+
const states = (key) => {
|
|
162
|
+
const mm = body.match(new RegExp(`${key}\\s*=\\s*\\[([^\\]]*)\\]`));
|
|
163
|
+
if (!mm)
|
|
164
|
+
return [];
|
|
165
|
+
const vals = mm[1].match(/"([^"]+)"/g) || [];
|
|
166
|
+
return vals.map((v) => v.slice(1, -1));
|
|
167
|
+
};
|
|
168
|
+
ann.pre_states = states("pre");
|
|
169
|
+
ann.post_states = states("post");
|
|
170
|
+
const inv = states("invalidate");
|
|
171
|
+
if (inv.length > 0)
|
|
172
|
+
ann.invalidate = inv;
|
|
173
|
+
return ann;
|
|
174
|
+
}
|
|
175
|
+
function docTag(text, tag) {
|
|
176
|
+
const m = text.match(new RegExp(`@${tag}\\s+([^\\n*]+)`));
|
|
177
|
+
return m ? m[1].trim() : undefined;
|
|
178
|
+
}
|
|
179
|
+
// ── 调用提取 ──
|
|
180
|
+
function extractCallsFromBody(body) {
|
|
181
|
+
const calls = new Set();
|
|
182
|
+
const re = /([A-Za-z_][A-Za-z0-9_]*)\s*\(/g;
|
|
183
|
+
let m;
|
|
184
|
+
while ((m = re.exec(body)) !== null) {
|
|
185
|
+
const name = m[1];
|
|
186
|
+
if (GO_KEYWORDS.has(name))
|
|
187
|
+
continue;
|
|
188
|
+
if (GO_KEYWORDS.has(name))
|
|
189
|
+
continue;
|
|
190
|
+
calls.add(name);
|
|
191
|
+
}
|
|
192
|
+
return Array.from(calls);
|
|
193
|
+
}
|
|
194
|
+
// ── 顶层参数切分 ──
|
|
195
|
+
function splitTopLevelParams(s) {
|
|
196
|
+
const parts = [];
|
|
197
|
+
let depth = 0;
|
|
198
|
+
let cur = "";
|
|
199
|
+
for (const ch of s) {
|
|
200
|
+
if (ch === "(" || ch === "[" || ch === "{")
|
|
201
|
+
depth++;
|
|
202
|
+
if (ch === ")" || ch === "]" || ch === "}")
|
|
203
|
+
depth--;
|
|
204
|
+
if (ch === "," && depth === 0) {
|
|
205
|
+
parts.push(cur.trim());
|
|
206
|
+
cur = "";
|
|
207
|
+
}
|
|
208
|
+
else
|
|
209
|
+
cur += ch;
|
|
210
|
+
}
|
|
211
|
+
if (cur.trim())
|
|
212
|
+
parts.push(cur.trim());
|
|
213
|
+
return parts.filter(Boolean);
|
|
214
|
+
}
|
|
215
|
+
// ── 单文件提取 ──
|
|
216
|
+
function extractGoFile(filePath) {
|
|
217
|
+
let content;
|
|
218
|
+
try {
|
|
219
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
return [];
|
|
223
|
+
}
|
|
224
|
+
const lines = content.split("\n");
|
|
225
|
+
const functions = [];
|
|
226
|
+
for (let i = 0; i < lines.length; i++) {
|
|
227
|
+
// 函数定义行:func Name( 或 func (receiver) Name(
|
|
228
|
+
const plain = lines[i].match(/^\s*func\s+([A-Za-z_][A-Za-z0-9_]*)/);
|
|
229
|
+
const receiver = lines[i].match(/^\s*func\s*\([^)]*\)\s*([A-Za-z_][A-Za-z0-9_]*)/);
|
|
230
|
+
const name = plain ? plain[1] : receiver ? receiver[1] : null;
|
|
231
|
+
if (!name)
|
|
232
|
+
continue;
|
|
233
|
+
// 多行签名:从 func 起拼接直到找到 { 或 ;——遇到下一个 func 行即停
|
|
234
|
+
// (无函数体声明行如 `func runtime_args() []string // in package runtime`
|
|
235
|
+
// 不能吞掉紧随其后的函数定义——proc.go::Getuid 漏检根因)
|
|
236
|
+
let sigText = lines[i];
|
|
237
|
+
let j = i;
|
|
238
|
+
while (!sigText.includes("{") && !sigText.includes(";") && j < lines.length - 1) {
|
|
239
|
+
j++;
|
|
240
|
+
if (/^\s*func\b/.test(lines[j]) && j > i)
|
|
241
|
+
break;
|
|
242
|
+
sigText += "\n" + lines[j];
|
|
243
|
+
}
|
|
244
|
+
const braceIdx = sigText.indexOf("{");
|
|
245
|
+
if (braceIdx < 0)
|
|
246
|
+
continue; // 接口方法声明(无函数体)
|
|
247
|
+
const signature = sigText.slice(0, braceIdx);
|
|
248
|
+
// 注释块注解:函数定义行上方的连续注释(// 与 /* */)
|
|
249
|
+
const commentLines = [];
|
|
250
|
+
for (let k = i - 1; k >= 0; k--) {
|
|
251
|
+
const t = lines[k].trim();
|
|
252
|
+
if (t.startsWith("//") || t.startsWith("/*") || t.startsWith("*")) {
|
|
253
|
+
commentLines.unshift(t);
|
|
254
|
+
}
|
|
255
|
+
else if (t === "") {
|
|
256
|
+
if (commentLines.length === 0)
|
|
257
|
+
continue;
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
const commentText = commentLines.join("\n");
|
|
265
|
+
// 注解解析
|
|
266
|
+
let protocol;
|
|
267
|
+
const ann = parseGoAnnotation(commentText);
|
|
268
|
+
if (ann) {
|
|
269
|
+
protocol = {
|
|
270
|
+
pre_states: ann.pre_states,
|
|
271
|
+
post_states: ann.post_states,
|
|
272
|
+
...(ann.invalidate ? { invalidate: ann.invalidate } : {}),
|
|
273
|
+
...(ann.namespace ? { namespace: ann.namespace } : {}),
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
const purpose = docTag(commentText, "purpose");
|
|
277
|
+
const tags = docTag(commentText, "tags")?.split(/[,\s]+/).filter(Boolean);
|
|
278
|
+
const requires = docTag(commentText, "requires")?.split(/[,\s]+/).filter(Boolean);
|
|
279
|
+
const produces = docTag(commentText, "produces")?.split(/[,\s]+/).filter(Boolean);
|
|
280
|
+
const useWhen = docTag(commentText, "useWhen")?.split(/[,\s]+/).filter(Boolean);
|
|
281
|
+
// 参数列表:签名中第一个 ( ... )——接收者方法的 receiver 括号组要先跳过
|
|
282
|
+
// (bug 修复:func (s *Service) Login(...) 的 (s *Service) 曾被当作参数组,
|
|
283
|
+
// returnType 取到整个签名)
|
|
284
|
+
let sigAfterReceiver = signature;
|
|
285
|
+
if (receiver) {
|
|
286
|
+
let depth = 0;
|
|
287
|
+
for (let ci = signature.indexOf("("); ci < signature.length; ci++) {
|
|
288
|
+
if (signature[ci] === "(")
|
|
289
|
+
depth++;
|
|
290
|
+
else if (signature[ci] === ")") {
|
|
291
|
+
depth--;
|
|
292
|
+
if (depth === 0) {
|
|
293
|
+
sigAfterReceiver = signature.slice(ci + 1);
|
|
294
|
+
break;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
const openParen = sigAfterReceiver.indexOf("(");
|
|
300
|
+
let closeParen = -1;
|
|
301
|
+
if (openParen >= 0) {
|
|
302
|
+
// 平衡括号匹配(参数可含 func 类型嵌套括号)
|
|
303
|
+
let depth = 0;
|
|
304
|
+
for (let ci = openParen; ci < sigAfterReceiver.length; ci++) {
|
|
305
|
+
if (sigAfterReceiver[ci] === "(")
|
|
306
|
+
depth++;
|
|
307
|
+
else if (sigAfterReceiver[ci] === ")") {
|
|
308
|
+
depth--;
|
|
309
|
+
if (depth === 0) {
|
|
310
|
+
closeParen = ci;
|
|
311
|
+
break;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
let params = [];
|
|
317
|
+
if (openParen >= 0 && closeParen > openParen) {
|
|
318
|
+
params = splitTopLevelParams(sigAfterReceiver.slice(openParen + 1, closeParen));
|
|
319
|
+
}
|
|
320
|
+
// 返回类型:签名尾部(剥除泛型括号)
|
|
321
|
+
const returnType = sigAfterReceiver.slice(closeParen + 1).replace(/\[[^\]]*\]/g, "").trim();
|
|
322
|
+
// 函数体:从签名行(j)的 { 起逐行扫描到匹配 }(掩码后括号平衡,
|
|
323
|
+
// 增量计数——每行只数一次,避免累积体重复计数)
|
|
324
|
+
let body = "";
|
|
325
|
+
let depth = 0;
|
|
326
|
+
let started = false;
|
|
327
|
+
let endLine = j;
|
|
328
|
+
const bodyMask = { inBlock: false, inString: false, inRaw: false, inRune: false };
|
|
329
|
+
for (let li = j; li < lines.length; li++) {
|
|
330
|
+
const maskedLine = maskLine(lines[li], bodyMask);
|
|
331
|
+
if (!started) {
|
|
332
|
+
const b = maskedLine.indexOf("{");
|
|
333
|
+
if (b < 0)
|
|
334
|
+
continue; // 多行签名的后续行还没到 {
|
|
335
|
+
started = true;
|
|
336
|
+
for (const ch of maskedLine.slice(b)) {
|
|
337
|
+
if (ch === "{")
|
|
338
|
+
depth++;
|
|
339
|
+
else if (ch === "}")
|
|
340
|
+
depth--;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
for (const ch of maskedLine) {
|
|
345
|
+
if (ch === "{")
|
|
346
|
+
depth++;
|
|
347
|
+
else if (ch === "}")
|
|
348
|
+
depth--;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
body += lines[li] + "\n";
|
|
352
|
+
if (depth === 0) {
|
|
353
|
+
endLine = li;
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
if (!started || depth !== 0)
|
|
358
|
+
continue;
|
|
359
|
+
// 调用提取(掩码态重置后重扫完整函数体)
|
|
360
|
+
const resetMask = { inBlock: false, inString: false, inRaw: false, inRune: false };
|
|
361
|
+
const maskedBody = body.split("\n").map((l) => maskLine(l, resetMask)).join("\n");
|
|
362
|
+
const calls = extractCallsFromBody(maskedBody).filter((c) => c !== name);
|
|
363
|
+
functions.push({
|
|
364
|
+
name,
|
|
365
|
+
file: filePath,
|
|
366
|
+
params: params.map((p) => ({ name: p.split(/[\s\[\]]/)[0] || p, type: p })),
|
|
367
|
+
returnType: returnType || "unknown",
|
|
368
|
+
calls,
|
|
369
|
+
exported: /^[A-Z]/.test(name),
|
|
370
|
+
tags: tags && tags.length > 0 ? tags : undefined,
|
|
371
|
+
...(purpose ? { purpose } : {}),
|
|
372
|
+
...(requires && requires.length > 0 ? { requires } : {}),
|
|
373
|
+
...(produces && produces.length > 0 ? { produces } : {}),
|
|
374
|
+
...(useWhen && useWhen.length > 0 ? { useWhen } : {}),
|
|
375
|
+
...(protocol ? { protocol } : {}),
|
|
376
|
+
});
|
|
377
|
+
i = endLine; // 跳过已消费的行
|
|
378
|
+
}
|
|
379
|
+
return functions;
|
|
380
|
+
}
|
|
381
|
+
// ── 项目级入口(注册表调用) ──
|
|
382
|
+
function collectGoFiles(root) {
|
|
383
|
+
const files = [];
|
|
384
|
+
const walk = (dir) => {
|
|
385
|
+
let entries;
|
|
386
|
+
try {
|
|
387
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
388
|
+
}
|
|
389
|
+
catch {
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
for (const e of entries) {
|
|
393
|
+
if (e.isSymbolicLink())
|
|
394
|
+
continue;
|
|
395
|
+
const full = path.join(dir, e.name);
|
|
396
|
+
if (e.isDirectory()) {
|
|
397
|
+
if (SKIP_DIRS.has(e.name))
|
|
398
|
+
continue;
|
|
399
|
+
walk(full);
|
|
400
|
+
}
|
|
401
|
+
else if (GO_EXTENSIONS.has(path.extname(e.name)) && !isTestFilename(e.name)) {
|
|
402
|
+
files.push(full);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
walk(root);
|
|
407
|
+
return files;
|
|
408
|
+
}
|
|
409
|
+
function extractIRGo(projectRoot) {
|
|
410
|
+
const files = collectGoFiles(projectRoot);
|
|
411
|
+
const all = [];
|
|
412
|
+
for (const file of files) {
|
|
413
|
+
for (const fn of extractGoFile(file)) {
|
|
414
|
+
fn.file = path.relative(projectRoot, fn.file);
|
|
415
|
+
all.push(fn);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return all;
|
|
419
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
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
|
+
/**
|
|
37
|
+
* extract-ir-go.test.ts — Go IR 提取器回归(临时目录夹具)
|
|
38
|
+
*
|
|
39
|
+
* 纯 TS 词法提取(与 C 提取器同哲学):函数签名(多行/接收者方法)、
|
|
40
|
+
* 调用(obj.Method() 取 Method)、注释注解 @progmune + 文档标签、
|
|
41
|
+
* exported=首字母大写、非生产表面过滤。
|
|
42
|
+
*/
|
|
43
|
+
const vitest_1 = require("vitest");
|
|
44
|
+
const fs = __importStar(require("fs"));
|
|
45
|
+
const os = __importStar(require("os"));
|
|
46
|
+
const path = __importStar(require("path"));
|
|
47
|
+
const extract_ir_go_1 = require("./extract-ir-go");
|
|
48
|
+
let dir;
|
|
49
|
+
(0, vitest_1.beforeEach)(() => {
|
|
50
|
+
dir = fs.mkdtempSync(path.join(os.tmpdir(), "go-ir-"));
|
|
51
|
+
});
|
|
52
|
+
(0, vitest_1.afterEach)(() => {
|
|
53
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
54
|
+
});
|
|
55
|
+
function write(rel, content) {
|
|
56
|
+
const full = path.join(dir, rel);
|
|
57
|
+
fs.mkdirSync(path.dirname(full), { recursive: true });
|
|
58
|
+
fs.writeFileSync(full, content);
|
|
59
|
+
}
|
|
60
|
+
(0, vitest_1.describe)("extract-ir-go", () => {
|
|
61
|
+
(0, vitest_1.it)("普通函数 + 调用提取(含 obj.Method() 成员调用)", () => {
|
|
62
|
+
write("main.go", `
|
|
63
|
+
package main
|
|
64
|
+
|
|
65
|
+
func doWork() {
|
|
66
|
+
helper()
|
|
67
|
+
obj.Save()
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
func helper() {}
|
|
71
|
+
`);
|
|
72
|
+
const ir = (0, extract_ir_go_1.extractIRGo)(dir);
|
|
73
|
+
const doWork = ir.find((f) => f.name === "doWork");
|
|
74
|
+
(0, vitest_1.expect)(doWork?.calls).toEqual(vitest_1.expect.arrayContaining(["helper", "Save"]));
|
|
75
|
+
});
|
|
76
|
+
(0, vitest_1.it)("接收者方法(func (r *Repo) Name)", () => {
|
|
77
|
+
write("repo.go", `
|
|
78
|
+
package main
|
|
79
|
+
|
|
80
|
+
func (r *Repo) SaveUser(u User) error {
|
|
81
|
+
return nil
|
|
82
|
+
}
|
|
83
|
+
`);
|
|
84
|
+
const ir = (0, extract_ir_go_1.extractIRGo)(dir);
|
|
85
|
+
(0, vitest_1.expect)(ir.some((f) => f.name === "SaveUser")).toBe(true);
|
|
86
|
+
(0, vitest_1.expect)(ir.find((f) => f.name === "SaveUser")?.exported).toBe(true);
|
|
87
|
+
});
|
|
88
|
+
(0, vitest_1.it)("接收者方法 returnType 与参数正确(receiver 括号组不被误取——回归)", () => {
|
|
89
|
+
write("svc.go", `
|
|
90
|
+
package main
|
|
91
|
+
|
|
92
|
+
func (s *Service) Login(ctx Context, user, password string) (Token, error) {
|
|
93
|
+
return Token{}, nil
|
|
94
|
+
}
|
|
95
|
+
`);
|
|
96
|
+
const ir = (0, extract_ir_go_1.extractIRGo)(dir);
|
|
97
|
+
const fn = ir.find((f) => f.name === "Login");
|
|
98
|
+
(0, vitest_1.expect)(fn?.returnType).toBe("(Token, error)");
|
|
99
|
+
// Go 语义:ctx / user / password 是 3 个参数(user 与 password 共享类型)
|
|
100
|
+
(0, vitest_1.expect)(fn?.params).toHaveLength(3);
|
|
101
|
+
(0, vitest_1.expect)(fn?.params[0].type).toContain("ctx");
|
|
102
|
+
(0, vitest_1.expect)(fn?.params[1].type).toContain("user");
|
|
103
|
+
(0, vitest_1.expect)(fn?.params[2].type).toContain("string");
|
|
104
|
+
});
|
|
105
|
+
(0, vitest_1.it)("多行签名 + 返回类型", () => {
|
|
106
|
+
write("multi.go", `
|
|
107
|
+
package main
|
|
108
|
+
|
|
109
|
+
func Process(
|
|
110
|
+
name string,
|
|
111
|
+
count int,
|
|
112
|
+
) (string, error) {
|
|
113
|
+
return name, nil
|
|
114
|
+
}
|
|
115
|
+
`);
|
|
116
|
+
const ir = (0, extract_ir_go_1.extractIRGo)(dir);
|
|
117
|
+
const fn = ir.find((f) => f.name === "Process");
|
|
118
|
+
(0, vitest_1.expect)(fn).toBeTruthy();
|
|
119
|
+
(0, vitest_1.expect)(fn?.returnType).toContain("string");
|
|
120
|
+
});
|
|
121
|
+
(0, vitest_1.it)("注释注解 @progmune + 文档标签", () => {
|
|
122
|
+
write("auth.go", `
|
|
123
|
+
package main
|
|
124
|
+
|
|
125
|
+
// @progmune(namespace="auth", pre=["UNAUTHENTICATED"], post=["PASSWORD_VERIFIED"])
|
|
126
|
+
// @purpose 凭证比对
|
|
127
|
+
// @tags auth
|
|
128
|
+
func VerifyPassword(user, password string) bool {
|
|
129
|
+
return password == "secret"
|
|
130
|
+
}
|
|
131
|
+
`);
|
|
132
|
+
const ir = (0, extract_ir_go_1.extractIRGo)(dir);
|
|
133
|
+
const fn = ir.find((f) => f.name === "VerifyPassword");
|
|
134
|
+
(0, vitest_1.expect)(fn?.protocol?.pre_states).toEqual(["UNAUTHENTICATED"]);
|
|
135
|
+
(0, vitest_1.expect)(fn?.protocol?.post_states).toEqual(["PASSWORD_VERIFIED"]);
|
|
136
|
+
(0, vitest_1.expect)(fn?.protocol?.namespace).toBe("auth");
|
|
137
|
+
(0, vitest_1.expect)(fn?.purpose).toBe("凭证比对");
|
|
138
|
+
(0, vitest_1.expect)(fn?.tags).toContain("auth");
|
|
139
|
+
(0, vitest_1.expect)(fn?.exported).toBe(true);
|
|
140
|
+
});
|
|
141
|
+
(0, vitest_1.it)("接口方法声明(无函数体)不提取", () => {
|
|
142
|
+
write("iface.go", `
|
|
143
|
+
package main
|
|
144
|
+
|
|
145
|
+
type Store interface {
|
|
146
|
+
Save(u User) error
|
|
147
|
+
}
|
|
148
|
+
`);
|
|
149
|
+
const ir = (0, extract_ir_go_1.extractIRGo)(dir);
|
|
150
|
+
(0, vitest_1.expect)(ir.some((f) => f.name === "Save")).toBe(false);
|
|
151
|
+
});
|
|
152
|
+
(0, vitest_1.it)("字符串/注释里的伪调用不提取", () => {
|
|
153
|
+
write("str.go", `
|
|
154
|
+
package main
|
|
155
|
+
|
|
156
|
+
func tricky() {
|
|
157
|
+
s := "notAFunction() inside string"
|
|
158
|
+
// notAFunction() in comment
|
|
159
|
+
realCall()
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
func realCall() {}
|
|
163
|
+
`);
|
|
164
|
+
const ir = (0, extract_ir_go_1.extractIRGo)(dir);
|
|
165
|
+
const fn = ir.find((f) => f.name === "tricky");
|
|
166
|
+
(0, vitest_1.expect)(fn?.calls).toContain("realCall");
|
|
167
|
+
(0, vitest_1.expect)(fn?.calls).not.toContain("notAFunction");
|
|
168
|
+
});
|
|
169
|
+
(0, vitest_1.it)("非生产表面过滤:vendor/testdata/测试文件跳过", () => {
|
|
170
|
+
write("main.go", `package main\nfunc Keep() {}\n`);
|
|
171
|
+
write("vendor/dep.go", `package dep\nfunc SkipVendor() {}\n`);
|
|
172
|
+
write("testdata/sample.go", `package sample\nfunc SkipTestdata() {}\n`);
|
|
173
|
+
write("x_test.go", `package main\nfunc SkipTest() {}\n`);
|
|
174
|
+
const ir = (0, extract_ir_go_1.extractIRGo)(dir);
|
|
175
|
+
(0, vitest_1.expect)(ir.map((f) => f.name)).toEqual(["Keep"]);
|
|
176
|
+
});
|
|
177
|
+
(0, vitest_1.it)("Go 关键字不提取为调用(for/if/go/defer 等)", () => {
|
|
178
|
+
write("ctrl.go", `
|
|
179
|
+
package main
|
|
180
|
+
|
|
181
|
+
func run() {
|
|
182
|
+
for i := 0; i < 3; i++ {
|
|
183
|
+
go step(i)
|
|
184
|
+
defer cleanup()
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
func step(i int) {}
|
|
189
|
+
func cleanup() {}
|
|
190
|
+
`);
|
|
191
|
+
const ir = (0, extract_ir_go_1.extractIRGo)(dir);
|
|
192
|
+
const fn = ir.find((f) => f.name === "run");
|
|
193
|
+
(0, vitest_1.expect)(fn?.calls).toEqual(vitest_1.expect.arrayContaining(["step", "cleanup"]));
|
|
194
|
+
(0, vitest_1.expect)(fn?.calls).not.toEqual(vitest_1.expect.arrayContaining(["for", "go", "defer", "if"]));
|
|
195
|
+
});
|
|
196
|
+
});
|
|
@@ -54,6 +54,7 @@ const path = __importStar(require("path"));
|
|
|
54
54
|
const extract_ir_1 = require("./extract-ir");
|
|
55
55
|
const extract_ir_python_1 = require("./extract-ir-python");
|
|
56
56
|
const extract_ir_c_1 = require("./extract-ir-c");
|
|
57
|
+
const extract_ir_go_1 = require("./extract-ir-go");
|
|
57
58
|
const SKIP_DIRS = new Set([
|
|
58
59
|
"node_modules", "dist", "build", ".git", ".progmune_corpus",
|
|
59
60
|
"__pycache__", "venv", ".venv",
|
|
@@ -105,6 +106,11 @@ exports.LANGUAGE_EXTRACTORS = [
|
|
|
105
106
|
detect: (p) => hasSourceFiles(p, new Set([".c", ".h"])),
|
|
106
107
|
extract: (p) => (0, extract_ir_c_1.extractIRC)(p),
|
|
107
108
|
},
|
|
109
|
+
{
|
|
110
|
+
language: "go",
|
|
111
|
+
detect: (p) => hasSourceFiles(p, new Set([".go"])),
|
|
112
|
+
extract: (p) => (0, extract_ir_go_1.extractIRGo)(p),
|
|
113
|
+
},
|
|
108
114
|
];
|
|
109
115
|
/** 项目检测到的语言列表(审计/标签用)。 */
|
|
110
116
|
function detectLanguages(projectRoot) {
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Fiber Framework Adapter — Protocol Detection for Fiber (Go)
|
|
4
|
+
*
|
|
5
|
+
* 第 12 个框架适配(Go 第 2 个,代码串级——Fiber 路由与 Express 同构):
|
|
6
|
+
*
|
|
7
|
+
* app.Post("/x", authMW, handler) 路由级认证中间件链
|
|
8
|
+
* app.Use(authMW) / group.Use(authMW) 全局/组级认证中间件
|
|
9
|
+
*
|
|
10
|
+
* 规则:
|
|
11
|
+
* FIBER_ROUTE_NO_AUTH mutation 路由(post/put/patch/delete)中间件
|
|
12
|
+
* 链无认证名中间件,且文件内无认证 Use 中间件
|
|
13
|
+
*
|
|
14
|
+
* 口径(如实):
|
|
15
|
+
* - get 读操作不检查;认证入口路径词汇豁免(login/regist/auth/token)
|
|
16
|
+
* - 认证中间件按名字词表识别(auth/jwt/login/permission/token/session/
|
|
17
|
+
* verify/guard/passport)
|
|
18
|
+
* - 文件级窗口(与 Express/Koa 检测器同款)
|
|
19
|
+
*/
|
|
20
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
21
|
+
if (k2 === undefined) k2 = k;
|
|
22
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
23
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
24
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
25
|
+
}
|
|
26
|
+
Object.defineProperty(o, k2, desc);
|
|
27
|
+
}) : (function(o, m, k, k2) {
|
|
28
|
+
if (k2 === undefined) k2 = k;
|
|
29
|
+
o[k2] = m[k];
|
|
30
|
+
}));
|
|
31
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
32
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
33
|
+
}) : function(o, v) {
|
|
34
|
+
o["default"] = v;
|
|
35
|
+
});
|
|
36
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
37
|
+
var ownKeys = function(o) {
|
|
38
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
39
|
+
var ar = [];
|
|
40
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
41
|
+
return ar;
|
|
42
|
+
};
|
|
43
|
+
return ownKeys(o);
|
|
44
|
+
};
|
|
45
|
+
return function (mod) {
|
|
46
|
+
if (mod && mod.__esModule) return mod;
|
|
47
|
+
var result = {};
|
|
48
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
49
|
+
__setModuleDefault(result, mod);
|
|
50
|
+
return result;
|
|
51
|
+
};
|
|
52
|
+
})();
|
|
53
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
54
|
+
exports.analyzeFiberApp = analyzeFiberApp;
|
|
55
|
+
exports.analyzeFiberFile = analyzeFiberFile;
|
|
56
|
+
const fs = __importStar(require("fs"));
|
|
57
|
+
const MUTATION_METHODS = new Set(["post", "put", "patch", "delete"]);
|
|
58
|
+
const AUTH_ENTRY_WORDS = [
|
|
59
|
+
"login", "signin", "sign_in", "regist", "signup", "sign_up",
|
|
60
|
+
"token", "auth", "health",
|
|
61
|
+
];
|
|
62
|
+
const AUTH_FN_WORDS = [
|
|
63
|
+
"auth", "login", "permission", "token", "credential", "session",
|
|
64
|
+
"jwt", "verify", "guard", "protect", "passport",
|
|
65
|
+
];
|
|
66
|
+
function isAuthEntryPath(pathName) {
|
|
67
|
+
const lower = pathName.toLowerCase();
|
|
68
|
+
return AUTH_ENTRY_WORDS.some((w) => lower.includes(w));
|
|
69
|
+
}
|
|
70
|
+
function isAuthFnName(name) {
|
|
71
|
+
const lower = name.toLowerCase();
|
|
72
|
+
return AUTH_FN_WORDS.some((w) => lower.includes(w));
|
|
73
|
+
}
|
|
74
|
+
// ── Analysis(代码串级) ──
|
|
75
|
+
function analyzeFiberApp(code) {
|
|
76
|
+
const issues = [];
|
|
77
|
+
const routes = [];
|
|
78
|
+
const authMiddleware = [];
|
|
79
|
+
const hasFiber = /fiber\.New\(|"github.com\/gofiber\/fiber/.test(code);
|
|
80
|
+
if (!hasFiber) {
|
|
81
|
+
return { hasFiber: false, routes, authMiddleware, issues };
|
|
82
|
+
}
|
|
83
|
+
// 全局/组级认证中间件:app.Use(authMW) / group.Use(authMW)
|
|
84
|
+
const useRe = /\.Use\s*\(\s*([A-Za-z_][\w]*)/g;
|
|
85
|
+
let m;
|
|
86
|
+
while ((m = useRe.exec(code)) !== null) {
|
|
87
|
+
if (isAuthFnName(m[1]))
|
|
88
|
+
authMiddleware.push(m[1]);
|
|
89
|
+
}
|
|
90
|
+
// 路由注册:app.Post("/x", mw1, mw2, handler)——Go 方法名大写(Post 惯例)
|
|
91
|
+
const routeRe = /\.(get|post|put|patch|delete)\s*\(\s*"([^"]+)"/gi;
|
|
92
|
+
while ((m = routeRe.exec(code)) !== null) {
|
|
93
|
+
const method = m[1].toLowerCase();
|
|
94
|
+
const pathName = m[2];
|
|
95
|
+
const window = code.slice(m.index + m[0].length, m.index + m[0].length + 300);
|
|
96
|
+
const mwNames = window.match(/[A-Za-z_][\w]*/g) || [];
|
|
97
|
+
const hasAuthMw = mwNames.some((name) => isAuthFnName(name));
|
|
98
|
+
routes.push({
|
|
99
|
+
method,
|
|
100
|
+
path: pathName,
|
|
101
|
+
protected: hasAuthMw,
|
|
102
|
+
line: code.slice(0, m.index).split("\n").length,
|
|
103
|
+
});
|
|
104
|
+
if (MUTATION_METHODS.has(method) && !hasAuthMw
|
|
105
|
+
&& authMiddleware.length === 0 && !isAuthEntryPath(pathName)) {
|
|
106
|
+
issues.push({
|
|
107
|
+
severity: "medium",
|
|
108
|
+
rule: "FIBER_ROUTE_NO_AUTH",
|
|
109
|
+
message: `Route ${method.toUpperCase()} ${pathName} has no auth middleware ` +
|
|
110
|
+
`and no auth Use middleware — any caller can reach it.`,
|
|
111
|
+
route: `${method.toUpperCase()} ${pathName}`,
|
|
112
|
+
line: code.slice(0, m.index).split("\n").length,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return { hasFiber: true, routes, authMiddleware, issues };
|
|
117
|
+
}
|
|
118
|
+
function analyzeFiberFile(filePath) {
|
|
119
|
+
if (!fs.existsSync(filePath))
|
|
120
|
+
return null;
|
|
121
|
+
const code = fs.readFileSync(filePath, "utf-8");
|
|
122
|
+
if (!/gofiber|fiber\.New\(/.test(code))
|
|
123
|
+
return null;
|
|
124
|
+
return analyzeFiberApp(code);
|
|
125
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/**
|
|
4
|
+
* fiber-detector.test.ts — Fiber 框架适配器规则回归(纯函数,无文件 I/O)
|
|
5
|
+
*/
|
|
6
|
+
const vitest_1 = require("vitest");
|
|
7
|
+
const fiber_detector_1 = require("./fiber-detector");
|
|
8
|
+
const app = (routes, extra = "") => `
|
|
9
|
+
import "github.com/gofiber/fiber/v2"
|
|
10
|
+
|
|
11
|
+
func main() {
|
|
12
|
+
app := fiber.New()
|
|
13
|
+
${extra}
|
|
14
|
+
${routes}
|
|
15
|
+
}
|
|
16
|
+
`;
|
|
17
|
+
(0, vitest_1.describe)("fiber-detector", () => {
|
|
18
|
+
(0, vitest_1.it)("R1:无认证中间件的 mutation 路由 → FIBER_ROUTE_NO_AUTH", () => {
|
|
19
|
+
const { issues } = (0, fiber_detector_1.analyzeFiberApp)(app(`
|
|
20
|
+
app.Post("/transfer", func(c *fiber.Ctx) error { return nil })
|
|
21
|
+
`));
|
|
22
|
+
(0, vitest_1.expect)(issues.map((i) => i.rule)).toContain("FIBER_ROUTE_NO_AUTH");
|
|
23
|
+
});
|
|
24
|
+
(0, vitest_1.it)("R1:路由级认证中间件保护不报", () => {
|
|
25
|
+
const { issues } = (0, fiber_detector_1.analyzeFiberApp)(app(`
|
|
26
|
+
app.Post("/transfer", authMiddleware, func(c *fiber.Ctx) error { return nil })
|
|
27
|
+
`));
|
|
28
|
+
(0, vitest_1.expect)(issues).toHaveLength(0);
|
|
29
|
+
});
|
|
30
|
+
(0, vitest_1.it)("R1:全局 Use 认证中间件保护不报", () => {
|
|
31
|
+
const { issues } = (0, fiber_detector_1.analyzeFiberApp)(app(`app.Post("/transfer", func(c *fiber.Ctx) error { return nil })`, `app.Use(authMiddleware)`));
|
|
32
|
+
(0, vitest_1.expect)(issues).toHaveLength(0);
|
|
33
|
+
});
|
|
34
|
+
(0, vitest_1.it)("R1:非认证中间件不视为保护", () => {
|
|
35
|
+
const { issues } = (0, fiber_detector_1.analyzeFiberApp)(app(`
|
|
36
|
+
app.Post("/transfer", logger, func(c *fiber.Ctx) error { return nil })
|
|
37
|
+
`));
|
|
38
|
+
(0, vitest_1.expect)(issues.map((i) => i.rule)).toContain("FIBER_ROUTE_NO_AUTH");
|
|
39
|
+
});
|
|
40
|
+
(0, vitest_1.it)("R1:GET 读操作不报", () => {
|
|
41
|
+
const { issues } = (0, fiber_detector_1.analyzeFiberApp)(app(`
|
|
42
|
+
app.Get("/articles", func(c *fiber.Ctx) error { return nil })
|
|
43
|
+
`));
|
|
44
|
+
(0, vitest_1.expect)(issues).toHaveLength(0);
|
|
45
|
+
});
|
|
46
|
+
(0, vitest_1.it)("R1 豁免:login 认证入口路径不报", () => {
|
|
47
|
+
const { issues } = (0, fiber_detector_1.analyzeFiberApp)(app(`
|
|
48
|
+
app.Post("/login", func(c *fiber.Ctx) error { return nil })
|
|
49
|
+
`));
|
|
50
|
+
(0, vitest_1.expect)(issues).toHaveLength(0);
|
|
51
|
+
});
|
|
52
|
+
(0, vitest_1.it)("非 Fiber 代码不产生任何问题", () => {
|
|
53
|
+
const { hasFiber, issues } = (0, fiber_detector_1.analyzeFiberApp)(`import express from "express";`);
|
|
54
|
+
(0, vitest_1.expect)(hasFiber).toBe(false);
|
|
55
|
+
(0, vitest_1.expect)(issues).toHaveLength(0);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Gin Framework Adapter — Protocol Detection for Gin (Go)
|
|
4
|
+
*
|
|
5
|
+
* 第 11 个框架适配(Go 第 1 个,代码串级——Go 路由注册与 Koa/Express 同构):
|
|
6
|
+
*
|
|
7
|
+
* r.POST("/x", authMW, handler) 路由级认证中间件链
|
|
8
|
+
* r.Use(authMW) / r.Group("/api", mw) 组级认证中间件
|
|
9
|
+
*
|
|
10
|
+
* 规则:
|
|
11
|
+
* GIN_ROUTE_NO_AUTH mutation 路由(post/put/patch/delete)中间件
|
|
12
|
+
* 链无认证名中间件,且文件内无认证 Use/Group
|
|
13
|
+
* 中间件——路由级 missing-auth
|
|
14
|
+
*
|
|
15
|
+
* 口径(如实):
|
|
16
|
+
* - get 读操作不检查;认证入口路径词汇豁免(login/regist/auth/token)
|
|
17
|
+
* - 认证中间件按名字词表识别(auth/jwt/login/permission/token/session/
|
|
18
|
+
* verify/guard/passport)
|
|
19
|
+
* - 文件级窗口(与 Express/Koa 检测器同款)
|
|
20
|
+
*/
|
|
21
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
22
|
+
if (k2 === undefined) k2 = k;
|
|
23
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
24
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
25
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
26
|
+
}
|
|
27
|
+
Object.defineProperty(o, k2, desc);
|
|
28
|
+
}) : (function(o, m, k, k2) {
|
|
29
|
+
if (k2 === undefined) k2 = k;
|
|
30
|
+
o[k2] = m[k];
|
|
31
|
+
}));
|
|
32
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
33
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
34
|
+
}) : function(o, v) {
|
|
35
|
+
o["default"] = v;
|
|
36
|
+
});
|
|
37
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
38
|
+
var ownKeys = function(o) {
|
|
39
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
40
|
+
var ar = [];
|
|
41
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
42
|
+
return ar;
|
|
43
|
+
};
|
|
44
|
+
return ownKeys(o);
|
|
45
|
+
};
|
|
46
|
+
return function (mod) {
|
|
47
|
+
if (mod && mod.__esModule) return mod;
|
|
48
|
+
var result = {};
|
|
49
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
50
|
+
__setModuleDefault(result, mod);
|
|
51
|
+
return result;
|
|
52
|
+
};
|
|
53
|
+
})();
|
|
54
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
55
|
+
exports.analyzeGinApp = analyzeGinApp;
|
|
56
|
+
exports.analyzeGinFile = analyzeGinFile;
|
|
57
|
+
const fs = __importStar(require("fs"));
|
|
58
|
+
const MUTATION_METHODS = new Set(["post", "put", "patch", "delete"]);
|
|
59
|
+
const AUTH_ENTRY_WORDS = [
|
|
60
|
+
"login", "signin", "sign_in", "regist", "signup", "sign_up",
|
|
61
|
+
"token", "auth", "health",
|
|
62
|
+
];
|
|
63
|
+
const AUTH_FN_WORDS = [
|
|
64
|
+
"auth", "login", "permission", "token", "credential", "session",
|
|
65
|
+
"jwt", "verify", "guard", "protect", "passport",
|
|
66
|
+
];
|
|
67
|
+
function isAuthEntryPath(pathName) {
|
|
68
|
+
const lower = pathName.toLowerCase();
|
|
69
|
+
return AUTH_ENTRY_WORDS.some((w) => lower.includes(w));
|
|
70
|
+
}
|
|
71
|
+
function isAuthFnName(name) {
|
|
72
|
+
const lower = name.toLowerCase();
|
|
73
|
+
// 注意:不含 "middleware" 词——loggerMiddleware 等工具中间件不该撞认证
|
|
74
|
+
// (authMiddleware 类靠 "auth" 命中)
|
|
75
|
+
return AUTH_FN_WORDS.some((w) => lower.includes(w));
|
|
76
|
+
}
|
|
77
|
+
// ── Analysis(代码串级) ──
|
|
78
|
+
function analyzeGinApp(code) {
|
|
79
|
+
const issues = [];
|
|
80
|
+
const routes = [];
|
|
81
|
+
const authGroupMiddleware = [];
|
|
82
|
+
const hasGin = /gin\.(Default|New)\(|"github.com\/gin-gonic\/gin"/.test(code);
|
|
83
|
+
if (!hasGin) {
|
|
84
|
+
return { hasGin: false, routes, authGroupMiddleware, issues };
|
|
85
|
+
}
|
|
86
|
+
// 组级认证中间件:r.Use(authMW) / r.Group("/api", authMW) / g.Use(authMW)
|
|
87
|
+
const useRe = /\.Use\s*\(\s*([A-Za-z_][\w]*)/g;
|
|
88
|
+
let m;
|
|
89
|
+
while ((m = useRe.exec(code)) !== null) {
|
|
90
|
+
if (isAuthFnName(m[1]))
|
|
91
|
+
authGroupMiddleware.push(m[1]);
|
|
92
|
+
}
|
|
93
|
+
const groupRe = /\.Group\s*\(\s*"[^"]*"\s*,\s*([A-Za-z_][\w]*)/g;
|
|
94
|
+
while ((m = groupRe.exec(code)) !== null) {
|
|
95
|
+
if (isAuthFnName(m[1]))
|
|
96
|
+
authGroupMiddleware.push(m[1]);
|
|
97
|
+
}
|
|
98
|
+
// 路由注册:r.POST("/x", mw1, mw2, handler)——Go 方法名大写(POST 惯例)
|
|
99
|
+
const routeRe = /\.(get|post|put|patch|delete)\s*\(\s*"([^"]+)"/gi;
|
|
100
|
+
while ((m = routeRe.exec(code)) !== null) {
|
|
101
|
+
const method = m[1].toLowerCase();
|
|
102
|
+
const pathName = m[2];
|
|
103
|
+
const window = code.slice(m.index + m[0].length, m.index + m[0].length + 300);
|
|
104
|
+
const mwNames = window.match(/[A-Za-z_][\w]*/g) || [];
|
|
105
|
+
const hasAuthMw = mwNames.some((name) => isAuthFnName(name));
|
|
106
|
+
routes.push({
|
|
107
|
+
method,
|
|
108
|
+
path: pathName,
|
|
109
|
+
protected: hasAuthMw,
|
|
110
|
+
line: code.slice(0, m.index).split("\n").length,
|
|
111
|
+
});
|
|
112
|
+
if (MUTATION_METHODS.has(method) && !hasAuthMw
|
|
113
|
+
&& authGroupMiddleware.length === 0 && !isAuthEntryPath(pathName)) {
|
|
114
|
+
issues.push({
|
|
115
|
+
severity: "medium",
|
|
116
|
+
rule: "GIN_ROUTE_NO_AUTH",
|
|
117
|
+
message: `Route ${method.toUpperCase()} ${pathName} has no auth middleware ` +
|
|
118
|
+
`and no auth Use/Group middleware — any caller can reach it.`,
|
|
119
|
+
route: `${method.toUpperCase()} ${pathName}`,
|
|
120
|
+
line: code.slice(0, m.index).split("\n").length,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return { hasGin: true, routes, authGroupMiddleware, issues };
|
|
125
|
+
}
|
|
126
|
+
function analyzeGinFile(filePath) {
|
|
127
|
+
if (!fs.existsSync(filePath))
|
|
128
|
+
return null;
|
|
129
|
+
const code = fs.readFileSync(filePath, "utf-8");
|
|
130
|
+
if (!/gin-gonic|gin\.(Default|New)\(/.test(code))
|
|
131
|
+
return null;
|
|
132
|
+
return analyzeGinApp(code);
|
|
133
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/**
|
|
4
|
+
* gin-detector.test.ts — Gin 框架适配器规则回归(纯函数,无文件 I/O)
|
|
5
|
+
*/
|
|
6
|
+
const vitest_1 = require("vitest");
|
|
7
|
+
const gin_detector_1 = require("./gin-detector");
|
|
8
|
+
const app = (routes, extra = "") => `
|
|
9
|
+
import "github.com/gin-gonic/gin"
|
|
10
|
+
|
|
11
|
+
func main() {
|
|
12
|
+
r := gin.Default()
|
|
13
|
+
${extra}
|
|
14
|
+
${routes}
|
|
15
|
+
}
|
|
16
|
+
`;
|
|
17
|
+
(0, vitest_1.describe)("gin-detector", () => {
|
|
18
|
+
(0, vitest_1.it)("R1:无认证中间件的 mutation 路由 → GIN_ROUTE_NO_AUTH", () => {
|
|
19
|
+
const { issues } = (0, gin_detector_1.analyzeGinApp)(app(`
|
|
20
|
+
r.POST("/transfer", func(c *gin.Context) {})
|
|
21
|
+
`));
|
|
22
|
+
(0, vitest_1.expect)(issues.map((i) => i.rule)).toContain("GIN_ROUTE_NO_AUTH");
|
|
23
|
+
});
|
|
24
|
+
(0, vitest_1.it)("R1:路由级认证中间件保护不报", () => {
|
|
25
|
+
const { issues } = (0, gin_detector_1.analyzeGinApp)(app(`
|
|
26
|
+
r.POST("/transfer", authMiddleware, func(c *gin.Context) {})
|
|
27
|
+
`));
|
|
28
|
+
(0, vitest_1.expect)(issues).toHaveLength(0);
|
|
29
|
+
});
|
|
30
|
+
(0, vitest_1.it)("R1:组级 Use 认证中间件保护不报", () => {
|
|
31
|
+
const { issues } = (0, gin_detector_1.analyzeGinApp)(app(`r.POST("/transfer", func(c *gin.Context) {})`, `r.Use(authMiddleware)`));
|
|
32
|
+
(0, vitest_1.expect)(issues).toHaveLength(0);
|
|
33
|
+
});
|
|
34
|
+
(0, vitest_1.it)("R1:Group 认证中间件保护不报", () => {
|
|
35
|
+
const { issues } = (0, gin_detector_1.analyzeGinApp)(app(`
|
|
36
|
+
api := r.Group("/api", authMiddleware)
|
|
37
|
+
api.POST("/transfer", func(c *gin.Context) {})
|
|
38
|
+
`));
|
|
39
|
+
(0, vitest_1.expect)(issues).toHaveLength(0);
|
|
40
|
+
});
|
|
41
|
+
(0, vitest_1.it)("R1:非认证中间件(logger)不视为保护", () => {
|
|
42
|
+
const { issues } = (0, gin_detector_1.analyzeGinApp)(app(`
|
|
43
|
+
r.POST("/transfer", loggerMiddleware, func(c *gin.Context) {})
|
|
44
|
+
`));
|
|
45
|
+
(0, vitest_1.expect)(issues.map((i) => i.rule)).toContain("GIN_ROUTE_NO_AUTH");
|
|
46
|
+
});
|
|
47
|
+
(0, vitest_1.it)("R1:GET 读操作不报", () => {
|
|
48
|
+
const { issues } = (0, gin_detector_1.analyzeGinApp)(app(`
|
|
49
|
+
r.GET("/articles", func(c *gin.Context) {})
|
|
50
|
+
`));
|
|
51
|
+
(0, vitest_1.expect)(issues).toHaveLength(0);
|
|
52
|
+
});
|
|
53
|
+
(0, vitest_1.it)("R1 豁免:login 认证入口路径不报", () => {
|
|
54
|
+
const { issues } = (0, gin_detector_1.analyzeGinApp)(app(`
|
|
55
|
+
r.POST("/login", func(c *gin.Context) {})
|
|
56
|
+
`));
|
|
57
|
+
(0, vitest_1.expect)(issues).toHaveLength(0);
|
|
58
|
+
});
|
|
59
|
+
(0, vitest_1.it)("非 Gin 代码不产生任何问题", () => {
|
|
60
|
+
const { hasGin, issues } = (0, gin_detector_1.analyzeGinApp)(`import express from "express";`);
|
|
61
|
+
(0, vitest_1.expect)(hasGin).toBe(false);
|
|
62
|
+
(0, vitest_1.expect)(issues).toHaveLength(0);
|
|
63
|
+
});
|
|
64
|
+
});
|
package/dist/frameworks/index.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* Next.js (App Router) — 7 dedicated detectors.
|
|
13
13
|
*/
|
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
-
exports.generateVersionAwarenessReport = exports.checkFileRename = exports.checkFrameworkConventions = exports.detectFrameworks = exports.analyzeNestJSFile = exports.analyzeNestJSProject = exports.analyzeHapiFile = exports.analyzeHapiApp = exports.analyzeKoaFile = exports.analyzeKoaApp = exports.readNextMiddleware = exports.analyzeNextApp = exports.analyzeFastifyFile = exports.analyzeFastifyApp = exports.analyzeFlaskStructure = exports.analyzeDjangoStructure = exports.analyzeFastapiStructure = exports.formatExpressReport = exports.classifyMiddleware = exports.extractGlobalMiddleware = exports.extractRoutes = exports.detectExpressApp = exports.analyzeExpressProject = exports.analyzeExpressFile = exports.analyzeExpressApp = void 0;
|
|
15
|
+
exports.generateVersionAwarenessReport = exports.checkFileRename = exports.checkFrameworkConventions = exports.detectFrameworks = exports.analyzeNestJSFile = exports.analyzeNestJSProject = exports.analyzeFiberFile = exports.analyzeFiberApp = exports.analyzeGinFile = exports.analyzeGinApp = exports.analyzeHapiFile = exports.analyzeHapiApp = exports.analyzeKoaFile = exports.analyzeKoaApp = exports.readNextMiddleware = exports.analyzeNextApp = exports.analyzeFastifyFile = exports.analyzeFastifyApp = exports.analyzeFlaskStructure = exports.analyzeDjangoStructure = exports.analyzeFastapiStructure = exports.formatExpressReport = exports.classifyMiddleware = exports.extractGlobalMiddleware = exports.extractRoutes = exports.detectExpressApp = exports.analyzeExpressProject = exports.analyzeExpressFile = exports.analyzeExpressApp = void 0;
|
|
16
16
|
var express_detector_1 = require("./express-detector");
|
|
17
17
|
Object.defineProperty(exports, "analyzeExpressApp", { enumerable: true, get: function () { return express_detector_1.analyzeExpressApp; } });
|
|
18
18
|
Object.defineProperty(exports, "analyzeExpressFile", { enumerable: true, get: function () { return express_detector_1.analyzeExpressFile; } });
|
|
@@ -40,6 +40,12 @@ Object.defineProperty(exports, "analyzeKoaFile", { enumerable: true, get: functi
|
|
|
40
40
|
var hapi_detector_1 = require("./hapi-detector");
|
|
41
41
|
Object.defineProperty(exports, "analyzeHapiApp", { enumerable: true, get: function () { return hapi_detector_1.analyzeHapiApp; } });
|
|
42
42
|
Object.defineProperty(exports, "analyzeHapiFile", { enumerable: true, get: function () { return hapi_detector_1.analyzeHapiFile; } });
|
|
43
|
+
var gin_detector_1 = require("./gin-detector");
|
|
44
|
+
Object.defineProperty(exports, "analyzeGinApp", { enumerable: true, get: function () { return gin_detector_1.analyzeGinApp; } });
|
|
45
|
+
Object.defineProperty(exports, "analyzeGinFile", { enumerable: true, get: function () { return gin_detector_1.analyzeGinFile; } });
|
|
46
|
+
var fiber_detector_1 = require("./fiber-detector");
|
|
47
|
+
Object.defineProperty(exports, "analyzeFiberApp", { enumerable: true, get: function () { return fiber_detector_1.analyzeFiberApp; } });
|
|
48
|
+
Object.defineProperty(exports, "analyzeFiberFile", { enumerable: true, get: function () { return fiber_detector_1.analyzeFiberFile; } });
|
|
43
49
|
var nestjs_detector_1 = require("./nestjs-detector");
|
|
44
50
|
Object.defineProperty(exports, "analyzeNestJSProject", { enumerable: true, get: function () { return nestjs_detector_1.analyzeNestJSProject; } });
|
|
45
51
|
Object.defineProperty(exports, "analyzeNestJSFile", { enumerable: true, get: function () { return nestjs_detector_1.analyzeNestJSFile; } });
|
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.14";
|
|
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
|
@@ -69,11 +69,14 @@ const fastify_detector_1 = require("../frameworks/fastify-detector");
|
|
|
69
69
|
const nextjs_detector_1 = require("../frameworks/nextjs-detector");
|
|
70
70
|
const koa_detector_1 = require("../frameworks/koa-detector");
|
|
71
71
|
const hapi_detector_1 = require("../frameworks/hapi-detector");
|
|
72
|
+
const gin_detector_1 = require("../frameworks/gin-detector");
|
|
73
|
+
const fiber_detector_1 = require("../frameworks/fiber-detector");
|
|
72
74
|
const ssg_bridge_1 = require("./ssg-bridge");
|
|
73
75
|
const call_sequence_1 = require("../call-sequence");
|
|
74
76
|
const extract_ir_1 = require("../extract-ir");
|
|
75
77
|
const extract_ir_python_1 = require("../extract-ir-python");
|
|
76
78
|
const extract_ir_c_1 = require("../extract-ir-c");
|
|
79
|
+
const extract_ir_go_1 = require("../extract-ir-go");
|
|
77
80
|
// ── Main Entry Point ──
|
|
78
81
|
/**
|
|
79
82
|
* Trust 决策主入口:收集 → 归一化 → 评分 → 决策 → 组装。
|
|
@@ -106,6 +109,8 @@ async function evaluateTrust(ctx) {
|
|
|
106
109
|
const nextjsResult = collectNextjsViolations(ctx);
|
|
107
110
|
const koaResult = collectKoaViolations(ctx);
|
|
108
111
|
const hapiResult = collectHapiViolations(ctx);
|
|
112
|
+
const ginResult = collectGinViolations(ctx);
|
|
113
|
+
const fiberResult = collectFiberViolations(ctx);
|
|
109
114
|
const coverageData = collectVerificationCoverage(ctx);
|
|
110
115
|
const governanceDefects = collectGovernanceDefects(ctx);
|
|
111
116
|
// ═══════════════════════════════════════
|
|
@@ -130,6 +135,8 @@ async function evaluateTrust(ctx) {
|
|
|
130
135
|
...nextjsResult.violations,
|
|
131
136
|
...koaResult.violations,
|
|
132
137
|
...hapiResult.violations,
|
|
138
|
+
...ginResult.violations,
|
|
139
|
+
...fiberResult.violations,
|
|
133
140
|
];
|
|
134
141
|
// ═══════════════════════════════════════
|
|
135
142
|
// PHASE 3: SCORE
|
|
@@ -312,6 +319,24 @@ async function evaluateTrust(ctx) {
|
|
|
312
319
|
issuesFound: hapiResult.violations.length,
|
|
313
320
|
}
|
|
314
321
|
: undefined,
|
|
322
|
+
/** Gin framework adapter coverage — Go route/auth-middleware analysis */
|
|
323
|
+
ginCoverage: ginResult.coverage.routes > 0
|
|
324
|
+
? {
|
|
325
|
+
appsDetected: ginResult.coverage.apps,
|
|
326
|
+
totalRoutes: ginResult.coverage.routes,
|
|
327
|
+
filesScanned: ginResult.coverage.filesScanned,
|
|
328
|
+
issuesFound: ginResult.violations.length,
|
|
329
|
+
}
|
|
330
|
+
: undefined,
|
|
331
|
+
/** Fiber framework adapter coverage — Go route/auth-middleware analysis */
|
|
332
|
+
fiberCoverage: fiberResult.coverage.routes > 0
|
|
333
|
+
? {
|
|
334
|
+
appsDetected: fiberResult.coverage.apps,
|
|
335
|
+
totalRoutes: fiberResult.coverage.routes,
|
|
336
|
+
filesScanned: fiberResult.coverage.filesScanned,
|
|
337
|
+
issuesFound: fiberResult.violations.length,
|
|
338
|
+
}
|
|
339
|
+
: undefined,
|
|
315
340
|
},
|
|
316
341
|
dimensions: {
|
|
317
342
|
policyCompliance: {
|
|
@@ -450,6 +475,70 @@ function mapPolicyViolation(rv, filePath, _enterprisePolicy) {
|
|
|
450
475
|
* Collect Express-specific security violations from the framework detector.
|
|
451
476
|
* Maps ExpressSecurityIssue[] → TrustViolation[].
|
|
452
477
|
*/
|
|
478
|
+
function collectGoFrameworkViolations(ctx, analyzer, rulePrefix, fixText, policyRef) {
|
|
479
|
+
const violations = [];
|
|
480
|
+
const coverage = { apps: 0, routes: 0, filesScanned: 0 };
|
|
481
|
+
try {
|
|
482
|
+
const fs = require("fs");
|
|
483
|
+
// Go 项目结构特殊:cmd/internal/pkg + 根目录 main.go 都要扫
|
|
484
|
+
const candidateDirs = ["cmd", "internal", "pkg", "src", "server", "app", "api", "routes", "lib", "handlers", "middleware"];
|
|
485
|
+
const extensions = languageToExtensions(ctx.language);
|
|
486
|
+
const files = [];
|
|
487
|
+
for (const dir of candidateDirs) {
|
|
488
|
+
const dirPath = path.join(ctx.projectPath, dir);
|
|
489
|
+
if (!fs.existsSync(dirPath))
|
|
490
|
+
continue;
|
|
491
|
+
try {
|
|
492
|
+
files.push(...walkDir(dirPath, extensions, 100));
|
|
493
|
+
}
|
|
494
|
+
catch { /* skip */ }
|
|
495
|
+
}
|
|
496
|
+
try {
|
|
497
|
+
for (const entry of fs.readdirSync(ctx.projectPath)) {
|
|
498
|
+
const full = path.join(ctx.projectPath, entry);
|
|
499
|
+
if (!fs.statSync(full).isFile())
|
|
500
|
+
continue;
|
|
501
|
+
if (extensions.some((ext) => entry.endsWith(ext)))
|
|
502
|
+
files.push(full);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
catch { /* best-effort */ }
|
|
506
|
+
for (const file of files) {
|
|
507
|
+
if (file.includes("_test."))
|
|
508
|
+
continue;
|
|
509
|
+
coverage.filesScanned++;
|
|
510
|
+
try {
|
|
511
|
+
const analysis = analyzer(file);
|
|
512
|
+
if (!analysis || !analysis[`has${rulePrefix}`])
|
|
513
|
+
continue;
|
|
514
|
+
coverage.apps++;
|
|
515
|
+
coverage.routes += analysis.routes.length;
|
|
516
|
+
for (const issue of analysis.issues) {
|
|
517
|
+
violations.push({
|
|
518
|
+
severity: issue.severity === "low" ? "low" : issue.severity,
|
|
519
|
+
rule_id: issue.rule,
|
|
520
|
+
file: path.relative(ctx.projectPath, file),
|
|
521
|
+
function: "unknown",
|
|
522
|
+
message: issue.message,
|
|
523
|
+
evidence: issue.route || "",
|
|
524
|
+
why: `Framework structural analysis: ${issue.message}`,
|
|
525
|
+
fix: fixText,
|
|
526
|
+
policy_ref: policyRef,
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
catch { /* skip unreadable files */ }
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
catch { /* best-effort */ }
|
|
534
|
+
return { violations, coverage };
|
|
535
|
+
}
|
|
536
|
+
function collectGinViolations(ctx) {
|
|
537
|
+
return collectGoFrameworkViolations(ctx, gin_detector_1.analyzeGinFile, "Gin", "Add an auth middleware to the route registration, or register auth middleware with r.Use/r.Group.", "framework-safety.gin");
|
|
538
|
+
}
|
|
539
|
+
function collectFiberViolations(ctx) {
|
|
540
|
+
return collectGoFrameworkViolations(ctx, fiber_detector_1.analyzeFiberFile, "Fiber", "Add an auth middleware to the route registration, or register auth middleware with app.Use.", "framework-safety.fiber");
|
|
541
|
+
}
|
|
453
542
|
function collectKoaViolations(ctx) {
|
|
454
543
|
const violations = [];
|
|
455
544
|
const coverage = { apps: 0, routes: 0, filesScanned: 0 };
|
|
@@ -1121,6 +1210,7 @@ async function collectProtocolViolations(ctx, callGraph) {
|
|
|
1121
1210
|
javascript: () => (0, extract_ir_1.extractIR)(ctx.projectPath),
|
|
1122
1211
|
python: () => (0, extract_ir_python_1.extractIRPython)(ctx.projectPath),
|
|
1123
1212
|
c: () => (0, extract_ir_c_1.extractIRC)(ctx.projectPath),
|
|
1213
|
+
go: () => (0, extract_ir_go_1.extractIRGo)(ctx.projectPath),
|
|
1124
1214
|
};
|
|
1125
1215
|
const extractFn = autoExtractor[lang];
|
|
1126
1216
|
if (extractFn) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "progmune-runtime",
|
|
3
|
-
"version": "3.7.
|
|
3
|
+
"version": "3.7.14",
|
|
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/",
|