guardvibe 3.18.0 → 3.20.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/CHANGELOG.md +22 -0
- package/README.md +38 -6
- package/build/data/rules/cve-versions.js +36 -0
- package/build/index.js +11 -1
- package/build/tools/secure-prompt.d.ts +65 -0
- package/build/tools/secure-prompt.js +434 -0
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,28 @@ All notable changes to GuardVibe are documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [3.20.0] - 2026-06-14
|
|
9
|
+
|
|
10
|
+
### Added — 3 fresh CVE version-pin rules from daily threat intel (442 → 445 rules / 38 tools)
|
|
11
|
+
- **VG1089 — js-cookie `assign()` prototype hijack → cookie-attribute injection (CVE-2026-46625 / GHSA-qjx8-664m-686j, high).** js-cookie < 3.0.7 enumerates `Object.prototype` keys through the internal `assign()` helper, so a pollution gadget can inject `domain=`/`path=`/`secure=`/`samesite=`/`expires=` attributes into written cookies. Fixed in 3.0.7. 0-FP semver: only exact/`=` pins in the 3.0.x line are flagged (a caret/tilde there resolves to the fixed 3.0.7); 0.x–2.x majors are flagged with any range.
|
|
12
|
+
- **VG1090 — PostCSS XSS via unescaped `</style>` in stringify output (CVE-2026-41305 / GHSA-qx2v-qp2m-jg93, medium).** postcss < 8.5.10 does not escape `</style>` when serializing a CSS AST; an app that re-emits user CSS into an inline `<style>` block can be broken out of for stored/reflected XSS. Fixed in 8.5.10. 0-FP semver: caret on the 8.x line resolves to the fix, so only exact/`=` pins (plus tilde within 8.0–8.4) on 8.x are flagged; 1.x–7.x majors flagged with any range.
|
|
13
|
+
- **VG1091 — Axios HTTP-adapter proxy prototype-pollution gadget (CVE-2026-44494 / GHSA-35jp-ww65-95wh, high).** axios < 1.16.0 reads `config.proxy` in the Node HTTP adapter without an own-property check; a `Object.prototype.proxy` gadget routes every request through an attacker-controlled proxy (MITM / credential theft). Fixed in 1.16.0. **Distinct from VG1042 (pre-1.15.2 cluster):** a project that pinned 1.15.2 on VG1042's advice is still exposed, so this rule flags exactly the residual 1.15.2–1.15.x window (caret resolves to the fixed 1.16.0 → not flagged), with no double-firing against VG1042.
|
|
14
|
+
- 26 new pattern tests in `tests/rules/cve-versions.test.ts` (detect affected pins, ignore patched + caret-resolves-to-fixed + adjacent-rule overlap). CVE version-pin rule count 71 → 74. All three sourced from the daily GHSA/OSV/CISA-KEV intel brief and verified against the upstream advisories; everything else in that brief (Clerk ×3, Drizzle, Next/RSC cluster, React RCE, Anthropic SDK memory tool, Vercel AI SDK filetype, MCP path traversal, Miasma) was already covered. Zero new runtime dependencies.
|
|
15
|
+
|
|
16
|
+
Gate green (build / lint / test / self-audit PASS / A / 0).
|
|
17
|
+
|
|
18
|
+
## [3.19.0] - 2026-06-10
|
|
19
|
+
|
|
20
|
+
### Added — secure_prompt: prompt-level security, shift left (442 rules / 37 → 38 tools)
|
|
21
|
+
- **New MCP tool `secure_prompt`** — analyzes a raw coding prompt BEFORE any code is written and returns a structured enhancement directive (`guardvibe.secure_prompt.v1`) the host LLM uses to rewrite the prompt with GuardVibe security requirements embedded. Fully deterministic: no LLM calls, no network, no API keys — same prompt = same directive.
|
|
22
|
+
- **Triage-first, "do no harm":** verdict `NO_MOD` (prompt already specific and security-aware, or touches no security surface → host proceeds with the ORIGINAL prompt unchanged), `LIGHT_MOD` (clear intent, missing security constraints → inject requirements only), `HEAVY_MOD` (vague AND security-relevant → requirements + up to 3 clarifying questions, never invented answers). Scoring heuristics (concrete nouns, security vocabulary, length/imperative specificity, sensitive surfaces) with thresholds in an exported `TRIAGE_CONFIG` constant.
|
|
23
|
+
- **Stack + attack-surface detection** from keyword/alias maps (Next.js, Supabase, Clerk, Stripe, Prisma, Express, Hono, Drizzle, Firebase, MongoDB, tRPC, FastAPI, Django...; auth, payments, file upload, user input, database/SQL, secrets, external APIs, deserialization, redirects), including surfaces implied by detected technologies. Optional `context` input merges client-known stack info. Token matching is boundary-checked `indexOf` — no dynamic RegExp (keeps the self-audit and ReDoS meta-test clean).
|
|
24
|
+
- **Rule matching over the existing 442-rule set** by name/description keywords for the detected stack + surfaces, severity-ranked (critical → info), near-duplicate guidance deduped, capped at the top 8; each requirement carries `[rule-id]`, title, severity, and the rule's fix phrased as an instruction. CVE version-pin rules excluded (they gate package pins, not prompts).
|
|
25
|
+
- Directive output: verdict + one-line reason, intent summary stated as a HARD CONSTRAINT, numbered security requirements, ambiguities (HEAVY_MOD only), explicit rewrite directive ("Do NOT add features the user did not request. Do NOT change the user's intent."), and the original prompt echoed verbatim (fence-safe even when the prompt contains code blocks).
|
|
26
|
+
- New module `src/tools/secure-prompt.ts`; 24 tests in `tests/tools/secure-prompt.test.ts` (NO_MOD short-circuit, LIGHT vs HEAVY classification, 7-framework stack detection, rule cap + severity ordering, empty/garbage input, determinism). README gains a "Prompt-Level Security (Shift Left)" section with a before/after example. Zero new runtime dependencies.
|
|
27
|
+
|
|
28
|
+
Gate green (build / lint / test / self-audit PASS / A / 0).
|
|
29
|
+
|
|
8
30
|
## [3.18.0] - 2026-06-09
|
|
9
31
|
|
|
10
32
|
### Added — FAZ 3 part c: AST BOLA mutation-guard detection for VG951 (442 rules / 37 tools)
|
package/README.md
CHANGED
|
@@ -13,20 +13,21 @@
|
|
|
13
13
|
- **🎯 Deterministic, not probabilistic.** Same code = same result, every run (content-hashed). Your AI guesses; GuardVibe doesn't.
|
|
14
14
|
- **🗺️ Sees the whole repo.** Cross-file taint + auth-coverage across every route — catches the unprotected endpoint your agent's narrow context missed.
|
|
15
15
|
- **🔍 An independent second pair of eyes.** The thing that wrote the code can't review itself. GuardVibe is the outside checker on AI-written code — in the loop *while* your AI codes (real-time edit hook), not after.
|
|
16
|
+
- **⬅️ NEW: Starts before the first line of code.** Every scanner on earth — including your agent reviewing itself — acts *after* the code exists. [`secure_prompt`](#prompt-level-security-shift-left) acts *before*: it analyzes the coding prompt itself, detects the stack and attack surfaces it implies, and embeds severity-ranked GuardVibe requirements into the prompt your AI executes. The vulnerability is prevented, not caught. Deterministic, zero LLM calls — and if the prompt is already secure, it passes through untouched.
|
|
16
17
|
|
|
17
|
-
**The security MCP built for vibe coding.**
|
|
18
|
+
**The security MCP built for vibe coding.** 445 security rules, 38 tools covering the entire AI-generated code journey — from the prompt itself to production deployment.
|
|
18
19
|
|
|
19
20
|
Works with **Claude Code, Cursor, Gemini CLI, Codex, VS Code (Copilot), Windsurf**, and any MCP-compatible coding agent.
|
|
20
21
|
|
|
21
22
|
## Why a tool, when your AI is so good?
|
|
22
23
|
|
|
23
|
-
"More rules" was never the moat — a strong model already knows most security rules by heart. What it *can't* do is be deterministic, know the CVE published after its training cutoff, hold your whole repo in context, or objectively review the code it just wrote. Those four gaps are structural; they don't close as models improve. GuardVibe is the layer that fills them — running *while* your AI codes, not in a separate audit later.
|
|
24
|
+
"More rules" was never the moat — a strong model already knows most security rules by heart. What it *can't* do is be deterministic, know the CVE published after its training cutoff, hold your whole repo in context, or objectively review the code it just wrote. Those four gaps are structural; they don't close as models improve. GuardVibe is the layer that fills them — running *while* your AI codes, not in a separate audit later. And since v3.19, it runs *before* your AI codes too: `secure_prompt` rewrites the task itself so the security requirements are in the prompt, not in the post-mortem.
|
|
24
25
|
|
|
25
26
|
## Why GuardVibe
|
|
26
27
|
|
|
27
28
|
Most security tools are built for enterprise security teams. GuardVibe is built for **you** — the developer using AI to build and ship web apps fast.
|
|
28
29
|
|
|
29
|
-
- **
|
|
30
|
+
- **445 security rules, 38 tools** purpose-built for the stacks AI agents generate
|
|
30
31
|
- **Zero setup friction** — `npx guardvibe` and you're scanning
|
|
31
32
|
- **No account required** — runs 100% locally, no API keys, no cloud
|
|
32
33
|
- **Understands your stack** — not generic SAST, but rules that know Next.js, Supabase, Stripe, Clerk, and the tools you actually use
|
|
@@ -64,7 +65,7 @@ GuardVibe is purpose-built for the AI coding workflow. Traditional tools are exc
|
|
|
64
65
|
| CVE version detection | 71 packages, refreshed daily | Extensive | Extensive |
|
|
65
66
|
| Compliance mapping (SOC2, PCI-DSS, HIPAA) | Built-in | Paid tier | None |
|
|
66
67
|
| SARIF CI/CD export | Yes | Yes | Limited |
|
|
67
|
-
| Rule count |
|
|
68
|
+
| Rule count | 445 (focused, 68 AI-native) | 5000+ (broad) | N/A |
|
|
68
69
|
|
|
69
70
|
**When to use GuardVibe:** You're building with AI agents and want security scanning integrated into your coding workflow — no dashboard, no account, no CI setup.
|
|
70
71
|
|
|
@@ -212,7 +213,37 @@ Maps security findings to SOC2, PCI-DSS, HIPAA, GDPR, ISO27001, and EU AI Act (E
|
|
|
212
213
|
### Supply Chain
|
|
213
214
|
Malicious postinstall scripts, unpinned GitHub Actions, CI `npm` provenance / `--ignore-scripts` hardening (VG1070), typosquat detection, `node-ipc` protestware versions (VG1069), Miasma `@redhat-cloud-services` namespace compromise IOC (VG1074, RHSB-2026-006), Session messenger exfil endpoint IOC (VG1075, `filev2.getsession.org`), `@tanstack/*` Mini Shai-Hulud mass-malware versions (May 2026), `@wdio/browserstack-service` command injection via git branch names (CVE-2026-25244), lockfile poisoning patterns
|
|
214
215
|
|
|
215
|
-
##
|
|
216
|
+
## Prompt-Level Security (Shift Left)
|
|
217
|
+
|
|
218
|
+
Most vulnerabilities in AI-generated code are born in the prompt: "add login to my app" says nothing about password hashing, session handling, or rate limiting — so the model picks defaults, and the defaults are where the CVEs live. `secure_prompt` moves the security gate to **before code generation**: it analyzes the raw prompt, detects the stack and attack surfaces it implies, matches them against GuardVibe's rule set, and returns a directive the host LLM uses to rewrite the prompt with security requirements embedded.
|
|
219
|
+
|
|
220
|
+
This is not a prompt beautifier. It is deterministic (no LLM, no network), it never restructures intent, and its first job is **do no harm**: a prompt that is already specific and security-aware gets verdict `NO_MOD` and passes through untouched.
|
|
221
|
+
|
|
222
|
+
- **`NO_MOD`** — prompt is already specific and security-aware → proceed with the original prompt unchanged
|
|
223
|
+
- **`LIGHT_MOD`** — intent is clear but security constraints are missing → inject requirements only
|
|
224
|
+
- **`HEAVY_MOD`** — prompt is vague *and* security-relevant → inject requirements + surface clarifying questions (never invent the answers)
|
|
225
|
+
|
|
226
|
+
**Before** (what the user typed):
|
|
227
|
+
|
|
228
|
+
```text
|
|
229
|
+
add login to my app
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
**After** (what the host LLM executes, having applied the `secure_prompt` directive):
|
|
233
|
+
|
|
234
|
+
```text
|
|
235
|
+
Add login to my app, with these security requirements:
|
|
236
|
+
- [VG001] Use environment variables or a secrets manager — never hardcode credentials.
|
|
237
|
+
- [VG1008] Always verify the caller has admin privileges before allowing role elevation.
|
|
238
|
+
- [VG105] Always specify allowed algorithms explicitly in jwt.verify().
|
|
239
|
+
|
|
240
|
+
Before implementing, confirm: which framework/stack is this for, and which auth
|
|
241
|
+
provider should be used (e.g. Clerk, Auth.js/NextAuth, Supabase Auth, custom JWT)?
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Same user intent — but the model now generates auth code with the guardrails stated up front, instead of GuardVibe catching the missing pieces after the fact.
|
|
245
|
+
|
|
246
|
+
## Tools (38 MCP tools)
|
|
216
247
|
|
|
217
248
|
| Tool | What it does |
|
|
218
249
|
|------|-------------|
|
|
@@ -253,10 +284,11 @@ Malicious postinstall scripts, unpinned GitHub Actions, CI `npm` provenance / `-
|
|
|
253
284
|
| `full_audit` | **Single source of truth** — runs ALL checks in one call, returns PASS/FAIL/WARN verdict + score + coverage % + deterministic result hash |
|
|
254
285
|
| `remediation_plan` | **Remediation plan** — generates section-by-section fix checklist after audit |
|
|
255
286
|
| `verify_remediation` | **Remediation verification** — compares before/after audit, flags skipped sections |
|
|
287
|
+
| `secure_prompt` | **Prompt-level security (shift left)** — analyze a coding prompt BEFORE code is written; deterministic triage (NO_MOD/LIGHT_MOD/HEAVY_MOD), stack + attack-surface detection, severity-ranked GuardVibe requirements embedded via a rewrite directive |
|
|
256
288
|
|
|
257
289
|
All scanning tools support `format: "json"` for machine-readable output.
|
|
258
290
|
|
|
259
|
-
## Security Rules (
|
|
291
|
+
## Security Rules (445 rules across 25 modules)
|
|
260
292
|
|
|
261
293
|
| Category | Rules | Coverage |
|
|
262
294
|
|----------|-------|----------|
|
|
@@ -829,4 +829,40 @@ export const cveVersionRules = [
|
|
|
829
829
|
fixCode: '// package.json\n"vite": "^5.4.9" // or ^6',
|
|
830
830
|
compliance: ["SOC2:CC7.1", "PCI-DSS:Req6.5.1"],
|
|
831
831
|
},
|
|
832
|
+
{
|
|
833
|
+
id: "VG1089",
|
|
834
|
+
name: "js-cookie assign() Prototype Hijack — Cookie Attribute Injection (CVE-2026-46625 / GHSA-qjx8-664m-686j)",
|
|
835
|
+
severity: "high",
|
|
836
|
+
owasp: "A03:2025 Injection",
|
|
837
|
+
description: "js-cookie versions before 3.0.7 are vulnerable to a per-instance prototype hijack in the internal assign() helper. assign() copies properties with a for…in loop plus plain assignment, so any key planted on Object.prototype is enumerated and copied into the merged attributes object. When set() later serialises that object, attacker-polluted keys land in the Set-Cookie string as attribute pairs — letting an attacker force domain=, path=, secure=, samesite=, or expires= on cookies the application writes (cookie scoping abuse / fixation). Fixed in 3.0.7. Only exact (or =) pins in the 3.0.x line are flagged — a caret/tilde range there resolves to the fixed 3.0.7; older 0.x–2.x majors are flagged with any range since they never reach the fix.",
|
|
838
|
+
pattern: /["']js-cookie["']\s*:\s*["'](?:(?:\^|~|>=?)?\s*[0-2]\.\d+\.\d+|=?\s*3\.0\.[0-6])["']/g,
|
|
839
|
+
languages: ["json"],
|
|
840
|
+
fix: "Upgrade js-cookie to 3.0.7 or later: npm install js-cookie@latest. As defence-in-depth, freeze Object.prototype at bootstrap (Object.freeze(Object.prototype)) and never spread untrusted objects into cookie-attribute options.",
|
|
841
|
+
fixCode: '// package.json\n"js-cookie": "^3.0.7" // or latest\n\n// Defence-in-depth — block prototype writes at startup\nObject.freeze(Object.prototype);',
|
|
842
|
+
compliance: ["SOC2:CC6.1", "PCI-DSS:Req6.2"],
|
|
843
|
+
},
|
|
844
|
+
{
|
|
845
|
+
id: "VG1090",
|
|
846
|
+
name: "PostCSS XSS via Unescaped </style> in Stringify Output (CVE-2026-41305 / GHSA-qx2v-qp2m-jg93)",
|
|
847
|
+
severity: "medium",
|
|
848
|
+
owasp: "A03:2025 Injection",
|
|
849
|
+
description: "postcss versions before 8.5.10 do not escape a </style> sequence when stringifying a CSS AST back to text. An app that parses user-submitted CSS and re-emits it into an inline <style> block lets an attacker close the style element early with </style> and inject arbitrary markup/script — a stored or reflected XSS. Fixed in 8.5.10. Caret ranges on the 8.x line resolve to the fixed 8.5.10, so only exact/= pins (and tilde within 8.0–8.4) on the 8.x line are flagged; 1.x–7.x majors are flagged with any range since they never reach the fix.",
|
|
850
|
+
pattern: /["']postcss["']\s*:\s*["'](?:(?:\^|~|>=?)?\s*[1-7]\.\d+\.\d+|~?\s*8\.[0-4]\.\d+|=?\s*8\.5\.[0-9](?![0-9]))["']/g,
|
|
851
|
+
languages: ["json"],
|
|
852
|
+
fix: "Upgrade postcss to 8.5.10 or later: npm install postcss@latest. If you embed processed CSS in an inline <style> tag, also HTML-escape </style> (or serve the CSS from an external stylesheet) as defence-in-depth.",
|
|
853
|
+
fixCode: '// package.json\n"postcss": "^8.5.10" // or latest',
|
|
854
|
+
compliance: ["SOC2:CC6.1", "PCI-DSS:Req6.5.7"],
|
|
855
|
+
},
|
|
856
|
+
{
|
|
857
|
+
id: "VG1091",
|
|
858
|
+
name: "Axios HTTP-Adapter Proxy Prototype-Pollution Gadget (CVE-2026-44494 / GHSA-35jp-ww65-95wh)",
|
|
859
|
+
severity: "high",
|
|
860
|
+
owasp: "A10:2025 SSRF",
|
|
861
|
+
description: "axios versions before 1.16.0 read config.proxy in the Node HTTP adapter without an own-property check. Because `proxy` is not set in axios defaults, a prototype-pollution gadget elsewhere in the process (Object.prototype.proxy = {...}) is picked up on every request, routing traffic through an attacker-controlled proxy — full MITM, credential theft, and response tampering. Fixed in 1.16.0 (own-property checks for proxy/socketPath/transport). Distinct from VG1042 (the pre-1.15.2 cluster): a project that took VG1042's advice and pinned 1.15.2–1.15.x is STILL exposed to this gadget, so this rule flags exactly that residual window (1.15.2 through 1.15.x). Caret ranges resolve to the fixed 1.16.0 and are not flagged.",
|
|
862
|
+
pattern: /["']axios["']\s*:\s*["'](?:~|=)?\s*1\.15\.(?:[2-9]|[1-9]\d)["']/g,
|
|
863
|
+
languages: ["json"],
|
|
864
|
+
fix: "Upgrade axios to 1.16.0 or later: npm install axios@latest. As defence-in-depth, freeze Object.prototype at startup so a pollution gadget cannot inject a proxy: Object.freeze(Object.prototype).",
|
|
865
|
+
fixCode: '// package.json\n"axios": "^1.16.0" // or latest\n\n// Defence-in-depth — block prototype writes at bootstrap\nObject.freeze(Object.prototype);',
|
|
866
|
+
compliance: ["SOC2:CC6.1", "SOC2:CC7.1", "PCI-DSS:Req6.2"],
|
|
867
|
+
},
|
|
832
868
|
];
|
package/build/index.js
CHANGED
|
@@ -42,6 +42,7 @@ import { formatHostFindings, redactSecrets } from "./server/types.js";
|
|
|
42
42
|
import { verifyFix } from "./tools/verify-fix.js";
|
|
43
43
|
import { fixCode as fixCodeTool } from "./tools/fix-code.js";
|
|
44
44
|
import { secureThis } from "./tools/secure-this.js";
|
|
45
|
+
import { securePrompt } from "./tools/secure-prompt.js";
|
|
45
46
|
import { buildAgentReport } from "./tools/agent-output.js";
|
|
46
47
|
import { analyzeAuthCoverage, formatAuthCoverage } from "./tools/auth-coverage.js";
|
|
47
48
|
import { buildDeepScanPrompt, parseDeepScanResult, formatDeepScanFindings, callLLM } from "./tools/deep-scan.js";
|
|
@@ -63,7 +64,7 @@ function mergeStatsIntoOutput(results, summary, format) {
|
|
|
63
64
|
const server = new McpServer({
|
|
64
65
|
name: "guardvibe",
|
|
65
66
|
version: pkg.version,
|
|
66
|
-
description:
|
|
67
|
+
description: `Security MCP for vibe coding — single source of truth for AI assistants. ${builtinRules.length} security rules and 38 tools. Call secure_prompt with the user's coding prompt BEFORE generating code to embed security requirements up front (shift left). Use full_audit for a comprehensive PASS/FAIL/WARN verdict with deterministic result hash, coverage %, and unified report across code, secrets, dependencies, config, taint analysis, and auth coverage. IMPORTANT: When full_audit returns FAIL/WARN, call remediation_plan to get a mandatory section-by-section fix checklist covering ALL 6 sections (not just code). After fixing, call verify_remediation to confirm all sections were addressed. Same code = same hash = same results regardless of which AI assistant runs it. Covers OWASP, Next.js, Supabase, Stripe, Clerk, Prisma, Hono, AI SDK, MCP server security, host hardening. Maps to SOC2, PCI-DSS, HIPAA, GDPR, ISO27001, EU AI Act. Runs 100% locally with zero configuration.`,
|
|
67
68
|
});
|
|
68
69
|
// Tool 1: Analyze code for security vulnerabilities
|
|
69
70
|
server.tool("check_code", "Analyze inline code for security vulnerabilities (OWASP Top 10, XSS, SQL injection, insecure patterns). Pass code as a string parameter. For scanning files on disk, use scan_file instead. Example: check_code({code: 'app.get(...)', language: 'javascript'})", {
|
|
@@ -1031,6 +1032,15 @@ server.tool("verify_remediation", "Compare before/after audit results to verify
|
|
|
1031
1032
|
lines.push("", "---", `**${summary}**`);
|
|
1032
1033
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
1033
1034
|
});
|
|
1035
|
+
// Tool 38: secure_prompt — shift-left security at the prompt level (enhance BEFORE code is written)
|
|
1036
|
+
server.tool("secure_prompt", "Shift-left security at the prompt level: analyze a raw coding prompt BEFORE any code is written and return a structured enhancement directive that embeds GuardVibe security requirements (auth checks, input validation, webhook signature verification, SQL injection prevention, secrets handling) into the prompt you are about to execute. Deterministic — no LLM, no network: triage verdict NO_MOD (prompt already specific and security-aware → proceed with the ORIGINAL prompt unchanged), LIGHT_MOD (inject missing security constraints only), or HEAVY_MOD (also surface clarifying questions — never invent answers to them). Detects stack (Next.js, Supabase, Clerk, Stripe, Prisma, Express, Hono...) and attack surfaces (auth, payments, file upload, user input, SQL, secrets, redirects) from the prompt text, matches them against GuardVibe's rule set, and returns verdict + intent summary + numbered [rule-id] requirements + rewrite directive. Call this with the user's prompt before generating code; prevents vulnerabilities before code generation instead of scanning after. Example: secure_prompt({raw_prompt: 'add login to my app'})", {
|
|
1037
|
+
raw_prompt: z.string().describe("The user's original coding prompt, verbatim"),
|
|
1038
|
+
context: z.string().optional().describe("Known stack/framework context if the client has it (e.g. 'Next.js app router, Supabase, Stripe')"),
|
|
1039
|
+
}, async ({ raw_prompt, context }) => {
|
|
1040
|
+
const rules = getRules();
|
|
1041
|
+
const result = securePrompt(raw_prompt, { context, rules });
|
|
1042
|
+
return { content: [{ type: "text", text: result.markdown }] };
|
|
1043
|
+
});
|
|
1034
1044
|
export async function startMcpServer() {
|
|
1035
1045
|
return main();
|
|
1036
1046
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { SecurityRule } from "../data/rules/types.js";
|
|
2
|
+
export type SecurePromptVerdict = "NO_MOD" | "LIGHT_MOD" | "HEAVY_MOD";
|
|
3
|
+
export interface SecurePromptRequirement {
|
|
4
|
+
ruleId: string;
|
|
5
|
+
title: string;
|
|
6
|
+
requirement: string;
|
|
7
|
+
severity: SecurityRule["severity"];
|
|
8
|
+
}
|
|
9
|
+
export interface SecurePromptResult {
|
|
10
|
+
verdict: SecurePromptVerdict;
|
|
11
|
+
reason: string;
|
|
12
|
+
intentSummary: string;
|
|
13
|
+
detectedStack: string[];
|
|
14
|
+
detectedSurfaces: string[];
|
|
15
|
+
securityRequirements: SecurePromptRequirement[];
|
|
16
|
+
ambiguities: string[];
|
|
17
|
+
originalPrompt: string;
|
|
18
|
+
/** The single markdown directive block returned to the host LLM. */
|
|
19
|
+
markdown: string;
|
|
20
|
+
}
|
|
21
|
+
/** Triage thresholds — tune here, never inline. */
|
|
22
|
+
export declare const TRIAGE_CONFIG: {
|
|
23
|
+
/** Distinct security terms at/above this → prompt counts as security-aware. */
|
|
24
|
+
readonly securityAwareTerms: 3;
|
|
25
|
+
/** Specificity score at/above this → prompt counts as specific (not vague). */
|
|
26
|
+
readonly specificityThreshold: 4;
|
|
27
|
+
/** Prompts shorter than this many words are vague regardless of other signals. */
|
|
28
|
+
readonly minWords: 6;
|
|
29
|
+
/** Matched rules surfaced as requirements are capped at this many. */
|
|
30
|
+
readonly maxRequirements: 8;
|
|
31
|
+
/** Clarifying questions (HEAVY_MOD only) are capped at this many. */
|
|
32
|
+
readonly maxAmbiguities: 3;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* A lowercased haystack searched in two forms: raw (so hyphenated tokens like
|
|
36
|
+
* "next-auth" match) and hyphen/underscore-normalized (so user phrasings like
|
|
37
|
+
* "sign-in"/"RLS-enabled"/"log_in" match space-joined tokens like "sign in").
|
|
38
|
+
*/
|
|
39
|
+
interface Haystack {
|
|
40
|
+
raw: string;
|
|
41
|
+
norm: string;
|
|
42
|
+
}
|
|
43
|
+
/** True if the token appears (word-boundary) in either form of the haystack. */
|
|
44
|
+
export declare function includesToken(haystack: Haystack | string, token: string): boolean;
|
|
45
|
+
/** Detect technologies named in the prompt (and optional client-provided context). */
|
|
46
|
+
export declare function detectPromptStack(rawPrompt: string, context?: string): string[];
|
|
47
|
+
/**
|
|
48
|
+
* Detect security-sensitive attack surfaces implied by the prompt. Surfaces describe
|
|
49
|
+
* what the user is BUILDING, so they are derived from the prompt text only — the
|
|
50
|
+
* optional `context` (which names the stack, not the task) deliberately does not
|
|
51
|
+
* manufacture surfaces, preserving the NO_MOD "do no harm" path for non-security
|
|
52
|
+
* prompts even when a host always attaches project context.
|
|
53
|
+
*/
|
|
54
|
+
export declare function detectPromptSurfaces(rawPrompt: string, context?: string): string[];
|
|
55
|
+
/** Rank rules against the detected stack + surfaces; severity first, cap at maxRequirements. */
|
|
56
|
+
export declare function matchRulesForPrompt(stack: string[], surfaces: string[], rules: SecurityRule[]): SecurePromptRequirement[];
|
|
57
|
+
/**
|
|
58
|
+
* Analyze a raw coding prompt BEFORE code generation and return a structured
|
|
59
|
+
* enhancement directive. Fully deterministic: same prompt = same directive.
|
|
60
|
+
*/
|
|
61
|
+
export declare function securePrompt(rawPrompt: string, opts?: {
|
|
62
|
+
context?: string;
|
|
63
|
+
rules?: SecurityRule[];
|
|
64
|
+
}): SecurePromptResult;
|
|
65
|
+
export {};
|
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
// secure_prompt — shift-left security at the prompt level.
|
|
2
|
+
// Deterministic pipeline (no LLM calls, no network, no filesystem): triage the raw
|
|
3
|
+
// prompt ("do no harm" first), detect stack + attack surfaces from keyword/alias maps,
|
|
4
|
+
// match the existing GuardVibe rule set, and emit a markdown enhancement directive
|
|
5
|
+
// (guardvibe.secure_prompt.v1) that the HOST LLM uses to rewrite the prompt with
|
|
6
|
+
// security requirements embedded — BEFORE any code is written.
|
|
7
|
+
//
|
|
8
|
+
// Keyword matching deliberately avoids dynamic RegExp construction (boundary-checked
|
|
9
|
+
// indexOf instead) so the scanner's own dynamic-regex and ReDoS audits stay clean.
|
|
10
|
+
import { builtinRules } from "../data/rules/index.js";
|
|
11
|
+
/** Triage thresholds — tune here, never inline. */
|
|
12
|
+
export const TRIAGE_CONFIG = {
|
|
13
|
+
/** Distinct security terms at/above this → prompt counts as security-aware. */
|
|
14
|
+
securityAwareTerms: 3,
|
|
15
|
+
/** Specificity score at/above this → prompt counts as specific (not vague). */
|
|
16
|
+
specificityThreshold: 4,
|
|
17
|
+
/** Prompts shorter than this many words are vague regardless of other signals. */
|
|
18
|
+
minWords: 6,
|
|
19
|
+
/** Matched rules surfaced as requirements are capped at this many. */
|
|
20
|
+
maxRequirements: 8,
|
|
21
|
+
/** Clarifying questions (HEAVY_MOD only) are capped at this many. */
|
|
22
|
+
maxAmbiguities: 3,
|
|
23
|
+
};
|
|
24
|
+
const TECHS = [
|
|
25
|
+
{ id: "nextjs", label: "Next.js", tokens: ["next.js", "nextjs", "next js", "app router", "server action", "server actions", "server component", "server components", "route handler"], ruleKeywords: ["next.js", "nextjs", "server action", "app router", "route handler", "next_public"], impliedSurfaces: [] },
|
|
26
|
+
{ id: "react", label: "React", tokens: ["react", "jsx", "tsx"], ruleKeywords: ["react", "dangerouslysetinnerhtml"], impliedSurfaces: [] },
|
|
27
|
+
{ id: "express", label: "Express", tokens: ["express", "expressjs"], ruleKeywords: ["express"], impliedSurfaces: [] },
|
|
28
|
+
{ id: "hono", label: "Hono", tokens: ["hono"], ruleKeywords: ["hono"], impliedSurfaces: [] },
|
|
29
|
+
{ id: "supabase", label: "Supabase", tokens: ["supabase", "row level security", "rls"], ruleKeywords: ["supabase", "row level security"], impliedSurfaces: ["database"] },
|
|
30
|
+
{ id: "clerk", label: "Clerk", tokens: ["clerk"], ruleKeywords: ["clerk"], impliedSurfaces: ["auth"] },
|
|
31
|
+
{ id: "nextauth", label: "Auth.js / NextAuth", tokens: ["next-auth", "nextauth", "auth.js", "authjs"], ruleKeywords: ["next-auth", "nextauth", "auth.js"], impliedSurfaces: ["auth"] },
|
|
32
|
+
{ id: "stripe", label: "Stripe", tokens: ["stripe"], ruleKeywords: ["stripe"], impliedSurfaces: ["payments"] },
|
|
33
|
+
{ id: "lemonsqueezy", label: "LemonSqueezy", tokens: ["lemonsqueezy", "lemon squeezy"], ruleKeywords: ["lemonsqueezy"], impliedSurfaces: ["payments"] },
|
|
34
|
+
{ id: "prisma", label: "Prisma", tokens: ["prisma"], ruleKeywords: ["prisma"], impliedSurfaces: ["database"] },
|
|
35
|
+
{ id: "drizzle", label: "Drizzle", tokens: ["drizzle"], ruleKeywords: ["drizzle"], impliedSurfaces: ["database"] },
|
|
36
|
+
{ id: "mongodb", label: "MongoDB / Mongoose", tokens: ["mongodb", "mongoose", "mongo"], ruleKeywords: ["mongo", "nosql"], impliedSurfaces: ["database"] },
|
|
37
|
+
{ id: "postgres", label: "PostgreSQL", tokens: ["postgres", "postgresql"], ruleKeywords: ["postgres", "sql"], impliedSurfaces: ["database"] },
|
|
38
|
+
{ id: "firebase", label: "Firebase", tokens: ["firebase", "firestore"], ruleKeywords: ["firebase", "firestore"], impliedSurfaces: ["database"] },
|
|
39
|
+
{ id: "trpc", label: "tRPC", tokens: ["trpc"], ruleKeywords: ["trpc", "procedure"], impliedSurfaces: [] },
|
|
40
|
+
{ id: "fastapi", label: "FastAPI", tokens: ["fastapi"], ruleKeywords: ["fastapi"], impliedSurfaces: [] },
|
|
41
|
+
{ id: "django", label: "Django", tokens: ["django"], ruleKeywords: ["django"], impliedSurfaces: [] },
|
|
42
|
+
];
|
|
43
|
+
const SURFACES = [
|
|
44
|
+
{
|
|
45
|
+
id: "auth", label: "authentication / access control",
|
|
46
|
+
tokens: ["auth", "authentication", "authorization", "login", "log in", "signin", "sign in", "signup", "sign up", "logout", "password", "session", "sessions", "jwt", "oauth", "sso", "2fa", "mfa", "role", "roles", "permission", "permissions", "admin", "account", "user management"],
|
|
47
|
+
ruleKeywords: ["auth", "session", "login", "access control", "unauthorized", "credential", "jwt", "bola", "idor"],
|
|
48
|
+
question: "Which auth provider or mechanism should be used (e.g. Clerk, Auth.js/NextAuth, Supabase Auth, custom JWT sessions)?",
|
|
49
|
+
answeredByTechs: ["clerk", "nextauth", "supabase", "firebase"],
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
id: "payments", label: "payments / billing",
|
|
53
|
+
tokens: ["payment", "payments", "checkout", "billing", "subscription", "subscriptions", "invoice", "refund", "pricing", "pay"],
|
|
54
|
+
ruleKeywords: ["stripe", "payment", "webhook", "checkout", "billing", "price"],
|
|
55
|
+
question: "Which payment provider is used, and which webhook events must be handled?",
|
|
56
|
+
answeredByTechs: ["stripe", "lemonsqueezy"],
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: "file-upload", label: "file upload",
|
|
60
|
+
tokens: ["upload", "uploads", "file upload", "avatar", "attachment", "attachments", "multipart", "image upload"],
|
|
61
|
+
ruleKeywords: ["upload", "file type", "multipart", "path traversal", "content-type"],
|
|
62
|
+
question: "What file types and maximum size should uploads accept, and where are files stored?",
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
id: "user-input", label: "user input handling",
|
|
66
|
+
tokens: ["form", "forms", "input", "inputs", "comment", "comments", "search", "user input", "query param", "query params", "request body", "post endpoint", "api endpoint", "endpoint", "contact form", "profile"],
|
|
67
|
+
ruleKeywords: ["validation", "sanitiz", "xss", "injection", "innerhtml", "user input"],
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
id: "database", label: "database / SQL",
|
|
71
|
+
tokens: ["sql", "database", "db", "query", "queries", "mysql", "sqlite", "orm", "table", "schema", "migration"],
|
|
72
|
+
ruleKeywords: ["sql", "injection", "query", "orm", "database", "mass assignment"],
|
|
73
|
+
question: "Which database/ORM is used (e.g. Prisma, Drizzle, Supabase, raw Postgres)?",
|
|
74
|
+
answeredByTechs: ["prisma", "drizzle", "supabase", "postgres", "mongodb", "firebase"],
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: "secrets", label: "secrets / credentials",
|
|
78
|
+
tokens: ["secret", "secrets", "api key", "api keys", "apikey", "token", "tokens", "credential", "credentials", ".env", "env var", "env vars", "environment variable", "environment variables", "private key"],
|
|
79
|
+
ruleKeywords: ["secret", "credential", "api key", "hardcoded", "env"],
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
id: "external-api", label: "external API calls",
|
|
83
|
+
tokens: ["external api", "third-party", "third party", "fetch", "webhook", "webhooks", "http request", "api call", "api calls", "integration", "proxy", "scrape", "scraper"],
|
|
84
|
+
ruleKeywords: ["ssrf", "request forgery", "external", "url"],
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
id: "deserialization", label: "deserialization / dynamic evaluation",
|
|
88
|
+
tokens: ["deserialize", "deserialization", "unserialize", "pickle", "yaml.load", "eval", "serialize"],
|
|
89
|
+
ruleKeywords: ["deserial", "eval", "prototype pollution", "unserialize"],
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
id: "redirect", label: "redirects / callbacks",
|
|
93
|
+
tokens: ["redirect", "redirects", "callback url", "return url", "returnto", "return to", "callback"],
|
|
94
|
+
ruleKeywords: ["redirect", "callback"],
|
|
95
|
+
},
|
|
96
|
+
];
|
|
97
|
+
// Explicit security-engineering vocabulary, grouped by CONCEPT. countSecurityTerms
|
|
98
|
+
// counts each group at most once, so synonyms/sub-phrases ("validation" +
|
|
99
|
+
// "input validation" + "schema validation") of a single concept never triple-count.
|
|
100
|
+
const SECURITY_TERM_GROUPS = [
|
|
101
|
+
["auth", "authn", "authentication", "authorization", "access control", "ownership check"],
|
|
102
|
+
["validate", "validates", "validation", "input validation", "schema validation", "zod"],
|
|
103
|
+
["sanitize", "sanitizes", "sanitization", "escape", "escaping"],
|
|
104
|
+
["rate limit", "rate-limit", "rate limiting", "rate-limiting", "throttle"],
|
|
105
|
+
["csrf"],
|
|
106
|
+
["xss"],
|
|
107
|
+
["sql injection", "injection", "parameterized", "prepared statement"],
|
|
108
|
+
["webhook signature", "signature verification", "verify the signature", "constructevent", "hmac", "timingsafeequal", "timing-safe"],
|
|
109
|
+
["secret manager", "secrets manager", "env var", "environment variable"],
|
|
110
|
+
["encrypt", "encryption", "hash", "hashed", "hashing", "bcrypt", "argon2", "scrypt"],
|
|
111
|
+
["jwt verification", "verify jwt"],
|
|
112
|
+
["rls", "row level security", "least privilege"],
|
|
113
|
+
["csp", "hsts", "x-frame-options", "security header", "security headers", "helmet"],
|
|
114
|
+
["cors"],
|
|
115
|
+
["2fa", "mfa"],
|
|
116
|
+
["owasp", "idor", "bola", "ssrf"],
|
|
117
|
+
["allowlist", "whitelist", "denylist"],
|
|
118
|
+
];
|
|
119
|
+
/** Markers of an underspecified ask — each hit lowers the specificity score. */
|
|
120
|
+
const VAGUE_MARKERS = [
|
|
121
|
+
"somehow", "something", "stuff", "make it work", "or whatever", "etc", "some kind of",
|
|
122
|
+
"quick and dirty", "simple app", "basic app", "a thing",
|
|
123
|
+
];
|
|
124
|
+
const SEVERITY_ORDER = { critical: 0, high: 1, medium: 2, low: 3, info: 4 };
|
|
125
|
+
/** Languages whose rules express things a code-writing prompt can actually satisfy. */
|
|
126
|
+
const CODE_LANGUAGES = ["javascript", "typescript", "python", "go"];
|
|
127
|
+
function isWordChar(ch) {
|
|
128
|
+
if (ch === "")
|
|
129
|
+
return false;
|
|
130
|
+
return /[a-z0-9_-]/.test(ch);
|
|
131
|
+
}
|
|
132
|
+
/** Word-boundary token search without dynamic RegExp (token is matched case-insensitively). */
|
|
133
|
+
function includesTokenIn(haystackLower, token) {
|
|
134
|
+
const needle = token.toLowerCase();
|
|
135
|
+
let idx = haystackLower.indexOf(needle);
|
|
136
|
+
while (idx !== -1) {
|
|
137
|
+
const before = idx === 0 ? "" : haystackLower[idx - 1];
|
|
138
|
+
const afterIdx = idx + needle.length;
|
|
139
|
+
const after = afterIdx >= haystackLower.length ? "" : haystackLower[afterIdx];
|
|
140
|
+
if (!isWordChar(before) && !isWordChar(after))
|
|
141
|
+
return true;
|
|
142
|
+
idx = haystackLower.indexOf(needle, idx + 1);
|
|
143
|
+
}
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
function makeHaystack(text) {
|
|
147
|
+
const raw = text.toLowerCase();
|
|
148
|
+
return { raw, norm: raw.replace(/[-_]+/g, " ") };
|
|
149
|
+
}
|
|
150
|
+
/** True if the token appears (word-boundary) in either form of the haystack. */
|
|
151
|
+
export function includesToken(haystack, token) {
|
|
152
|
+
const h = typeof haystack === "string" ? makeHaystack(haystack) : haystack;
|
|
153
|
+
return includesTokenIn(h.raw, token) || includesTokenIn(h.norm, token);
|
|
154
|
+
}
|
|
155
|
+
/** Detect technologies named in the prompt (and optional client-provided context). */
|
|
156
|
+
export function detectPromptStack(rawPrompt, context) {
|
|
157
|
+
const h = makeHaystack(`${rawPrompt}\n${context ?? ""}`);
|
|
158
|
+
return TECHS.filter((t) => t.tokens.some((tok) => includesToken(h, tok))).map((t) => t.id);
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Detect security-sensitive attack surfaces implied by the prompt. Surfaces describe
|
|
162
|
+
* what the user is BUILDING, so they are derived from the prompt text only — the
|
|
163
|
+
* optional `context` (which names the stack, not the task) deliberately does not
|
|
164
|
+
* manufacture surfaces, preserving the NO_MOD "do no harm" path for non-security
|
|
165
|
+
* prompts even when a host always attaches project context.
|
|
166
|
+
*/
|
|
167
|
+
export function detectPromptSurfaces(rawPrompt, context) {
|
|
168
|
+
void context;
|
|
169
|
+
const h = makeHaystack(rawPrompt);
|
|
170
|
+
const direct = SURFACES.filter((s) => s.tokens.some((tok) => includesToken(h, tok))).map((s) => s.id);
|
|
171
|
+
const implied = TECHS.filter((t) => t.tokens.some((tok) => includesToken(h, tok))).flatMap((t) => t.impliedSurfaces);
|
|
172
|
+
return [...new Set([...direct, ...implied])];
|
|
173
|
+
}
|
|
174
|
+
/** Count DISTINCT security concepts present (each term group counts at most once). */
|
|
175
|
+
function countSecurityTerms(textLower) {
|
|
176
|
+
const h = makeHaystack(textLower);
|
|
177
|
+
let count = 0;
|
|
178
|
+
for (const group of SECURITY_TERM_GROUPS) {
|
|
179
|
+
if (group.some((term) => includesToken(h, term)))
|
|
180
|
+
count++;
|
|
181
|
+
}
|
|
182
|
+
return count;
|
|
183
|
+
}
|
|
184
|
+
function specificityScore(rawPrompt, stackCount) {
|
|
185
|
+
const collapsed = rawPrompt.replace(/\s+/g, " ").trim();
|
|
186
|
+
const words = collapsed.length === 0 ? [] : collapsed.split(" ");
|
|
187
|
+
const lower = collapsed.toLowerCase();
|
|
188
|
+
let score = Math.min(4, stackCount * 2);
|
|
189
|
+
// Concrete nouns: file paths / extensions and code identifiers.
|
|
190
|
+
let pathTokens = 0;
|
|
191
|
+
let codeTokens = 0;
|
|
192
|
+
for (const w of words) {
|
|
193
|
+
const cleaned = w.replace(/[,;:!?)]+$/, "");
|
|
194
|
+
if (cleaned.includes("/") && cleaned.length > 3)
|
|
195
|
+
pathTokens++;
|
|
196
|
+
else if (/\.[a-z]{2,4}$/i.test(cleaned))
|
|
197
|
+
pathTokens++;
|
|
198
|
+
else if (/[a-z][A-Z]/.test(cleaned) || cleaned.includes("(") || cleaned.includes("`"))
|
|
199
|
+
codeTokens++;
|
|
200
|
+
}
|
|
201
|
+
score += Math.min(2, pathTokens) + Math.min(2, codeTokens);
|
|
202
|
+
// Length tiers reward elaborated asks.
|
|
203
|
+
if (words.length >= 25)
|
|
204
|
+
score += 2;
|
|
205
|
+
else if (words.length >= 12)
|
|
206
|
+
score += 1;
|
|
207
|
+
// Vagueness markers subtract.
|
|
208
|
+
let vagueHits = 0;
|
|
209
|
+
for (const marker of VAGUE_MARKERS) {
|
|
210
|
+
if (includesToken(lower, marker))
|
|
211
|
+
vagueHits++;
|
|
212
|
+
}
|
|
213
|
+
score -= Math.min(2, vagueHits) * 2;
|
|
214
|
+
return score;
|
|
215
|
+
}
|
|
216
|
+
function triage(rawPrompt, stack, surfaces) {
|
|
217
|
+
const collapsed = rawPrompt.replace(/\s+/g, " ").trim();
|
|
218
|
+
if (collapsed.length === 0) {
|
|
219
|
+
return { verdict: "NO_MOD", reason: "Empty prompt — nothing to analyze; proceed as-is.", securityTermCount: 0 };
|
|
220
|
+
}
|
|
221
|
+
const lower = collapsed.toLowerCase();
|
|
222
|
+
const securityTermCount = countSecurityTerms(lower);
|
|
223
|
+
const securityRelevant = surfaces.length > 0 || securityTermCount > 0;
|
|
224
|
+
if (!securityRelevant) {
|
|
225
|
+
return { verdict: "NO_MOD", reason: "No security-sensitive surface detected — injecting security requirements would be noise; proceed as-is.", securityTermCount };
|
|
226
|
+
}
|
|
227
|
+
const wordCount = collapsed.split(" ").length;
|
|
228
|
+
const specific = wordCount >= TRIAGE_CONFIG.minWords
|
|
229
|
+
&& specificityScore(rawPrompt, stack.length) >= TRIAGE_CONFIG.specificityThreshold;
|
|
230
|
+
const securityAware = securityTermCount >= TRIAGE_CONFIG.securityAwareTerms;
|
|
231
|
+
if (specific && securityAware) {
|
|
232
|
+
return {
|
|
233
|
+
verdict: "NO_MOD",
|
|
234
|
+
reason: `Prompt is already specific and security-aware (${securityTermCount} security terms, concrete stack/detail) — modification would risk altering intent.`,
|
|
235
|
+
securityTermCount,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
if (specific) {
|
|
239
|
+
return {
|
|
240
|
+
verdict: "LIGHT_MOD",
|
|
241
|
+
reason: "Intent is clear and specific but explicit security constraints are missing — inject requirements only, do not restructure.",
|
|
242
|
+
securityTermCount,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
return {
|
|
246
|
+
verdict: "HEAVY_MOD",
|
|
247
|
+
reason: "Prompt is vague/underspecified and touches security-sensitive surfaces — inject requirements and surface clarifying questions.",
|
|
248
|
+
securityTermCount,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
/** Rank rules against the detected stack + surfaces; severity first, cap at maxRequirements. */
|
|
252
|
+
export function matchRulesForPrompt(stack, surfaces, rules) {
|
|
253
|
+
const techDefs = TECHS.filter((t) => stack.includes(t.id));
|
|
254
|
+
const surfaceDefs = SURFACES.filter((s) => surfaces.includes(s.id));
|
|
255
|
+
if (techDefs.length === 0 && surfaceDefs.length === 0)
|
|
256
|
+
return [];
|
|
257
|
+
const scored = [];
|
|
258
|
+
for (const rule of rules) {
|
|
259
|
+
// Only code-level rules become prompt-level requirements. This drops version-pin
|
|
260
|
+
// advisories and config/manifest rules (languages json/yaml only — you can't
|
|
261
|
+
// satisfy "upgrade package X" by writing code) while keeping behavioral js/ts/
|
|
262
|
+
// python/go rules even when they cite a CVE in their name (e.g. Drizzle sql.raw
|
|
263
|
+
// injection, Axios redirect leak, Hono SSE injection).
|
|
264
|
+
if (!rule.languages.some((l) => CODE_LANGUAGES.includes(l)))
|
|
265
|
+
continue;
|
|
266
|
+
const text = `${rule.name} ${rule.description}`.toLowerCase();
|
|
267
|
+
let score = 0;
|
|
268
|
+
for (const t of techDefs) {
|
|
269
|
+
if (t.ruleKeywords.some((k) => text.includes(k)))
|
|
270
|
+
score += 2;
|
|
271
|
+
}
|
|
272
|
+
for (const s of surfaceDefs) {
|
|
273
|
+
if (s.ruleKeywords.some((k) => text.includes(k)))
|
|
274
|
+
score += 1;
|
|
275
|
+
}
|
|
276
|
+
if (score > 0)
|
|
277
|
+
scored.push({ rule, score });
|
|
278
|
+
}
|
|
279
|
+
scored.sort((a, b) => (SEVERITY_ORDER[a.rule.severity] ?? 99) - (SEVERITY_ORDER[b.rule.severity] ?? 99)
|
|
280
|
+
|| b.score - a.score
|
|
281
|
+
|| a.rule.id.localeCompare(b.rule.id));
|
|
282
|
+
// Dedupe near-identical guidance (e.g. three "use parameterized queries" rules)
|
|
283
|
+
// so the capped list spends its slots on diverse requirements.
|
|
284
|
+
const seen = new Set();
|
|
285
|
+
const requirements = [];
|
|
286
|
+
for (const { rule } of scored) {
|
|
287
|
+
if (requirements.length >= TRIAGE_CONFIG.maxRequirements)
|
|
288
|
+
break;
|
|
289
|
+
const requirement = firstSentence(rule.fix);
|
|
290
|
+
// Key on the instruction itself, ignoring an attached code example (": db.query(...)").
|
|
291
|
+
const key = requirement.split(":")[0].toLowerCase().replace(/[^a-z0-9 ]/g, "").replace(/ +/g, " ").trim();
|
|
292
|
+
if (seen.has(key))
|
|
293
|
+
continue;
|
|
294
|
+
seen.add(key);
|
|
295
|
+
requirements.push({ ruleId: rule.id, title: rule.name, requirement, severity: rule.severity });
|
|
296
|
+
}
|
|
297
|
+
return requirements;
|
|
298
|
+
}
|
|
299
|
+
/** Common abbreviations whose trailing "." is not a sentence boundary. */
|
|
300
|
+
const ABBREVIATIONS = ["e.g", "i.e", "etc", "vs", "cf", "approx", "no", "fig", "al"];
|
|
301
|
+
function firstSentence(text) {
|
|
302
|
+
const collapsed = text.replace(/\s+/g, " ").trim();
|
|
303
|
+
// Find the first real sentence boundary, skipping ellipses ("...") and abbreviations
|
|
304
|
+
// ("e.g. ", "etc. ") so an example mid-fix doesn't truncate the actionable instruction.
|
|
305
|
+
let idx = collapsed.indexOf(". ");
|
|
306
|
+
while (idx > 0) {
|
|
307
|
+
const isEllipsis = collapsed[idx - 1] === ".";
|
|
308
|
+
const before = collapsed.slice(0, idx).toLowerCase();
|
|
309
|
+
const isAbbrev = ABBREVIATIONS.some((a) => before.endsWith(a) && !isWordChar(before[before.length - a.length - 1] ?? ""));
|
|
310
|
+
if (!isEllipsis && !isAbbrev)
|
|
311
|
+
break;
|
|
312
|
+
idx = collapsed.indexOf(". ", idx + 1);
|
|
313
|
+
}
|
|
314
|
+
if (idx === -1)
|
|
315
|
+
return collapsed;
|
|
316
|
+
const sentence = collapsed.slice(0, idx + 1);
|
|
317
|
+
// Never cut inside an unclosed inline code span.
|
|
318
|
+
const backticks = sentence.split("`").length - 1;
|
|
319
|
+
return backticks % 2 === 0 ? sentence : collapsed;
|
|
320
|
+
}
|
|
321
|
+
function buildAmbiguities(stack, surfaces) {
|
|
322
|
+
const questions = [];
|
|
323
|
+
if (stack.length === 0) {
|
|
324
|
+
questions.push("Which framework/stack is this for (e.g. Next.js, Express, Hono)? Only generic security rules could be matched without it.");
|
|
325
|
+
}
|
|
326
|
+
for (const surface of SURFACES) {
|
|
327
|
+
if (!surfaces.includes(surface.id) || !surface.question)
|
|
328
|
+
continue;
|
|
329
|
+
const answered = surface.answeredByTechs?.some((t) => stack.includes(t)) ?? false;
|
|
330
|
+
if (!answered)
|
|
331
|
+
questions.push(surface.question);
|
|
332
|
+
}
|
|
333
|
+
if (questions.length === 0) {
|
|
334
|
+
questions.push("The request is broad — which routes/files are in scope, and what does a successful result look like?");
|
|
335
|
+
}
|
|
336
|
+
return questions.slice(0, TRIAGE_CONFIG.maxAmbiguities);
|
|
337
|
+
}
|
|
338
|
+
function buildIntentSummary(rawPrompt) {
|
|
339
|
+
const collapsed = rawPrompt.replace(/\s+/g, " ").trim();
|
|
340
|
+
const clipped = collapsed.length > 220 ? `${collapsed.slice(0, 217)}...` : collapsed;
|
|
341
|
+
return `The user wants to: ${clipped}`;
|
|
342
|
+
}
|
|
343
|
+
/** Pick a code fence longer than any backtick run in the prompt so it embeds verbatim. */
|
|
344
|
+
function fenceFor(text) {
|
|
345
|
+
let longest = 0;
|
|
346
|
+
let run = 0;
|
|
347
|
+
for (const ch of text) {
|
|
348
|
+
run = ch === "`" ? run + 1 : 0;
|
|
349
|
+
if (run > longest)
|
|
350
|
+
longest = run;
|
|
351
|
+
}
|
|
352
|
+
return "`".repeat(Math.max(3, longest + 1));
|
|
353
|
+
}
|
|
354
|
+
const REWRITE_DIRECTIVE = "Rewrite the user's prompt incorporating the security requirements above. " +
|
|
355
|
+
"Do NOT add features the user did not request. Do NOT change the user's intent. " +
|
|
356
|
+
"If verdict is NO_MOD, use the original prompt as-is.";
|
|
357
|
+
const NO_MOD_DIRECTIVE = "Verdict is NO_MOD: use the ORIGINAL prompt below as-is. " +
|
|
358
|
+
"Do NOT rewrite, augment, or reinterpret it. " +
|
|
359
|
+
"Do NOT add features the user did not request. Do NOT change the user's intent.";
|
|
360
|
+
function surfaceLabel(id) {
|
|
361
|
+
return SURFACES.find((s) => s.id === id)?.label ?? id;
|
|
362
|
+
}
|
|
363
|
+
function techLabel(id) {
|
|
364
|
+
return TECHS.find((t) => t.id === id)?.label ?? id;
|
|
365
|
+
}
|
|
366
|
+
function buildMarkdown(result) {
|
|
367
|
+
const fence = fenceFor(result.originalPrompt);
|
|
368
|
+
const lines = [
|
|
369
|
+
"## GuardVibe secure_prompt directive (guardvibe.secure_prompt.v1)",
|
|
370
|
+
"",
|
|
371
|
+
`- **verdict:** ${result.verdict}`,
|
|
372
|
+
`- **reason:** ${result.reason}`,
|
|
373
|
+
];
|
|
374
|
+
if (result.verdict === "NO_MOD") {
|
|
375
|
+
lines.push("", "### rewrite_directive", NO_MOD_DIRECTIVE, "", "### original_prompt", fence + "text", result.originalPrompt, fence);
|
|
376
|
+
return lines.join("\n");
|
|
377
|
+
}
|
|
378
|
+
lines.push("", "### intent_summary (HARD CONSTRAINT — preserve this intent exactly)", `${result.intentSummary}`, "The rewritten prompt MUST preserve this intent exactly: no added features, no scope changes.");
|
|
379
|
+
if (result.detectedStack.length > 0 || result.detectedSurfaces.length > 0) {
|
|
380
|
+
lines.push("", "### detected_context");
|
|
381
|
+
if (result.detectedStack.length > 0) {
|
|
382
|
+
lines.push(`- **stack:** ${result.detectedStack.map(techLabel).join(", ")}`);
|
|
383
|
+
}
|
|
384
|
+
if (result.detectedSurfaces.length > 0) {
|
|
385
|
+
lines.push(`- **attack surfaces:** ${result.detectedSurfaces.map(surfaceLabel).join(", ")}`);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
lines.push("", "### security_requirements");
|
|
389
|
+
if (result.securityRequirements.length === 0) {
|
|
390
|
+
lines.push("_No specific GuardVibe rules matched the detected stack/surfaces — apply standard input validation and authentication practices._");
|
|
391
|
+
}
|
|
392
|
+
else {
|
|
393
|
+
result.securityRequirements.forEach((req, i) => {
|
|
394
|
+
lines.push(`${i + 1}. [${req.ruleId}] (${req.severity}) ${req.title} — ${req.requirement}`);
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
if (result.verdict === "HEAVY_MOD" && result.ambiguities.length > 0) {
|
|
398
|
+
lines.push("", "### ambiguities (ask the user — do NOT invent answers)");
|
|
399
|
+
result.ambiguities.forEach((q, i) => lines.push(`${i + 1}. ${q}`));
|
|
400
|
+
}
|
|
401
|
+
lines.push("", "### rewrite_directive", REWRITE_DIRECTIVE, "", "### original_prompt", fence + "text", result.originalPrompt, fence);
|
|
402
|
+
return lines.join("\n");
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Analyze a raw coding prompt BEFORE code generation and return a structured
|
|
406
|
+
* enhancement directive. Fully deterministic: same prompt = same directive.
|
|
407
|
+
*/
|
|
408
|
+
export function securePrompt(rawPrompt, opts) {
|
|
409
|
+
const effectiveRules = opts?.rules && opts.rules.length > 0 ? opts.rules : builtinRules;
|
|
410
|
+
// Full known stack (prompt + context) — informs display and answers "which provider"
|
|
411
|
+
// clarifying questions. promptStack (prompt only) drives triage and rule selection so
|
|
412
|
+
// that always-attached project context can never escalate a non-security prompt or
|
|
413
|
+
// manufacture off-topic requirements (the "do no harm" guarantee).
|
|
414
|
+
const detectedStack = detectPromptStack(rawPrompt, opts?.context);
|
|
415
|
+
const promptStack = detectPromptStack(rawPrompt);
|
|
416
|
+
const detectedSurfaces = detectPromptSurfaces(rawPrompt);
|
|
417
|
+
const { verdict, reason } = triage(rawPrompt, promptStack, detectedSurfaces);
|
|
418
|
+
// NO_MOD short-circuits: original prompt untouched, no requirements computed.
|
|
419
|
+
const securityRequirements = verdict === "NO_MOD"
|
|
420
|
+
? []
|
|
421
|
+
: matchRulesForPrompt(promptStack, detectedSurfaces, effectiveRules);
|
|
422
|
+
const ambiguities = verdict === "HEAVY_MOD" ? buildAmbiguities(detectedStack, detectedSurfaces) : [];
|
|
423
|
+
const base = {
|
|
424
|
+
verdict,
|
|
425
|
+
reason,
|
|
426
|
+
intentSummary: buildIntentSummary(rawPrompt),
|
|
427
|
+
detectedStack,
|
|
428
|
+
detectedSurfaces,
|
|
429
|
+
securityRequirements,
|
|
430
|
+
ambiguities,
|
|
431
|
+
originalPrompt: rawPrompt,
|
|
432
|
+
};
|
|
433
|
+
return { ...base, markdown: buildMarkdown(base) };
|
|
434
|
+
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "guardvibe",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.20.0",
|
|
4
4
|
"mcpName": "io.github.goklab/guardvibe",
|
|
5
|
-
"description": "Security infrastructure your AI can't be — deterministic, current past your model's training cutoff, whole-repo-aware, author-independent. Security MCP for vibe coding.
|
|
5
|
+
"description": "Security infrastructure your AI can't be — deterministic, current past your model's training cutoff, whole-repo-aware, author-independent. Security MCP for vibe coding. 445 rules, 38 tools, CLI + doctor. Prompt-level shift-left security (secure_prompt — embed security requirements BEFORE code generation), host security, auth coverage mapping, LLM-powered deep scan (IDOR/business logic), taint analysis. 74 CVE rules refreshed daily from GHSA/OSV/CISA KEV — js-cookie cookie-attribute injection, PostCSS </style> stringify XSS, Axios proxy prototype-pollution gadget, Vite dev-server RCE, React Router 7 cluster, DOMPurify XSS, Better Auth bypass, Miasma @redhat-cloud-services compromise, Next.js May 2026 13-advisory cluster, Drizzle/MikroORM/Kysely SQL injection, Axios proxy-auth redirect leak, Hono setCookie attribute injection, Clerk SSRF, tRPC prototype pollution, @tanstack supply-chain, node-ipc protestware, OpenClaude sandbox bypass, plus the full AI-generated stack (Supabase, Stripe, Prisma, Hono, GraphQL, Convex, Turso, Uploadthing, AI SDK). 68 AI-native rules including OWASP MCP Top 10 tool-description prompt injection (VG1068), model-controlled sandbox-disable flag detection (VG1063), Session messenger exfil endpoint IOC (VG1075), and CI/CD supply-chain hardening (VG1070 npm --expect-provenance / --ignore-scripts enforcement).",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
8
8
|
"guardvibe": "build/cli.js",
|
|
@@ -119,7 +119,7 @@
|
|
|
119
119
|
"@types/node": "^25.5.2",
|
|
120
120
|
"c8": "^11.0.0",
|
|
121
121
|
"eslint": "^10.2.0",
|
|
122
|
-
"tsx": "^4.
|
|
122
|
+
"tsx": "^4.22.4",
|
|
123
123
|
"typescript-eslint": "^8.58.0"
|
|
124
124
|
},
|
|
125
125
|
"engines": {
|