guardvibe 3.18.0 → 3.19.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 +12 -0
- package/README.md +34 -3
- 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 +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,18 @@ 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.19.0] - 2026-06-10
|
|
9
|
+
|
|
10
|
+
### Added — secure_prompt: prompt-level security, shift left (442 rules / 37 → 38 tools)
|
|
11
|
+
- **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.
|
|
12
|
+
- **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.
|
|
13
|
+
- **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).
|
|
14
|
+
- **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).
|
|
15
|
+
- 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).
|
|
16
|
+
- 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.
|
|
17
|
+
|
|
18
|
+
Gate green (build / lint / test / self-audit PASS / A / 0).
|
|
19
|
+
|
|
8
20
|
## [3.18.0] - 2026-06-09
|
|
9
21
|
|
|
10
22
|
### Added — FAZ 3 part c: AST BOLA mutation-guard detection for VG951 (442 rules / 37 tools)
|
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
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
16
|
|
|
17
|
-
**The security MCP built for vibe coding.** 442 security rules,
|
|
17
|
+
**The security MCP built for vibe coding.** 442 security rules, 38 tools covering the entire AI-generated code journey — from the prompt itself to production deployment.
|
|
18
18
|
|
|
19
19
|
Works with **Claude Code, Cursor, Gemini CLI, Codex, VS Code (Copilot), Windsurf**, and any MCP-compatible coding agent.
|
|
20
20
|
|
|
@@ -26,7 +26,7 @@ Works with **Claude Code, Cursor, Gemini CLI, Codex, VS Code (Copilot), Windsurf
|
|
|
26
26
|
|
|
27
27
|
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
28
|
|
|
29
|
-
- **442 security rules,
|
|
29
|
+
- **442 security rules, 38 tools** purpose-built for the stacks AI agents generate
|
|
30
30
|
- **Zero setup friction** — `npx guardvibe` and you're scanning
|
|
31
31
|
- **No account required** — runs 100% locally, no API keys, no cloud
|
|
32
32
|
- **Understands your stack** — not generic SAST, but rules that know Next.js, Supabase, Stripe, Clerk, and the tools you actually use
|
|
@@ -212,7 +212,37 @@ Maps security findings to SOC2, PCI-DSS, HIPAA, GDPR, ISO27001, and EU AI Act (E
|
|
|
212
212
|
### Supply Chain
|
|
213
213
|
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
214
|
|
|
215
|
-
##
|
|
215
|
+
## Prompt-Level Security (Shift Left)
|
|
216
|
+
|
|
217
|
+
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.
|
|
218
|
+
|
|
219
|
+
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.
|
|
220
|
+
|
|
221
|
+
- **`NO_MOD`** — prompt is already specific and security-aware → proceed with the original prompt unchanged
|
|
222
|
+
- **`LIGHT_MOD`** — intent is clear but security constraints are missing → inject requirements only
|
|
223
|
+
- **`HEAVY_MOD`** — prompt is vague *and* security-relevant → inject requirements + surface clarifying questions (never invent the answers)
|
|
224
|
+
|
|
225
|
+
**Before** (what the user typed):
|
|
226
|
+
|
|
227
|
+
```text
|
|
228
|
+
add login to my app
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
**After** (what the host LLM executes, having applied the `secure_prompt` directive):
|
|
232
|
+
|
|
233
|
+
```text
|
|
234
|
+
Add login to my app, with these security requirements:
|
|
235
|
+
- [VG001] Use environment variables or a secrets manager — never hardcode credentials.
|
|
236
|
+
- [VG1008] Always verify the caller has admin privileges before allowing role elevation.
|
|
237
|
+
- [VG105] Always specify allowed algorithms explicitly in jwt.verify().
|
|
238
|
+
|
|
239
|
+
Before implementing, confirm: which framework/stack is this for, and which auth
|
|
240
|
+
provider should be used (e.g. Clerk, Auth.js/NextAuth, Supabase Auth, custom JWT)?
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
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.
|
|
244
|
+
|
|
245
|
+
## Tools (38 MCP tools)
|
|
216
246
|
|
|
217
247
|
| Tool | What it does |
|
|
218
248
|
|------|-------------|
|
|
@@ -253,6 +283,7 @@ Malicious postinstall scripts, unpinned GitHub Actions, CI `npm` provenance / `-
|
|
|
253
283
|
| `full_audit` | **Single source of truth** — runs ALL checks in one call, returns PASS/FAIL/WARN verdict + score + coverage % + deterministic result hash |
|
|
254
284
|
| `remediation_plan` | **Remediation plan** — generates section-by-section fix checklist after audit |
|
|
255
285
|
| `verify_remediation` | **Remediation verification** — compares before/after audit, flags skipped sections |
|
|
286
|
+
| `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
287
|
|
|
257
288
|
All scanning tools support `format: "json"` for machine-readable output.
|
|
258
289
|
|
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.19.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. 442 rules,
|
|
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. 442 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. 71 CVE rules refreshed daily from GHSA/OSV/CISA KEV — 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",
|