progmune-runtime 3.7.17 → 3.7.19
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/extract-ir-java.js
CHANGED
|
@@ -75,9 +75,10 @@ function extractJavaFile(filePath) {
|
|
|
75
75
|
const name = m[1];
|
|
76
76
|
if (JAVA_KEYWORDS.has(name))
|
|
77
77
|
continue;
|
|
78
|
-
//
|
|
78
|
+
// 前置字符过滤:排除 '.'/'='/'( ' 前导(调用/赋值/子表达式误匹配);
|
|
79
|
+
// '@' 允许——@Override protected void … 是注解修饰的合法方法
|
|
79
80
|
const pre = code.slice(Math.max(0, m.index - 1), m.index);
|
|
80
|
-
if (/[
|
|
81
|
+
if (/[.=(]/.test(pre))
|
|
81
82
|
continue;
|
|
82
83
|
// 可见性(近似):行内是否有 public/protected(或文件在接口里默认 public)
|
|
83
84
|
const head = code.slice(m.index, m.index + 40);
|
|
@@ -101,12 +102,41 @@ function extractJavaFile(filePath) {
|
|
|
101
102
|
}
|
|
102
103
|
// 返回类型:方法名前的一段(简化取最近一个类型令牌)
|
|
103
104
|
const returnType = "unknown";
|
|
105
|
+
// calls:方法体(自头正则消费的 '{' 起平衡到 '}')内的方法调用
|
|
106
|
+
// (词法近似:标识符 + '(',过滤关键字;obj.method → 取末段)
|
|
107
|
+
const calls = [];
|
|
108
|
+
const bodyStart = m.index + m[0].length; // 正则已含 '{'
|
|
109
|
+
let depth = 1;
|
|
110
|
+
let bodyEnd = bodyStart;
|
|
111
|
+
while (bodyEnd < code.length && depth > 0) {
|
|
112
|
+
const ch = code[bodyEnd];
|
|
113
|
+
if (ch === "{")
|
|
114
|
+
depth++;
|
|
115
|
+
else if (ch === "}")
|
|
116
|
+
depth--;
|
|
117
|
+
bodyEnd++;
|
|
118
|
+
}
|
|
119
|
+
const body = code.slice(bodyStart, Math.min(bodyEnd, bodyStart + 8000));
|
|
120
|
+
const callRe = /(?:^|[^\w$])([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)\s*\(/g;
|
|
121
|
+
let cm;
|
|
122
|
+
while ((cm = callRe.exec(body)) !== null) {
|
|
123
|
+
const full = cm[1];
|
|
124
|
+
const seg = full.split(".").pop() || "";
|
|
125
|
+
if (JAVA_KEYWORDS.has(seg))
|
|
126
|
+
continue;
|
|
127
|
+
if (/^(if|for|while|switch|catch|new|return|case|do)$/.test(seg))
|
|
128
|
+
continue;
|
|
129
|
+
// 排除声明后立即调用形态(如 new Foo( 里 Foo)——new 已过滤
|
|
130
|
+
if (calls.length < 400 && !calls.includes(seg))
|
|
131
|
+
calls.push(seg);
|
|
132
|
+
}
|
|
104
133
|
out.push({
|
|
105
134
|
name,
|
|
106
135
|
params,
|
|
107
136
|
returnType,
|
|
108
137
|
file: filePath,
|
|
109
138
|
exported,
|
|
139
|
+
calls,
|
|
110
140
|
});
|
|
111
141
|
}
|
|
112
142
|
return out;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
/**
|
|
37
|
+
* extract-ir-java.test.ts — Java 提取器回归(纯字符串 + 临时目录)
|
|
38
|
+
*/
|
|
39
|
+
const vitest_1 = require("vitest");
|
|
40
|
+
const fs = __importStar(require("fs"));
|
|
41
|
+
const os = __importStar(require("os"));
|
|
42
|
+
const path = __importStar(require("path"));
|
|
43
|
+
const extract_ir_java_1 = require("./extract-ir-java");
|
|
44
|
+
let dir;
|
|
45
|
+
(0, vitest_1.beforeEach)(() => { dir = fs.mkdtempSync(path.join(os.tmpdir(), "javair-")); });
|
|
46
|
+
(0, vitest_1.afterEach)(() => { fs.rmSync(dir, { recursive: true, force: true }); });
|
|
47
|
+
const SAMPLE = `package app;
|
|
48
|
+
public class JwtTokenFilter extends OncePerRequestFilter {
|
|
49
|
+
@Override
|
|
50
|
+
protected void doFilterInternal(
|
|
51
|
+
HttpServletRequest request, HttpServletResponse response, FilterChain chain)
|
|
52
|
+
throws ServletException, IOException {
|
|
53
|
+
getTokenString(request.getHeader("Authorization"))
|
|
54
|
+
.flatMap(token -> jwtService.getSubFromToken(token))
|
|
55
|
+
.ifPresent(id -> {
|
|
56
|
+
if (SecurityContextHolder.getContext().getAuthentication() == null) {
|
|
57
|
+
setAuthentication(id, request);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
private Optional<String> getTokenString(String h) {
|
|
62
|
+
if (h == null) return Optional.empty();
|
|
63
|
+
return Optional.of(h.substring(7));
|
|
64
|
+
}
|
|
65
|
+
}`;
|
|
66
|
+
(0, vitest_1.describe)("extract-ir-java", () => {
|
|
67
|
+
(0, vitest_1.it)("@Override 注解方法被提取(不会被 @ 前导过滤误杀)", () => {
|
|
68
|
+
const fp = path.join(dir, "JwtTokenFilter.java");
|
|
69
|
+
fs.writeFileSync(fp, SAMPLE);
|
|
70
|
+
const fns = (0, extract_ir_java_1.extractJavaFile)(fp);
|
|
71
|
+
(0, vitest_1.expect)(fns.some((f) => f.name === "doFilterInternal")).toBe(true);
|
|
72
|
+
(0, vitest_1.expect)(fns.some((f) => f.name === "getTokenString")).toBe(true);
|
|
73
|
+
});
|
|
74
|
+
(0, vitest_1.it)("方法调用边(calls)被提取——JWT 认证链可见", () => {
|
|
75
|
+
const fp = path.join(dir, "JwtTokenFilter.java");
|
|
76
|
+
fs.writeFileSync(fp, SAMPLE);
|
|
77
|
+
const fns = (0, extract_ir_java_1.extractJavaFile)(fp);
|
|
78
|
+
const filter = fns.find((f) => f.name === "doFilterInternal");
|
|
79
|
+
(0, vitest_1.expect)(filter.calls).toContain("getSubFromToken");
|
|
80
|
+
(0, vitest_1.expect)(filter.calls).toContain("getAuthentication");
|
|
81
|
+
(0, vitest_1.expect)(filter.calls).toContain("setAuthentication");
|
|
82
|
+
// 关键字不算调用
|
|
83
|
+
(0, vitest_1.expect)(filter.calls).not.toContain("if");
|
|
84
|
+
});
|
|
85
|
+
(0, vitest_1.it)("基础方法提取", () => {
|
|
86
|
+
const fp = path.join(dir, "Plain.java");
|
|
87
|
+
fs.writeFileSync(fp, `package app;
|
|
88
|
+
public class Plain {
|
|
89
|
+
public Plain() {}
|
|
90
|
+
public int add(int a, int b) { return a + b; }
|
|
91
|
+
}`);
|
|
92
|
+
const fns = (0, extract_ir_java_1.extractJavaFile)(fp);
|
|
93
|
+
(0, vitest_1.expect)(fns.some((f) => f.name === "add")).toBe(true);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
// ── 协议行金标 v1:token 生命周期(verify 先于 use,2026-09-02)──
|
|
97
|
+
/** 真实语料 token 链的 verify-before-use 判定(金标规则 v1):
|
|
98
|
+
* doFilterInternal 的调用序须满足 getSubFromToken(verify)先于
|
|
99
|
+
* setAuthentication(use/信任)。 */
|
|
100
|
+
function tokenVerifyBeforeUse(calls) {
|
|
101
|
+
if (!calls)
|
|
102
|
+
return { ok: false, why: "无调用边" };
|
|
103
|
+
const verifyIdx = calls.findIndex((c) => /getSubFromToken|verify/.test(c));
|
|
104
|
+
const useIdx = calls.findIndex((c) => c === "setAuthentication");
|
|
105
|
+
if (useIdx === -1)
|
|
106
|
+
return { ok: true, why: "无 use(本链不消费认证)" };
|
|
107
|
+
if (verifyIdx === -1)
|
|
108
|
+
return { ok: false, why: "use(setAuthentication) 之前无 verify" };
|
|
109
|
+
return verifyIdx < useIdx
|
|
110
|
+
? { ok: true, why: "verify→use 序正确" }
|
|
111
|
+
: { ok: false, why: "use 先于 verify" };
|
|
112
|
+
}
|
|
113
|
+
const REAL_FILTER = `package io.spring.api.security;
|
|
114
|
+
public class JwtTokenFilter {
|
|
115
|
+
@Override
|
|
116
|
+
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
|
|
117
|
+
throws Exception {
|
|
118
|
+
getTokenString(request.getHeader(header))
|
|
119
|
+
.flatMap(token -> jwtService.getSubFromToken(token))
|
|
120
|
+
.ifPresent(id -> {
|
|
121
|
+
if (SecurityContextHolder.getContext().getAuthentication() == null) {
|
|
122
|
+
setAuthentication(id, request); // use:信任已验 token
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
chain.doFilter(request, response);
|
|
126
|
+
}
|
|
127
|
+
}`;
|
|
128
|
+
// 变异:删掉 verify(真实违规:未验 token 直接信任)
|
|
129
|
+
const MUT_FILTER = REAL_FILTER.replace(" .flatMap(token -> jwtService.getSubFromToken(token))", " .map(id -> id)");
|
|
130
|
+
(0, vitest_1.describe)("协议行金标 v1 — token 生命周期(verify-before-use)", () => {
|
|
131
|
+
(0, vitest_1.it)("原文链:verify 先于 use → 合规", () => {
|
|
132
|
+
const fp = path.join(dir, "JwtTokenFilter.java");
|
|
133
|
+
fs.writeFileSync(fp, REAL_FILTER);
|
|
134
|
+
const fns = (0, extract_ir_java_1.extractJavaFile)(fp);
|
|
135
|
+
const calls = fns.find((f) => f.name === "doFilterInternal").calls;
|
|
136
|
+
(0, vitest_1.expect)(tokenVerifyBeforeUse(calls).ok).toBe(true);
|
|
137
|
+
(0, vitest_1.expect)(tokenVerifyBeforeUse(calls).why).toContain("verify→use");
|
|
138
|
+
});
|
|
139
|
+
(0, vitest_1.it)("变异(摘 verify):use 前无 verify → 违规被判定(0-FP 语义负例)", () => {
|
|
140
|
+
const fp = path.join(dir, "JwtTokenFilter.java");
|
|
141
|
+
fs.writeFileSync(fp, MUT_FILTER);
|
|
142
|
+
const fns = (0, extract_ir_java_1.extractJavaFile)(fp);
|
|
143
|
+
const calls = fns.find((f) => f.name === "doFilterInternal").calls;
|
|
144
|
+
(0, vitest_1.expect)(tokenVerifyBeforeUse(calls).ok).toBe(false);
|
|
145
|
+
(0, vitest_1.expect)(tokenVerifyBeforeUse(calls).why).toContain("无 verify");
|
|
146
|
+
});
|
|
147
|
+
});
|
|
@@ -76,8 +76,10 @@ function antToRegex(pattern) {
|
|
|
76
76
|
function parseSecurityConfig(code) {
|
|
77
77
|
const rules = [];
|
|
78
78
|
let catchAll = null;
|
|
79
|
-
// 逐个 .antMatchers(...) 与其后 .access() 配对
|
|
80
|
-
|
|
79
|
+
// 逐个 .antMatchers/.requestMatchers(...) 与其后 .access() 配对
|
|
80
|
+
// (旧 DSL antMatchers + 新式 SecurityFilterChain authorizeHttpRequests
|
|
81
|
+
// requestMatchers——2026-09-02 Spring 方言扩展)
|
|
82
|
+
const re = /\.(?:antMatchers|requestMatchers)\s*\(\s*([^)]*)\)\s*\.(\w+)\s*\(|\.anyRequest\(\)\s*\.(\w+)\s*\(/g;
|
|
81
83
|
let m;
|
|
82
84
|
while ((m = re.exec(code)) !== null) {
|
|
83
85
|
if (m[3] !== undefined) {
|
|
@@ -212,6 +214,8 @@ function analyzeSpringProject(projectRoot) {
|
|
|
212
214
|
// 类级 @RequestMapping 只认类声明头(方法级 @RequestMapping 不算前缀)
|
|
213
215
|
const clsReq = c.header.match(/@RequestMapping\s*\((?:path\s*=\s*)?["']([^"']*)["']/);
|
|
214
216
|
const prefix = clsReq ? clsReq[1].replace(/^\//, "") : "";
|
|
217
|
+
// 类级方法安全:@PreAuthorize/@Secured 于类声明头 → 全部方法受保护
|
|
218
|
+
const classPreAuth = /@PreAuthorize|@Secured/.test(c.header);
|
|
215
219
|
const annRe = /@(Get|Post|Put|Delete|Patch|Request)Mapping\s*(\([^)]*\))?/g;
|
|
216
220
|
let m;
|
|
217
221
|
while ((m = annRe.exec(c.body)) !== null) {
|
|
@@ -224,7 +228,7 @@ function analyzeSpringProject(projectRoot) {
|
|
|
224
228
|
// 找到该 handler 起始行(用于 line + 后续注解如 @PreAuthorize)
|
|
225
229
|
const line = c.body.slice(0, m.index).split("\n").length;
|
|
226
230
|
const window = c.body.slice(m.index, m.index + 600);
|
|
227
|
-
const preAuth = /@PreAuthorize/.test(window);
|
|
231
|
+
const preAuth = classPreAuth || /@PreAuthorize|@Secured/.test(window);
|
|
228
232
|
allPaths.push(fullPath);
|
|
229
233
|
routes.push({
|
|
230
234
|
method,
|
|
@@ -120,3 +120,52 @@ public class ArticleApi {
|
|
|
120
120
|
(0, vitest_1.expect)(a.issues.some((i) => i.route === "POST /articles")).toBe(true);
|
|
121
121
|
});
|
|
122
122
|
});
|
|
123
|
+
// ── Spring 方言扩展:SecurityFilterChain bean + requestMatchers + @PreAuthorize ──
|
|
124
|
+
const SEC_BEAN = `${PKG}
|
|
125
|
+
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
|
126
|
+
@Configuration @EnableWebSecurity
|
|
127
|
+
public class WebSecurityConfig {
|
|
128
|
+
@Bean
|
|
129
|
+
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
|
130
|
+
http.csrf().disable().authorizeHttpRequests(auth -> auth
|
|
131
|
+
.requestMatchers(HttpMethod.POST, "/users", "/users/login").permitAll()
|
|
132
|
+
.requestMatchers(HttpMethod.GET, "/articles/**", "/tags").permitAll()
|
|
133
|
+
.requestMatchers(HttpMethod.GET, "/articles/feed").authenticated()
|
|
134
|
+
.anyRequest().authenticated());
|
|
135
|
+
return http.build();
|
|
136
|
+
}
|
|
137
|
+
}`;
|
|
138
|
+
const ADMIN_CTRL = `${PKG}
|
|
139
|
+
@RestController @RequestMapping(path = "admin")
|
|
140
|
+
@PreAuthorize("hasRole('ADMIN')")
|
|
141
|
+
public class AdminApi {
|
|
142
|
+
@DeleteMapping("/users/{id}") public Object ban() { return null; }
|
|
143
|
+
@PostMapping("/reset") public Object reset() { return null; }
|
|
144
|
+
}`;
|
|
145
|
+
(0, vitest_1.describe)("spring 方言扩展(2026-09-02)", () => {
|
|
146
|
+
(0, vitest_1.it)("SecurityFilterChain bean + authorizeHttpRequests + requestMatchers 解析", () => {
|
|
147
|
+
writeJava("sec/WebSecurityConfig.java", SEC_BEAN);
|
|
148
|
+
writeJava("api/ArticleApi.java", CTRL);
|
|
149
|
+
const a = (0, spring_detector_1.analyzeSpringProject)(dir);
|
|
150
|
+
(0, vitest_1.expect)(a.hasSecurityConfig).toBe(true);
|
|
151
|
+
(0, vitest_1.expect)(a.catchAll).toBe("authenticated");
|
|
152
|
+
(0, vitest_1.expect)(a.issues).toHaveLength(0);
|
|
153
|
+
const post = a.routes.find((r) => r.method === "post" && r.path === "/articles");
|
|
154
|
+
(0, vitest_1.expect)(post.access).toBe("authenticated");
|
|
155
|
+
});
|
|
156
|
+
(0, vitest_1.it)("类级 @PreAuthorize → 该类 mutation 全部受保护(不报)", () => {
|
|
157
|
+
writeJava("sec/WebSecurityConfig.java", SEC("permitAll")); // 兜底全公开
|
|
158
|
+
writeJava("api/AdminApi.java", ADMIN_CTRL);
|
|
159
|
+
const a = (0, spring_detector_1.analyzeSpringProject)(dir);
|
|
160
|
+
// 兜底 permitAll 下,无注解类会报;@PreAuthorize 类不报
|
|
161
|
+
(0, vitest_1.expect)(a.issues.some((i) => i.route === "DELETE /admin/users/{id}")).toBe(false);
|
|
162
|
+
(0, vitest_1.expect)(a.issues.some((i) => i.route === "POST /admin/reset")).toBe(false);
|
|
163
|
+
});
|
|
164
|
+
(0, vitest_1.it)("方言反证:requestMatchers permitAll 的 mutation 公开(register 豁免外仍查)", () => {
|
|
165
|
+
const cfg = SEC_BEAN.replace(".requestMatchers(HttpMethod.GET, \"/articles/**\", \"/tags\").permitAll()", ".requestMatchers(HttpMethod.GET, \"/articles/**\", \"/tags\", \"/payments\").permitAll()").replace(/\.anyRequest\(\)\.authenticated\(\)/, ".anyRequest().permitAll()");
|
|
166
|
+
writeJava("sec/WebSecurityConfig.java", cfg);
|
|
167
|
+
writeJava("api/ArticleApi.java", CTRL);
|
|
168
|
+
const a = (0, spring_detector_1.analyzeSpringProject)(dir);
|
|
169
|
+
(0, vitest_1.expect)(a.issues.some((i) => i.route === "POST /articles")).toBe(true);
|
|
170
|
+
});
|
|
171
|
+
});
|
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.19";
|
|
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.19",
|
|
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/",
|