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.
@@ -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 (/[.=@(]/.test(pre))
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
- const re = /\.antMatchers\(\s*([^)]*)\)\s*\.(\w+)\s*\(|\.anyRequest\(\)\s*\.(\w+)\s*\(/g;
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.17";
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.17",
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/",