progmune-runtime 3.2.0 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
[](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
|
|
|
@@ -23,6 +23,21 @@ Output: `APPROVED` / `NEEDS_REVIEW` / `BLOCKED` — with Trust Score, evidence,
|
|
|
23
23
|
|
|
24
24
|
---
|
|
25
25
|
|
|
26
|
+
## Two Paths: Intercept or Verify
|
|
27
|
+
|
|
28
|
+
Progmune covers **two code sources** with two complementary mechanisms:
|
|
29
|
+
|
|
30
|
+
| | **Generate path** (agent-time interception) | **Verify path** (post-hoc checking) |
|
|
31
|
+
|---|---|---|
|
|
32
|
+
| **Covers** | Code generated *through Progmune* (`progmune_generate` / `progmune_execute`) | Code written anywhere — Copilot, Cursor, humans (`progmune_trust_check` / SDK / CI) |
|
|
33
|
+
| **Mechanism** | 8 validation gates inside the generation loop: JSON parse → schema → SVL-1 symbol → SVL-2 types → SVL-3 dataflow → SVL-4 protocol state machine → BFS deterministic repair → semantic contract. Violating code is **never written to disk** — it is corrected or retried before emission. | Trust Engine: 4-dimension weighted scoring (policy 35% / protocol 30% / coverage 20% / governance 15%) → Decision + evidence trail |
|
|
34
|
+
| **When errors are handled** | At creation time — the error never exists | After the fact — the file already exists |
|
|
35
|
+
| **Cost curve** | Zero — the violation never landed | Higher the later it's found |
|
|
36
|
+
|
|
37
|
+
This is the core product thesis: **verify at generation time, not after the fact.** LLM outputs are proposals; the state machine is the referee. LLMs can be persuaded — state machines cannot.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
26
41
|
## What Progmune Detects
|
|
27
42
|
|
|
28
43
|
AI code generators produce syntactically valid code that often violates **protocol lifecycles** — the correct sequence of operations like open→read→close or auth→validate→respond. These violations are invisible to traditional static analysis.
|
|
@@ -34,6 +49,10 @@ AI code generators produce syntactically valid code that often violates **protoc
|
|
|
34
49
|
| **Payment** | Order without verification, refund without authorization, webhook without signature check |
|
|
35
50
|
| **Resource** | File opened but not closed, connection without cleanup, malloc without free |
|
|
36
51
|
| **Data Integrity** | Mutation without audit trail, missing input validation |
|
|
52
|
+
| **Injection (Python, source-level)** | SQL built with f-string/`%`/`.format`/concatenation, command injection via dynamic subprocess args, SSRF via user-controlled URL fetches, SSTI via template-string sinks, XXE via external-entity parser config, eval/exec on user input |
|
|
53
|
+
| **Web (Python, source-level)** | XSS via `{{ var\|safe }}`/autoescape-off templates, path traversal via user-controlled file paths, CSRF via `@csrf_exempt` or GET state changes, authorization by client cookies, hardcoded JWT secrets (incl. cross-module constants) |
|
|
54
|
+
|
|
55
|
+
Source-level detections use an extractor-marker architecture: the IR extractor performs taint tracking, import resolution, and cross-file analysis (templates, module constants), emitting synthetic markers that rules consume — zero pipeline changes, fully auditable.
|
|
37
56
|
|
|
38
57
|
---
|
|
39
58
|
|
|
@@ -80,12 +99,20 @@ Progmune is honest about what it can and cannot verify.
|
|
|
80
99
|
|
|
81
100
|
| Language | Status | Evidence |
|
|
82
101
|
|----------|--------|----------|
|
|
83
|
-
| **TypeScript / JavaScript** | ✅ Production | Blind benchmark:
|
|
84
|
-
| **
|
|
85
|
-
| **
|
|
102
|
+
| **TypeScript / JavaScript** | ✅ Production | Blind benchmark: **recall 98.5% / precision 100%** (795 gold findings, 100 projects) |
|
|
103
|
+
| **Python** | ✅ Production | Blind benchmark: **recall 100% / precision 100%** (729 gold findings, 90 projects); real-world validation: PyGoat (OWASP vulnerable-by-design Django app) **67 TP / 0 FP, 100% labeled precision**; three well-written apps (django/fastapi realworld, django-unicorn) with 0 false-positive true findings |
|
|
104
|
+
| **C** | ⚠️ Research-only | Gold benchmark F1=16.5%. L3 cross-function experiment terminated; L4 not planned. See [C Language Status](docs/c-language-status.md). |
|
|
86
105
|
| **Go, Java** | ❌ None | Planned |
|
|
87
106
|
|
|
88
|
-
**Framework adapters:
|
|
107
|
+
**Framework adapters: 2/13.** Express ✅ and tRPC ✅ have dedicated detectors; Next.js has version-aware governance; NestJS is partial. Django, FastAPI and 8 more remain — framework adaptation is the #1 product gap.
|
|
108
|
+
|
|
109
|
+
### What Progmune does NOT cover (honest boundaries)
|
|
110
|
+
|
|
111
|
+
- **TS-side taint-based injection flaws** — the source-level SQLi/XSS/SSRF detections ship for Python; the TypeScript extractor is name/call-based, so TS injection classes remain uncovered (documented, not hidden).
|
|
112
|
+
- **SCA / dependency vulnerabilities** — hallucinated package names, supply-chain issues. Separate tooling exists for this.
|
|
113
|
+
- **Runtime behavior** — Progmune is static analysis only; no DAST/sandbox execution.
|
|
114
|
+
- **Framework internals** — well-known framework dispatch/cache machinery (e.g. django-unicorn internals) can produce a small number of boundary false positives; they are documented per-corpus in the benchmark gold files.
|
|
115
|
+
- **Known failure boundaries are documented** rather than hidden: if Progmune cannot verify a language (e.g. Go), Confidence is lowered instead of pretending 100%.
|
|
89
116
|
|
|
90
117
|
→ [Full Coverage Matrix](docs/coverage-matrix.md)
|
|
91
118
|
|
|
@@ -95,23 +122,35 @@ Progmune is honest about what it can and cannot verify.
|
|
|
95
122
|
|
|
96
123
|
Public, reproducible precision data. All numbers measured against gold-annotated benchmarks.
|
|
97
124
|
|
|
98
|
-
### TypeScript (Blind Benchmark v6)
|
|
125
|
+
### TypeScript (Blind Benchmark v6 — 100 projects)
|
|
99
126
|
|
|
100
127
|
| Metric | Value |
|
|
101
128
|
|--------|-------|
|
|
102
|
-
| Precision |
|
|
103
|
-
| Recall |
|
|
104
|
-
|
|
|
105
|
-
|
|
129
|
+
| Precision | **100%** (0 factual FPs) |
|
|
130
|
+
| Recall | **98.5%** (effective 100% — the 12 non-detected findings are excluded by methodology) |
|
|
131
|
+
| Gold findings | 795 across 100 projects (90 style-variants + 10 model-variants) |
|
|
132
|
+
|
|
133
|
+
### Python (Blind Benchmark v1 — 90 projects)
|
|
134
|
+
|
|
135
|
+
| Metric | Value |
|
|
136
|
+
|--------|-------|
|
|
137
|
+
| Precision | **100%** |
|
|
138
|
+
| Recall | **100%** |
|
|
139
|
+
| Gold findings | 729 across 90 style-variant projects |
|
|
140
|
+
|
|
141
|
+
### Real-world validation (PyGoat, OWASP vulnerable-by-design Django app)
|
|
142
|
+
|
|
143
|
+
| Metric | Value |
|
|
144
|
+
|--------|-------|
|
|
145
|
+
| Labeled precision | **100%** (67 true positives / 0 false positives, per-detection human review) |
|
|
146
|
+
| Classes covered | 14 vulnerability classes incl. SQLi, SSRF, path traversal, XSS, SSTI, XXE, command injection, deserialization, CSRF (both shapes), cookie authorization, hardcoded secrets |
|
|
147
|
+
| Well-written apps | django-realworld, fastapi-realworld, django-unicorn — 0 false-positive true findings; 3 documented framework-internal boundary FPs |
|
|
148
|
+
|
|
149
|
+
→ [Real-world validation report](blind-benchmark/REALWORLD_APP_V1.md) · [Benchmark baseline](blind-benchmark/BASELINE_v6.md)
|
|
106
150
|
|
|
107
|
-
### C (Gold Benchmark
|
|
151
|
+
### C (Gold Benchmark — research status)
|
|
108
152
|
|
|
109
|
-
|
|
110
|
-
|------|-----------|--------|-----|---------|
|
|
111
|
-
| curl | 30.9% | 87.5% | 45.7% | 85 |
|
|
112
|
-
| libssh | 36.0% | 64.3% | 46.2% | 47 |
|
|
113
|
-
| nginx | — | — | 0 FP | 50 |
|
|
114
|
-
| redis | — | — | 0 FP | 50 |
|
|
153
|
+
C analysis is **research-only**: gold benchmark F1=16.5% across 4 repos (curl, libssh, nginx, openssl). The bottleneck is rule coverage, not context. L3 (cross-function) was terminated with data; L4 (pointer/CFG) is a multi-year research problem and not planned. See [C Language Status](docs/c-language-status.md) for the full picture and reasoning.
|
|
115
154
|
|
|
116
155
|
### P0-P3 Rule Injection (2026-08)
|
|
117
156
|
|
|
@@ -131,7 +170,9 @@ SDK (src/sdk.ts) verify() → APPROVED / NEEDS_REVIEW / BLOCKED
|
|
|
131
170
|
├─ Policy Engine Enterprise policy enforcement (ALLOW/WARN/BLOCK)
|
|
132
171
|
├─ SSG Validator Protocol state machine verification
|
|
133
172
|
├─ Protocol Detector Regex-based protocol step detection (22 detectors)
|
|
134
|
-
├─ IR
|
|
173
|
+
├─ IR Extractors TypeScript (ts-morph) + Python (ast module) → function IR;
|
|
174
|
+
│ source-level markers: taint tracking, import resolution,
|
|
175
|
+
│ qualified call chains, cross-file template analysis
|
|
135
176
|
├─ Repair Executor detect → plan → fix → validate → commit/rollback
|
|
136
177
|
└─ Knowledge Base 31 domains, 140 rules, evidence chains
|
|
137
178
|
```
|
|
@@ -150,9 +191,9 @@ SDK (src/sdk.ts) verify() → APPROVED / NEEDS_REVIEW / BLOCKED
|
|
|
150
191
|
|
|
151
192
|
## Scientific Foundation
|
|
152
193
|
|
|
153
|
-
Progmune is built on the premise that **LLM outputs are statistical performances, not reasoning**
|
|
194
|
+
Progmune is built on the premise that **LLM outputs are statistical performances, not reasoning** — a view developed by Subbarao Kambhampati et al. in the position paper ["Stop Anthropomorphizing Intermediate Tokens as Reasoning/Thinking Traces!"](https://arxiv.org/abs/2505.22285) (arXiv:2505.22285, 2025) and elaborated in his ICML 2026 talk "On the Role of Verifiers and Thinking Traces in Reasoning Models". Rather than trusting what the model says about code, Progmune verifies what the program actually does — using protocol state machines, IR extraction, and evidence-backed decision chains.
|
|
154
195
|
|
|
155
|
-
|
|
196
|
+
Coverage-gap analysis borrows the "two-hump problem" terminology **as a cross-domain analogy** from Sergei Gukov's work in mathematical physics (the Andrews-Curtis conjecture in group theory, 2026) — it describes a bimodal coverage distribution, not a collaboration. See [Two-Hump Report](docs/two-hump-report.md) for the full methodology.
|
|
156
197
|
|
|
157
198
|
→ [Investor Whitepaper](docs/Progmune_投资人白皮书_v2.0.html) · [Trust Decision Model](docs/ai-trust-decision-model-v1.md)
|
|
158
199
|
|
|
@@ -174,10 +215,10 @@ High-impact contribution areas:
|
|
|
174
215
|
- **Runtime Pipeline:** Detect → Explain → Repair → Validate (L1–L4)
|
|
175
216
|
- **Trust Engine:** 4-dimension scoring with binary explainability gate
|
|
176
217
|
- **MCP Tools:** 19 — `progmune_trust_check`, `progmune_score`, `progmune_policy_check`, `progmune_certify`, and more
|
|
177
|
-
- **Framework Adapters:** Express
|
|
178
|
-
- **Knowledge Base:** 31 domains,
|
|
179
|
-
- **Corpus:** 2,500+ trajectories across 6+ repositories
|
|
180
|
-
- **Current focus:**
|
|
218
|
+
- **Framework Adapters:** Express ✅, tRPC ✅, NestJS partial (2/13)
|
|
219
|
+
- **Knowledge Base:** 31 domains, 148 protocol rules, 22 detectors, 26 safeguards, PLSB 13/13 categories — plus 15 source-level detection rules (Python)
|
|
220
|
+
- **Corpus:** 2,500+ trajectories across 6+ repositories; blind benchmarks 100 (TS) + 90 (Python) projects; real-world validation on 4 application repos
|
|
221
|
+
- **Current focus:** Enterprise PoC validation + remaining framework-internal boundary FPs
|
|
181
222
|
|
|
182
223
|
---
|
|
183
224
|
|
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
|
|
@@ -346,7 +346,7 @@
|
|
|
346
346
|
<div class="bar"><span class="bl">人工审查能力</span><div class="bt"><div class="bf" style="width:10%"></div></div><span class="bv">1x</span></div>
|
|
347
347
|
|
|
348
348
|
<h3>3.2 问题本质:AI 写代码的原理决定了它"看不见"错误</h3>
|
|
349
|
-
<p>大语言模型生成代码的方式是<strong>统计预测</strong>——根据训练数据猜"下一个最可能的词"
|
|
349
|
+
<p>大语言模型生成代码的方式是<strong>统计预测</strong>——根据训练数据猜"下一个最可能的词"。这一观点的代表性论述来自 Subbarao Kambhampati 等的立场论文《Stop Anthropomorphizing Intermediate Tokens as Reasoning/Thinking Traces!》(arXiv:2505.22285)及其 ICML 2026 关于 verifier 的主题演讲:LLM 输出的"逐步推理"是统计表演,中间 token 与真实推理之间没有可靠的因果对应。</p>
|
|
350
350
|
<p>这意味着:AI 写出"看似合理的代码"和"实际正确的代码"是两件事。它会一本正经地调用不存在的函数、跳过关键的认证步骤——<strong>而且完全静默,不报错</strong>。</p>
|
|
351
351
|
|
|
352
352
|
<h3>3.3 现有方案为什么不够</h3>
|
|
@@ -470,7 +470,18 @@ AI 生成: create_session(...) ← 当前状态 UNAUTHENTICATED
|
|
|
470
470
|
<h3>5.4 API 语义映射:让检查认识"陌生函数"</h3>
|
|
471
471
|
<p>真实项目使用的函数名千变万化(<code>createSessionToken</code>、<code>signSession</code>、<code>jwt.sign</code>……)。系统用三层策略把陌生函数映射到协议规则:① 本地前缀/别名表(含项目自定义 <code>.progmune_aliases.json</code>)→ ② 单词切分匹配 → ③ LLM 批量语义分类(带磁盘缓存,重复运行零 LLM 成本)。</p>
|
|
472
472
|
|
|
473
|
-
<h3>5.5
|
|
473
|
+
<h3>5.5 双路径架构:生成时拦截 vs 事后检查</h3>
|
|
474
|
+
<p>Progmune 用两条互补路径覆盖两类代码来源——这是理解产品的关键,也是最容易被误读的地方:</p>
|
|
475
|
+
<table>
|
|
476
|
+
<tr><th></th><th><strong>生成路径</strong>(生成时拦截)</th><th><strong>验证路径</strong>(事后检查)</th></tr>
|
|
477
|
+
<tr><td>覆盖场景</td><td>通过 <code>progmune_generate</code> 生成的代码</td><td>任何来源的代码(Copilot、Cursor、人工)</td></tr>
|
|
478
|
+
<tr><td>机制</td><td>生成循环内 8 道验证关卡:JSON 解析 → 结构预检 → 符号纠正 → 类型修正 → 数据流 → 协议状态机 → BFS 确定性修复 → 语义合约。违规代码<strong>从不写入文件</strong></td><td>信任引擎五阶段流水线(收集→评分→决策→组装),四维加权输出部署决策</td></tr>
|
|
479
|
+
<tr><td>错误处理时机</td><td>创建时刻——错误从未存在</td><td>事后——文件已存在</td></tr>
|
|
480
|
+
<tr><td>成本曲线</td><td>零——违规没有落地</td><td>发现越晚越贵</td></tr>
|
|
481
|
+
</table>
|
|
482
|
+
<p>核心产品论:<strong>在生成时刻验证,而非事后补救。</strong>LLM 的输出只是提议,状态机才是裁判——LLM 可以被劝服,状态机不会。</p>
|
|
483
|
+
|
|
484
|
+
<h3>5.6 信任引擎:五阶段流水线</h3>
|
|
474
485
|
<div class="flow">
|
|
475
486
|
<div class="node">1. 收集<br><span style="font-size:.8em;font-weight:400">策略+协议+框架数据</span></div>
|
|
476
487
|
<span class="arrow">→</span>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "progmune-runtime",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.0",
|
|
4
4
|
"description": "Progmune — AI Trust Decision Engine. Verify AI-generated code before it reaches production. Outputs APPROVED / NEEDS_REVIEW / BLOCKED with evidence.",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist/",
|
|
@@ -106,4 +106,4 @@
|
|
|
106
106
|
"tsx": "^4.22.4",
|
|
107
107
|
"vitest": "^3.2.6"
|
|
108
108
|
}
|
|
109
|
-
}
|
|
109
|
+
}
|