progmune-runtime 3.7.14 → 3.7.16
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/dist/frameworks/django-detector.js +6 -0
- package/dist/frameworks/django-detector.test.js +29 -0
- package/dist/frameworks/express-detector.js +59 -32
- package/dist/frameworks/express-detector.test.js +69 -0
- package/dist/frameworks/fastify-detector.js +91 -27
- package/dist/frameworks/fastify-detector.test.js +99 -0
- package/dist/frameworks/fiber-detector.js +266 -9
- package/dist/frameworks/fiber-detector.test.js +188 -0
- package/dist/frameworks/gin-detector.js +123 -10
- package/dist/frameworks/gin-detector.test.js +183 -0
- package/dist/frameworks/hapi-detector.js +105 -31
- package/dist/frameworks/hapi-detector.test.js +91 -0
- package/dist/frameworks/koa-detector.js +25 -10
- package/dist/frameworks/koa-detector.test.js +38 -0
- package/dist/frameworks/nestjs-detector.js +145 -3
- package/dist/frameworks/nestjs-detector.test.js +103 -0
- package/dist/frameworks/nextjs-detector.js +4 -1
- package/dist/frameworks/nextjs-detector.test.js +35 -0
- package/dist/frameworks/route-window.js +124 -0
- package/dist/frameworks/trpc-detector.js +82 -16
- package/dist/frameworks/trpc-detector.test.js +177 -0
- package/dist/sdk.js +1 -1
- package/package.json +4 -3
- package/tools/extract_framework_django.py +62 -1
- package/tools/extract_framework_flask.py +3 -1
- package/tools/extract_framework_py.py +6 -2
|
@@ -26,7 +26,9 @@ exports.extractRouterNames = extractRouterNames;
|
|
|
26
26
|
exports.extractProcedures = extractProcedures;
|
|
27
27
|
exports.analyzeTRPCFile = analyzeTRPCFile;
|
|
28
28
|
// ── Detection Patterns ──
|
|
29
|
-
|
|
29
|
+
// 注意:detect 用途不带 /g——带 /g 的 test() 会跨文件泄漏 lastIndex,
|
|
30
|
+
// 导致逐文件扫描结果随顺序漂移(实测 4/19 vs 7/19)
|
|
31
|
+
const PROCEDURE_TYPE_PATTERN = /\b(publicProcedure|protectedProcedure|adminProcedure)\b/;
|
|
30
32
|
const DB_WRITE_PATTERN = /\b(db\.(insert|update|delete|create|upsert|execute)|prisma\.\w+\.(create|update|delete|upsert|createMany|updateMany|deleteMany)|drizzle\.(insert|update|delete)|\.(insert|update|delete|create|upsert)\s*\()/i;
|
|
31
33
|
/**
|
|
32
34
|
* Detect whether this file contains tRPC definitions.
|
|
@@ -57,30 +59,94 @@ function extractRouterNames(code) {
|
|
|
57
59
|
*/
|
|
58
60
|
function extractProcedures(code) {
|
|
59
61
|
const procedures = [];
|
|
60
|
-
//
|
|
61
|
-
|
|
62
|
+
// 过程起点:name: <procedureType>(不含链)——v11 惯用法 t.procedure
|
|
63
|
+
// (V4 遗留缺口:只认 XxxProcedure 命名包装,内联 t.procedure 不可见)
|
|
64
|
+
const procStartRe = /([A-Za-z_][A-Za-z0-9_]*)\s*:\s*(t\s*\.\s*procedure|publicProcedure|protectedProcedure|adminProcedure)\b/g;
|
|
62
65
|
let m;
|
|
63
|
-
while ((m =
|
|
66
|
+
while ((m = procStartRe.exec(code)) !== null) {
|
|
64
67
|
const name = m[1];
|
|
65
|
-
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
|
|
68
|
+
// t.procedure = v11 基础构造器(无包装 → 默认公开语义);命名包装去掉后缀
|
|
69
|
+
const procType = (m[2].includes(".") ? "public" : m[2].replace(/Procedure$/, ""));
|
|
70
|
+
// ── 链扫描(括号感知)──
|
|
71
|
+
// 自 procedure 类型后逐个解析 .method(balancedArgs),容忍嵌套括号与
|
|
72
|
+
// 多行(.input(z.object({...})) 等标准形态),直至 .query(/.mutation(
|
|
73
|
+
// 或链中断。旧实现用 (?:\.\w+\([^()]*\))* 不跨嵌套括号 → 标准
|
|
74
|
+
// zod input 链整体失明(V4 缺陷)。
|
|
75
|
+
let pos = procStartRe.lastIndex;
|
|
76
|
+
let hasInputSchema = false;
|
|
77
|
+
let kind = null;
|
|
78
|
+
let kindOpenIdx = -1; // .query( 或 .mutation( 的 '(' 下标
|
|
79
|
+
const skipWs = () => {
|
|
80
|
+
while (pos < code.length && /\s/.test(code[pos]))
|
|
81
|
+
pos++;
|
|
82
|
+
};
|
|
83
|
+
const consumeBalanced = (open, close) => {
|
|
84
|
+
let depth = 1;
|
|
85
|
+
let quote = null;
|
|
86
|
+
pos++; // 跳过 open
|
|
87
|
+
while (pos < code.length && depth > 0) {
|
|
88
|
+
const ch = code[pos];
|
|
89
|
+
if (quote) {
|
|
90
|
+
if (ch === quote && code[pos - 1] !== "\\")
|
|
91
|
+
quote = null;
|
|
92
|
+
}
|
|
93
|
+
else if (ch === '"' || ch === "'" || ch === "`") {
|
|
94
|
+
quote = ch;
|
|
95
|
+
}
|
|
96
|
+
else if (ch === open) {
|
|
97
|
+
depth++;
|
|
98
|
+
}
|
|
99
|
+
else if (ch === close) {
|
|
100
|
+
depth--;
|
|
101
|
+
}
|
|
102
|
+
pos++;
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
for (let step = 0; step < 100; step++) {
|
|
106
|
+
skipWs();
|
|
107
|
+
if (code[pos] !== ".")
|
|
108
|
+
break;
|
|
109
|
+
pos++;
|
|
110
|
+
const methStart = pos;
|
|
111
|
+
while (pos < code.length && /[A-Za-z0-9_$]/.test(code[pos]))
|
|
112
|
+
pos++;
|
|
113
|
+
const method = code.slice(methStart, pos);
|
|
114
|
+
skipWs();
|
|
115
|
+
if (code[pos] !== "(")
|
|
116
|
+
break;
|
|
117
|
+
if (method === "query" || method === "mutation") {
|
|
118
|
+
kind = method;
|
|
119
|
+
kindOpenIdx = pos; // '(' 位置
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
if (method === "input")
|
|
123
|
+
hasInputSchema = true;
|
|
124
|
+
consumeBalanced("(", ")");
|
|
125
|
+
}
|
|
126
|
+
if (kind === null || kindOpenIdx < 0)
|
|
127
|
+
continue; // 非完整过程定义
|
|
128
|
+
// ── body:自 kind 的 '(' 后到匹配闭合括号(字符串感知)──
|
|
73
129
|
let depth = 1;
|
|
74
|
-
let closeIdx =
|
|
130
|
+
let closeIdx = kindOpenIdx + 1;
|
|
131
|
+
let quote = null;
|
|
75
132
|
while (closeIdx < code.length && depth > 0) {
|
|
76
133
|
const ch = code[closeIdx];
|
|
77
|
-
if (
|
|
134
|
+
if (quote) {
|
|
135
|
+
if (ch === quote && code[closeIdx - 1] !== "\\")
|
|
136
|
+
quote = null;
|
|
137
|
+
}
|
|
138
|
+
else if (ch === '"' || ch === "'" || ch === "`") {
|
|
139
|
+
quote = ch;
|
|
140
|
+
}
|
|
141
|
+
else if (ch === "(") {
|
|
78
142
|
depth++;
|
|
79
|
-
|
|
143
|
+
}
|
|
144
|
+
else if (ch === ")") {
|
|
80
145
|
depth--;
|
|
146
|
+
}
|
|
81
147
|
closeIdx++;
|
|
82
148
|
}
|
|
83
|
-
const body = code.slice(
|
|
149
|
+
const body = code.slice(kindOpenIdx + 1, Math.min(closeIdx - 1, kindOpenIdx + 2000));
|
|
84
150
|
const usesInputInBody = /input\s*\./.test(body) || /\binput\b/.test(body);
|
|
85
151
|
const doesDbWrite = DB_WRITE_PATTERN.test(body);
|
|
86
152
|
procedures.push({
|
|
@@ -0,0 +1,177 @@
|
|
|
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
|
+
* trpc-detector.test.ts — tRPC 检测器回归(纯函数,无文件 I/O)
|
|
38
|
+
*
|
|
39
|
+
* 覆盖 V4 真实语料(netflx-web)暴露的两项缺陷:
|
|
40
|
+
* 1. 链匹配正则不跨嵌套括号 → 标准 .input(z.object({...})) 过程失明
|
|
41
|
+
* 2. PROCEDURE_TYPE_PATTERN /g lastIndex 泄漏 → 逐文件扫描漂移
|
|
42
|
+
*/
|
|
43
|
+
const vitest_1 = require("vitest");
|
|
44
|
+
const trpc_detector_1 = require("./trpc-detector");
|
|
45
|
+
const fs = __importStar(require("fs"));
|
|
46
|
+
const os = __importStar(require("os"));
|
|
47
|
+
const path = __importStar(require("path"));
|
|
48
|
+
// ── 修复 1:标准 .input 链(嵌套括号/多行)必须可见 ──
|
|
49
|
+
const ROUTER_WITH_INPUT = `
|
|
50
|
+
const t = initTRPC.context<{ db: Db }>().create();
|
|
51
|
+
export const postRouter = t.router({
|
|
52
|
+
addComment: protectedProcedure
|
|
53
|
+
.input(
|
|
54
|
+
z.object({
|
|
55
|
+
articleId: z.string(),
|
|
56
|
+
body: z.string().min(1),
|
|
57
|
+
})
|
|
58
|
+
)
|
|
59
|
+
.mutation(async ({ ctx, input }) => {
|
|
60
|
+
await ctx.db.comment.create({ data: { articleId: input.articleId } });
|
|
61
|
+
}),
|
|
62
|
+
list: publicProcedure.query(async ({ ctx }) => ctx.db.comment.findMany()),
|
|
63
|
+
});`;
|
|
64
|
+
const ROUTER_WITH_BARE_MUTATION = `
|
|
65
|
+
export const appRouter = t.router({
|
|
66
|
+
deleteAll: publicProcedure.mutation(async ({ ctx, input }) => {
|
|
67
|
+
await ctx.prisma.post.deleteMany();
|
|
68
|
+
}),
|
|
69
|
+
});`;
|
|
70
|
+
(0, vitest_1.describe)("trpc extractProcedures — 括号感知链", () => {
|
|
71
|
+
(0, vitest_1.it)("标准多行 .input(z.object({...})) mutation 可见且有 input schema(V4 缺陷回归)", () => {
|
|
72
|
+
const procs = (0, trpc_detector_1.extractProcedures)(ROUTER_WITH_INPUT);
|
|
73
|
+
const add = procs.find((p) => p.name === "addComment");
|
|
74
|
+
(0, vitest_1.expect)(add).toBeDefined();
|
|
75
|
+
(0, vitest_1.expect)(add.kind).toBe("mutation");
|
|
76
|
+
(0, vitest_1.expect)(add.procedureType).toBe("protected");
|
|
77
|
+
(0, vitest_1.expect)(add.hasInputSchema).toBe(true);
|
|
78
|
+
// 无 schema 的 query 也照常可见
|
|
79
|
+
(0, vitest_1.expect)(procs.some((p) => p.name === "list" && p.hasInputSchema === false)).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
(0, vitest_1.it)("单行 .input(z.string()) 链可见", () => {
|
|
82
|
+
const procs = (0, trpc_detector_1.extractProcedures)(`
|
|
83
|
+
export const r = t.router({
|
|
84
|
+
getOne: protectedProcedure.input(z.string()).query(async ({ ctx, input }) => {
|
|
85
|
+
return ctx.db.get(input);
|
|
86
|
+
}),
|
|
87
|
+
});`);
|
|
88
|
+
const p = procs.find((x) => x.name === "getOne");
|
|
89
|
+
(0, vitest_1.expect)(p).toBeDefined();
|
|
90
|
+
(0, vitest_1.expect)(p.hasInputSchema).toBe(true);
|
|
91
|
+
});
|
|
92
|
+
(0, vitest_1.it)("裸链 mutation(无 input)仍可见并可触发规则", () => {
|
|
93
|
+
const procs = (0, trpc_detector_1.extractProcedures)(ROUTER_WITH_BARE_MUTATION);
|
|
94
|
+
const del = procs.find((p) => p.name === "deleteAll");
|
|
95
|
+
(0, vitest_1.expect)(del).toBeDefined();
|
|
96
|
+
(0, vitest_1.expect)(del.kind).toBe("mutation");
|
|
97
|
+
(0, vitest_1.expect)(del.hasInputSchema).toBe(false);
|
|
98
|
+
(0, vitest_1.expect)(del.doesDbWrite).toBe(true);
|
|
99
|
+
});
|
|
100
|
+
(0, vitest_1.it)("完整分析:合规 router 0 issues,裸 public mutation 报 TRPC_PUBLIC_MUTATION", () => {
|
|
101
|
+
const tmp = path.join(os.tmpdir(), "trpc-good-router.ts");
|
|
102
|
+
fs.writeFileSync(tmp, ROUTER_WITH_INPUT);
|
|
103
|
+
try {
|
|
104
|
+
const good = (0, trpc_detector_1.analyzeTRPCFile)(tmp);
|
|
105
|
+
(0, vitest_1.expect)(good.issues).toHaveLength(0);
|
|
106
|
+
(0, vitest_1.expect)(good.procedures.length).toBe(2);
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
fs.unlinkSync(tmp);
|
|
110
|
+
}
|
|
111
|
+
const tmp2 = path.join(os.tmpdir(), "trpc-bad-router.ts");
|
|
112
|
+
fs.writeFileSync(tmp2, ROUTER_WITH_BARE_MUTATION);
|
|
113
|
+
try {
|
|
114
|
+
const bad = (0, trpc_detector_1.analyzeTRPCFile)(tmp2);
|
|
115
|
+
(0, vitest_1.expect)(bad.issues.map((i) => i.rule)).toContain("TRPC_PUBLIC_MUTATION");
|
|
116
|
+
(0, vitest_1.expect)(bad.issues.map((i) => i.rule)).toContain("TRPC_MUTATION_WITHOUT_INPUT_SCHEMA");
|
|
117
|
+
}
|
|
118
|
+
finally {
|
|
119
|
+
fs.unlinkSync(tmp2);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
// ── 修复 2:lastIndex 泄漏回归 ──
|
|
124
|
+
(0, vitest_1.describe)("detectTRPCApp — 无 /g lastIndex 泄漏", () => {
|
|
125
|
+
(0, vitest_1.it)("连续多次调用结果稳定(旧 /g 实现会漂移)", () => {
|
|
126
|
+
const trpcCode = `const t = initTRPC.create(); export const r = t.router({ a: publicProcedure.query(() => 1) });`;
|
|
127
|
+
const plainCode = `export const sum = (a: number, b: number) => a + b;`;
|
|
128
|
+
// 交替调用多次:泄漏时第二次起结果不稳定
|
|
129
|
+
const results = [];
|
|
130
|
+
for (let i = 0; i < 6; i++) {
|
|
131
|
+
results.push((0, trpc_detector_1.detectTRPCApp)(trpcCode)); // 应为 true
|
|
132
|
+
results.push((0, trpc_detector_1.detectTRPCApp)(plainCode)); // 应为 false
|
|
133
|
+
}
|
|
134
|
+
(0, vitest_1.expect)(results.filter(Boolean)).toHaveLength(6); // 恰好 6 个 true
|
|
135
|
+
(0, vitest_1.expect)(results).toEqual([
|
|
136
|
+
true, false, true, false, true, false, true, false, true, false, true, false,
|
|
137
|
+
]);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
// ── tRPC v11:内联 t.procedure 形态(V4 遗留缺口)──
|
|
141
|
+
(0, vitest_1.describe)("trpc v11 t.procedure 支持", () => {
|
|
142
|
+
(0, vitest_1.it)("t.procedure.input(z.object).mutation 可见且视为公开(默认语义)", () => {
|
|
143
|
+
const procs = (0, trpc_detector_1.extractProcedures)(`
|
|
144
|
+
import { initTRPC } from "@trpc/server";
|
|
145
|
+
const t = initTRPC.create();
|
|
146
|
+
export const r = t.router({
|
|
147
|
+
ping: t.procedure
|
|
148
|
+
.input(z.object({ msg: z.string() }))
|
|
149
|
+
.mutation(async ({ ctx, input }) => {
|
|
150
|
+
await ctx.prisma.log.create({ data: { msg: input.msg } });
|
|
151
|
+
}),
|
|
152
|
+
list: t.procedure.query(async () => []),
|
|
153
|
+
});`);
|
|
154
|
+
const ping = procs.find((p) => p.name === "ping");
|
|
155
|
+
(0, vitest_1.expect)(ping).toBeDefined();
|
|
156
|
+
(0, vitest_1.expect)(ping.kind).toBe("mutation");
|
|
157
|
+
(0, vitest_1.expect)(ping.procedureType).toBe("public");
|
|
158
|
+
(0, vitest_1.expect)(ping.hasInputSchema).toBe(true);
|
|
159
|
+
(0, vitest_1.expect)(procs.some((p) => p.name === "list")).toBe(true);
|
|
160
|
+
});
|
|
161
|
+
(0, vitest_1.it)("裸 t.procedure mutation(无 input)触发规则(敏感性与命名包装一致)", () => {
|
|
162
|
+
const procs = (0, trpc_detector_1.extractProcedures)(`
|
|
163
|
+
export const r = t.router({
|
|
164
|
+
nuke: t.procedure.mutation(async ({ ctx }) => {
|
|
165
|
+
await ctx.prisma.post.deleteMany();
|
|
166
|
+
}),
|
|
167
|
+
});`);
|
|
168
|
+
const nuke = procs.find((p) => p.name === "nuke");
|
|
169
|
+
(0, vitest_1.expect)(nuke).toBeDefined();
|
|
170
|
+
(0, vitest_1.expect)(nuke.hasInputSchema).toBe(false);
|
|
171
|
+
(0, vitest_1.expect)(nuke.doesDbWrite).toBe(true);
|
|
172
|
+
});
|
|
173
|
+
(0, vitest_1.it)("netflx v10 命名包装形态不受影响(19/19 保持)", () => {
|
|
174
|
+
const procs = (0, trpc_detector_1.extractProcedures)(ROUTER_WITH_INPUT);
|
|
175
|
+
(0, vitest_1.expect)(procs.length).toBe(2);
|
|
176
|
+
});
|
|
177
|
+
});
|
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.16";
|
|
24
24
|
function verify(filePath) {
|
|
25
25
|
const cert = (0, certify_1.certify)(filePath);
|
|
26
26
|
const kb = (0, protocol_knowledge_1.buildKnowledgeBase)();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "progmune-runtime",
|
|
3
|
-
"version": "3.7.
|
|
3
|
+
"version": "3.7.16",
|
|
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/",
|
|
@@ -88,7 +88,8 @@
|
|
|
88
88
|
"test:soak": "vitest run tests/soak/",
|
|
89
89
|
"test:p7": "vitest run tests/p7-*/",
|
|
90
90
|
"fetch:cves": "tsx scripts/fetch-cves.ts",
|
|
91
|
-
"test:cve": "vitest run src/cve-benchmark.test.ts"
|
|
91
|
+
"test:cve": "vitest run src/cve-benchmark.test.ts",
|
|
92
|
+
"audit:realworld": "node scripts/realworld-audit.js"
|
|
92
93
|
},
|
|
93
94
|
"keywords": [
|
|
94
95
|
"program-synthesis",
|
|
@@ -114,4 +115,4 @@
|
|
|
114
115
|
"tsx": "^4.22.4",
|
|
115
116
|
"vitest": "^3.2.6"
|
|
116
117
|
}
|
|
117
|
-
}
|
|
118
|
+
}
|
|
@@ -161,14 +161,68 @@ class UrlCollector(ast.NodeVisitor):
|
|
|
161
161
|
def __init__(self, filepath):
|
|
162
162
|
self.file = filepath
|
|
163
163
|
self.routes = []
|
|
164
|
+
# DRF ViewSet/DefaultRouter(REALWORLD_STRUCTURAL_V3:router.register
|
|
165
|
+
# + include(router.urls) 的路由由运行时生成,urlpatterns 解析不可见)
|
|
166
|
+
self.router_regs = {} # router 变量 -> [(prefix, viewName)]
|
|
167
|
+
self.router_slash = {} # router 变量 -> trailing_slash
|
|
168
|
+
self.router_uses = [] # (prefixBase, routerVar) urlpatterns 里的 include
|
|
164
169
|
|
|
165
170
|
def visit_Assign(self, node):
|
|
166
171
|
for t in node.targets:
|
|
167
|
-
if isinstance(t, ast.Name)
|
|
172
|
+
if not isinstance(t, ast.Name):
|
|
173
|
+
continue
|
|
174
|
+
# DefaultRouter 定义(trailing_slash 关键字,默认 True)
|
|
175
|
+
if isinstance(node.value, ast.Call) and isinstance(node.value.func, ast.Name) \
|
|
176
|
+
and node.value.func.id == "DefaultRouter":
|
|
177
|
+
slash = True
|
|
178
|
+
for kw in node.value.keywords:
|
|
179
|
+
if kw.arg == "trailing_slash" and isinstance(kw.value, ast.Constant):
|
|
180
|
+
slash = bool(kw.value.value)
|
|
181
|
+
self.router_slash[t.id] = slash
|
|
182
|
+
self.router_regs.setdefault(t.id, [])
|
|
183
|
+
# urlpatterns:扫描直连条目,并展开 include(router.urls) 的 ViewSet
|
|
184
|
+
if t.id == "urlpatterns" and isinstance(node.value, (ast.List, ast.Tuple)):
|
|
168
185
|
for elt in node.value.elts:
|
|
169
186
|
self._scan_entry(elt)
|
|
187
|
+
for (prefix, var) in self.router_uses:
|
|
188
|
+
self._expand_router_routes(prefix, var)
|
|
170
189
|
self.generic_visit(node)
|
|
171
190
|
|
|
191
|
+
def visit_Expr(self, node):
|
|
192
|
+
# router.register(r'articles', ArticleViewSet, ...) 语句
|
|
193
|
+
call = node.value
|
|
194
|
+
if isinstance(call, ast.Call) and isinstance(call.func, ast.Attribute) \
|
|
195
|
+
and call.func.attr == "register":
|
|
196
|
+
var = name_of(call.func.value)
|
|
197
|
+
if not var or var not in self.router_regs:
|
|
198
|
+
return
|
|
199
|
+
prefix = ""
|
|
200
|
+
if call.args and isinstance(call.args[0], ast.Constant):
|
|
201
|
+
prefix = str(call.args[0].value)
|
|
202
|
+
view = None
|
|
203
|
+
if len(call.args) > 1:
|
|
204
|
+
a1 = call.args[1]
|
|
205
|
+
if isinstance(a1, ast.Name):
|
|
206
|
+
view = a1.id
|
|
207
|
+
elif isinstance(a1, ast.Attribute):
|
|
208
|
+
view = a1.attr
|
|
209
|
+
if view:
|
|
210
|
+
self.router_regs.setdefault(var, []).append((prefix, view))
|
|
211
|
+
self.generic_visit(node)
|
|
212
|
+
|
|
213
|
+
def _expand_router_routes(self, prefix_base, var):
|
|
214
|
+
# 本工具不做 include 前缀传播(urlconf 各文件 pattern 独立)——
|
|
215
|
+
# prefix_base 仅作路由存在性触发,不拼进 pattern
|
|
216
|
+
for (prefix, view) in self.router_regs.get(var, []):
|
|
217
|
+
base = "^" + prefix
|
|
218
|
+
# DRF 默认 action 路由:list/create 集合级、retrieve/update/
|
|
219
|
+
# destroy 详情级(权限由 views 表判定,规则在 TS 检测器侧)
|
|
220
|
+
for pat in (base + "/?$", base + "/(?P<pk>[^/.]+)/?$"):
|
|
221
|
+
self.routes.append({
|
|
222
|
+
"pattern": pat, "urlname": "", "view": view, "kind": "cbv",
|
|
223
|
+
"file": self.file, "viewset": True,
|
|
224
|
+
})
|
|
225
|
+
|
|
172
226
|
def _scan_entry(self, elt):
|
|
173
227
|
if not isinstance(elt, ast.Call):
|
|
174
228
|
return
|
|
@@ -199,6 +253,13 @@ class UrlCollector(ast.NodeVisitor):
|
|
|
199
253
|
elif isinstance(view_ref, ast.Call) and isinstance(view_ref.func, ast.Name):
|
|
200
254
|
if view_ref.func.id == "include":
|
|
201
255
|
kind = "include"
|
|
256
|
+
# include(router.urls):记录 router 变量(同一文件的
|
|
257
|
+
# router.register 展开见 _expand_router_routes)
|
|
258
|
+
if view_ref.args and isinstance(view_ref.args[0], ast.Attribute) \
|
|
259
|
+
and view_ref.args[0].attr == "urls":
|
|
260
|
+
rvar = name_of(view_ref.args[0].value)
|
|
261
|
+
if rvar:
|
|
262
|
+
self.router_uses.append((pattern, rvar))
|
|
202
263
|
else:
|
|
203
264
|
kind = "fbv"
|
|
204
265
|
view_name = view_ref.func.id
|
|
@@ -26,8 +26,10 @@ SKIP_DIRS = {"tests", "test", "deps", "venv", "env", "node_modules", "vendor",
|
|
|
26
26
|
".git", "migrations", "__pycache__", "scripts", "docs",
|
|
27
27
|
"staticfiles", "static"}
|
|
28
28
|
|
|
29
|
+
# jwt 必在词表:flask_jwt_extended 的 @jwt_required 是生态头号认证装饰器
|
|
30
|
+
# (REALWORLD_STRUCTURAL_V4:缺 "jwt" → 10 个受保护 mutation 全误报 FP)
|
|
29
31
|
AUTH_WORDS = ("auth", "login", "permission", "token", "credential", "session",
|
|
30
|
-
"user")
|
|
32
|
+
"user", "jwt")
|
|
31
33
|
|
|
32
34
|
MUTATION_METHODS = {"POST", "PUT", "PATCH", "DELETE"}
|
|
33
35
|
|
|
@@ -30,9 +30,13 @@ import sys
|
|
|
30
30
|
|
|
31
31
|
# 认证词表(依赖函数名命中即视为 auth-like;与 TS 侧 annotation-suggest 词表同源)
|
|
32
32
|
# 注意:扫描器只做「结构提取」,规则判定(豁免词/方法门控)在 TS 检测器侧。
|
|
33
|
+
# 注意:不含裸 "user"——get_profile_by_username_from_path 等 DB 查询
|
|
34
|
+
# 依赖名含 user 会被误标认证(REALWORLD_STRUCTURAL_V2 假保护 FN)。
|
|
35
|
+
# 真认证依赖用 current_user/authorizer 等强词识别(get_current_user
|
|
36
|
+
# 含 current_user ✓)。
|
|
33
37
|
AUTH_WORDS = (
|
|
34
|
-
"auth", "login", "token", "
|
|
35
|
-
"bearer", "permission", "current_user", "api_key", "oauth",
|
|
38
|
+
"auth", "login", "token", "credential", "session",
|
|
39
|
+
"bearer", "permission", "current_user", "api_key", "oauth", "jwt",
|
|
36
40
|
)
|
|
37
41
|
|
|
38
42
|
SKIP_DIRS = {"tests", "test", "deps", "venv", "env", "node_modules", "vendor",
|