progmune-runtime 3.7.10 → 3.7.12
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 +24 -0
- package/README.md +2 -2
- package/README.zh-CN.md +2 -2
- package/dist/frameworks/hapi-detector.js +132 -0
- package/dist/frameworks/hapi-detector.test.js +60 -0
- package/dist/frameworks/index.js +10 -1
- package/dist/frameworks/koa-detector.js +128 -0
- package/dist/frameworks/koa-detector.test.js +57 -0
- 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 +148 -44
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [3.7.12] — 2026-09-01
|
|
4
|
+
|
|
5
|
+
### Koa + Hapi 框架结构适配——10/13
|
|
6
|
+
|
|
7
|
+
- **Koa(第 9 个)**:`src/frameworks/koa-detector.ts`(代码串级,镜像 express/koa 中间件模式)——`KOA_ROUTE_NO_AUTH`:mutation 路由注册中间件链无认证名中间件,且文件内无认证 `app.use` 全局中间件;认证名按词表分类(auth/login/permission/jwt/verify/guard…);GET 读操作与认证入口路径豁免
|
|
8
|
+
- **Hapi(第 10 个)**:`src/frameworks/hapi-detector.ts`(路由配置模式)——`HAPI_ROUTE_NO_AUTH`:mutation 路由 options 无 auth 字段或显式 `auth: false`(显式公开 mutation 检出);`auth: 'strategy'` / `auth: { strategy: 'x' }` 两种形态均识别为受保护;`auth.strategy` 声明被记录
|
|
9
|
+
- **引擎接线**:collectKoaViolations / collectHapiViolations + overall `koaCoverage`/`hapiCoverage`(加性 best-effort);frameworks barrel 补全(11 个适配器模块)
|
|
10
|
+
- **验证**:15 个新单测(koa 7 + hapi 8,含显式 auth:false 与 strategy 对象形态);引擎冒烟:Koa 全局认证中间件 0 误报 APPROVED 90、Hapi 无 auth 路由检出 + login 豁免 APPROVED 87;全套件 **181/181**;Python 盲测 v1.2 64 零漂移
|
|
11
|
+
- **文档**:10/13 口径(README 双语/覆盖矩阵双语/CLAUDE.md/落地页 + Koa/Hapi 矩阵行)
|
|
12
|
+
- **边界(如实)**:文件级窗口(跨文件全局中间件不可见,与 Express 检测器同款);认证中间件按名词语汇识别;Hapi 路由窗口 500 字符(超长配置截断)
|
|
13
|
+
|
|
14
|
+
## [3.7.11] — 2026-09-01
|
|
15
|
+
|
|
16
|
+
### NestJS 补全——框架适配 8/13
|
|
17
|
+
|
|
18
|
+
- **三缺口补全**(`src/frameworks/nestjs-detector.ts`):
|
|
19
|
+
①**全局 APP_GUARD 守卫识别**——@Module providers 的 `{ provide: APP_GUARD, useClass: X }`(装饰器参数与类属性两种形态)→ 全局认证守卫存在时 mutation 路由不再误报;
|
|
20
|
+
②**@Public()/@SkipAuth()/@AllowAnon() 豁免装饰器**(类级+方法级)——全局守卫模式下的公开路由标记;**显式绕过全局守卫的 mutation 路由 → NESTJS_NO_AUTH**(消息注明绕过的是哪个全局守卫);
|
|
21
|
+
③**守卫名认证分类**——ThrottlerGuard/RateLimit/Logger 等非认证守卫不再算认证(限流≠认证,实测误报源)
|
|
22
|
+
- **引擎接线升级**:collectNestJSViolations 从 per-file 切 **项目级一次装载**(analyzeNestJSProject)——全局守卫与 @Public 需要跨文件上下文,per-file 分析无法识别全局守卫(系统性误报根因);coverage.filesScanned 口径改为分析单元=项目
|
|
23
|
+
- **合成金标**(`generate-projects-nestjs.ts` + `scan-protocol-nestjs.ts`):6 项目(类级守卫/全局 APP_GUARD+@Public login/无守卫/显式绕过/ThrottlerGuard 非认证/敏感 GET 公开)——**TP 12 / FP 0 / FN 0 → P=R=100%**
|
|
24
|
+
- **验证**:6 个新单测(含 @Public login 在全局守卫下不报);引擎冒烟 N2 APPROVED 87(全局守卫正确识别)、N4 BLOCKED(显式绕过 critical 拦截);全套件 166/166;Python 盲测 v1.2 64 零漂移;C 演示不变
|
|
25
|
+
- **文档**:NestJS ⚠️ 部分 → ✅ 结构分析(README 双语/覆盖矩阵双语/CLAUDE.md/落地页「8/13 专用检测器」口径)
|
|
26
|
+
|
|
3
27
|
## [3.7.10] — 2026-09-01
|
|
4
28
|
|
|
5
29
|
### 安装态 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:
|
|
111
|
+
**Framework adapters: 10/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), NestJS ✅ (decorator route parsing + global APP_GUARD recognition + @Public exemption), Koa ✅ (route/auth-middleware chain analysis) and Hapi ✅ (route-config auth strategy 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
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 ✅ (10/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
|
+
**框架适配器:10/13。** Express ✅、tRPC ✅、FastAPI ✅、Django ✅(结构级路由认证分析——逐条检查写操作入口的认证)、Flask ✅(路由/before_request 认证守卫分析)、Fastify ✅(路由 preHandler/钩子认证分析)与 Next.js ✅(App Router 路由处理器认证分析)、NestJS ✅(装饰器路由解析 + 全局 APP_GUARD 守卫识别 + @Public 豁免)、Koa ✅(路由/认证中间件链分析)与 Hapi ✅(路由配置认证策略分析)有专用检测器;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 ✅(10/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 验证 + 剩余框架内部边界误报
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Hapi Framework Adapter — Protocol Detection for Hapi
|
|
4
|
+
*
|
|
5
|
+
* 第 10 个框架适配(TS/JS 第 6 个专用检测器,代码串级):
|
|
6
|
+
*
|
|
7
|
+
* server.auth.strategy('jwt', 'jwt', {...}) 认证策略声明
|
|
8
|
+
* server.route({ method: 'POST', path: '/x',
|
|
9
|
+
* options: { auth: 'jwt' } }) 路由级认证
|
|
10
|
+
*
|
|
11
|
+
* 规则:
|
|
12
|
+
* HAPI_ROUTE_NO_AUTH mutation 路由(method 含 POST/PUT/PATCH/
|
|
13
|
+
* DELETE)的 options 无 auth 字段,或显式
|
|
14
|
+
* auth: false——路由级 missing-auth / 显式公开
|
|
15
|
+
*
|
|
16
|
+
* 口径(如实):
|
|
17
|
+
* - get 读操作不检查;认证入口路径词汇豁免(login/regist/auth/token)
|
|
18
|
+
* - 策略名本身不作为认证证明——必须出现在路由 options.auth
|
|
19
|
+
* (auth: 'jwt' / auth: { strategy: 'jwt' } 均识别)
|
|
20
|
+
* - auth: 'try'(可选认证)视为受保护(非公开)
|
|
21
|
+
*/
|
|
22
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
23
|
+
if (k2 === undefined) k2 = k;
|
|
24
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
25
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
26
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
27
|
+
}
|
|
28
|
+
Object.defineProperty(o, k2, desc);
|
|
29
|
+
}) : (function(o, m, k, k2) {
|
|
30
|
+
if (k2 === undefined) k2 = k;
|
|
31
|
+
o[k2] = m[k];
|
|
32
|
+
}));
|
|
33
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
34
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
35
|
+
}) : function(o, v) {
|
|
36
|
+
o["default"] = v;
|
|
37
|
+
});
|
|
38
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
39
|
+
var ownKeys = function(o) {
|
|
40
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
41
|
+
var ar = [];
|
|
42
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
43
|
+
return ar;
|
|
44
|
+
};
|
|
45
|
+
return ownKeys(o);
|
|
46
|
+
};
|
|
47
|
+
return function (mod) {
|
|
48
|
+
if (mod && mod.__esModule) return mod;
|
|
49
|
+
var result = {};
|
|
50
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
51
|
+
__setModuleDefault(result, mod);
|
|
52
|
+
return result;
|
|
53
|
+
};
|
|
54
|
+
})();
|
|
55
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
56
|
+
exports.analyzeHapiApp = analyzeHapiApp;
|
|
57
|
+
exports.analyzeHapiFile = analyzeHapiFile;
|
|
58
|
+
const fs = __importStar(require("fs"));
|
|
59
|
+
const MUTATION_METHODS = new Set(["post", "put", "patch", "delete"]);
|
|
60
|
+
const AUTH_ENTRY_WORDS = [
|
|
61
|
+
"login", "signin", "sign_in", "regist", "signup", "sign_up",
|
|
62
|
+
"token", "auth", "health",
|
|
63
|
+
];
|
|
64
|
+
function isAuthEntryPath(pathName) {
|
|
65
|
+
const lower = pathName.toLowerCase();
|
|
66
|
+
return AUTH_ENTRY_WORDS.some((w) => lower.includes(w));
|
|
67
|
+
}
|
|
68
|
+
// ── Analysis(代码串级) ──
|
|
69
|
+
function analyzeHapiApp(code) {
|
|
70
|
+
const issues = [];
|
|
71
|
+
const routes = [];
|
|
72
|
+
const strategies = [];
|
|
73
|
+
const hasHapi = /@hapi\/hapi|\bHapi\.server\b|\bhapi\.server\b/.test(code);
|
|
74
|
+
if (!hasHapi) {
|
|
75
|
+
return { hasHapi: false, routes, strategies, issues };
|
|
76
|
+
}
|
|
77
|
+
// 认证策略声明:server.auth.strategy('name', ...)
|
|
78
|
+
const strategyRe = /\.auth\.strategy\s*\(\s*['"]([^'"]+)['"]/g;
|
|
79
|
+
let m;
|
|
80
|
+
while ((m = strategyRe.exec(code)) !== null) {
|
|
81
|
+
strategies.push(m[1]);
|
|
82
|
+
}
|
|
83
|
+
// 路由块:server.route({ ... }) —— 从 route( 向后截 500 字符窗口
|
|
84
|
+
const routeRe = /\.route\s*\(\s*\{/g;
|
|
85
|
+
while ((m = routeRe.exec(code)) !== null) {
|
|
86
|
+
const window = code.slice(m.index, m.index + 500);
|
|
87
|
+
const methodM = window.match(/method\s*:\s*['"]([^'"]+)['"]/);
|
|
88
|
+
const pathM = window.match(/path\s*:\s*['"]([^'"]+)['"]/);
|
|
89
|
+
const authM = window.match(/auth\s*:\s*(?:['"]([^'"]+)['"]|\{\s*strategy\s*:\s*['"]([^'"]+)['"]|\s*(false|true))/);
|
|
90
|
+
if (!methodM || !pathM)
|
|
91
|
+
continue;
|
|
92
|
+
const method = methodM[1].toLowerCase();
|
|
93
|
+
const pathName = pathM[1];
|
|
94
|
+
// auth: false → 显式公开;无 auth 字段 → authM null
|
|
95
|
+
const authOption = authM
|
|
96
|
+
? (authM[3] === "false" ? "false" : authM[1] || authM[2] || authM[3] || null)
|
|
97
|
+
: null;
|
|
98
|
+
routes.push({
|
|
99
|
+
method,
|
|
100
|
+
path: pathName,
|
|
101
|
+
authOption,
|
|
102
|
+
line: code.slice(0, m.index).split("\n").length,
|
|
103
|
+
});
|
|
104
|
+
if (!MUTATION_METHODS.has(method))
|
|
105
|
+
continue;
|
|
106
|
+
if (isAuthEntryPath(pathName))
|
|
107
|
+
continue;
|
|
108
|
+
// 无 auth 字段(authOption null 且非显式 false 已涵盖)或显式 false → 报
|
|
109
|
+
if (authOption === null || authOption === "false") {
|
|
110
|
+
issues.push({
|
|
111
|
+
severity: "medium",
|
|
112
|
+
rule: "HAPI_ROUTE_NO_AUTH",
|
|
113
|
+
message: authOption === "false"
|
|
114
|
+
? `Mutation route ${method.toUpperCase()} ${pathName} is explicitly ` +
|
|
115
|
+
`public (auth: false) — any caller can reach it.`
|
|
116
|
+
: `Mutation route ${method.toUpperCase()} ${pathName} has no auth ` +
|
|
117
|
+
`option in its route config — any caller can reach it.`,
|
|
118
|
+
route: `${method.toUpperCase()} ${pathName}`,
|
|
119
|
+
line: code.slice(0, m.index).split("\n").length,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return { hasHapi: true, routes, strategies, issues };
|
|
124
|
+
}
|
|
125
|
+
function analyzeHapiFile(filePath) {
|
|
126
|
+
if (!fs.existsSync(filePath))
|
|
127
|
+
return null;
|
|
128
|
+
const code = fs.readFileSync(filePath, "utf-8");
|
|
129
|
+
if (!/@hapi\/hapi|@hapi\/hawk|\bHapi\.server\b/.test(code))
|
|
130
|
+
return null;
|
|
131
|
+
return analyzeHapiApp(code);
|
|
132
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/**
|
|
4
|
+
* hapi-detector.test.ts — Hapi 框架适配器规则回归(纯函数,无文件 I/O)
|
|
5
|
+
*/
|
|
6
|
+
const vitest_1 = require("vitest");
|
|
7
|
+
const hapi_detector_1 = require("./hapi-detector");
|
|
8
|
+
const app = (routes, extra = "") => `
|
|
9
|
+
import Hapi from "@hapi/hapi";
|
|
10
|
+
const server = Hapi.server({ port: 3000 });
|
|
11
|
+
${extra}
|
|
12
|
+
${routes}
|
|
13
|
+
`;
|
|
14
|
+
(0, vitest_1.describe)("hapi-detector", () => {
|
|
15
|
+
(0, vitest_1.it)("R1:无 auth 字段的 mutation 路由 → HAPI_ROUTE_NO_AUTH", () => {
|
|
16
|
+
const { issues } = (0, hapi_detector_1.analyzeHapiApp)(app(`
|
|
17
|
+
server.route({ method: "POST", path: "/transfer", handler: () => "ok" });
|
|
18
|
+
`));
|
|
19
|
+
(0, vitest_1.expect)(issues.map((i) => i.rule)).toContain("HAPI_ROUTE_NO_AUTH");
|
|
20
|
+
});
|
|
21
|
+
(0, vitest_1.it)("R1:options.auth 策略引用保护不报", () => {
|
|
22
|
+
const { issues } = (0, hapi_detector_1.analyzeHapiApp)(app(`
|
|
23
|
+
server.route({ method: "POST", path: "/transfer", options: { auth: "jwt" }, handler: () => "ok" });
|
|
24
|
+
`));
|
|
25
|
+
(0, vitest_1.expect)(issues).toHaveLength(0);
|
|
26
|
+
});
|
|
27
|
+
(0, vitest_1.it)("R1:auth 对象形态(strategy 字段)保护不报", () => {
|
|
28
|
+
const { issues } = (0, hapi_detector_1.analyzeHapiApp)(app(`
|
|
29
|
+
server.route({ method: "PUT", path: "/update", options: { auth: { strategy: "jwt" } }, handler: () => "ok" });
|
|
30
|
+
`));
|
|
31
|
+
(0, vitest_1.expect)(issues).toHaveLength(0);
|
|
32
|
+
});
|
|
33
|
+
(0, vitest_1.it)("R1:显式 auth: false → 报(显式公开 mutation)", () => {
|
|
34
|
+
const { issues } = (0, hapi_detector_1.analyzeHapiApp)(app(`
|
|
35
|
+
server.route({ method: "POST", path: "/transfer", options: { auth: false }, handler: () => "ok" });
|
|
36
|
+
`));
|
|
37
|
+
(0, vitest_1.expect)(issues.map((i) => i.rule)).toContain("HAPI_ROUTE_NO_AUTH");
|
|
38
|
+
});
|
|
39
|
+
(0, vitest_1.it)("R1:GET 读操作不报", () => {
|
|
40
|
+
const { issues } = (0, hapi_detector_1.analyzeHapiApp)(app(`
|
|
41
|
+
server.route({ method: "GET", path: "/articles", handler: () => [] });
|
|
42
|
+
`));
|
|
43
|
+
(0, vitest_1.expect)(issues).toHaveLength(0);
|
|
44
|
+
});
|
|
45
|
+
(0, vitest_1.it)("R1 豁免:login 认证入口路径不报", () => {
|
|
46
|
+
const { issues } = (0, hapi_detector_1.analyzeHapiApp)(app(`
|
|
47
|
+
server.route({ method: "POST", path: "/login", handler: () => "token" });
|
|
48
|
+
`));
|
|
49
|
+
(0, vitest_1.expect)(issues).toHaveLength(0);
|
|
50
|
+
});
|
|
51
|
+
(0, vitest_1.it)("策略声明被记录(auth.strategy 提取)", () => {
|
|
52
|
+
const { strategies } = (0, hapi_detector_1.analyzeHapiApp)(app(`server.route({ method: "GET", path: "/me", options: { auth: "jwt" }, handler: () => "me" });`, `server.auth.strategy("jwt", "jwt", { keys: ["secret"] });`));
|
|
53
|
+
(0, vitest_1.expect)(strategies).toContain("jwt");
|
|
54
|
+
});
|
|
55
|
+
(0, vitest_1.it)("非 Hapi 代码不产生任何问题", () => {
|
|
56
|
+
const { hasHapi, issues } = (0, hapi_detector_1.analyzeHapiApp)(`import express from "express"; const app = express(); app.post("/x", h);`);
|
|
57
|
+
(0, vitest_1.expect)(hasHapi).toBe(false);
|
|
58
|
+
(0, vitest_1.expect)(issues).toHaveLength(0);
|
|
59
|
+
});
|
|
60
|
+
});
|
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.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; } });
|
|
@@ -34,6 +34,15 @@ 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 koa_detector_1 = require("./koa-detector");
|
|
38
|
+
Object.defineProperty(exports, "analyzeKoaApp", { enumerable: true, get: function () { return koa_detector_1.analyzeKoaApp; } });
|
|
39
|
+
Object.defineProperty(exports, "analyzeKoaFile", { enumerable: true, get: function () { return koa_detector_1.analyzeKoaFile; } });
|
|
40
|
+
var hapi_detector_1 = require("./hapi-detector");
|
|
41
|
+
Object.defineProperty(exports, "analyzeHapiApp", { enumerable: true, get: function () { return hapi_detector_1.analyzeHapiApp; } });
|
|
42
|
+
Object.defineProperty(exports, "analyzeHapiFile", { enumerable: true, get: function () { return hapi_detector_1.analyzeHapiFile; } });
|
|
43
|
+
var nestjs_detector_1 = require("./nestjs-detector");
|
|
44
|
+
Object.defineProperty(exports, "analyzeNestJSProject", { enumerable: true, get: function () { return nestjs_detector_1.analyzeNestJSProject; } });
|
|
45
|
+
Object.defineProperty(exports, "analyzeNestJSFile", { enumerable: true, get: function () { return nestjs_detector_1.analyzeNestJSFile; } });
|
|
37
46
|
// ── Framework Version-Aware Governance ──
|
|
38
47
|
var version_awareness_1 = require("./version-awareness");
|
|
39
48
|
Object.defineProperty(exports, "detectFrameworks", { enumerable: true, get: function () { return version_awareness_1.detectFrameworks; } });
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Koa Framework Adapter — Protocol Detection for Koa
|
|
4
|
+
*
|
|
5
|
+
* 第 9 个框架适配(TS/JS 第 5 个专用检测器,代码串级镜像 express-detector):
|
|
6
|
+
*
|
|
7
|
+
* app.use(authMiddleware) 全局认证中间件
|
|
8
|
+
* router.post('/x', authMW, handler) 路由级认证中间件链
|
|
9
|
+
*
|
|
10
|
+
* 规则:
|
|
11
|
+
* KOA_ROUTE_NO_AUTH mutation 路由注册(post/put/patch/delete/del)
|
|
12
|
+
* 中间件链里没有认证名中间件,且文件内无认证
|
|
13
|
+
* 全局 app.use——路由级 missing-auth
|
|
14
|
+
*
|
|
15
|
+
* 口径(如实):
|
|
16
|
+
* - get 读操作不检查;认证入口路径词汇豁免(login/regist/auth/token)
|
|
17
|
+
* - 认证中间件按名字词表识别(auth/login/permission/token/session/
|
|
18
|
+
* jwt/verify/guard);自定义认证名不含词表漏判(保守方向=漏报)
|
|
19
|
+
* - 文件级窗口(与 Express 检测器同款):跨文件注册的全局中间件不可见
|
|
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.analyzeKoaApp = analyzeKoaApp;
|
|
56
|
+
exports.analyzeKoaFile = analyzeKoaFile;
|
|
57
|
+
const fs = __importStar(require("fs"));
|
|
58
|
+
const MUTATION_METHODS = new Set(["post", "put", "patch", "delete", "del"]);
|
|
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
|
+
return AUTH_FN_WORDS.some((w) => lower.includes(w));
|
|
74
|
+
}
|
|
75
|
+
// ── Analysis(代码串级) ──
|
|
76
|
+
function analyzeKoaApp(code) {
|
|
77
|
+
const issues = [];
|
|
78
|
+
const routes = [];
|
|
79
|
+
const authGlobalMiddleware = [];
|
|
80
|
+
const hasKoa = /\bKoa\b|\bkoa\b/.test(code);
|
|
81
|
+
if (!hasKoa) {
|
|
82
|
+
return { hasKoa: false, routes, authGlobalMiddleware, issues };
|
|
83
|
+
}
|
|
84
|
+
// 全局认证中间件:app.use(authFn)
|
|
85
|
+
const useRe = /\.use\s*\(\s*([A-Za-z_$][\w$]*)\s*\)/g;
|
|
86
|
+
let m;
|
|
87
|
+
while ((m = useRe.exec(code)) !== null) {
|
|
88
|
+
if (isAuthFnName(m[1]))
|
|
89
|
+
authGlobalMiddleware.push(m[1]);
|
|
90
|
+
}
|
|
91
|
+
// 路由注册:router.post('/x', mw1, mw2, handler) / .del()
|
|
92
|
+
const routeRe = /\.(get|post|put|patch|delete|del)\s*\(\s*['"]([^'"]+)['"]/g;
|
|
93
|
+
while ((m = routeRe.exec(code)) !== null) {
|
|
94
|
+
const method = m[1].toLowerCase() === "del" ? "delete" : m[1].toLowerCase();
|
|
95
|
+
const pathName = m[2];
|
|
96
|
+
// 从路径串向后截 300 字符窗口,收集中间件名(认证名判定)
|
|
97
|
+
const windowStart = m.index + m[0].length;
|
|
98
|
+
const window = code.slice(windowStart, windowStart + 300);
|
|
99
|
+
const mwNames = window.match(/[A-Za-z_$][\w$]*/g) || [];
|
|
100
|
+
const hasAuthMw = mwNames.some((name) => isAuthFnName(name));
|
|
101
|
+
routes.push({
|
|
102
|
+
method,
|
|
103
|
+
path: pathName,
|
|
104
|
+
protected: hasAuthMw,
|
|
105
|
+
line: code.slice(0, m.index).split("\n").length,
|
|
106
|
+
});
|
|
107
|
+
if (MUTATION_METHODS.has(method) && !hasAuthMw
|
|
108
|
+
&& authGlobalMiddleware.length === 0 && !isAuthEntryPath(pathName)) {
|
|
109
|
+
issues.push({
|
|
110
|
+
severity: "medium",
|
|
111
|
+
rule: "KOA_ROUTE_NO_AUTH",
|
|
112
|
+
message: `Route ${method.toUpperCase()} ${pathName} has no auth middleware ` +
|
|
113
|
+
`and the app registers no auth middleware — any caller can reach it.`,
|
|
114
|
+
route: `${method.toUpperCase()} ${pathName}`,
|
|
115
|
+
line: code.slice(0, m.index).split("\n").length,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return { hasKoa: true, routes, authGlobalMiddleware, issues };
|
|
120
|
+
}
|
|
121
|
+
function analyzeKoaFile(filePath) {
|
|
122
|
+
if (!fs.existsSync(filePath))
|
|
123
|
+
return null;
|
|
124
|
+
const code = fs.readFileSync(filePath, "utf-8");
|
|
125
|
+
if (!/from\s+['"]koa|require\(['"]koa/.test(code))
|
|
126
|
+
return null;
|
|
127
|
+
return analyzeKoaApp(code);
|
|
128
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/**
|
|
4
|
+
* koa-detector.test.ts — Koa 框架适配器规则回归(纯函数,无文件 I/O)
|
|
5
|
+
*/
|
|
6
|
+
const vitest_1 = require("vitest");
|
|
7
|
+
const koa_detector_1 = require("./koa-detector");
|
|
8
|
+
const app = (routes, extra = "") => `
|
|
9
|
+
import Koa from "koa";
|
|
10
|
+
import Router from "@koa/router";
|
|
11
|
+
const app = new Koa();
|
|
12
|
+
const router = new Router();
|
|
13
|
+
${extra}
|
|
14
|
+
${routes}
|
|
15
|
+
app.use(router.routes());
|
|
16
|
+
`;
|
|
17
|
+
(0, vitest_1.describe)("koa-detector", () => {
|
|
18
|
+
(0, vitest_1.it)("R1:无认证中间件的 mutation 路由 → KOA_ROUTE_NO_AUTH", () => {
|
|
19
|
+
const { issues } = (0, koa_detector_1.analyzeKoaApp)(app(`
|
|
20
|
+
router.post("/transfer", async (ctx) => { ctx.body = "ok"; });
|
|
21
|
+
`));
|
|
22
|
+
(0, vitest_1.expect)(issues.map((i) => i.rule)).toContain("KOA_ROUTE_NO_AUTH");
|
|
23
|
+
});
|
|
24
|
+
(0, vitest_1.it)("R1:路由级认证中间件保护不报", () => {
|
|
25
|
+
const { issues } = (0, koa_detector_1.analyzeKoaApp)(app(`
|
|
26
|
+
router.post("/transfer", authenticate, async (ctx) => { ctx.body = "ok"; });
|
|
27
|
+
`));
|
|
28
|
+
(0, vitest_1.expect)(issues).toHaveLength(0);
|
|
29
|
+
});
|
|
30
|
+
(0, vitest_1.it)("R1:全局 app.use 认证中间件保护不报", () => {
|
|
31
|
+
const { issues } = (0, koa_detector_1.analyzeKoaApp)(app(`router.post("/transfer", async (ctx) => { ctx.body = "ok"; });`, `app.use(authenticate);`));
|
|
32
|
+
(0, vitest_1.expect)(issues).toHaveLength(0);
|
|
33
|
+
});
|
|
34
|
+
(0, vitest_1.it)("R1:非认证中间件(日志)不视为保护", () => {
|
|
35
|
+
const { issues } = (0, koa_detector_1.analyzeKoaApp)(app(`
|
|
36
|
+
router.post("/transfer", logger, async (ctx) => { ctx.body = "ok"; });
|
|
37
|
+
`));
|
|
38
|
+
(0, vitest_1.expect)(issues.map((i) => i.rule)).toContain("KOA_ROUTE_NO_AUTH");
|
|
39
|
+
});
|
|
40
|
+
(0, vitest_1.it)("R1:GET 读操作不报", () => {
|
|
41
|
+
const { issues } = (0, koa_detector_1.analyzeKoaApp)(app(`
|
|
42
|
+
router.get("/articles", async (ctx) => { ctx.body = []; });
|
|
43
|
+
`));
|
|
44
|
+
(0, vitest_1.expect)(issues).toHaveLength(0);
|
|
45
|
+
});
|
|
46
|
+
(0, vitest_1.it)("R1 豁免:login 认证入口路径不报", () => {
|
|
47
|
+
const { issues } = (0, koa_detector_1.analyzeKoaApp)(app(`
|
|
48
|
+
router.post("/login", async (ctx) => { ctx.body = "token"; });
|
|
49
|
+
`));
|
|
50
|
+
(0, vitest_1.expect)(issues).toHaveLength(0);
|
|
51
|
+
});
|
|
52
|
+
(0, vitest_1.it)("非 Koa 代码不产生任何问题", () => {
|
|
53
|
+
const { hasKoa, issues } = (0, koa_detector_1.analyzeKoaApp)(`import express from "express"; const app = express(); app.post("/x", h);`);
|
|
54
|
+
(0, vitest_1.expect)(hasKoa).toBe(false);
|
|
55
|
+
(0, vitest_1.expect)(issues).toHaveLength(0);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -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.12";
|
|
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
|
@@ -67,6 +67,8 @@ const django_detector_1 = require("../frameworks/django-detector");
|
|
|
67
67
|
const flask_detector_1 = require("../frameworks/flask-detector");
|
|
68
68
|
const fastify_detector_1 = require("../frameworks/fastify-detector");
|
|
69
69
|
const nextjs_detector_1 = require("../frameworks/nextjs-detector");
|
|
70
|
+
const koa_detector_1 = require("../frameworks/koa-detector");
|
|
71
|
+
const hapi_detector_1 = require("../frameworks/hapi-detector");
|
|
70
72
|
const ssg_bridge_1 = require("./ssg-bridge");
|
|
71
73
|
const call_sequence_1 = require("../call-sequence");
|
|
72
74
|
const extract_ir_1 = require("../extract-ir");
|
|
@@ -102,6 +104,8 @@ async function evaluateTrust(ctx) {
|
|
|
102
104
|
const flaskResult = collectFlaskViolations(ctx);
|
|
103
105
|
const fastifyResult = collectFastifyViolations(ctx);
|
|
104
106
|
const nextjsResult = collectNextjsViolations(ctx);
|
|
107
|
+
const koaResult = collectKoaViolations(ctx);
|
|
108
|
+
const hapiResult = collectHapiViolations(ctx);
|
|
105
109
|
const coverageData = collectVerificationCoverage(ctx);
|
|
106
110
|
const governanceDefects = collectGovernanceDefects(ctx);
|
|
107
111
|
// ═══════════════════════════════════════
|
|
@@ -124,6 +128,8 @@ async function evaluateTrust(ctx) {
|
|
|
124
128
|
...flaskResult.violations,
|
|
125
129
|
...fastifyResult.violations,
|
|
126
130
|
...nextjsResult.violations,
|
|
131
|
+
...koaResult.violations,
|
|
132
|
+
...hapiResult.violations,
|
|
127
133
|
];
|
|
128
134
|
// ═══════════════════════════════════════
|
|
129
135
|
// PHASE 3: SCORE
|
|
@@ -288,6 +294,24 @@ async function evaluateTrust(ctx) {
|
|
|
288
294
|
issuesFound: nextjsResult.violations.length,
|
|
289
295
|
}
|
|
290
296
|
: undefined,
|
|
297
|
+
/** Koa framework adapter coverage — route/auth-middleware analysis */
|
|
298
|
+
koaCoverage: koaResult.coverage.routes > 0
|
|
299
|
+
? {
|
|
300
|
+
appsDetected: koaResult.coverage.apps,
|
|
301
|
+
totalRoutes: koaResult.coverage.routes,
|
|
302
|
+
filesScanned: koaResult.coverage.filesScanned,
|
|
303
|
+
issuesFound: koaResult.violations.length,
|
|
304
|
+
}
|
|
305
|
+
: undefined,
|
|
306
|
+
/** Hapi framework adapter coverage — route-config auth analysis */
|
|
307
|
+
hapiCoverage: hapiResult.coverage.routes > 0
|
|
308
|
+
? {
|
|
309
|
+
appsDetected: hapiResult.coverage.apps,
|
|
310
|
+
totalRoutes: hapiResult.coverage.routes,
|
|
311
|
+
filesScanned: hapiResult.coverage.filesScanned,
|
|
312
|
+
issuesFound: hapiResult.violations.length,
|
|
313
|
+
}
|
|
314
|
+
: undefined,
|
|
291
315
|
},
|
|
292
316
|
dimensions: {
|
|
293
317
|
policyCompliance: {
|
|
@@ -426,6 +450,104 @@ function mapPolicyViolation(rv, filePath, _enterprisePolicy) {
|
|
|
426
450
|
* Collect Express-specific security violations from the framework detector.
|
|
427
451
|
* Maps ExpressSecurityIssue[] → TrustViolation[].
|
|
428
452
|
*/
|
|
453
|
+
function collectKoaViolations(ctx) {
|
|
454
|
+
const violations = [];
|
|
455
|
+
const coverage = { apps: 0, routes: 0, filesScanned: 0 };
|
|
456
|
+
try {
|
|
457
|
+
const fs = require("fs");
|
|
458
|
+
const candidateDirs = ["src", "server", "app", "api", "routes", "lib"];
|
|
459
|
+
const extensions = languageToExtensions(ctx.language);
|
|
460
|
+
for (const dir of candidateDirs) {
|
|
461
|
+
const dirPath = path.join(ctx.projectPath, dir);
|
|
462
|
+
if (!fs.existsSync(dirPath))
|
|
463
|
+
continue;
|
|
464
|
+
let files;
|
|
465
|
+
try {
|
|
466
|
+
files = walkDir(dirPath, extensions, 100);
|
|
467
|
+
}
|
|
468
|
+
catch {
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
for (const file of files) {
|
|
472
|
+
if (/\.(test|spec)\.(ts|tsx|js|jsx)$/.test(file))
|
|
473
|
+
continue;
|
|
474
|
+
coverage.filesScanned++;
|
|
475
|
+
try {
|
|
476
|
+
const analysis = (0, koa_detector_1.analyzeKoaFile)(file);
|
|
477
|
+
if (!analysis || !analysis.hasKoa)
|
|
478
|
+
continue;
|
|
479
|
+
coverage.apps++;
|
|
480
|
+
coverage.routes += analysis.routes.length;
|
|
481
|
+
for (const issue of analysis.issues) {
|
|
482
|
+
violations.push({
|
|
483
|
+
severity: issue.severity === "low" ? "low" : issue.severity,
|
|
484
|
+
rule_id: issue.rule,
|
|
485
|
+
file: path.relative(ctx.projectPath, file),
|
|
486
|
+
function: "unknown",
|
|
487
|
+
message: issue.message,
|
|
488
|
+
evidence: issue.route || "",
|
|
489
|
+
why: `Framework structural analysis: ${issue.message}`,
|
|
490
|
+
fix: `Add an auth middleware to the route registration, or register an auth middleware globally with app.use.`,
|
|
491
|
+
policy_ref: "framework-safety.koa",
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
catch { /* skip unreadable files */ }
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
catch { /* best-effort */ }
|
|
500
|
+
return { violations, coverage };
|
|
501
|
+
}
|
|
502
|
+
function collectHapiViolations(ctx) {
|
|
503
|
+
const violations = [];
|
|
504
|
+
const coverage = { apps: 0, routes: 0, filesScanned: 0 };
|
|
505
|
+
try {
|
|
506
|
+
const fs = require("fs");
|
|
507
|
+
const candidateDirs = ["src", "server", "app", "api", "routes", "lib"];
|
|
508
|
+
const extensions = languageToExtensions(ctx.language);
|
|
509
|
+
for (const dir of candidateDirs) {
|
|
510
|
+
const dirPath = path.join(ctx.projectPath, dir);
|
|
511
|
+
if (!fs.existsSync(dirPath))
|
|
512
|
+
continue;
|
|
513
|
+
let files;
|
|
514
|
+
try {
|
|
515
|
+
files = walkDir(dirPath, extensions, 100);
|
|
516
|
+
}
|
|
517
|
+
catch {
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
for (const file of files) {
|
|
521
|
+
if (/\.(test|spec)\.(ts|tsx|js|jsx)$/.test(file))
|
|
522
|
+
continue;
|
|
523
|
+
coverage.filesScanned++;
|
|
524
|
+
try {
|
|
525
|
+
const analysis = (0, hapi_detector_1.analyzeHapiFile)(file);
|
|
526
|
+
if (!analysis || !analysis.hasHapi)
|
|
527
|
+
continue;
|
|
528
|
+
coverage.apps++;
|
|
529
|
+
coverage.routes += analysis.routes.length;
|
|
530
|
+
for (const issue of analysis.issues) {
|
|
531
|
+
violations.push({
|
|
532
|
+
severity: issue.severity === "low" ? "low" : issue.severity,
|
|
533
|
+
rule_id: issue.rule,
|
|
534
|
+
file: path.relative(ctx.projectPath, file),
|
|
535
|
+
function: "unknown",
|
|
536
|
+
message: issue.message,
|
|
537
|
+
evidence: issue.route || "",
|
|
538
|
+
why: `Framework structural analysis: ${issue.message}`,
|
|
539
|
+
fix: `Add an auth strategy reference to the route options (auth: "<strategy>"), or remove the explicit auth: false.`,
|
|
540
|
+
policy_ref: "framework-safety.hapi",
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
catch { /* skip unreadable files */ }
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
catch { /* best-effort */ }
|
|
549
|
+
return { violations, coverage };
|
|
550
|
+
}
|
|
429
551
|
function collectNextjsViolations(ctx) {
|
|
430
552
|
const violations = [];
|
|
431
553
|
const coverage = { apps: 0, routes: 0, filesScanned: 0 };
|
|
@@ -840,50 +962,32 @@ function collectNestJSViolations(ctx) {
|
|
|
840
962
|
let routes = 0;
|
|
841
963
|
let filesScanned = 0;
|
|
842
964
|
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
|
-
}
|
|
965
|
+
// 项目级分析(一次 ts-morph Project 装载):全局 APP_GUARD 守卫与
|
|
966
|
+
// @Public 豁免需要跨文件上下文——per-file 分析无法识别全局守卫,
|
|
967
|
+
// 会把受全局保护的 mutation 路由系统性误报(补全轮实测根因)。
|
|
968
|
+
const { analyzeNestJSProject } = require("../frameworks/nestjs-detector");
|
|
969
|
+
const analysis = analyzeNestJSProject(ctx.projectPath);
|
|
970
|
+
if (!analysis || analysis.controllers.length === 0) {
|
|
971
|
+
return { violations, coverage: { controllers, routes, filesScanned } };
|
|
972
|
+
}
|
|
973
|
+
controllers = analysis.controllers.length;
|
|
974
|
+
routes = analysis.routes.length;
|
|
975
|
+
filesScanned = 1; // 项目级单次装载(口径:分析单元 = 项目)
|
|
976
|
+
for (const issue of analysis.issues) {
|
|
977
|
+
const severity = issue.severity === "critical" ? "critical" :
|
|
978
|
+
issue.severity === "high" ? "high" :
|
|
979
|
+
issue.severity === "medium" ? "medium" : "low";
|
|
980
|
+
violations.push({
|
|
981
|
+
severity,
|
|
982
|
+
rule_id: issue.type,
|
|
983
|
+
file: issue.controller || "",
|
|
984
|
+
function: `${issue.controller}.${issue.route}`,
|
|
985
|
+
message: issue.message,
|
|
986
|
+
evidence: `NestJS route: ${issue.route} | Controller: ${issue.controller}`,
|
|
987
|
+
why: `NestJS decorator analysis: ${issue.type}`,
|
|
988
|
+
fix: issue.fix,
|
|
989
|
+
policy_ref: "framework.nestjs",
|
|
990
|
+
});
|
|
887
991
|
}
|
|
888
992
|
}
|
|
889
993
|
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.12",
|
|
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/",
|