progmune-runtime 3.2.0 → 3.3.0

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/README.md CHANGED
@@ -4,8 +4,8 @@
4
4
 
5
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
6
  [![MCP](https://img.shields.io/badge/MCP-Compatible-blue)](https://modelcontextprotocol.io)
7
- [![TS Benchmark](https://img.shields.io/badge/TS%20F1-85.2%25-22c55e)]()
8
- [![Tests](https://img.shields.io/badge/Tests-92%20passing-22c55e)]()
7
+ [![TS Benchmark](https://img.shields.io/badge/TS%20R98.5%25%20P100%25-22c55e)]()
8
+ [![Python Benchmark](https://img.shields.io/badge/Python%20R100%25%20P100%25-22c55e)]()
9
9
 
10
10
  **Verify AI-generated code before it reaches production.** Progmune checks whether your AI-generated code follows correct protocol lifecycles — TLS handshakes, auth flows, payment integrity, resource management — violations that SAST and SCA tools cannot see because they span sequences of function calls, not single statements.
11
11
 
@@ -23,6 +23,21 @@ Output: `APPROVED` / `NEEDS_REVIEW` / `BLOCKED` — with Trust Score, evidence,
23
23
 
24
24
  ---
25
25
 
26
+ ## Two Paths: Intercept or Verify
27
+
28
+ Progmune covers **two code sources** with two complementary mechanisms:
29
+
30
+ | | **Generate path** (agent-time interception) | **Verify path** (post-hoc checking) |
31
+ |---|---|---|
32
+ | **Covers** | Code generated *through Progmune* (`progmune_generate` / `progmune_execute`) | Code written anywhere — Copilot, Cursor, humans (`progmune_trust_check` / SDK / CI) |
33
+ | **Mechanism** | 8 validation gates inside the generation loop: JSON parse → schema → SVL-1 symbol → SVL-2 types → SVL-3 dataflow → SVL-4 protocol state machine → BFS deterministic repair → semantic contract. Violating code is **never written to disk** — it is corrected or retried before emission. | Trust Engine: 4-dimension weighted scoring (policy 35% / protocol 30% / coverage 20% / governance 15%) → Decision + evidence trail |
34
+ | **When errors are handled** | At creation time — the error never exists | After the fact — the file already exists |
35
+ | **Cost curve** | Zero — the violation never landed | Higher the later it's found |
36
+
37
+ This is the core product thesis: **verify at generation time, not after the fact.** LLM outputs are proposals; the state machine is the referee. LLMs can be persuaded — state machines cannot.
38
+
39
+ ---
40
+
26
41
  ## What Progmune Detects
27
42
 
28
43
  AI code generators produce syntactically valid code that often violates **protocol lifecycles** — the correct sequence of operations like open→read→close or auth→validate→respond. These violations are invisible to traditional static analysis.
@@ -34,6 +49,10 @@ AI code generators produce syntactically valid code that often violates **protoc
34
49
  | **Payment** | Order without verification, refund without authorization, webhook without signature check |
35
50
  | **Resource** | File opened but not closed, connection without cleanup, malloc without free |
36
51
  | **Data Integrity** | Mutation without audit trail, missing input validation |
52
+ | **Injection (Python, source-level)** | SQL built with f-string/`%`/`.format`/concatenation, command injection via dynamic subprocess args, SSRF via user-controlled URL fetches, SSTI via template-string sinks, XXE via external-entity parser config, eval/exec on user input |
53
+ | **Web (Python, source-level)** | XSS via `{{ var\|safe }}`/autoescape-off templates, path traversal via user-controlled file paths, CSRF via `@csrf_exempt` or GET state changes, authorization by client cookies, hardcoded JWT secrets (incl. cross-module constants) |
54
+
55
+ Source-level detections use an extractor-marker architecture: the IR extractor performs taint tracking, import resolution, and cross-file analysis (templates, module constants), emitting synthetic markers that rules consume — zero pipeline changes, fully auditable.
37
56
 
38
57
  ---
39
58
 
@@ -80,12 +99,20 @@ Progmune is honest about what it can and cannot verify.
80
99
 
81
100
  | Language | Status | Evidence |
82
101
  |----------|--------|----------|
83
- | **TypeScript / JavaScript** | ✅ Production | Blind benchmark: P=86.8%, R=83.6%, F1=85.2% (432 sequences, 10 projects) |
84
- | **C** | ⚠️ Research | Gold benchmark: curl F1=45.7%, libssh F1=46.2%. L3 cross-function experiment terminated L4 (pointer/CFG) needed. |
85
- | **Python** | 🔨 IR only | IR extractor exists (`extract-ir-python.ts`), no verification rules yet |
102
+ | **TypeScript / JavaScript** | ✅ Production | Blind benchmark: **recall 98.5% / precision 100%** (795 gold findings, 100 projects) |
103
+ | **Python** | Production | Blind benchmark: **recall 100% / precision 100%** (729 gold findings, 90 projects); real-world validation: PyGoat (OWASP vulnerable-by-design Django app) **67 TP / 0 FP, 100% labeled precision**; three well-written apps (django/fastapi realworld, django-unicorn) with 0 false-positive true findings |
104
+ | **C** | ⚠️ Research-only | Gold benchmark F1=16.5%. L3 cross-function experiment terminated; L4 not planned. See [C Language Status](docs/c-language-status.md). |
86
105
  | **Go, Java** | ❌ None | Planned |
87
106
 
88
- **Framework adapters: 1/13.** Express is the first. Next.js, NestJS, Fastify, Django, FastAPI 12 remaining. Framework adaptation is the #1 product gap.
107
+ **Framework adapters: 2/13.** Express and tRPC ✅ have dedicated detectors; Next.js has version-aware governance; NestJS is partial. Django, FastAPI and 8 more remain — framework adaptation is the #1 product gap.
108
+
109
+ ### What Progmune does NOT cover (honest boundaries)
110
+
111
+ - **TS-side taint-based injection flaws** — the source-level SQLi/XSS/SSRF detections ship for Python; the TypeScript extractor is name/call-based, so TS injection classes remain uncovered (documented, not hidden).
112
+ - **SCA / dependency vulnerabilities** — hallucinated package names, supply-chain issues. Separate tooling exists for this.
113
+ - **Runtime behavior** — Progmune is static analysis only; no DAST/sandbox execution.
114
+ - **Framework internals** — well-known framework dispatch/cache machinery (e.g. django-unicorn internals) can produce a small number of boundary false positives; they are documented per-corpus in the benchmark gold files.
115
+ - **Known failure boundaries are documented** rather than hidden: if Progmune cannot verify a language (e.g. Go), Confidence is lowered instead of pretending 100%.
89
116
 
90
117
  → [Full Coverage Matrix](docs/coverage-matrix.md)
91
118
 
@@ -95,23 +122,35 @@ Progmune is honest about what it can and cannot verify.
95
122
 
96
123
  Public, reproducible precision data. All numbers measured against gold-annotated benchmarks.
97
124
 
98
- ### TypeScript (Blind Benchmark v6)
125
+ ### TypeScript (Blind Benchmark v6 — 100 projects)
99
126
 
100
127
  | Metric | Value |
101
128
  |--------|-------|
102
- | Precision | 86.8% |
103
- | Recall | 83.6% |
104
- | F1 | 85.2% |
105
- | Projects | 10 (ecommerce, blog, chat, crm, forum, wiki, issuetracker, filestorage, todo, scheduler) |
129
+ | Precision | **100%** (0 factual FPs) |
130
+ | Recall | **98.5%** (effective 100% — the 12 non-detected findings are excluded by methodology) |
131
+ | Gold findings | 795 across 100 projects (90 style-variants + 10 model-variants) |
132
+
133
+ ### Python (Blind Benchmark v1 — 90 projects)
134
+
135
+ | Metric | Value |
136
+ |--------|-------|
137
+ | Precision | **100%** |
138
+ | Recall | **100%** |
139
+ | Gold findings | 729 across 90 style-variant projects |
140
+
141
+ ### Real-world validation (PyGoat, OWASP vulnerable-by-design Django app)
142
+
143
+ | Metric | Value |
144
+ |--------|-------|
145
+ | Labeled precision | **100%** (67 true positives / 0 false positives, per-detection human review) |
146
+ | Classes covered | 14 vulnerability classes incl. SQLi, SSRF, path traversal, XSS, SSTI, XXE, command injection, deserialization, CSRF (both shapes), cookie authorization, hardcoded secrets |
147
+ | Well-written apps | django-realworld, fastapi-realworld, django-unicorn — 0 false-positive true findings; 3 documented framework-internal boundary FPs |
148
+
149
+ → [Real-world validation report](blind-benchmark/REALWORLD_APP_V1.md) · [Benchmark baseline](blind-benchmark/BASELINE_v6.md)
106
150
 
107
- ### C (Gold Benchmark v7)
151
+ ### C (Gold Benchmark — research status)
108
152
 
109
- | Repo | Precision | Recall | F1 | Samples |
110
- |------|-----------|--------|-----|---------|
111
- | curl | 30.9% | 87.5% | 45.7% | 85 |
112
- | libssh | 36.0% | 64.3% | 46.2% | 47 |
113
- | nginx | — | — | 0 FP | 50 |
114
- | redis | — | — | 0 FP | 50 |
153
+ C analysis is **research-only**: gold benchmark F1=16.5% across 4 repos (curl, libssh, nginx, openssl). The bottleneck is rule coverage, not context. L3 (cross-function) was terminated with data; L4 (pointer/CFG) is a multi-year research problem and not planned. See [C Language Status](docs/c-language-status.md) for the full picture and reasoning.
115
154
 
116
155
  ### P0-P3 Rule Injection (2026-08)
117
156
 
@@ -131,7 +170,9 @@ SDK (src/sdk.ts) verify() → APPROVED / NEEDS_REVIEW / BLOCKED
131
170
  ├─ Policy Engine Enterprise policy enforcement (ALLOW/WARN/BLOCK)
132
171
  ├─ SSG Validator Protocol state machine verification
133
172
  ├─ Protocol Detector Regex-based protocol step detection (22 detectors)
134
- ├─ IR Extractor TypeScript AST → function IR (ts-morph)
173
+ ├─ IR Extractors TypeScript (ts-morph) + Python (ast module) → function IR;
174
+ │ source-level markers: taint tracking, import resolution,
175
+ │ qualified call chains, cross-file template analysis
135
176
  ├─ Repair Executor detect → plan → fix → validate → commit/rollback
136
177
  └─ Knowledge Base 31 domains, 140 rules, evidence chains
137
178
  ```
@@ -150,9 +191,9 @@ SDK (src/sdk.ts) verify() → APPROVED / NEEDS_REVIEW / BLOCKED
150
191
 
151
192
  ## Scientific Foundation
152
193
 
153
- Progmune is built on the premise that **LLM outputs are statistical performances, not reasoning** (Kambhampati et al., ICML 2026). Rather than trusting what the model says about code, Progmune verifies what the program actually does — using protocol state machines, IR extraction, and evidence-backed decision chains.
194
+ Progmune is built on the premise that **LLM outputs are statistical performances, not reasoning** — a view developed by Subbarao Kambhampati et al. in the position paper ["Stop Anthropomorphizing Intermediate Tokens as Reasoning/Thinking Traces!"](https://arxiv.org/abs/2505.22285) (arXiv:2505.22285, 2025) and elaborated in his ICML 2026 talk "On the Role of Verifiers and Thinking Traces in Reasoning Models". Rather than trusting what the model says about code, Progmune verifies what the program actually does — using protocol state machines, IR extraction, and evidence-backed decision chains.
154
195
 
155
- Recent work applies Sergei Gukov's "Two-Hump Problem" framework to understand and systematically close detection capability gaps.
196
+ Coverage-gap analysis borrows the "two-hump problem" terminology **as a cross-domain analogy** from Sergei Gukov's work in mathematical physics (the Andrews-Curtis conjecture in group theory, 2026) it describes a bimodal coverage distribution, not a collaboration. See [Two-Hump Report](docs/two-hump-report.md) for the full methodology.
156
197
 
157
198
  → [Investor Whitepaper](docs/Progmune_投资人白皮书_v2.0.html) · [Trust Decision Model](docs/ai-trust-decision-model-v1.md)
158
199
 
@@ -174,10 +215,10 @@ High-impact contribution areas:
174
215
  - **Runtime Pipeline:** Detect → Explain → Repair → Validate (L1–L4)
175
216
  - **Trust Engine:** 4-dimension scoring with binary explainability gate
176
217
  - **MCP Tools:** 19 — `progmune_trust_check`, `progmune_score`, `progmune_policy_check`, `progmune_certify`, and more
177
- - **Framework Adapters:** Express (1/13)
178
- - **Knowledge Base:** 31 domains, 140 protocol rules, 22 detectors, 26 safeguards
179
- - **Corpus:** 2,500+ trajectories across 6+ repositories
180
- - **Current focus:** Framework adaptation + enterprise PoC validation
218
+ - **Framework Adapters:** Express ✅, tRPC ✅, NestJS partial (2/13)
219
+ - **Knowledge Base:** 31 domains, 148 protocol rules, 22 detectors, 26 safeguards, PLSB 13/13 categories — plus 15 source-level detection rules (Python)
220
+ - **Corpus:** 2,500+ trajectories across 6+ repositories; blind benchmarks 100 (TS) + 90 (Python) projects; real-world validation on 4 application repos
221
+ - **Current focus:** Enterprise PoC validation + remaining framework-internal boundary FPs
181
222
 
182
223
  ---
183
224
 
@@ -352,6 +352,19 @@ function extractDirectCalls(func) {
352
352
  if (ts_morph_1.Node.isFunctionDeclaration(node) || ts_morph_1.Node.isArrowFunction(node))
353
353
  traversal.skip();
354
354
  });
355
+ // Semantic markers (mirroring the Python extractor):
356
+ // - token issuance: set_cookie calls or token/session-named assignments —
357
+ // the Token Security rule's requireMarker precondition consumes it.
358
+ // - inline ownership comparison: ownerId/authorId compared with ==/!== —
359
+ // the Ownership Check rules' satisfier consumes it (the call-name
360
+ // interface cannot see inline comparisons).
361
+ const text = func.getText();
362
+ if (/set_cookie\(|setCookie\(|\btoken\s*[:=]|\bsession_token\s*[:=]/.test(text)) {
363
+ calls.push("__progmune_token_issued__");
364
+ }
365
+ if (/ownerId\s*[!=]==?|authorId\s*[!=]==?|createdBy\s*[!=]==?|\.owner\s*[!=]==?|userId\s*[!=]==?/.test(text)) {
366
+ calls.push("__progmune_ownership_checked__");
367
+ }
355
368
  return [...new Set(calls)];
356
369
  }
357
370
  /**
@@ -272,9 +272,16 @@ const SAFEGUARD_RULES = [
272
272
  {
273
273
  name: "Password Hashing",
274
274
  category: "password_hashing",
275
- trigger: /\b(register|signUp|createUser|createAccount|registerUser)\b/i,
275
+ trigger: /\b(register|signUp|createUser|createAccount|registerUser|sign_up|create_user|create_account|register_user|register_new_user)\b/i,
276
276
  safeguards: [
277
277
  { pattern: /\b(bcrypt|argon2|scrypt|pbkdf2|hash|hashPassword|createHash|hashSync|hash_password)\b/i, label: "secure_hash" },
278
+ // Framework delegation (qualified chains only — a bare custom create_user
279
+ // is NOT treated as secure): Django's built-in user manager and password
280
+ // setters hash internally; repository/service create_user methods delegate
281
+ // to the model (users_repo.create_user, self.create_user). Also
282
+ // XForm(request.POST).save() — Django form validation + hashing
283
+ // (extractor marker).
284
+ { pattern: /\.create_user\b|\.(change_password|set_password)\b|__progmune_django_form__|__progmune_template_tag__/i, label: "framework_hashing" },
278
285
  ],
279
286
  violationMessage: "User registration function does not call a secure password hashing function (bcrypt/argon2/scrypt). Passwords may be stored in plaintext or with weak hashing.",
280
287
  conceptMissing: ["PasswordHash", "KeyDerivation"],
@@ -284,9 +291,14 @@ const SAFEGUARD_RULES = [
284
291
  {
285
292
  name: "Password Hashing (Weak)",
286
293
  category: "password_hashing",
287
- trigger: /\b(register|signUp|createUser|createAccount|registerUser)\b/i,
294
+ trigger: /\b(register|signUp|createUser|createAccount|registerUser|sign_up|create_user|create_account|register_user|register_new_user)\b/i,
288
295
  safeguards: [
289
296
  { pattern: /\b(bcrypt|argon2|scrypt|pbkdf2)\b/i, label: "strong_hash" },
297
+ // Framework delegation (qualified chains only)
298
+ { pattern: /\.create_user\b|\.(change_password|set_password)\b|__progmune_django_form__|__progmune_template_tag__/i, label: "framework_hashing" },
299
+ ],
300
+ excludePatterns: [
301
+ /register\.(simple_tag|tag|filter|inclusion_tag)/, // Django template-tag registration
290
302
  ],
291
303
  violationMessage: "User registration uses weak or no password hashing. SHA256/MD5 detected — use bcrypt/argon2 instead.",
292
304
  conceptMissing: ["StrongHash", "SaltGeneration"],
@@ -294,16 +306,26 @@ const SAFEGUARD_RULES = [
294
306
  },
295
307
  // ── Authorization / Ownership Check ──
296
308
  // v2: narrowed triggers — removed "process" and "set" (too generic for C libraries)
309
+ // v3 (2026-08-15): identity lookups (getUser/validateToken/getCurrentUser...) removed
310
+ // from satisfiers — authentication is NOT ownership. A mutation calling only
311
+ // getUser(token) without comparing ownerId/authorId is the 90-FN class found by
312
+ // the 100-project gold benchmark. Satisfiers are now: explicit ownership
313
+ // comparison names, owner-check helpers, or permission/role gates.
314
+ // Limitation: inline `p.ownerId !== u.id` comparisons are not visible in the
315
+ // call-list interface of this detector (would need AST-level analysis).
297
316
  {
298
317
  name: "Authorization (Ownership Check)",
299
318
  category: "authorization",
300
- trigger: /\b(delete|remove|toggle|modify|edit|lock|ban|refund|assign|transfer|share|schedule|upload)(?:[A-Z]\w*|_\w+)|(?:[A-Z]\w*|_\w+)(Delete|Remove|Toggle|Modify|Edit|Lock|Ban|Refund|Assign|Transfer|Share|Schedule|Upload)\b/i,
319
+ paramGated: true,
320
+ trigger: /\b(delete|remove|toggle|modify|edit|lock|ban|refund|assign|transfer|share|schedule|upload|update)(?:[A-Z]\w*|_\w+)|(?:[A-Z]\w*|_\w+)(Delete|Remove|Toggle|Modify|Edit|Lock|Ban|Refund|Assign|Transfer|Share|Schedule|Upload|Update)\b/i,
301
321
  safeguards: [
302
- { pattern: /\b(getUser|validateToken|verifySession|getSessionUser|getCurrentUser|checkOwner|authorId\s*[!=]==?|ownerId\s*[!=]==?|userId\s*[!=]==?|\.owner\s*[!=]==?|hasPermission|checkPermission|checkAccess|isAuthorized|checkRole|requireRole|adminCheck|isAdmin|canModify|canDelete|canEdit)\b/i, label: "auth_check" },
322
+ { pattern: /\b(checkOwner|isOwner|ownerId\s*[!=]==?|authorId\s*[!=]==?|userId\s*[!=]==?|createdBy\s*[!=]==?|\.owner\s*[!=]==?|\.user\s*[!=]==?)\b/i, label: "ownership_check" },
323
+ { pattern: /\b(hasPermission|checkPermission|checkAccess|isAuthorized|checkRole|requireRole|adminCheck|isAdmin|canModify|canDelete|canEdit)\b/i, label: "authz_check" },
324
+ { pattern: /\b(__progmune_ownership_checked__)\b/, label: "inline_ownership_check" },
303
325
  ],
304
- violationMessage: "Mutation operation does not verify user ownership or authorization before modifying data.",
326
+ violationMessage: "Mutation operation does not verify that the acting user owns the resource or holds the required permission before modifying data.",
305
327
  conceptMissing: ["OwnershipCheck", "AuthorizationGuard"],
306
- conceptExpected: ["getUser", "validateToken", "ownerId check"],
328
+ conceptExpected: ["ownerId comparison", "authorId check", "permission check"],
307
329
  excludePatterns: [
308
330
  /_hd_/, // HPACK header compression internals
309
331
  /_frame_/, // protocol frame handlers
@@ -323,9 +345,10 @@ const SAFEGUARD_RULES = [
323
345
  name: "Authorization (Unauthenticated Access)",
324
346
  category: "authorization",
325
347
  languages: ["typescript", "javascript", "python"],
348
+ paramGated: true,
326
349
  trigger: /\b(list|download|view|fetch)(?:[A-Z]\w*|_\w+)|get(?:[A-Z]\w+|_\w+)/i,
327
350
  safeguards: [
328
- { pattern: /\b(getUser|validateToken|verifySession|getSessionUser|getCurrentUser|token\w*(Check|Verify|Valid)|session\w*(Check|Verify|Valid)|auth\w*(Check|Verify|Valid|Guard|Middleware|Required)|requireAuth|withAuth|authenticate\w*(User|Request|Token)?|checkAuth|isAuth|hasAuth|checkAccess|hasAccess)\b/i, label: "auth_check" },
351
+ { pattern: /\b(getUser|validateToken|verifySession|getSessionUser|getCurrentUser|token\w*(Check|Verify|Valid)|session\w*(Check|Verify|Valid)|auth\w*(Check|Verify|Valid|Guard|Middleware|Required)|requireAuth|withAuth|authenticate\w*(User|Request|Token)?|checkAuth|isAuth|hasAuth|checkAccess|hasAccess|get_user|get_session_user|get_current_user|validate_session|verify_token|require_auth|with_auth|check_auth|auth_required|authenticate_user|authenticate_request|authenticate_token|token_check|token_verify|token_valid|session_check|session_verify|session_valid|auth_check|auth_guard|auth_middleware|get_current_user_authorizer|current_user_authorizer|login_required|permission_required|user_passes_test|check_authorization|check_permission|jwt\.decode|decode_token|__progmune_auth_checked__|__progmune_credential_check__|__progmune_drf_permissions__|__progmune_auth_machinery__)\b/i, label: "auth_check" },
329
352
  ],
330
353
  violationMessage: "Data access function does not check authentication. Anyone can access data without credentials.",
331
354
  conceptMissing: ["AuthenticationCheck", "AccessControl"],
@@ -342,8 +365,38 @@ const SAFEGUARD_RULES = [
342
365
  /findBig|findKey|findPk/, // internal search (not API)
343
366
  ],
344
367
  },
368
+ // Mutations without any authentication. The Unauthenticated Access rule above
369
+ // only covers read verbs (list/get/download/view/fetch); create/add/post/update/
370
+ // set verbs had no auth coverage (3 gold FNs: addProduct, addCategory, setMilestone).
371
+ // v3 (2026-08-15)
372
+ {
373
+ name: "Authorization (Unauthenticated Mutation)",
374
+ category: "authorization",
375
+ languages: ["typescript", "javascript", "python"],
376
+ paramGated: true,
377
+ // Note: "post" deliberately excluded — it collides with the Post entity name
378
+ // (listPosts/getPost/deletePost fire via identifier-parsed words).
379
+ trigger: /\b(add|create|update|set|publish|insert|submit)(?:[A-Z]\w*|_\w+)|(?:[A-Z]\w*|_\w+)(Add|Create|Update|Set|Publish|Insert|Submit)\b/i,
380
+ safeguards: [
381
+ { pattern: /\b(getUser|validateToken|verifyToken|verifySession|validateSession|getSessionUser|getSession\b|getCurrentUser|token\w*(Check|Verify|Valid)|session\w*(Check|Verify|Valid)|auth\w*(Check|Verify|Valid|Guard|Middleware|Required)|requireAuth|withAuth|authenticate\w*(User|Request|Token)?|checkAuth|isAuth|hasAuth|checkAccess|hasAccess|get_user|get_session_user|get_current_user|validate_session|verify_token|require_auth|with_auth|check_auth|auth_required|authenticate_user|authenticate_request|authenticate_token|token_check|token_verify|token_valid|session_check|session_verify|session_valid|auth_check|auth_guard|auth_middleware|get_current_user_authorizer|current_user_authorizer|login_required|permission_required|user_passes_test|check_authorization|check_permission|create_access_token|create_refresh_token|create_jwt_token|__progmune_auth_checked__|__progmune_credential_check__|__progmune_drf_permissions__|__progmune_auth_machinery__)\b/i, label: "auth_check" },
382
+ ],
383
+ violationMessage: "Mutation function does not check authentication. Anyone can create or modify data without credentials.",
384
+ conceptMissing: ["AuthenticationCheck", "AccessControl"],
385
+ conceptExpected: ["token validation", "session check", "auth middleware"],
386
+ excludePatterns: [
387
+ /set_authn_id/, // internal auth setter
388
+ /set_ssl_/, // SSL config setter
389
+ /set_config/, // configuration setter
390
+ /set_option/, // option setter
391
+ ],
392
+ },
345
393
  // ── Data Integrity (Foreign Key Validation) ──
346
394
  // v2: removed "process" and "send" (too generic for C)
395
+ // v3 (2026-08-15): param-aware. The old safeguard counted ANY get*/find* call
396
+ // as a foreign-key check — including getSessionUser/getUser auth lookups, which
397
+ // suppressed the rule on addComment/addNote/createReply (4 gold FNs). When param
398
+ // names are known, the rule only applies to functions taking a parent-reference
399
+ // parameter (…Id / entityType) and requires a NON-auth lookup call.
347
400
  {
348
401
  name: "Data Integrity (Foreign Key)",
349
402
  category: "data_integrity",
@@ -354,6 +407,11 @@ const SAFEGUARD_RULES = [
354
407
  violationMessage: "Creates a child entity without verifying the parent entity exists. Orphaned references possible.",
355
408
  conceptMissing: ["ForeignKeyValidation", "ReferentialIntegrity"],
356
409
  conceptExpected: ["checkExists", "getParent", "validateReference"],
410
+ parentRefGated: true,
411
+ strictSafeguards: [
412
+ // Entity lookups only — authentication lookups do NOT verify a parent exists.
413
+ { pattern: /\b(?!get(Session|Current)?User\b|getClient\b|verifyToken\b|validateSession\b)(get|find|check|exists|lookup|status|validate|verify)(?:[A-Z]\w*|_\w+)\b/i, label: "fk_check_strict" },
414
+ ],
357
415
  excludePatterns: [
358
416
  /_hd_/, // HPACK header compression
359
417
  /add_auth_info/, // internal auth metadata
@@ -388,7 +446,7 @@ const SAFEGUARD_RULES = [
388
446
  {
389
447
  name: "TLS Enforcement",
390
448
  category: "tls_enforcement",
391
- trigger: /\b(createServer|listen|handleRequest|app\.listen|express)\b/i,
449
+ trigger: /\b(createServer|listen|handleRequest|handle_request|app\.listen|express)\b/i,
392
450
  safeguards: [
393
451
  { pattern: /\b(https|tls|ssl|cert|key|TLS|SSL|HTTPS|createSecureContext|credentials)\b/i, label: "tls_config" },
394
452
  ],
@@ -401,9 +459,21 @@ const SAFEGUARD_RULES = [
401
459
  name: "Token Security (Weak Generation)",
402
460
  category: "token_security",
403
461
  languages: ["typescript", "javascript", "python"],
404
- trigger: /\b(authenticate|login|signIn|logIn|createSession|generateToken)\b/i,
462
+ trigger: /\b(authenticate|login|signIn|logIn|createSession|generateToken|do_login|sign_in|log_in|generate_token|create_session|reset_password|password_reset|forgot_password|reset_token|create_reset_token|generate_reset_token)\b/i,
463
+ // Semantic precondition: only fire when the function actually issues
464
+ // token material (set_cookie / token-named assignment — extractor marker).
465
+ // Login-named page renderers (login_otp) no longer fire.
466
+ requireMarker: "__progmune_token_issued__",
405
467
  safeguards: [
406
- { pattern: /\b(crypto\.randomUUID|jwt\.sign|jsonwebtoken|nanoid|randomBytes|cryptoRandomString)\b/i, label: "secure_token" },
468
+ { pattern: /\b(crypto\.randomUUID|jwt\.sign|jsonwebtoken|nanoid|randomBytes|cryptoRandomString|secrets\.token_urlsafe|secrets\.token_hex|token_urlsafe|token_hex|uuid\.uuid4|os\.urandom)\b/i, label: "secure_token" },
469
+ // Framework delegation: calling a token-issuing layer means the session
470
+ // material is handled by that layer, not generated inline. NOTE: bare
471
+ // jwt.encode is deliberately NOT here — a hardcoded secret key makes the
472
+ // JWT layer itself the vulnerability (PyGoat sec_misconfig_lab3).
473
+ { pattern: /\b(create_access_token|create_refresh_token|create_jwt_token|\.check_password\b|get_current_user_authorizer|login_required|permission_required|__progmune_framework_auth__|__progmune_auth_machinery__)\b/i, label: "framework_token" },
474
+ ],
475
+ excludePatterns: [
476
+ /login_not_required|login_required/, // decorators — the auth layer itself
407
477
  ],
408
478
  violationMessage: "Token/session generated without cryptographically secure random source. Tokens may be predictable or forgeable.",
409
479
  conceptMissing: ["SecureRandom", "TokenEntropy", "CryptographicSignature"],
@@ -413,9 +483,11 @@ const SAFEGUARD_RULES = [
413
483
  {
414
484
  name: "Authorization (Resource Ownership)",
415
485
  category: "authorization",
486
+ paramGated: true,
416
487
  trigger: /\b(toggle|remove)(?:[A-Z]\w*|_\w+)\b/i,
417
488
  safeguards: [
418
489
  { pattern: /\b(ownerId\s*[!=]==?|authorId\s*[!=]==?|userId\s*[!=]==?|createdBy|\.owner\s*[!=]==?)/i, label: "ownership_comparison" },
490
+ { pattern: /\b(__progmune_ownership_checked__)\b/, label: "inline_ownership_check" },
419
491
  ],
420
492
  violationMessage: "Resource mutation checks authentication but does NOT verify the resource belongs to the requesting user. Missing ownerId/authorId comparison.",
421
493
  conceptMissing: ["ResourceOwnership", "HorizontalAuthorization"],
@@ -461,7 +533,7 @@ const SAFEGUARD_RULES = [
461
533
  {
462
534
  name: "Rate Limiting",
463
535
  category: "rate_limiting",
464
- trigger: /\b(createServer|listen|handleRequest|app\.listen|express|router\.(post|get|put|delete|patch))\b/i,
536
+ trigger: /\b(createServer|listen|handleRequest|handle_request|app\.listen|express|router\.(post|get|put|delete|patch))\b/i,
465
537
  safeguards: [
466
538
  { pattern: /\b(rateLimit|rate_limit|throttle|RateLimiter|expressRateLimit|rateLimiterMiddleware|limiter)\b/i, label: "rate_limit" },
467
539
  ],
@@ -513,9 +585,14 @@ const SAFEGUARD_RULES = [
513
585
  name: "Command Injection",
514
586
  category: "input_validation",
515
587
  languages: ["python"],
516
- trigger: /\b(os\.system|os\.popen|subprocess\.(call|check_call|Popen|run)|commands\.getoutput|pty\.spawn)\b/i,
588
+ // Marker-driven: the extractor emits __progmune_command_dynamic__ only
589
+ // when a subprocess/os command receives a NON-static argument (static
590
+ // string/list invocations like installers stay silent), and
591
+ // __progmune_command_taint_flow__ when a tainted value flows to a
592
+ // command-named helper.
593
+ trigger: /\b(__progmune_command_dynamic__|__progmune_command_taint_flow__)\b/,
517
594
  safeguards: [
518
- { pattern: /\b(shlex\.quote|shlex\.split|pipes\.quote|shell\s*=\s*False)\b/i, label: "safe_command" },
595
+ { pattern: /\b(shlex\.quote|shlex\.split|pipes\.quote)\b/i, label: "safe_command" },
519
596
  ],
520
597
  violationMessage: "Shell command execution without input quoting. Vulnerable to command injection.",
521
598
  conceptMissing: ["CommandInjectionPrevention", "InputSanitization"],
@@ -525,10 +602,12 @@ const SAFEGUARD_RULES = [
525
602
  name: "Hardcoded Secrets",
526
603
  category: "token_security",
527
604
  languages: ["python"],
528
- trigger: /\b(password|secret|api_key|API_KEY|token|\w*TOKEN\w*)\s*=\s*["'][^"']+["']/i,
529
- safeguards: [
530
- { pattern: /\b(os\.environ|os\.getenv|config|\.env|python-dotenv|load_dotenv|SecretStr|Secret)\b/i, label: "env_secret" },
531
- ],
605
+ trigger: /\b(password|secret|api_key|API_KEY|token|\w*TOKEN\w*)\s*=\s*["'][^"']+["']|__progmune_hardcoded_secret__/i,
606
+ // Empty safeguards: the extractor marker is the complete evidence. (The old
607
+ // env-secret safeguard suppressed the marker itself — identifierParse of
608
+ // __progmune_hardcoded_secret__ yields the word "secret", matching the
609
+ // safeguard's "Secret" alternative.)
610
+ safeguards: [],
532
611
  violationMessage: "Sensitive credentials hardcoded in source code. Use environment variables or a secrets manager.",
533
612
  conceptMissing: ["SecretManagement", "ConfigurationSecurity"],
534
613
  conceptExpected: ["os.environ", "os.getenv", "dotenv"],
@@ -537,7 +616,7 @@ const SAFEGUARD_RULES = [
537
616
  name: "Dynamic Code Execution",
538
617
  category: "input_validation",
539
618
  languages: ["python"],
540
- trigger: /\b(eval|exec|compile|__import__)\s*\(/i,
619
+ trigger: /\b(eval|exec|compile|__import__)\s*\(|__progmune_eval_user_input__/i,
541
620
  safeguards: [
542
621
  { pattern: /\b(ast\.literal_eval|json\.loads|safe_eval)\b/i, label: "safe_eval" },
543
622
  ],
@@ -561,14 +640,134 @@ const SAFEGUARD_RULES = [
561
640
  name: "SQL Injection (Python)",
562
641
  category: "input_validation",
563
642
  languages: ["python"],
564
- trigger: /\b(execute|executemany)\b/i,
565
- safeguards: [
566
- { pattern: /\b(%s|\?|:\w+|parameterize|\.execute\s*\(\s*\w+\s*,\s*[\[(])/i, label: "parameterized_query" },
567
- ],
568
- violationMessage: "SQL executed with string formatting instead of parameterized queries. Vulnerable to SQL injection.",
643
+ // Source-level detection: the Python extractor emits a synthetic marker
644
+ // call when a SQL-executing call (execute/executemany/raw/...) builds its
645
+ // SQL text with dynamic formatting (f-string / % / .format / concatenation).
646
+ // Parameterized calls (execute("... %s", (args,))) produce no marker and
647
+ // are correctly NOT flagged. No satisfier possible the marker IS the
648
+ // violation evidence.
649
+ trigger: /\b(__progmune_sql_unparameterized__)\b/,
650
+ safeguards: [],
651
+ violationMessage: "SQL built with string formatting (f-string / % / .format / concatenation) instead of parameterized queries. Vulnerable to SQL injection.",
569
652
  conceptMissing: ["SQLInjectionPrevention", "ParameterizedQueries"],
570
653
  conceptExpected: ["parameterized query", "%s placeholder"],
571
654
  },
655
+ {
656
+ name: "SSRF (User-Controlled URL Fetch)",
657
+ category: "ssrf",
658
+ languages: ["python"],
659
+ // Source-level detection: the Python extractor emits a synthetic marker
660
+ // call when an HTTP fetch (requests.*/urllib.*/httpx.*/aiohttp.*/urlopen)
661
+ // receives a URL tainted by request-derived user input (directly or via
662
+ // single-hop assignment). No satisfier possible — the marker IS the
663
+ // violation evidence.
664
+ trigger: /\b(__progmune_ssrf_user_url__)\b/,
665
+ safeguards: [],
666
+ violationMessage: "HTTP fetch whose URL derives from user-controlled request input — server-side request forgery.",
667
+ conceptMissing: ["SSRFPrevention", "URLValidation"],
668
+ conceptExpected: ["URL allowlist", "scheme validation"],
669
+ },
670
+ {
671
+ name: "Path Traversal (User-Controlled File Path)",
672
+ category: "path_traversal",
673
+ languages: ["python"],
674
+ // Source-level detection: the Python extractor emits a synthetic marker
675
+ // call when a file sink (open / io.open / os.open / Path(...).read_text)
676
+ // receives a path tainted by request-derived user input (directly or via
677
+ // single-hop assignment — os.path.join chains resolve through assignment
678
+ // tracking). No satisfier possible — the marker IS the violation evidence.
679
+ trigger: /\b(__progmune_path_traversal__)\b/,
680
+ safeguards: [],
681
+ violationMessage: "File opened with a path derived from user-controlled request input — path traversal / arbitrary file access.",
682
+ conceptMissing: ["PathTraversalPrevention", "InputPathValidation"],
683
+ conceptExpected: ["path allowlist", "basename normalization", "path sanitization"],
684
+ },
685
+ {
686
+ name: "XSS (Unsafe Template Rendering)",
687
+ category: "xss",
688
+ languages: ["python"],
689
+ // Cross-file detection: the Python extractor scans templates for variables
690
+ // rendered without escaping ({{ var|safe }}, {% autoescape off %}) and emits
691
+ // a synthetic marker when a render/render_to_string call binds tainted
692
+ // request-derived values to those variables — or when mark_safe() is
693
+ // applied to tainted input.
694
+ trigger: /\b(__progmune_xss_unsafe_render__)\b/,
695
+ safeguards: [],
696
+ violationMessage: "User-controlled input rendered in a template without escaping (|safe / autoescape off / mark_safe) — stored or reflected XSS.",
697
+ conceptMissing: ["XSSPrevention", "OutputEncoding"],
698
+ conceptExpected: ["template autoescape", "output escaping"],
699
+ },
700
+ {
701
+ name: "SSTI (Template Injection)",
702
+ category: "ssti",
703
+ languages: ["python"],
704
+ // Source-level detection: the Python extractor emits a synthetic marker
705
+ // when (S1) a template-string sink (render_template_string / Template /
706
+ // from_string) receives tainted input, or (S2) tainted content is written
707
+ // to a file opened under a template path — the Django dynamic-template
708
+ // pattern where user input becomes template source.
709
+ trigger: /\b(__progmune_ssti_template_injection__)\b/,
710
+ safeguards: [],
711
+ violationMessage: "User-controlled input used as template source — server-side template injection.",
712
+ conceptMissing: ["SSTIPrevention", "TemplateSandbox"],
713
+ conceptExpected: ["static template files", "no user template syntax"],
714
+ },
715
+ {
716
+ name: "XXE (External Entity Processing)",
717
+ category: "xxe",
718
+ languages: ["python"],
719
+ // Source-level detection: the Python extractor emits a synthetic marker
720
+ // when BOTH signals co-occur — an explicitly unsafe parser configuration
721
+ // (setFeature(feature_external_*, True) / XMLParser(resolve_entities=True))
722
+ // AND parsing of tainted request-derived XML (parse/parseString/fromstring).
723
+ // Config-only or taint-only alone is not flagged.
724
+ trigger: /\b(__progmune_xxe_external_entities__)\b/,
725
+ safeguards: [],
726
+ violationMessage: "XML parsed from user-controlled input with external entity processing explicitly enabled — XXE.",
727
+ conceptMissing: ["XXEPrevention", "EntityExpansionControl"],
728
+ conceptExpected: ["disable external entities", "secure parser config"],
729
+ },
730
+ {
731
+ name: "CSRF Protection Disabled",
732
+ category: "csrf",
733
+ languages: ["python"],
734
+ // Source-level detection: the Python extractor emits a synthetic marker
735
+ // when a function carries the @csrf_exempt decorator — Django CSRF
736
+ // protection explicitly disabled on the view.
737
+ trigger: /\b(__progmune_csrf_disabled__)\b/,
738
+ safeguards: [],
739
+ violationMessage: "View decorated with @csrf_exempt — CSRF protection explicitly disabled.",
740
+ conceptMissing: ["CSRFProtection", "StateChangingRequestValidation"],
741
+ conceptExpected: ["csrf token validation", "SameSite cookies"],
742
+ },
743
+ {
744
+ name: "CSRF Exposed GET State Change",
745
+ category: "csrf",
746
+ languages: ["python"],
747
+ // Source-level detection: the Python extractor emits a synthetic marker
748
+ // when a `request.method == 'GET'` branch performs state-changing calls
749
+ // (.save/.update/.delete/.create) — state change on GET, CSRF-exposed
750
+ // even without @csrf_exempt.
751
+ trigger: /\b(__progmune_get_state_change__)\b/,
752
+ safeguards: [],
753
+ violationMessage: "State-changing operation executed in a GET branch — CSRF-exposed without token validation.",
754
+ conceptMissing: ["CSRFProtection", "SafeMethodEnforcement"],
755
+ conceptExpected: ["POST for state changes", "csrf token validation"],
756
+ },
757
+ {
758
+ name: "Authorization via Client Cookie",
759
+ category: "authorization",
760
+ languages: ["python"],
761
+ // Source-level detection: the Python extractor emits a synthetic marker
762
+ // when a client-controlled cookie value (request.COOKIES, incl. single-hop
763
+ // assignment chains like cookie.split('|')[0]) participates in a comparison
764
+ // or branch test — authorization decided by cookie contents.
765
+ trigger: /\b(__progmune_cookie_authorization__)\b/,
766
+ safeguards: [],
767
+ violationMessage: "Authorization decision based on a client-controlled cookie value — cookie contents are user-editable.",
768
+ conceptMissing: ["ServerSideAuthorization", "SessionIntegrity"],
769
+ conceptExpected: ["server-side session checks", "signed sessions"],
770
+ },
572
771
  // ═══════════════════════════════════════════════════════════════
573
772
  // P0 Injection: Payment + Session safeguard rules
574
773
  // ═══════════════════════════════════════════════════════════════
@@ -612,7 +811,7 @@ const SAFEGUARD_RULES = [
612
811
  {
613
812
  name: "Session No Timeout",
614
813
  category: "session",
615
- trigger: /\b(\w*session\w*create|\w*create\w*session|\w*session\w*new|\w*session\w*start|\w*login\w*session|\w*session\w*init|signIn|signin|login\b|authenticate\b|createSession|create_session)\b/i,
814
+ trigger: /\b(\w*session\w*create|\w*create\w*session|\w*session\w*new|\w*session\w*start|\w*login\w*session|\w*session\w*init|signIn|signin|login\b|authenticate\b|createSession|create_session|do_login|sign_in|log_in)\b/i,
616
815
  safeguards: [
617
816
  { pattern: /\b(\w*expir|\w*ttl|\w*timeout|\w*max\w*age|\w*maxAge|\w*max_age|\w*lifetime|\w*duration|\w*expires|\w*deadline|\w*valid\w*for|\w*valid\w*until)/i, label: "timeout_set" },
618
817
  ],
@@ -636,6 +835,8 @@ const SAFEGUARD_RULES = [
636
835
  trigger: /\b(\w*password\w*change|\w*password\w*reset|\w*change\w*password|\w*reset\w*password|\w*update\w*password|\w*privilege|\w*role\w*change|\w*escalat|\w*enable\w*2fa|\w*mfa\w*enable|\w*email\w*change)\b/i,
637
836
  safeguards: [
638
837
  { pattern: /\b(\w*revoke|\w*rotate|\w*invalidate|\w*reissue|\w*regenerate|\w*new\w*token|\w*token\w*refresh|\w*session\w*refresh|\w*renew)/i, label: "token_rotate" },
838
+ // Password-change machinery itself (the material IS rotated/reissued here).
839
+ { pattern: /\.(change_password|set_password|update_password|generate_salt|get_password_hash)\b/i, label: "password_machinery" },
639
840
  ],
640
841
  violationMessage: "Privilege-changing operation detected without subsequent token rotation or session invalidation. Stolen pre-change tokens remain valid.",
641
842
  conceptMissing: ["TokenRotation", "SessionInvalidation", "FixationPrevention"],
@@ -648,7 +849,7 @@ const SAFEGUARD_RULES = [
648
849
  {
649
850
  name: "Registration Without Email Verification",
650
851
  category: "registration",
651
- trigger: /\b(register|signup|signUp|registerUser|createUser|createAccount)\b/i,
852
+ trigger: /\b(register|signup|signUp|registerUser|createUser|createAccount|sign_up|create_user|create_account|register_user|register_new_user)\b/i,
652
853
  safeguards: [
653
854
  { pattern: /\b(send\w*(Code|Otp|Token|Verif|Email|Sms|Link)|(code|otp|token|verif)\w*send|verification|confirmEmail|verifyEmail|sendVerification|verify_user_email)\b/i, label: "email_verify" },
654
855
  ],
@@ -793,13 +994,23 @@ const SAFEGUARD_RULES = [
793
994
  // Privilege Escalation, API Contract
794
995
  // ═══════════════════════════════════════════════════════════════
795
996
  // ── PLS-005: Session Fixation — session not invalidated on logout ──
997
+ // v2 (2026-08-15): recognize store-based invalidation — splicing/filtering the
998
+ // session store IS invalidation (144 FPs on the 100-project benchmark came from
999
+ // logouts that do `sessions.splice(idx, 1)`). Also, a function that delegates to
1000
+ // a logout-named function is not itself failing to invalidate — the logout
1001
+ // function's own body is where the check belongs (callsOnly guard).
796
1002
  {
797
1003
  name: "Session Fixation (Logout without Invalidation)",
798
1004
  category: "session_fixation",
799
1005
  languages: ["typescript", "javascript", "python"],
800
- trigger: /\b(logout|signOut|logOut|signout|doLogout|handleLogout|endSession|clearSession)\b/i,
1006
+ trigger: /\b(logout|signOut|logOut|signout|doLogout|handleLogout|endSession|clearSession|do_logout|sign_out|log_out|handle_logout|end_session|clear_session|invalidate_session)\b/i,
801
1007
  safeguards: [
802
1008
  { pattern: /\b(session\w*destroy|destroy\w*session|session\w*invalidate|invalidate\w*session|session\w*revoke|revoke\w*session|session\w*clear|clear\w*session|session\w*end|end\w*session|session\w*expire|expire\w*session|token\w*revoke|revoke\w*token|token\w*blacklist|blacklist\w*token|invalidate\w*token)\b/i, label: "session_invalidate" },
1009
+ // Store-based invalidation: remove the session entry from the store.
1010
+ { pattern: /\b(splice|filter|pop|shift|clear|delete_cookie|delete_cookies)\b/i, label: "store_invalidate" },
1011
+ // Delegation: calling a logout-named function hands invalidation to that
1012
+ // function (its own body is checked separately).
1013
+ { pattern: /\b(logout|signOut|logOut|signout|doLogout|handleLogout|endSession|clearSession|do_logout|sign_out|log_out|handle_logout|end_session|clear_session|invalidate_session)\b/i, label: "delegated_logout", callsOnly: true },
803
1014
  ],
804
1015
  violationMessage: "Logout function does not destroy/invalidate the session. Old session tokens remain valid, enabling session hijacking (session fixation).",
805
1016
  conceptMissing: ["SessionInvalidation", "SessionRevocation"],
@@ -869,10 +1080,14 @@ function identifierParse(name) {
869
1080
  * Detect missing safeguards in function call sequences.
870
1081
  * Uses identifier parsing to match compound names (registerNewUser → register).
871
1082
  */
872
- function detectSafeguardViolations(calls, enclosingFuncName, language) {
1083
+ function detectSafeguardViolations(calls, enclosingFuncName, language, params, exposed) {
873
1084
  const violations = [];
874
- // Build effective calls: raw names + identifier-parsed words
875
- const rawCalls = enclosingFuncName ? [enclosingFuncName, ...calls] : [...calls];
1085
+ // Build effective calls: raw names + identifier-parsed words.
1086
+ // Class-qualified names (Class.method) contribute only their METHOD name —
1087
+ // a class name like SetCommands/ListCommands must not leak "Set"/"List"
1088
+ // words into trigger matching (real-world collision class found in redis-py).
1089
+ const ownName = enclosingFuncName ? (enclosingFuncName.split(".").pop() || enclosingFuncName) : undefined;
1090
+ const rawCalls = ownName ? [ownName, ...calls] : [...calls];
876
1091
  const parsedWords = [];
877
1092
  for (const c of rawCalls) {
878
1093
  parsedWords.push(...identifierParse(c));
@@ -880,7 +1095,7 @@ function detectSafeguardViolations(calls, enclosingFuncName, language) {
880
1095
  const effectiveCalls = [...new Set([...rawCalls, ...parsedWords])];
881
1096
  // Skip authorization rules for auth functions — check both raw lowercased name and parsed words
882
1097
  const rawLower = enclosingFuncName?.toLowerCase() || "";
883
- const AUTH_PATTERN = /\b(register|signup|signin|login|authenticate|createuser|createaccount|registeruser|registernewuser|dologin|verifytoken|validatesession|getuser|getsessionuser|getcurrentuser|endsession|logout|signout|dologout|destroysession|invalidatesession|invalidate|signout)\b/i;
1098
+ const AUTH_PATTERN = /\b(register|signup|signin|login|authenticate|createuser|createaccount|registeruser|registernewuser|dologin|verifytoken|validatesession|getuser|getsessionuser|getcurrentuser|endsession|logout|signout|dologout|destroysession|invalidatesession|invalidate|signout|create_account|register_new_user|register_user|sign_up|create_user|do_login|sign_in|log_in|verify_token|validate_session|get_user|get_session_user|get_current_user|do_logout|sign_out|log_out|end_session|invalidate_session|clear_session)\b/i;
884
1099
  const isAuthFunction = enclosingFuncName != null && (AUTH_PATTERN.test(rawLower) ||
885
1100
  identifierParse(enclosingFuncName).some(w => AUTH_PATTERN.test(w)));
886
1101
  // Filter rules by language
@@ -889,14 +1104,37 @@ function detectSafeguardViolations(calls, enclosingFuncName, language) {
889
1104
  : SAFEGUARD_RULES;
890
1105
  for (const rule of activeRules) {
891
1106
  // Check if trigger matches
892
- const triggerMatch = effectiveCalls.some(c => rule.trigger.test(c));
1107
+ const triggerCalls = rule.triggerCallsOnly ? rawCalls : effectiveCalls;
1108
+ const triggerMatch = triggerCalls.some(c => rule.trigger.test(c));
893
1109
  if (!triggerMatch)
894
1110
  continue;
1111
+ // Semantic precondition marker (extractor-emitted)
1112
+ if (rule.requireMarker && !effectiveCalls.includes(rule.requireMarker))
1113
+ continue;
895
1114
  // Skip authorization rules for auth functions — they ARE the auth
896
1115
  if (isAuthFunction && rule.category === "authorization")
897
1116
  continue;
1117
+ // Param-gated rules (parentRefGated): only apply when the function takes a
1118
+ // parent-reference parameter. Requires the caller to pass param names.
1119
+ if (rule.parentRefGated && params) {
1120
+ const hasParentRef = params.some(p => /Id$/i.test(p) || /^entityType$/i.test(p));
1121
+ if (!hasParentRef)
1122
+ continue;
1123
+ }
1124
+ // Surface gate (paramGated): only apply to functions that can plausibly
1125
+ // authenticate — routed by a web handler (exposed) or taking an
1126
+ // identity-ish parameter. Requires the caller to pass param names.
1127
+ if (rule.paramGated && params) {
1128
+ const hasIdentity = params.some(p => /\b(token|session|user|auth|request|scope|cookie|credential|permission|role|identity)\b/i.test(p));
1129
+ if (!hasIdentity && !exposed)
1130
+ continue;
1131
+ }
898
1132
  // Check if at least one safeguard matches
899
- const matchedSafeguard = rule.safeguards.find(s => effectiveCalls.some(c => s.pattern.test(c)));
1133
+ const guards = (params && rule.strictSafeguards) ? rule.strictSafeguards : rule.safeguards;
1134
+ const matchedSafeguard = guards.find(s => {
1135
+ const testCalls = s.callsOnly ? (calls || []) : effectiveCalls;
1136
+ return testCalls.some(c => s.pattern.test(c));
1137
+ });
900
1138
  if (matchedSafeguard)
901
1139
  continue;
902
1140
  // Check excludePatterns (library functions where safeguard is deferred to separate API)
@@ -1001,7 +1239,7 @@ function detectSafeguardViolationsV7(calls, enclosingFuncName, callerMap, funcCa
1001
1239
  safeContext.add(c);
1002
1240
  // Skip authorization rules for auth functions
1003
1241
  const rawLower = enclosingFuncName?.toLowerCase() || "";
1004
- const AUTH_PATTERN = /\b(register|signup|signin|login|authenticate|createuser|createaccount|registeruser|registernewuser|dologin|verifytoken|validatesession|getuser|getsessionuser|getcurrentuser|endsession|logout|signout|dologout|destroysession|invalidatesession|invalidate|signout)\b/i;
1242
+ const AUTH_PATTERN = /\b(register|signup|signin|login|authenticate|createuser|createaccount|registeruser|registernewuser|dologin|verifytoken|validatesession|getuser|getsessionuser|getcurrentuser|endsession|logout|signout|dologout|destroysession|invalidatesession|invalidate|signout|create_account|register_new_user|register_user|sign_up|create_user|do_login|sign_in|log_in|verify_token|validate_session|get_user|get_session_user|get_current_user|do_logout|sign_out|log_out|end_session|invalidate_session|clear_session)\b/i;
1005
1243
  const isAuthFunction = enclosingFuncName != null && (AUTH_PATTERN.test(rawLower) ||
1006
1244
  identifierParse(enclosingFuncName).some(w => AUTH_PATTERN.test(w)));
1007
1245
  // Filter rules by language
@@ -346,7 +346,7 @@
346
346
  <div class="bar"><span class="bl">人工审查能力</span><div class="bt"><div class="bf" style="width:10%"></div></div><span class="bv">1x</span></div>
347
347
 
348
348
  <h3>3.2 问题本质:AI 写代码的原理决定了它"看不见"错误</h3>
349
- <p>大语言模型生成代码的方式是<strong>统计预测</strong>——根据训练数据猜"下一个最可能的词"。学术研究(Kambhampati et al., ICML 2026)明确指出:LLM 输出的"逐步推理"是统计表演,中间 token 与真实推理之间没有可靠的因果对应。</p>
349
+ <p>大语言模型生成代码的方式是<strong>统计预测</strong>——根据训练数据猜"下一个最可能的词"。这一观点的代表性论述来自 Subbarao Kambhampati 等的立场论文《Stop Anthropomorphizing Intermediate Tokens as Reasoning/Thinking Traces!》(arXiv:2505.22285)及其 ICML 2026 关于 verifier 的主题演讲:LLM 输出的"逐步推理"是统计表演,中间 token 与真实推理之间没有可靠的因果对应。</p>
350
350
  <p>这意味着:AI 写出"看似合理的代码"和"实际正确的代码"是两件事。它会一本正经地调用不存在的函数、跳过关键的认证步骤——<strong>而且完全静默,不报错</strong>。</p>
351
351
 
352
352
  <h3>3.3 现有方案为什么不够</h3>
@@ -470,7 +470,18 @@ AI 生成: create_session(...) ← 当前状态 UNAUTHENTICATED
470
470
  <h3>5.4 API 语义映射:让检查认识"陌生函数"</h3>
471
471
  <p>真实项目使用的函数名千变万化(<code>createSessionToken</code>、<code>signSession</code>、<code>jwt.sign</code>……)。系统用三层策略把陌生函数映射到协议规则:① 本地前缀/别名表(含项目自定义 <code>.progmune_aliases.json</code>)→ ② 单词切分匹配 → ③ LLM 批量语义分类(带磁盘缓存,重复运行零 LLM 成本)。</p>
472
472
 
473
- <h3>5.5 信任引擎:五阶段流水线</h3>
473
+ <h3>5.5 双路径架构:生成时拦截 vs 事后检查</h3>
474
+ <p>Progmune 用两条互补路径覆盖两类代码来源——这是理解产品的关键,也是最容易被误读的地方:</p>
475
+ <table>
476
+ <tr><th></th><th><strong>生成路径</strong>(生成时拦截)</th><th><strong>验证路径</strong>(事后检查)</th></tr>
477
+ <tr><td>覆盖场景</td><td>通过 <code>progmune_generate</code> 生成的代码</td><td>任何来源的代码(Copilot、Cursor、人工)</td></tr>
478
+ <tr><td>机制</td><td>生成循环内 8 道验证关卡:JSON 解析 → 结构预检 → 符号纠正 → 类型修正 → 数据流 → 协议状态机 → BFS 确定性修复 → 语义合约。违规代码<strong>从不写入文件</strong></td><td>信任引擎五阶段流水线(收集→评分→决策→组装),四维加权输出部署决策</td></tr>
479
+ <tr><td>错误处理时机</td><td>创建时刻——错误从未存在</td><td>事后——文件已存在</td></tr>
480
+ <tr><td>成本曲线</td><td>零——违规没有落地</td><td>发现越晚越贵</td></tr>
481
+ </table>
482
+ <p>核心产品论:<strong>在生成时刻验证,而非事后补救。</strong>LLM 的输出只是提议,状态机才是裁判——LLM 可以被劝服,状态机不会。</p>
483
+
484
+ <h3>5.6 信任引擎:五阶段流水线</h3>
474
485
  <div class="flow">
475
486
  <div class="node">1. 收集<br><span style="font-size:.8em;font-weight:400">策略+协议+框架数据</span></div>
476
487
  <span class="arrow">→</span>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "progmune-runtime",
3
- "version": "3.2.0",
3
+ "version": "3.3.0",
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/",
@@ -106,4 +106,4 @@
106
106
  "tsx": "^4.22.4",
107
107
  "vitest": "^3.2.6"
108
108
  }
109
- }
109
+ }