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 +38 -15
- package/dist/extract-ir.js +13 -0
- package/dist/protocol-detector.js +271 -33
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
[](https://opensource.org/licenses/MIT)
|
|
6
6
|
[](https://modelcontextprotocol.io)
|
|
7
|
-
[]()
|
|
8
|
+
[]()
|
|
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:
|
|
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
|
-
- **
|
|
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
|
-
- **
|
|
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 |
|
|
126
|
-
| Recall |
|
|
127
|
-
|
|
|
128
|
-
|
|
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
|
|
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:**
|
|
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
|
|
package/dist/extract-ir.js
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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
|
|
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: ["
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
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/",
|