progmune-runtime 3.2.1 → 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
 
@@ -49,6 +49,10 @@ AI code generators produce syntactically valid code that often violates **protoc
49
49
  | **Payment** | Order without verification, refund without authorization, webhook without signature check |
50
50
  | **Resource** | File opened but not closed, connection without cleanup, malloc without free |
51
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.
52
56
 
53
57
  ---
54
58
 
@@ -95,19 +99,19 @@ Progmune is honest about what it can and cannot verify.
95
99
 
96
100
  | Language | Status | Evidence |
97
101
  |----------|--------|----------|
98
- | **TypeScript / JavaScript** | ✅ Production | Blind benchmark: P=86.8%, R=83.6%, F1=85.2% (432 sequences, 10 projects) |
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 |
99
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). |
100
- | **Python** | 🔨 IR only | IR extractor exists (`extract-ir-python.ts`), no verification rules yet |
101
105
  | **Go, Java** | ❌ None | Planned |
102
106
 
103
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.
104
108
 
105
109
  ### What Progmune does NOT cover (honest boundaries)
106
110
 
107
- - **Taint-based injection flaws** — SQL injection, XSS, command injection. These require dataflow/taint tracking, which is deliberately out of scope in Phase 1 (adding it would make Progmune a generic SAST competitor; protocol-sequence verification is the differentiator).
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).
108
112
  - **SCA / dependency vulnerabilities** — hallucinated package names, supply-chain issues. Separate tooling exists for this.
109
113
  - **Runtime behavior** — Progmune is static analysis only; no DAST/sandbox execution.
110
- - **Obfuscated or dynamic code** — `eval`, `Function` constructor, and heavily obfuscated flows degrade regex/IR detection recall.
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.
111
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%.
112
116
 
113
117
  → [Full Coverage Matrix](docs/coverage-matrix.md)
@@ -118,14 +122,31 @@ Progmune is honest about what it can and cannot verify.
118
122
 
119
123
  Public, reproducible precision data. All numbers measured against gold-annotated benchmarks.
120
124
 
121
- ### TypeScript (Blind Benchmark v6)
125
+ ### TypeScript (Blind Benchmark v6 — 100 projects)
126
+
127
+ | Metric | Value |
128
+ |--------|-------|
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)
122
134
 
123
135
  | Metric | Value |
124
136
  |--------|-------|
125
- | Precision | 86.8% |
126
- | Recall | 83.6% |
127
- | F1 | 85.2% |
128
- | Projects | 10 (ecommerce, blog, chat, crm, forum, wiki, issuetracker, filestorage, todo, scheduler) |
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)
129
150
 
130
151
  ### C (Gold Benchmark — research status)
131
152
 
@@ -149,7 +170,9 @@ SDK (src/sdk.ts) verify() → APPROVED / NEEDS_REVIEW / BLOCKED
149
170
  ├─ Policy Engine Enterprise policy enforcement (ALLOW/WARN/BLOCK)
150
171
  ├─ SSG Validator Protocol state machine verification
151
172
  ├─ Protocol Detector Regex-based protocol step detection (22 detectors)
152
- ├─ 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
153
176
  ├─ Repair Executor detect → plan → fix → validate → commit/rollback
154
177
  └─ Knowledge Base 31 domains, 140 rules, evidence chains
155
178
  ```
@@ -193,9 +216,9 @@ High-impact contribution areas:
193
216
  - **Trust Engine:** 4-dimension scoring with binary explainability gate
194
217
  - **MCP Tools:** 19 — `progmune_trust_check`, `progmune_score`, `progmune_policy_check`, `progmune_certify`, and more
195
218
  - **Framework Adapters:** Express ✅, tRPC ✅, NestJS partial (2/13)
196
- - **Knowledge Base:** 31 domains, 148 protocol rules, 22 detectors, 26 safeguards, PLSB 13/13 categories
197
- - **Corpus:** 2,500+ trajectories across 6+ repositories
198
- - **Current focus:** Framework adaptation + enterprise PoC validation
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
199
222
 
200
223
  ---
201
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "progmune-runtime",
3
- "version": "3.2.1",
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/",