progmune-runtime 3.7.10 → 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 CHANGED
@@ -1,5 +1,18 @@
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
+
3
16
  ## [3.7.10] — 2026-09-01
4
17
 
5
18
  ### 安装态 Python 全链路修复(npm 包正确性)
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: 7/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) have dedicated detectors; NestJS is partial. Next.js version-aware governance also exists. Spring Boot/Go frameworks and a few TS frameworks remain — framework adaptation is the #1 product gap.
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 partial (7/13)
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
- **框架适配器:7/13。** Express ✅、tRPC ✅、FastAPI ✅、Django ✅(结构级路由认证分析——逐条检查写操作入口的认证)、Flask ✅(路由/before_request 认证守卫分析)、Fastify ✅(路由 preHandler/钩子认证分析)与 Next.js ✅(App Router 路由处理器认证分析)有专用检测器;NestJS 部分支持,Next.js 另有版本感知治理。Spring Boot、Go 框架及少数 TS 框架待适配——框架适配是 #1 产品缺口。
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 部分(7/13)
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 验证 + 剩余框架内部边界误报
@@ -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: guards.length > 0,
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 @UseGuards. Anyone can call it.`,
95
- fix: `Add @UseGuards(AuthGuard) to the method or controller class.`,
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)) && !route.hasAuthGuard) {
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 @UseGuards.`,
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.10";
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)();
@@ -840,50 +840,32 @@ function collectNestJSViolations(ctx) {
840
840
  let routes = 0;
841
841
  let filesScanned = 0;
842
842
  try {
843
- const { analyzeNestJSFile } = require("../frameworks/nestjs-detector");
844
- const fs = require("fs");
845
- const candidateDirs = ["src", "server", "app", "api", "modules"];
846
- const extensions = languageToExtensions(ctx.language);
847
- for (const dir of candidateDirs) {
848
- const dirPath = path.join(ctx.projectPath, dir);
849
- if (!fs.existsSync(dirPath))
850
- continue;
851
- let files;
852
- try {
853
- files = walkDir(dirPath, extensions, 100);
854
- }
855
- catch {
856
- continue;
857
- }
858
- for (const file of files) {
859
- if (/\.(test|spec)\.(ts|tsx|js|jsx)$/.test(file))
860
- continue;
861
- filesScanned++;
862
- try {
863
- const analysis = analyzeNestJSFile(file);
864
- if (!analysis || analysis.controllers.length === 0)
865
- continue;
866
- controllers += analysis.controllers.length;
867
- routes += analysis.routes.length;
868
- for (const issue of analysis.issues) {
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.10",
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/",