progmune-runtime 3.7.9 → 3.7.11
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 +22 -0
- package/README.md +2 -2
- package/README.zh-CN.md +2 -2
- package/dist/frameworks/index.js +4 -1
- package/dist/frameworks/nestjs-detector.js +100 -8
- package/dist/frameworks/nestjs-detector.test.js +140 -0
- package/dist/sdk.js +1 -1
- package/dist/trust/engine.js +26 -44
- package/package.json +7 -3
- package/tools/extract_framework_django.py +269 -0
- package/tools/extract_framework_flask.py +169 -0
- package/tools/extract_framework_py.py +237 -0
- package/tools/extract_ir.py +1109 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [3.7.11] — 2026-09-01
|
|
4
|
+
|
|
5
|
+
### NestJS 补全——框架适配 8/13
|
|
6
|
+
|
|
7
|
+
- **三缺口补全**(`src/frameworks/nestjs-detector.ts`):
|
|
8
|
+
①**全局 APP_GUARD 守卫识别**——@Module providers 的 `{ provide: APP_GUARD, useClass: X }`(装饰器参数与类属性两种形态)→ 全局认证守卫存在时 mutation 路由不再误报;
|
|
9
|
+
②**@Public()/@SkipAuth()/@AllowAnon() 豁免装饰器**(类级+方法级)——全局守卫模式下的公开路由标记;**显式绕过全局守卫的 mutation 路由 → NESTJS_NO_AUTH**(消息注明绕过的是哪个全局守卫);
|
|
10
|
+
③**守卫名认证分类**——ThrottlerGuard/RateLimit/Logger 等非认证守卫不再算认证(限流≠认证,实测误报源)
|
|
11
|
+
- **引擎接线升级**:collectNestJSViolations 从 per-file 切 **项目级一次装载**(analyzeNestJSProject)——全局守卫与 @Public 需要跨文件上下文,per-file 分析无法识别全局守卫(系统性误报根因);coverage.filesScanned 口径改为分析单元=项目
|
|
12
|
+
- **合成金标**(`generate-projects-nestjs.ts` + `scan-protocol-nestjs.ts`):6 项目(类级守卫/全局 APP_GUARD+@Public login/无守卫/显式绕过/ThrottlerGuard 非认证/敏感 GET 公开)——**TP 12 / FP 0 / FN 0 → P=R=100%**
|
|
13
|
+
- **验证**:6 个新单测(含 @Public login 在全局守卫下不报);引擎冒烟 N2 APPROVED 87(全局守卫正确识别)、N4 BLOCKED(显式绕过 critical 拦截);全套件 166/166;Python 盲测 v1.2 64 零漂移;C 演示不变
|
|
14
|
+
- **文档**:NestJS ⚠️ 部分 → ✅ 结构分析(README 双语/覆盖矩阵双语/CLAUDE.md/落地页「8/13 专用检测器」口径)
|
|
15
|
+
|
|
16
|
+
## [3.7.10] — 2026-09-01
|
|
17
|
+
|
|
18
|
+
### 安装态 Python 全链路修复(npm 包正确性)
|
|
19
|
+
|
|
20
|
+
- **修复**:`tools/extract_ir.py` + 三个框架扫描器(`extract_framework_py.py` / `extract_framework_django.py` / `extract_framework_flask.py`)加入 npm 包 `files` 白名单——**此前安装态(MCP 主产品形态)的 Python IR 提取与框架结构扫描全部静默失效**(脚本不在包内,execSync 失败被 best-effort 吞掉)。M1/M2 验证时发现的既有遗留,非 3.7.8/3.7.9 引入
|
|
21
|
+
- **安装态端到端验证**(此前从未有过):打包 tarball → 临时目录 npm install → 用安装态的 dist 跑 evaluateTrust——FastAPI 合成项目检出 `FASTAPI_ROUTE_NO_AUTH`(框架扫描生效)、Python 盲测项目检出 SSG 违规(IR 提取生效)、**安装态与仓库态 A/B 完全一致**;`tools/__pycache__` 未入包(files 精确文件路径)
|
|
22
|
+
- **落地页**:语言覆盖现状更新入版(C 注解驱动 Beta 行 + 框架适配 7/13 行 + 双语 i18n + 证据链接)
|
|
23
|
+
- 包体:377 文件(+4 个 .py,约 71.5KB)
|
|
24
|
+
|
|
3
25
|
## [3.7.9] — 2026-08-28
|
|
4
26
|
|
|
5
27
|
### Flask / Fastify / Next.js 框架结构适配(M4——框架适配 7/13)
|
package/README.md
CHANGED
|
@@ -108,7 +108,7 @@ Progmune is honest about what it can and cannot verify.
|
|
|
108
108
|
|
|
109
109
|
**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
110
|
|
|
111
|
-
**Framework adapters:
|
|
111
|
+
**Framework adapters: 8/13.** 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) and NestJS ✅ (decorator route parsing + global APP_GUARD recognition + @Public exemption) 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
112
|
|
|
113
113
|
### What Progmune does NOT cover (honest boundaries)
|
|
114
114
|
|
|
@@ -234,7 +234,7 @@ High-impact contribution areas:
|
|
|
234
234
|
- **Runtime Pipeline:** Detect → Explain → Repair → Validate (L1–L4)
|
|
235
235
|
- **Trust Engine:** 4-dimension scoring with binary explainability gate
|
|
236
236
|
- **MCP Tools:** 19 — `progmune_trust_check`, `progmune_score`, `progmune_policy_check`, `progmune_certify`, and more
|
|
237
|
-
- **Framework Adapters:** Express ✅, tRPC ✅, FastAPI ✅, Django ✅, Flask ✅, Fastify ✅, Next.js ✅, NestJS
|
|
237
|
+
- **Framework Adapters:** Express ✅, tRPC ✅, FastAPI ✅, Django ✅, Flask ✅, Fastify ✅, Next.js ✅, NestJS ✅ (8/13)
|
|
238
238
|
- **Knowledge Base:** 31 domains, 148 protocol rules, 22 detectors, 26 safeguards, PLSB 13/13 categories — plus 15 source-level detection rules (Python)
|
|
239
239
|
- **Corpus:** 2,500+ trajectories across 6+ repositories; blind benchmarks 100 (TS) + 90 (Python) projects; real-world validation on 4 application repos
|
|
240
240
|
- **Current focus:** Enterprise PoC validation + remaining framework-internal boundary FPs
|
package/README.zh-CN.md
CHANGED
|
@@ -108,7 +108,7 @@ Progmune 对能验证什么、不能验证什么保持诚实。
|
|
|
108
108
|
|
|
109
109
|
**多语言 IR(注册表式)。** TypeScript(ts-morph)与 Python(AST)提取器是同一注册表(`src/extract-project-ir.ts`)中的注册项;`extractProjectIR` 把检测到的所有语言合并为一份函数 IR,由 agent loop、`execute()` 与 MCP server 共享——agent 可在两种语言上编排函数协议链。新增语言(Go、Java……)= 注册一条提取器,调用方零改动。
|
|
110
110
|
|
|
111
|
-
**框架适配器:
|
|
111
|
+
**框架适配器:8/13。** Express ✅、tRPC ✅、FastAPI ✅、Django ✅(结构级路由认证分析——逐条检查写操作入口的认证)、Flask ✅(路由/before_request 认证守卫分析)、Fastify ✅(路由 preHandler/钩子认证分析)与 Next.js ✅(App Router 路由处理器认证分析)与 NestJS ✅(装饰器路由解析 + 全局 APP_GUARD 守卫识别 + @Public 豁免)有专用检测器;Next.js 另有版本感知治理。Spring Boot、Go 框架及少数 TS 框架待适配——框架适配是 #1 产品缺口。
|
|
112
112
|
|
|
113
113
|
### Progmune 不覆盖什么(诚实边界)
|
|
114
114
|
|
|
@@ -233,7 +233,7 @@ Progmune 建立在"**LLM 输出是统计表演而非推理**"这一前提上—
|
|
|
233
233
|
- **运行时管线:** 检测 → 解释 → 修复 → 验证(L1–L4)
|
|
234
234
|
- **信任引擎:** 四维评分 + 二元可解释性门
|
|
235
235
|
- **MCP 工具:** 19 个——`progmune_trust_check`、`progmune_score`、`progmune_policy_check`、`progmune_certify` 等
|
|
236
|
-
- **框架适配器:** Express ✅、tRPC ✅、FastAPI ✅、Django ✅、Flask ✅、Fastify ✅、Next.js ✅、NestJS
|
|
236
|
+
- **框架适配器:** Express ✅、tRPC ✅、FastAPI ✅、Django ✅、Flask ✅、Fastify ✅、Next.js ✅、NestJS ✅(8/13)
|
|
237
237
|
- **知识库:** 31 个域、148 条协议规则、22 个检测器、26 条防护规则、PLSB 13/13 类别——另加 15 条源码级检测规则(Python)
|
|
238
238
|
- **语料:** 6+ 仓库 2,500+ 轨迹;盲测基准 100(TS)+ 90(Python)个项目;4 个应用仓库真实验证
|
|
239
239
|
- **当前重点:** 企业 PoC 验证 + 剩余框架内部边界误报
|
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.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.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; } });
|
|
@@ -34,6 +34,9 @@ Object.defineProperty(exports, "analyzeFastifyFile", { enumerable: true, get: fu
|
|
|
34
34
|
var nextjs_detector_1 = require("./nextjs-detector");
|
|
35
35
|
Object.defineProperty(exports, "analyzeNextApp", { enumerable: true, get: function () { return nextjs_detector_1.analyzeNextApp; } });
|
|
36
36
|
Object.defineProperty(exports, "readNextMiddleware", { enumerable: true, get: function () { return nextjs_detector_1.readNextMiddleware; } });
|
|
37
|
+
var nestjs_detector_1 = require("./nestjs-detector");
|
|
38
|
+
Object.defineProperty(exports, "analyzeNestJSProject", { enumerable: true, get: function () { return nestjs_detector_1.analyzeNestJSProject; } });
|
|
39
|
+
Object.defineProperty(exports, "analyzeNestJSFile", { enumerable: true, get: function () { return nestjs_detector_1.analyzeNestJSFile; } });
|
|
37
40
|
// ── Framework Version-Aware Governance ──
|
|
38
41
|
var version_awareness_1 = require("./version-awareness");
|
|
39
42
|
Object.defineProperty(exports, "detectFrameworks", { enumerable: true, get: function () { return version_awareness_1.detectFrameworks; } });
|
|
@@ -36,14 +36,34 @@ function analyzeNestJSProject(projectRoot) {
|
|
|
36
36
|
project.addSourceFilesAtPaths(`${projectRoot}/**/*.ts`);
|
|
37
37
|
}
|
|
38
38
|
catch {
|
|
39
|
-
return { controllers: [], routes: [], issues: [] };
|
|
39
|
+
return { controllers: [], routes: [], issues: [], globalAuthGuards: [] };
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
const analysis = {
|
|
43
43
|
controllers: [],
|
|
44
44
|
routes: [],
|
|
45
45
|
issues: [],
|
|
46
|
+
globalAuthGuards: [],
|
|
46
47
|
};
|
|
48
|
+
// ── 第一遍:全局守卫(@Module providers 里的 APP_GUARD)──
|
|
49
|
+
for (const file of project.getSourceFiles()) {
|
|
50
|
+
if (file.getFilePath().includes("node_modules"))
|
|
51
|
+
continue;
|
|
52
|
+
if (/\.(test|spec)\.ts$/.test(file.getFilePath()))
|
|
53
|
+
continue;
|
|
54
|
+
for (const cls of file.getClasses()) {
|
|
55
|
+
const moduleDec = cls.getDecorator("Module");
|
|
56
|
+
if (!moduleDec)
|
|
57
|
+
continue;
|
|
58
|
+
const guardNames = extractAppGuardNames(moduleDec, cls);
|
|
59
|
+
for (const name of guardNames) {
|
|
60
|
+
if (isAuthGuardName(name) && !analysis.globalAuthGuards.includes(name)) {
|
|
61
|
+
analysis.globalAuthGuards.push(name);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const hasGlobalAuthGuard = analysis.globalAuthGuards.length > 0;
|
|
47
67
|
for (const file of project.getSourceFiles()) {
|
|
48
68
|
// Skip node_modules and test files
|
|
49
69
|
if (file.getFilePath().includes("node_modules"))
|
|
@@ -59,6 +79,9 @@ function analyzeNestJSProject(projectRoot) {
|
|
|
59
79
|
const basePath = getStringArg(ctrlDec, 0) || "";
|
|
60
80
|
const classGuards = extractGuardNames(cls.getDecorator("UseGuards"));
|
|
61
81
|
const classPipes = extractGuardNames(cls.getDecorator("UsePipes"));
|
|
82
|
+
const classPublic = cls.getDecorator("Public") !== undefined
|
|
83
|
+
|| cls.getDecorator("SkipAuth") !== undefined
|
|
84
|
+
|| cls.getDecorator("AllowAnon") !== undefined;
|
|
62
85
|
for (const method of cls.getMethods()) {
|
|
63
86
|
const httpMethod = getHttpMethod(method);
|
|
64
87
|
if (!httpMethod)
|
|
@@ -70,29 +93,41 @@ function analyzeNestJSProject(projectRoot) {
|
|
|
70
93
|
const methodPipes = extractGuardNames(method.getDecorator("UsePipes"));
|
|
71
94
|
const guards = methodGuards.length > 0 ? methodGuards : classGuards;
|
|
72
95
|
const pipes = methodPipes.length > 0 ? methodPipes : classPipes;
|
|
96
|
+
const isPublicDecorated = classPublic
|
|
97
|
+
|| method.getDecorator("Public") !== undefined
|
|
98
|
+
|| method.getDecorator("SkipAuth") !== undefined
|
|
99
|
+
|| method.getDecorator("AllowAnon") !== undefined;
|
|
100
|
+
// 认证守卫 = 认证名分类后的守卫(ThrottlerGuard 等限流守卫不算认证)
|
|
101
|
+
const authGuards = guards.filter(isAuthGuardName);
|
|
73
102
|
const route = {
|
|
74
103
|
method: httpMethod,
|
|
75
104
|
path: fullPath,
|
|
76
105
|
controller: controllerName,
|
|
77
106
|
handler: method.getName() || "unknown",
|
|
78
|
-
hasAuthGuard:
|
|
107
|
+
hasAuthGuard: authGuards.length > 0,
|
|
79
108
|
hasValidationPipe: pipes.length > 0,
|
|
80
109
|
guards,
|
|
81
110
|
pipes,
|
|
111
|
+
isPublicDecorated,
|
|
82
112
|
};
|
|
83
113
|
analysis.routes.push(route);
|
|
114
|
+
// 路由级保护判定:类/方法认证守卫,或全局 APP_GUARD(除非 @Public 豁免)
|
|
115
|
+
const protectedByGlobal = hasGlobalAuthGuard && !isPublicDecorated;
|
|
84
116
|
// ── Security Checks ──
|
|
85
117
|
// POST/PUT/DELETE without auth guard
|
|
86
118
|
// Skip intentionally public routes (login, register, health, etc.)
|
|
87
119
|
if (["POST", "PUT", "DELETE", "PATCH"].includes(httpMethod) && !isPublicRoute(fullPath)) {
|
|
88
|
-
if (!route.hasAuthGuard) {
|
|
120
|
+
if (!route.hasAuthGuard && !protectedByGlobal) {
|
|
89
121
|
analysis.issues.push({
|
|
90
122
|
type: "NESTJS_NO_AUTH",
|
|
91
123
|
severity: "critical",
|
|
92
124
|
route: `${httpMethod} ${fullPath}`,
|
|
93
125
|
controller: controllerName,
|
|
94
|
-
message: `Mutation route ${httpMethod} ${fullPath} has no
|
|
95
|
-
|
|
126
|
+
message: `Mutation route ${httpMethod} ${fullPath} has no auth guard and ` +
|
|
127
|
+
(hasGlobalAuthGuard
|
|
128
|
+
? `is explicitly marked public — it bypasses the global ${analysis.globalAuthGuards.join("/")}.`
|
|
129
|
+
: `no global APP_GUARD protects the app. Anyone can call it.`),
|
|
130
|
+
fix: `Add @UseGuards(AuthGuard) to the method or controller class, or remove the @Public marker.`,
|
|
96
131
|
});
|
|
97
132
|
}
|
|
98
133
|
if (!route.hasValidationPipe) {
|
|
@@ -109,13 +144,14 @@ function analyzeNestJSProject(projectRoot) {
|
|
|
109
144
|
// Sensitive GET routes without auth
|
|
110
145
|
if (httpMethod === "GET") {
|
|
111
146
|
const sensitiveTerms = ["admin", "private", "secret", "manage"];
|
|
112
|
-
if (sensitiveTerms.some(t => fullPath.toLowerCase().includes(t))
|
|
147
|
+
if (sensitiveTerms.some(t => fullPath.toLowerCase().includes(t))
|
|
148
|
+
&& !route.hasAuthGuard && !protectedByGlobal) {
|
|
113
149
|
analysis.issues.push({
|
|
114
150
|
type: "NESTJS_SENSITIVE_PUBLIC",
|
|
115
151
|
severity: "high",
|
|
116
152
|
route: `${httpMethod} ${fullPath}`,
|
|
117
153
|
controller: controllerName,
|
|
118
|
-
message: `Sensitive GET route ${fullPath} is publicly accessible without
|
|
154
|
+
message: `Sensitive GET route ${fullPath} is publicly accessible without auth protection.`,
|
|
119
155
|
fix: `Add @UseGuards(AuthGuard) to protect this route.`,
|
|
120
156
|
});
|
|
121
157
|
}
|
|
@@ -126,6 +162,60 @@ function analyzeNestJSProject(projectRoot) {
|
|
|
126
162
|
return analysis;
|
|
127
163
|
}
|
|
128
164
|
// ── Helpers ──
|
|
165
|
+
/**
|
|
166
|
+
* 守卫名认证分类:auth/jwt/session/permission/role/access 等为认证守卫;
|
|
167
|
+
* throttler/rate/logger 等非认证守卫不算(限流≠认证——实测误报源)。
|
|
168
|
+
*/
|
|
169
|
+
function isAuthGuardName(name) {
|
|
170
|
+
const lower = name.toLowerCase();
|
|
171
|
+
if (/throttler|rate.?limit|logger|logging|cache/.test(lower))
|
|
172
|
+
return false;
|
|
173
|
+
return /auth|jwt|session|permission|role|access|apikey|api_key|token|login|passport/.test(lower);
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* 从 @Module 装饰器的 providers 提取 APP_GUARD 类名:
|
|
177
|
+
* providers: [{ provide: APP_GUARD, useClass: AuthGuard }]
|
|
178
|
+
* 支持装饰器参数对象字面量与类属性 providers 两种形态。
|
|
179
|
+
*/
|
|
180
|
+
function extractAppGuardNames(moduleDec, cls) {
|
|
181
|
+
const names = [];
|
|
182
|
+
const scanProviders = (arg) => {
|
|
183
|
+
if (!arg)
|
|
184
|
+
return;
|
|
185
|
+
// @Module({ providers: [...] })
|
|
186
|
+
const obj = arg.asKind(ts_morph_1.SyntaxKind.ObjectLiteralExpression);
|
|
187
|
+
if (obj) {
|
|
188
|
+
const providersProp = obj.getProperty("providers");
|
|
189
|
+
if (providersProp) {
|
|
190
|
+
const initializer = providersProp.getInitializer?.();
|
|
191
|
+
if (initializer && initializer.asKind(ts_morph_1.SyntaxKind.ArrayLiteralExpression)) {
|
|
192
|
+
for (const el of initializer.getElements()) {
|
|
193
|
+
const elObj = el.asKind(ts_morph_1.SyntaxKind.ObjectLiteralExpression);
|
|
194
|
+
if (!elObj)
|
|
195
|
+
continue;
|
|
196
|
+
const provideProp = elObj.getProperty("provide");
|
|
197
|
+
const useClassProp = elObj.getProperty("useClass");
|
|
198
|
+
const provideName = provideProp?.getInitializer?.()?.getText?.();
|
|
199
|
+
const useClassName = useClassProp?.getInitializer?.()?.getText?.();
|
|
200
|
+
if (provideName === "APP_GUARD" && useClassName) {
|
|
201
|
+
names.push(useClassName);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
// 装饰器参数形态
|
|
209
|
+
const decArg = moduleDec.getArguments()[0];
|
|
210
|
+
scanProviders(decArg);
|
|
211
|
+
// 类属性形态:providers = [...]
|
|
212
|
+
for (const prop of cls.getProperties()) {
|
|
213
|
+
if (prop.getName() === "providers") {
|
|
214
|
+
scanProviders(prop.getInitializer());
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return names;
|
|
218
|
+
}
|
|
129
219
|
/** Check if a route is intentionally public (login, register, health, etc.). */
|
|
130
220
|
function isPublicRoute(path) {
|
|
131
221
|
// Normalize: ensure leading slash for consistent matching
|
|
@@ -200,6 +290,7 @@ function analyzeNestJSFile(filePath) {
|
|
|
200
290
|
controllers: [],
|
|
201
291
|
routes: [],
|
|
202
292
|
issues: [],
|
|
293
|
+
globalAuthGuards: [], // 单文件分析无全局守卫上下文(项目级请用 analyzeNestJSProject)
|
|
203
294
|
};
|
|
204
295
|
for (const file of project.getSourceFiles()) {
|
|
205
296
|
for (const cls of file.getClasses()) {
|
|
@@ -227,10 +318,11 @@ function analyzeNestJSFile(filePath) {
|
|
|
227
318
|
path: fullPath,
|
|
228
319
|
controller: controllerName,
|
|
229
320
|
handler: method.getName() || "unknown",
|
|
230
|
-
hasAuthGuard: guards.length > 0,
|
|
321
|
+
hasAuthGuard: guards.filter(isAuthGuardName).length > 0,
|
|
231
322
|
hasValidationPipe: pipes.length > 0,
|
|
232
323
|
guards,
|
|
233
324
|
pipes,
|
|
325
|
+
isPublicDecorated: false, // 单文件分析不解析 @Public(项目级用 analyzeNestJSProject)
|
|
234
326
|
};
|
|
235
327
|
analysis.routes.push(route);
|
|
236
328
|
// Security checks
|
|
@@ -0,0 +1,140 @@
|
|
|
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
|
+
* nestjs-detector.test.ts — NestJS 补全轮回归(文件系统夹具,临时目录)
|
|
38
|
+
*
|
|
39
|
+
* 锁定三缺口修复:全局 APP_GUARD 识别、@Public 豁免、守卫名认证分类
|
|
40
|
+
* (ThrottlerGuard ≠ 认证)。
|
|
41
|
+
*/
|
|
42
|
+
const vitest_1 = require("vitest");
|
|
43
|
+
const fs = __importStar(require("fs"));
|
|
44
|
+
const os = __importStar(require("os"));
|
|
45
|
+
const path = __importStar(require("path"));
|
|
46
|
+
const nestjs_detector_1 = require("./nestjs-detector");
|
|
47
|
+
let dir;
|
|
48
|
+
(0, vitest_1.beforeEach)(() => {
|
|
49
|
+
dir = fs.mkdtempSync(path.join(os.tmpdir(), "nestjs-det-"));
|
|
50
|
+
});
|
|
51
|
+
(0, vitest_1.afterEach)(() => {
|
|
52
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
53
|
+
});
|
|
54
|
+
function write(rel, content) {
|
|
55
|
+
const full = path.join(dir, rel);
|
|
56
|
+
fs.mkdirSync(path.dirname(full), { recursive: true });
|
|
57
|
+
fs.writeFileSync(full, content);
|
|
58
|
+
}
|
|
59
|
+
const TSCONFIG = `{
|
|
60
|
+
"compilerOptions": { "target": "ES2020", "module": "commonjs",
|
|
61
|
+
"experimentalDecorators": true, "strict": false, "skipLibCheck": true },
|
|
62
|
+
"include": ["src/**/*"]
|
|
63
|
+
}`;
|
|
64
|
+
const GUARD = `
|
|
65
|
+
import { CanActivate } from "@nestjs/common";
|
|
66
|
+
export class AuthGuard implements CanActivate { canActivate(): boolean { return true; } }
|
|
67
|
+
`;
|
|
68
|
+
const CONTROLLER_BARE = `
|
|
69
|
+
import { Controller, Post, Get } from "@nestjs/common";
|
|
70
|
+
|
|
71
|
+
@Controller("api")
|
|
72
|
+
export class ApiController {
|
|
73
|
+
@Post("transfer")
|
|
74
|
+
transfer() { return "done"; }
|
|
75
|
+
}
|
|
76
|
+
`;
|
|
77
|
+
const MODULE_GLOBAL_GUARD = `
|
|
78
|
+
import { Module } from "@nestjs/common";
|
|
79
|
+
import { APP_GUARD } from "@nestjs/core";
|
|
80
|
+
import { AuthGuard } from "./auth.guard";
|
|
81
|
+
|
|
82
|
+
@Module({ providers: [{ provide: APP_GUARD, useClass: AuthGuard }] })
|
|
83
|
+
export class AppModule {}
|
|
84
|
+
`;
|
|
85
|
+
const MODULE_EMPTY = `
|
|
86
|
+
import { Module } from "@nestjs/common";
|
|
87
|
+
@Module({ providers: [] })
|
|
88
|
+
export class AppModule {}
|
|
89
|
+
`;
|
|
90
|
+
(0, vitest_1.describe)("nestjs-detector 补全", () => {
|
|
91
|
+
(0, vitest_1.it)("全局 APP_GUARD:无类/方法守卫的 mutation 路由不报 NO_AUTH", () => {
|
|
92
|
+
write("tsconfig.json", TSCONFIG);
|
|
93
|
+
write("src/auth.guard.ts", GUARD);
|
|
94
|
+
write("src/api.controller.ts", CONTROLLER_BARE);
|
|
95
|
+
write("src/app.module.ts", MODULE_GLOBAL_GUARD);
|
|
96
|
+
const { globalAuthGuards, issues } = (0, nestjs_detector_1.analyzeNestJSProject)(dir);
|
|
97
|
+
(0, vitest_1.expect)(globalAuthGuards).toContain("AuthGuard");
|
|
98
|
+
(0, vitest_1.expect)(issues.map((i) => i.type)).not.toContain("NESTJS_NO_AUTH");
|
|
99
|
+
});
|
|
100
|
+
(0, vitest_1.it)("@Public 绕过全局守卫 → NESTJS_NO_AUTH(显式绕过检出)", () => {
|
|
101
|
+
write("tsconfig.json", TSCONFIG);
|
|
102
|
+
write("src/auth.guard.ts", GUARD);
|
|
103
|
+
write("src/api.controller.ts", CONTROLLER_BARE.replace('@Post("transfer")', '@Public()\n @Post("transfer")'));
|
|
104
|
+
write("src/app.module.ts", MODULE_GLOBAL_GUARD);
|
|
105
|
+
const { issues } = (0, nestjs_detector_1.analyzeNestJSProject)(dir);
|
|
106
|
+
(0, vitest_1.expect)(issues.map((i) => i.type)).toContain("NESTJS_NO_AUTH");
|
|
107
|
+
});
|
|
108
|
+
(0, vitest_1.it)("ThrottlerGuard 不是认证守卫 → 仍报 NESTJS_NO_AUTH", () => {
|
|
109
|
+
write("tsconfig.json", TSCONFIG);
|
|
110
|
+
write("src/auth.guard.ts", GUARD);
|
|
111
|
+
write("src/api.controller.ts", CONTROLLER_BARE.replace('@Post("transfer")', '@UseGuards(ThrottlerGuard)\n @Post("transfer")').replace('import { Controller, Post, Get } from "@nestjs/common";', 'import { Controller, Post, Get, UseGuards } from "@nestjs/common";\nimport { ThrottlerGuard } from "@nestjs/throttler";'));
|
|
112
|
+
write("src/app.module.ts", MODULE_EMPTY);
|
|
113
|
+
const { issues } = (0, nestjs_detector_1.analyzeNestJSProject)(dir);
|
|
114
|
+
(0, vitest_1.expect)(issues.map((i) => i.type)).toContain("NESTJS_NO_AUTH");
|
|
115
|
+
});
|
|
116
|
+
(0, vitest_1.it)("无任何守卫且无全局守卫 → NESTJS_NO_AUTH", () => {
|
|
117
|
+
write("tsconfig.json", TSCONFIG);
|
|
118
|
+
write("src/auth.guard.ts", GUARD);
|
|
119
|
+
write("src/api.controller.ts", CONTROLLER_BARE);
|
|
120
|
+
write("src/app.module.ts", MODULE_EMPTY);
|
|
121
|
+
const { issues } = (0, nestjs_detector_1.analyzeNestJSProject)(dir);
|
|
122
|
+
(0, vitest_1.expect)(issues.map((i) => i.type)).toContain("NESTJS_NO_AUTH");
|
|
123
|
+
});
|
|
124
|
+
(0, vitest_1.it)("类级 AuthGuard 保护 → 不报 NESTJS_NO_AUTH", () => {
|
|
125
|
+
write("tsconfig.json", TSCONFIG);
|
|
126
|
+
write("src/auth.guard.ts", GUARD);
|
|
127
|
+
write("src/api.controller.ts", CONTROLLER_BARE.replace('@Controller("api")', '@Controller("api")\n@UseGuards(AuthGuard)').replace('import { Controller, Post, Get } from "@nestjs/common";', 'import { Controller, Post, Get, UseGuards } from "@nestjs/common";\nimport { AuthGuard } from "./auth.guard";'));
|
|
128
|
+
write("src/app.module.ts", MODULE_EMPTY);
|
|
129
|
+
const { issues } = (0, nestjs_detector_1.analyzeNestJSProject)(dir);
|
|
130
|
+
(0, vitest_1.expect)(issues.map((i) => i.type)).not.toContain("NESTJS_NO_AUTH");
|
|
131
|
+
});
|
|
132
|
+
(0, vitest_1.it)("@Public 登录入口在全局守卫下不报", () => {
|
|
133
|
+
write("tsconfig.json", TSCONFIG);
|
|
134
|
+
write("src/auth.guard.ts", GUARD);
|
|
135
|
+
write("src/api.controller.ts", CONTROLLER_BARE.replace('@Post("transfer")', '@Public()\n @Post("login")').replace('transfer() { return "done"; }', 'login() { return "token"; }'));
|
|
136
|
+
write("src/app.module.ts", MODULE_GLOBAL_GUARD);
|
|
137
|
+
const { issues } = (0, nestjs_detector_1.analyzeNestJSProject)(dir);
|
|
138
|
+
(0, vitest_1.expect)(issues.map((i) => i.type)).not.toContain("NESTJS_NO_AUTH");
|
|
139
|
+
});
|
|
140
|
+
});
|
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.11";
|
|
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
|
@@ -840,50 +840,32 @@ function collectNestJSViolations(ctx) {
|
|
|
840
840
|
let routes = 0;
|
|
841
841
|
let filesScanned = 0;
|
|
842
842
|
try {
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
const
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
const severity = issue.severity === "critical" ? "critical" :
|
|
870
|
-
issue.severity === "high" ? "high" :
|
|
871
|
-
issue.severity === "medium" ? "medium" : "low";
|
|
872
|
-
violations.push({
|
|
873
|
-
severity,
|
|
874
|
-
rule_id: issue.type,
|
|
875
|
-
file: path.relative(ctx.projectPath, file),
|
|
876
|
-
function: `${issue.controller}.${issue.route}`,
|
|
877
|
-
message: issue.message,
|
|
878
|
-
evidence: `NestJS route: ${issue.route} | Controller: ${issue.controller}`,
|
|
879
|
-
why: `NestJS decorator analysis: ${issue.type}`,
|
|
880
|
-
fix: issue.fix,
|
|
881
|
-
policy_ref: "framework.nestjs",
|
|
882
|
-
});
|
|
883
|
-
}
|
|
884
|
-
}
|
|
885
|
-
catch { /* skip unreadable files */ }
|
|
886
|
-
}
|
|
843
|
+
// 项目级分析(一次 ts-morph Project 装载):全局 APP_GUARD 守卫与
|
|
844
|
+
// @Public 豁免需要跨文件上下文——per-file 分析无法识别全局守卫,
|
|
845
|
+
// 会把受全局保护的 mutation 路由系统性误报(补全轮实测根因)。
|
|
846
|
+
const { analyzeNestJSProject } = require("../frameworks/nestjs-detector");
|
|
847
|
+
const analysis = analyzeNestJSProject(ctx.projectPath);
|
|
848
|
+
if (!analysis || analysis.controllers.length === 0) {
|
|
849
|
+
return { violations, coverage: { controllers, routes, filesScanned } };
|
|
850
|
+
}
|
|
851
|
+
controllers = analysis.controllers.length;
|
|
852
|
+
routes = analysis.routes.length;
|
|
853
|
+
filesScanned = 1; // 项目级单次装载(口径:分析单元 = 项目)
|
|
854
|
+
for (const issue of analysis.issues) {
|
|
855
|
+
const severity = issue.severity === "critical" ? "critical" :
|
|
856
|
+
issue.severity === "high" ? "high" :
|
|
857
|
+
issue.severity === "medium" ? "medium" : "low";
|
|
858
|
+
violations.push({
|
|
859
|
+
severity,
|
|
860
|
+
rule_id: issue.type,
|
|
861
|
+
file: issue.controller || "",
|
|
862
|
+
function: `${issue.controller}.${issue.route}`,
|
|
863
|
+
message: issue.message,
|
|
864
|
+
evidence: `NestJS route: ${issue.route} | Controller: ${issue.controller}`,
|
|
865
|
+
why: `NestJS decorator analysis: ${issue.type}`,
|
|
866
|
+
fix: issue.fix,
|
|
867
|
+
policy_ref: "framework.nestjs",
|
|
868
|
+
});
|
|
887
869
|
}
|
|
888
870
|
}
|
|
889
871
|
catch { /* best-effort */ }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "progmune-runtime",
|
|
3
|
-
"version": "3.7.
|
|
3
|
+
"version": "3.7.11",
|
|
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/",
|
|
@@ -8,7 +8,11 @@
|
|
|
8
8
|
"c-aliases.json",
|
|
9
9
|
"CHANGELOG.md",
|
|
10
10
|
"docs/Progmune_项目全解.html",
|
|
11
|
-
"docs/Progmune_投资人白皮书_v2.0.html"
|
|
11
|
+
"docs/Progmune_投资人白皮书_v2.0.html",
|
|
12
|
+
"tools/extract_ir.py",
|
|
13
|
+
"tools/extract_framework_py.py",
|
|
14
|
+
"tools/extract_framework_django.py",
|
|
15
|
+
"tools/extract_framework_flask.py"
|
|
12
16
|
],
|
|
13
17
|
"main": "dist/mcp-server.mjs",
|
|
14
18
|
"bin": {
|
|
@@ -110,4 +114,4 @@
|
|
|
110
114
|
"tsx": "^4.22.4",
|
|
111
115
|
"vitest": "^3.2.6"
|
|
112
116
|
}
|
|
113
|
-
}
|
|
117
|
+
}
|