progmune-runtime 3.2.1 → 3.3.1
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/check.js +17 -4
- package/dist/extract-ir.js +13 -0
- package/dist/ir-utils.js +19 -5
- package/dist/mcp-server.mjs +11 -8
- package/dist/planner.js +95 -23
- package/dist/protocol-detector.js +271 -33
- package/dist/validator.js +11 -1
- package/docs/Progmune_/351/241/271/347/233/256/345/205/250/350/247/243.html +17 -12
- package/package.json +4 -4
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/check.js
CHANGED
|
@@ -193,10 +193,14 @@ if (cliArg === "--verify") {
|
|
|
193
193
|
}
|
|
194
194
|
}
|
|
195
195
|
// ── 1. IR 提取 ──
|
|
196
|
+
// 目标项目:PROGMUNE_PROJECT_DIR > CWD。使用编译产物提取器
|
|
197
|
+
// (安装态下包内没有 src/ 也没有 ts-node)。
|
|
196
198
|
step("1/6 IR 提取");
|
|
197
199
|
try {
|
|
198
|
-
|
|
199
|
-
const
|
|
200
|
+
const projectDir = path.resolve(process.env.PROGMUNE_PROJECT_DIR || process.cwd());
|
|
201
|
+
const extractor = path.resolve(__dirname, "extract-ir.js");
|
|
202
|
+
(0, child_process_1.execSync)(`node "${extractor}" "${projectDir}"`, { stdio: "pipe" });
|
|
203
|
+
const irRaw = JSON.parse(fs.readFileSync(path.resolve(projectDir, "ir.json"), "utf-8"));
|
|
200
204
|
const ir = Array.isArray(irRaw) ? irRaw : (irRaw.functions || []);
|
|
201
205
|
const externalCount = ir.filter((f) => f.external).length;
|
|
202
206
|
pass(`IR 提取完成: ${ir.length} 个函数 (${externalCount} 外部)`);
|
|
@@ -207,8 +211,17 @@ catch (e) {
|
|
|
207
211
|
// ── 2. TypeScript 编译 ──
|
|
208
212
|
step("2/6 TypeScript 类型检查");
|
|
209
213
|
try {
|
|
210
|
-
|
|
211
|
-
|
|
214
|
+
const projectDir = path.resolve(process.env.PROGMUNE_PROJECT_DIR || process.cwd());
|
|
215
|
+
const tsconfig = path.resolve(projectDir, "tsconfig.json");
|
|
216
|
+
if (!fs.existsSync(tsconfig)) {
|
|
217
|
+
warn("跳过(项目无 tsconfig.json)");
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
// 用包内 typescript 依赖的 tsc(安装态可用),而非全局 npx
|
|
221
|
+
const tscBin = path.resolve(__dirname, "..", "node_modules", "typescript", "bin", "tsc");
|
|
222
|
+
(0, child_process_1.execSync)(`node "${tscBin}" --noEmit -p "${tsconfig}"`, { stdio: "pipe" });
|
|
223
|
+
pass("零类型错误");
|
|
224
|
+
}
|
|
212
225
|
}
|
|
213
226
|
catch (e) {
|
|
214
227
|
const stderr = e.stderr?.toString() || e.stdout?.toString() || "";
|
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
|
/**
|
package/dist/ir-utils.js
CHANGED
|
@@ -46,11 +46,25 @@ const path = __importStar(require("path"));
|
|
|
46
46
|
* is a bare array or an object with a `functions` key.
|
|
47
47
|
*/
|
|
48
48
|
function loadIR(filePath) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
49
|
+
// Resolution order: explicit path → PROGMUNE_PROJECT_DIR → CWD →
|
|
50
|
+
// package directory (legacy fallback). The package dir must be LAST —
|
|
51
|
+
// in an installed-package setup the project's ir.json lives in the
|
|
52
|
+
// consuming project, not inside node_modules/progmune-runtime.
|
|
53
|
+
const candidates = [];
|
|
54
|
+
if (filePath)
|
|
55
|
+
candidates.push(filePath);
|
|
56
|
+
const projectDir = process.env.PROGMUNE_PROJECT_DIR;
|
|
57
|
+
if (projectDir)
|
|
58
|
+
candidates.push(path.resolve(projectDir, "ir.json"));
|
|
59
|
+
candidates.push(path.resolve(process.cwd(), "ir.json"));
|
|
60
|
+
candidates.push(path.resolve(__dirname, "../ir.json"));
|
|
61
|
+
for (const irPath of candidates) {
|
|
62
|
+
if (fs.existsSync(irPath)) {
|
|
63
|
+
const raw = JSON.parse(fs.readFileSync(irPath, "utf-8"));
|
|
64
|
+
return Array.isArray(raw) ? raw : (raw.functions || []);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return [];
|
|
54
68
|
}
|
|
55
69
|
/** Count exported functions in an IR function list.
|
|
56
70
|
* @requires IR_FUNCTIONS @produces EXPORT_COUNT
|
package/dist/mcp-server.mjs
CHANGED
|
@@ -9,8 +9,11 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
9
9
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
10
10
|
import * as fs from "fs";
|
|
11
11
|
import * as path from "path";
|
|
12
|
-
import {
|
|
13
|
-
|
|
12
|
+
import { fileURLToPath } from "url";
|
|
13
|
+
import { createLogger } from "./logger.js";
|
|
14
|
+
// ── ESM-compatible __dirname (the compiled output is an .mjs module) ──
|
|
15
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
// ── Load .env ──
|
|
14
17
|
const envPath = path.resolve(__dirname, "..", ".env");
|
|
15
18
|
if (fs.existsSync(envPath)) {
|
|
16
19
|
const envContent = fs.readFileSync(envPath, "utf-8");
|
|
@@ -27,12 +30,12 @@ if (fs.existsSync(envPath)) {
|
|
|
27
30
|
process.env[key] = value;
|
|
28
31
|
}
|
|
29
32
|
}
|
|
30
|
-
import { plan } from "./planner";
|
|
31
|
-
import { extractIR } from "./extract-ir";
|
|
32
|
-
import { extractIRPython, isPythonProject } from "./extract-ir-python";
|
|
33
|
-
import { emitCode } from "./emitter";
|
|
34
|
-
import { recordRun } from "./feedback";
|
|
35
|
-
import { reportFingerprints } from "./immune-reporter";
|
|
33
|
+
import { plan } from "./planner.js";
|
|
34
|
+
import { extractIR } from "./extract-ir.js";
|
|
35
|
+
import { extractIRPython, isPythonProject } from "./extract-ir-python.js";
|
|
36
|
+
import { emitCode } from "./emitter.js";
|
|
37
|
+
import { recordRun } from "./feedback.js";
|
|
38
|
+
import { reportFingerprints } from "./immune-reporter.js";
|
|
36
39
|
const OPT_IN_FILE = path.resolve(__dirname, "..", ".progmune_memory", "opt_in.json");
|
|
37
40
|
// ── Structured logging (stderr, not stdout JSON-RPC) ──
|
|
38
41
|
const log = createLogger("progmune");
|
package/dist/planner.js
CHANGED
|
@@ -49,6 +49,7 @@ const semantic_snapshot_1 = require("./semantic-snapshot");
|
|
|
49
49
|
const strategy_planner_1 = require("./strategy-planner");
|
|
50
50
|
const semantic_topology_1 = require("./semantic-topology");
|
|
51
51
|
const planner_prompts_1 = require("./planner-prompts");
|
|
52
|
+
const ir_utils_1 = require("./ir-utils");
|
|
52
53
|
const fs = __importStar(require("fs"));
|
|
53
54
|
function enrichActions(actions, ir) {
|
|
54
55
|
return actions.map(a => {
|
|
@@ -389,6 +390,49 @@ function validateProtocolWithTransitions(actions, protocols, namespaceInitialSta
|
|
|
389
390
|
ctx.currentState = transition.statesAfter;
|
|
390
391
|
}
|
|
391
392
|
}
|
|
393
|
+
// End-of-sequence check: held resources must be released (resource leak).
|
|
394
|
+
// A state S is resource-holding when some rule REQUIRES S and INVALIDATES S
|
|
395
|
+
// (acquire/release semantics — e.g. FILE_OPEN set by open_file, released by
|
|
396
|
+
// close_file). UNAUTHENTICATED is not resource-holding (release rules for it
|
|
397
|
+
// do not require it as a precondition).
|
|
398
|
+
const heldStates = [];
|
|
399
|
+
for (const p of protocols) {
|
|
400
|
+
const ann = p.protocol;
|
|
401
|
+
if (!ann)
|
|
402
|
+
continue;
|
|
403
|
+
const inv = ann.invalidate || [];
|
|
404
|
+
const pre = ann.pre_states || [];
|
|
405
|
+
for (const s of inv) {
|
|
406
|
+
if (pre.includes(s))
|
|
407
|
+
heldStates.push({ state: s, releaseFn: p.function, namespace: ann.namespace || "" });
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
for (const hs of heldStates) {
|
|
411
|
+
const cur = ctx.currentState[hs.namespace] || [];
|
|
412
|
+
if (cur.includes(hs.state)) {
|
|
413
|
+
const trace = transitions.map(t => ({
|
|
414
|
+
function: t.function,
|
|
415
|
+
statesBefore: t.statesBefore,
|
|
416
|
+
statesAfter: t.statesAfter,
|
|
417
|
+
}));
|
|
418
|
+
return {
|
|
419
|
+
valid: false,
|
|
420
|
+
rejection: {
|
|
421
|
+
blocked: "(end-of-sequence)",
|
|
422
|
+
currentState: cur,
|
|
423
|
+
requiredState: [],
|
|
424
|
+
missingFunctions: [hs.releaseFn],
|
|
425
|
+
fixPath: [hs.releaseFn],
|
|
426
|
+
namespace: hs.namespace,
|
|
427
|
+
endState: true,
|
|
428
|
+
},
|
|
429
|
+
index: actions.length,
|
|
430
|
+
trace,
|
|
431
|
+
transitions,
|
|
432
|
+
ruleHash,
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
}
|
|
392
436
|
// Invariant check on full ledger
|
|
393
437
|
const consistency = (0, ssg_validator_1.checkLedgerConsistency)(transitions, namespaceInitialStates);
|
|
394
438
|
if (!consistency.consistent) {
|
|
@@ -403,34 +447,47 @@ function validateProtocolWithTransitions(actions, protocols, namespaceInitialSta
|
|
|
403
447
|
function attemptSSGRepair(actions, rejection, ir, protocols, namespaceInitialStates) {
|
|
404
448
|
if (!rejection.fixPath || rejection.fixPath.length === 0)
|
|
405
449
|
return null;
|
|
406
|
-
//
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
450
|
+
// 名称归一化:内置规则可能是下划线风格(generate_jwt),项目 IR 是
|
|
451
|
+
// camelCase(generateJwt)——修复动作必须使用 IR 中的真实函数名。
|
|
452
|
+
const normalizeName = (n) => n.replace(/[_-]/g, "").toLowerCase();
|
|
453
|
+
const resolveIR = (fnName) => ir.find((f) => f.name === fnName)
|
|
454
|
+
|| ir.find((f) => normalizeName(f.name) === normalizeName(fnName));
|
|
410
455
|
// 为修复路径中的每个函数创建合成 Action
|
|
411
456
|
const repairActions = [];
|
|
412
457
|
for (const fnName of rejection.fixPath) {
|
|
413
|
-
const def =
|
|
458
|
+
const def = resolveIR(fnName);
|
|
414
459
|
if (!def)
|
|
415
460
|
return null;
|
|
461
|
+
const realName = def.name;
|
|
416
462
|
const args = (def.params || []).map((p, i) => ({
|
|
417
463
|
name: p.name || `p${i}`,
|
|
418
464
|
type: p.type || 'any',
|
|
419
465
|
value: "",
|
|
420
466
|
}));
|
|
421
467
|
const assignTo = def.returnType && def.returnType !== 'void' && def.returnType !== 'undefined'
|
|
422
|
-
? `${
|
|
423
|
-
const action = { kind: 'call', function:
|
|
468
|
+
? `${realName}_result` : undefined;
|
|
469
|
+
const action = { kind: 'call', function: realName, args };
|
|
424
470
|
if (assignTo)
|
|
425
471
|
action.assignTo = assignTo;
|
|
426
472
|
repairActions.push(action);
|
|
427
473
|
}
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
...repairActions
|
|
432
|
-
|
|
433
|
-
|
|
474
|
+
let repaired;
|
|
475
|
+
if (rejection.endState) {
|
|
476
|
+
// 末尾状态违规(资源未释放):释放函数追加到序列末尾
|
|
477
|
+
repaired = [...actions, ...repairActions];
|
|
478
|
+
}
|
|
479
|
+
else {
|
|
480
|
+
// 找到被拦截函数在序列中的位置
|
|
481
|
+
const blockedIdx = actions.findIndex(a => a.kind === "call" && a.function === rejection.blocked);
|
|
482
|
+
if (blockedIdx === -1)
|
|
483
|
+
return null;
|
|
484
|
+
// 在被拦截函数前插入修复函数
|
|
485
|
+
repaired = [
|
|
486
|
+
...actions.slice(0, blockedIdx),
|
|
487
|
+
...repairActions,
|
|
488
|
+
...actions.slice(blockedIdx),
|
|
489
|
+
];
|
|
490
|
+
}
|
|
434
491
|
// 重新验证
|
|
435
492
|
const recheck = validateProtocolWithTransitions(repaired, protocols, namespaceInitialStates);
|
|
436
493
|
if (recheck.valid) {
|
|
@@ -448,9 +505,8 @@ function attemptSSGRepair(actions, rejection, ir, protocols, namespaceInitialSta
|
|
|
448
505
|
/** @requires INTENT @produces ACTION_PLAN */
|
|
449
506
|
async function plan(userIntent, llmSeeds) {
|
|
450
507
|
(0, llm_1.resetCallCount)();
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
const ir = Array.isArray(irRaw) ? irRaw : (irRaw.functions || []);
|
|
508
|
+
// IR 读取走 loadIR 的解析顺序(显式路径 → PROGMUNE_PROJECT_DIR → CWD → 包目录回退)
|
|
509
|
+
const ir = (0, ir_utils_1.loadIR)();
|
|
454
510
|
// P1: Build Semantic Topology (once per plan call, cached)
|
|
455
511
|
try {
|
|
456
512
|
(0, semantic_topology_1.rebuildTopology)(ir);
|
|
@@ -458,7 +514,7 @@ async function plan(userIntent, llmSeeds) {
|
|
|
458
514
|
catch { /* topology rebuild — optional */ }
|
|
459
515
|
// Helper: wrap actions into PlanResult
|
|
460
516
|
let repairMetrics = { applied: false, count: 0, branchIds: [] };
|
|
461
|
-
const wrapResult = (actions, repair, degraded = false) => ({
|
|
517
|
+
const wrapResult = (actions, repair, degraded = false, blocked, blockedReason) => ({
|
|
462
518
|
actions,
|
|
463
519
|
sessionId: session?.sessionId || "",
|
|
464
520
|
ruleHash: session?.ruleHash,
|
|
@@ -466,6 +522,8 @@ async function plan(userIntent, llmSeeds) {
|
|
|
466
522
|
repairApplied: repair?.applied ?? repairMetrics.applied,
|
|
467
523
|
repairCount: repair?.count ?? repairMetrics.count,
|
|
468
524
|
repairBranchIds: repair?.branchIds ?? repairMetrics.branchIds,
|
|
525
|
+
blocked,
|
|
526
|
+
blockedReason,
|
|
469
527
|
});
|
|
470
528
|
// 初始化执行会话和快照(需在抗体快速通道前创建,以便记录 antibody hits)
|
|
471
529
|
const sessionId = (0, runtime_types_1.generateSessionId)();
|
|
@@ -963,15 +1021,19 @@ ${planner_prompts_1.RETRY_HINT}
|
|
|
963
1021
|
}
|
|
964
1022
|
}
|
|
965
1023
|
// 1) 基础序列校验
|
|
1024
|
+
// 预检查的协议违规("需要先调用 X")不属于符号/类型错误——
|
|
1025
|
+
// 它们由下方 SSG 块以 SVL-4 处理(含确定性修复)。
|
|
1026
|
+
// 只有符号/类型类错误走本回退分支,避免把 SVL-4 误标为 SVL-1。
|
|
1027
|
+
const preCheckSymbolErrors = preCheckErrors.filter(e => e.includes("函数不存在") || e.includes("参数数量"));
|
|
966
1028
|
const seqResult = (0, validator_1.validateActionSequence)(filtered);
|
|
967
|
-
if (!seqResult.valid ||
|
|
968
|
-
const errorsFlat = [...
|
|
1029
|
+
if (!seqResult.valid || preCheckSymbolErrors.length > 0) {
|
|
1030
|
+
const errorsFlat = [...preCheckSymbolErrors, ...seqResult.errors.flat()];
|
|
969
1031
|
console.error("⚠️ 序列校验失败:", errorsFlat.join(", "));
|
|
970
1032
|
// Use structured violations directly from validator
|
|
971
1033
|
const violations = seqResult.violations.length > 0
|
|
972
1034
|
? seqResult.violations
|
|
973
|
-
:
|
|
974
|
-
? [{ svl: 1, violatedConstraint: "symbol_existence", actionIndex: 0, description:
|
|
1035
|
+
: preCheckSymbolErrors.length > 0
|
|
1036
|
+
? [{ svl: 1, violatedConstraint: "symbol_existence", actionIndex: 0, description: preCheckSymbolErrors.join("; ") }]
|
|
975
1037
|
: [{ svl: 1, violatedConstraint: "symbol_existence", actionIndex: 0, description: errorsFlat.join("; ") }];
|
|
976
1038
|
const primarySvl = `SVL-${violations[0].svl}`;
|
|
977
1039
|
const attempt = {
|
|
@@ -1004,8 +1066,8 @@ ${planner_prompts_1.RETRY_HINT}
|
|
|
1004
1066
|
});
|
|
1005
1067
|
(0, memory_layer_1.recordEpisode)({ intent: userIntent, actions: filtered, success: false, svlViolated: primarySvl });
|
|
1006
1068
|
// Build targeted retry prompt based on pre-check results
|
|
1007
|
-
const specificErrors =
|
|
1008
|
-
? `精确错误:\n${
|
|
1069
|
+
const specificErrors = preCheckSymbolErrors.length > 0
|
|
1070
|
+
? `精确错误:\n${preCheckSymbolErrors.map(e => ` - ${e}`).join("\n")}`
|
|
1009
1071
|
: `错误:${errorsFlat.join(";")}`;
|
|
1010
1072
|
currentPrompt = `可用函数:\n${compactFuncList}${protocolChainHint}\n\n需求:${userIntent}${antibodyHint}\n\n${specificErrors}\n请修正上述问题。\n${planner_prompts_1.RETRY_HINT}\n只输出 JSON。`;
|
|
1011
1073
|
useSystem = false;
|
|
@@ -1290,6 +1352,16 @@ ${planner_prompts_1.RETRY_HINT}
|
|
|
1290
1352
|
session.endedAt = Date.now();
|
|
1291
1353
|
(0, failure_corpus_1.recordSession)(session);
|
|
1292
1354
|
(0, failure_corpus_1.clearCheckpoint)(userIntent);
|
|
1355
|
+
// 显式失败信号:所有尝试(含本地回退)都被约束拦截——
|
|
1356
|
+
// 调用方必须能区分"无事可做"与"被拦截"。
|
|
1357
|
+
const lastViolation = session.attempts.length > 0
|
|
1358
|
+
? session.attempts[session.attempts.length - 1].violations[0]
|
|
1359
|
+
: undefined;
|
|
1360
|
+
const reason = lastViolation
|
|
1361
|
+
? `所有生成尝试均被 SVL-${lastViolation.svl} 拦截: ${lastViolation.violatedConstraint}${lastViolation.fixPath?.length ? `(修复路径: ${lastViolation.fixPath.join(" → ")})` : ""}`
|
|
1362
|
+
: "所有生成尝试均被约束拦截,本地回退亦失败";
|
|
1363
|
+
console.error(`[拦截] ${reason}`);
|
|
1364
|
+
return wrapResult([], undefined, true, true, reason);
|
|
1293
1365
|
}
|
|
1294
1366
|
return wrapResult(finalActions);
|
|
1295
1367
|
}
|
|
@@ -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/dist/validator.js
CHANGED
|
@@ -86,7 +86,17 @@ function checkVariableFlow(actions) {
|
|
|
86
86
|
};
|
|
87
87
|
const processAction = (action) => {
|
|
88
88
|
if (action.kind === "call") {
|
|
89
|
-
// call
|
|
89
|
+
// call 参数值若是变量引用($ 前缀,或 planner 剥离 $ 后的裸标识符),
|
|
90
|
+
// 必须已声明——白皮书 SVL-3:"变量先声明后使用"。
|
|
91
|
+
for (const arg of action.args || []) {
|
|
92
|
+
const val = arg?.value;
|
|
93
|
+
if (typeof val !== "string")
|
|
94
|
+
continue;
|
|
95
|
+
const bare = val.startsWith("$") ? val.slice(1) : val;
|
|
96
|
+
if (bare && !isLiteral(bare) && /^[a-zA-Z_]\w*$/.test(bare) && !declared.has(bare)) {
|
|
97
|
+
errors.push(`变量 '${bare}' 在使用前未声明 (call 参数 '${arg.name || "?"}')`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
90
100
|
if (action.assignTo) {
|
|
91
101
|
declared.set(action.assignTo, "any");
|
|
92
102
|
}
|
|
@@ -465,6 +465,8 @@ AI 生成: create_session(...) ← 当前状态 UNAUTHENTICATED
|
|
|
465
465
|
<ul>
|
|
466
466
|
<li><strong>22 个协议检测器</strong>:TLS 握手、SSH 连接、HTTP 请求、支付处理、会话管理、文件上传、注册流程等</li>
|
|
467
467
|
<li><strong>26 条防护规则(safeguard)</strong>:触发式检查——"如果出现了 X,就必须有 Y"。例:注册函数必须调用安全哈希(bcrypt/argon2);管理操作必须检查角色;tRPC mutation 必须有 <code>.input()</code> 校验 schema</li>
|
|
468
|
+
<li><strong>15 条源码级检测规则(Python,2026-08 上线)</strong>:SQL 注入(f-string/%/.format/拼接,参数化写法正确放行)、SSRF(限定接收者的 HTTP 抓取 + request 污点追踪)、路径穿越(文件 sink + 污点)、XSS(跨文件:模板 <code>{{ var|safe }}</code> × 污点上下文绑定)、SSTI、XXE(解析器配置 + 污点双信号)、命令注入(仅动态命令参数)、反序列化、CSRF 双形态(<code>@csrf_exempt</code> / GET 状态变更)、eval 代码执行、硬编码 JWT 密钥(含跨模块常量)、cookie 授权、可预测重置令牌</li>
|
|
469
|
+
<li><strong>提取器标记架构</strong>:IR 提取器做源码级分析(污点追踪、import 解析、限定调用链、模板扫描、跨模块常量表)→ 合成标记 → 规则消费。零管道改动、完全可审计,已同步移植到 TS(ts-morph)提取器</li>
|
|
468
470
|
</ul>
|
|
469
471
|
|
|
470
472
|
<h3>5.4 API 语义映射:让检查认识"陌生函数"</h3>
|
|
@@ -501,12 +503,15 @@ AI 生成: create_session(...) ← 当前状态 UNAUTHENTICATED
|
|
|
501
503
|
<h3>5.7 基准测试:用数据证明能力</h3>
|
|
502
504
|
<table>
|
|
503
505
|
<tr><th>基准</th><th>方法</th><th>结果</th></tr>
|
|
504
|
-
<tr><td><strong>盲测基准</strong>(TypeScript
|
|
506
|
+
<tr><td><strong>盲测基准</strong>(TypeScript,100 项目)</td><td>90 个合成风格变体 + 10 个模型变体项目、795 条 gold finding、严格定位匹配</td><td>精确率 <strong>100%</strong> · 召回率 <strong>98.5%</strong>(有效口径 100%)· 事实性误报 <strong>0</strong></td></tr>
|
|
507
|
+
<tr><td><strong>盲测基准</strong>(Python,90 项目)</td><td>90 个合成风格变体、729 条 gold finding</td><td>精确率 <strong>100%</strong> · 召回率 <strong>100%</strong> · 事实性误报 <strong>0</strong></td></tr>
|
|
508
|
+
<tr><td><strong>真实应用验证</strong>(PyGoat)</td><td>OWASP 故意脆弱 Django 应用、232 条检测逐条人工核实</td><td>标记精确率 <strong>100%</strong>(67 真阳性 / 0 误报);覆盖 14 个漏洞类别</td></tr>
|
|
509
|
+
<tr><td><strong>良构应用</strong>(django/fastapi realworld、django-unicorn)</td><td>176 条检测</td><td>0 条误报真阳性;3 条框架内部边界 FP(已定性归档)</td></tr>
|
|
505
510
|
<tr><td><strong>黄金基准</strong>(C 语言)</td><td>curl/libssh/nginx/openssl 等真实 CVE 案例</td><td>F1 16.5% —— <strong>研究阶段</strong>,诚实披露</td></tr>
|
|
506
511
|
<tr><td><strong>PLSB v1.0</strong></td><td>13 类协议安全弱点、39 个手工验证缺陷案例</td><td>覆盖 <strong>13/13 全类别</strong>(业界唯一)</td></tr>
|
|
507
512
|
</table>
|
|
508
513
|
<div class="plain">
|
|
509
|
-
<strong>为什么披露 C 语言的低分?</strong>因为"诚实"是产品原则。一个只报告好消息的基准没有参考价值。TS
|
|
514
|
+
<strong>为什么披露 C 语言的低分?</strong>因为"诚实"是产品原则。一个只报告好消息的基准没有参考价值。TS 与 Python 已生产可用、C 为研究课题——能力边界清清楚楚。所有基准均有 gold 标注文件入库,可复现。
|
|
510
515
|
</div>
|
|
511
516
|
</section>
|
|
512
517
|
|
|
@@ -528,21 +533,21 @@ AI 生成: create_session(...) ← 当前状态 UNAUTHENTICATED
|
|
|
528
533
|
<h3>6.2 语言与框架覆盖(诚实版)</h3>
|
|
529
534
|
<table>
|
|
530
535
|
<tr><th>能力</th><th>状态</th><th>说明</th></tr>
|
|
531
|
-
<tr><td>TypeScript / JavaScript</td><td><span style="color:var(--green);font-weight:700">✅ 生产</span></td><td
|
|
532
|
-
<tr><td>Python</td><td><span style="color:var(--
|
|
536
|
+
<tr><td>TypeScript / JavaScript</td><td><span style="color:var(--green);font-weight:700">✅ 生产</span></td><td>盲测 100 项目:精确率 100%、召回率 98.5%</td></tr>
|
|
537
|
+
<tr><td>Python</td><td><span style="color:var(--green);font-weight:700">✅ 生产</span></td><td>盲测 90 项目:精确率/召回率双 100%;15 条源码级检测规则;PyGoat 真实验证 67 TP / 0 FP</td></tr>
|
|
533
538
|
<tr><td>C</td><td><span style="color:var(--amber);font-weight:700">⚠️ 研究</span></td><td>F1 16.5%,瓶颈已定位(规则覆盖),L3 实验已终止</td></tr>
|
|
534
539
|
<tr><td>Go / Java</td><td><span style="color:var(--ink-3)">❌ 未支持</span></td><td>路线图内</td></tr>
|
|
535
|
-
<tr><td>框架适配</td><td><span style="color:var(--amber);font-weight:700">2/13</span></td><td>Express ✅、tRPC ✅、NestJS 部分;Next.js
|
|
540
|
+
<tr><td>框架适配</td><td><span style="color:var(--amber);font-weight:700">2/13</span></td><td>Express ✅、tRPC ✅、NestJS 部分;Next.js 版本感知已实现;Django/FastAPI 经 Python 源码级检测间接覆盖</td></tr>
|
|
536
541
|
</table>
|
|
537
542
|
|
|
538
543
|
<h3>6.3 知识库规模</h3>
|
|
539
544
|
<div class="stats">
|
|
540
545
|
<div class="stat"><div class="n">148</div><div class="l">协议规则</div></div>
|
|
541
546
|
<div class="stat"><div class="n">21</div><div class="l">协议命名空间全覆盖</div></div>
|
|
542
|
-
<div class="stat"><div class="n">22+26</div><div class="l">检测器 +
|
|
547
|
+
<div class="stat"><div class="n">22+26+15</div><div class="l">检测器 + 防护 + 源码级规则</div></div>
|
|
543
548
|
<div class="stat"><div class="n">39</div><div class="l">黄金缺陷案例(人工验证)</div></div>
|
|
544
|
-
<div class="stat"><div class="n">
|
|
545
|
-
<div class="stat"><div class="n">
|
|
549
|
+
<div class="stat"><div class="n">190</div><div class="l">盲测项目(TS 100 + Python 90)</div></div>
|
|
550
|
+
<div class="stat"><div class="n">4</div><div class="l">真实应用验证仓库</div></div>
|
|
546
551
|
</div>
|
|
547
552
|
</section>
|
|
548
553
|
|
|
@@ -655,7 +660,7 @@ AI 生成: create_session(...) ← 当前状态 UNAUTHENTICATED
|
|
|
655
660
|
<tr><th>风险</th><th>等级</th><th>内容</th><th>缓解措施</th></tr>
|
|
656
661
|
<tr><td><strong>市场教育成本</strong></td><td><span style="color:var(--red);font-weight:700">高</span></td><td>多数企业尚未意识到"AI 代码需要验证"这个问题本身</td><td>公开缺陷基准库、从受监管行业切入、开源社区自然教育</td></tr>
|
|
657
662
|
<tr><td><strong>大厂竞争</strong></td><td><span style="color:var(--amber);font-weight:700">中</span></td><td>GitHub/Microsoft 可能内置类似功能</td><td>聚焦"验证层"差异化;大厂内置方案通常浅层;开源先发</td></tr>
|
|
658
|
-
<tr><td><strong>验证的边界性</strong></td><td><span style="color:var(--amber);font-weight:700">中</span></td><td
|
|
663
|
+
<tr><td><strong>验证的边界性</strong></td><td><span style="color:var(--amber);font-weight:700">中</span></td><td>TS 与 Python 已生产可用;协议规则不可能覆盖所有场景——存在漏检的可能</td><td>诚实披露置信度;数据飞轮持续补规则;规则来自真实 CVE 案例</td></tr>
|
|
659
664
|
<tr><td><strong>LLM 依赖</strong></td><td><span style="color:var(--green);font-weight:700">低</span></td><td>代码生成环节依赖底层模型能力</td><td>架构上模型无关;验证器本身不依赖任何 LLM;模型越强产品越有价值</td></tr>
|
|
660
665
|
<tr><td><strong>商业化节奏</strong></td><td><span style="color:var(--amber);font-weight:700">中</span></td><td>开源到付费的转化路径需要时间验证;尚无企业 PoC</td><td>GitLab/Databricks 模式已成熟;优先做 2-3 个企业 PoC 验证 PMF</td></tr>
|
|
661
666
|
<tr><td><strong>核心假说未验证</strong></td><td><span style="color:var(--amber);font-weight:700">中</span></td><td>"每个新代码库让所有验证更强"的知识网络假说尚无实证</td><td>以 Phase 2 的 PoC 数据为准;假说不成立也不影响单点验证的产品价值</td></tr>
|
|
@@ -672,9 +677,9 @@ AI 生成: create_session(...) ← 当前状态 UNAUTHENTICATED
|
|
|
672
677
|
|
|
673
678
|
<table>
|
|
674
679
|
<tr><th>阶段</th><th>时间</th><th>目标</th><th>关键交付</th></tr>
|
|
675
|
-
<tr><td><strong>当前</strong></td><td>2026-08</td><td>Trust Foundation
|
|
680
|
+
<tr><td><strong>当前</strong></td><td>2026-08</td><td>Trust Foundation + 语言扩展完成</td><td>信任引擎 ✅、策略引擎 ✅、PLSB 13/13 ✅、tRPC/Express 适配 ✅、PrintLab 案例 ✅、<strong>Python 生产级 ✅(盲测双 100%、PyGoat 真实验证 0 误报)✅、15 条源码级检测规则 ✅、TS/Python 盲测 precision 100% ✅</strong></td></tr>
|
|
676
681
|
<tr><td><strong>Phase 1 收官</strong></td><td>2026 Q3</td><td>可信度校准</td><td>已知风险项目上的 Score 校准、真实项目验收、Trust Report PDF</td></tr>
|
|
677
|
-
<tr><td><strong>Phase 2</strong></td><td>2026 Q4</td><td
|
|
682
|
+
<tr><td><strong>Phase 2</strong></td><td>2026 Q4</td><td>企业验证</td><td>企业 PoC × 2-3、Evolution 追踪、CI/CD 门禁集成、良构应用框架内部 FP 收尾(类属性建模)</td></tr>
|
|
678
683
|
<tr><td><strong>Phase 3</strong></td><td>2027+</td><td>平台化</td><td>Go/Java、SaaS 仪表盘、公开基准排行榜、全球免疫网络、协议模板市场</td></tr>
|
|
679
684
|
</table>
|
|
680
685
|
</section>
|
|
@@ -694,7 +699,7 @@ AI 生成: create_session(...) ← 当前状态 UNAUTHENTICATED
|
|
|
694
699
|
<h3>为什么需要谨慎</h3>
|
|
695
700
|
<ol>
|
|
696
701
|
<li>企业 PoC 为零——PMF 未验证,商业化路径尚需数据</li>
|
|
697
|
-
<li
|
|
702
|
+
<li>语言覆盖有限(TS/Python 生产可用,Go/Java 未支持)</li>
|
|
698
703
|
<li>"信任"类产品教育成本高,销售周期长</li>
|
|
699
704
|
<li>大厂随时可能跟进(但架构先发 + 数据积累构成时间壁垒)</li>
|
|
700
705
|
</ol>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "progmune-runtime",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.1",
|
|
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/",
|
|
@@ -16,15 +16,15 @@
|
|
|
16
16
|
"prepare": "npm run build",
|
|
17
17
|
"build": "tsc -p tsconfig.json && tsc -p tsconfig.mcp.json && mv dist-mcp/mcp-server.js dist/mcp-server.mjs && rm -rf dist-mcp",
|
|
18
18
|
"start": "node dist/mcp-server.mjs",
|
|
19
|
-
"test": "node dist/
|
|
19
|
+
"test": "node dist/check.js",
|
|
20
20
|
"test:unit": "vitest run --exclude '**/stress/**' --exclude '**/soak/**' --exclude '**/chaos/**'",
|
|
21
21
|
"test:watch": "vitest",
|
|
22
22
|
"test:coverage": "vitest run --coverage",
|
|
23
|
-
"ir": "
|
|
23
|
+
"ir": "node dist/extract-ir.js .",
|
|
24
24
|
"guard": "node -e \"require('./dist/validator.js'); console.log('✅ 校验器已加载')\"",
|
|
25
25
|
"obs": "npx ts-node src/semantic-trace.ts",
|
|
26
26
|
"obs-web": "npx ts-node src/obs-web.ts",
|
|
27
|
-
"check": "
|
|
27
|
+
"check": "node dist/check.js",
|
|
28
28
|
"audit": "npx ts-node src/audit.ts",
|
|
29
29
|
"governance": "npx ts-node src/audit/cli.ts --all",
|
|
30
30
|
"governance:json": "npx ts-node src/audit/cli.ts --all --json",
|