guardvibe 3.0.56 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/build/cli/deep-scan.d.ts +1 -0
- package/build/cli/deep-scan.js +79 -0
- package/build/cli.js +5 -0
- package/build/index.js +9 -5
- package/build/tools/deep-scan.d.ts +13 -2
- package/build/tools/deep-scan.js +50 -9
- package/build/tools/fix-code.js +89 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@ Most security tools are built for enterprise security teams. GuardVibe is built
|
|
|
20
20
|
- **Understands your stack** — not generic SAST, but rules that know Next.js, Supabase, Stripe, Clerk, and the tools you actually use
|
|
21
21
|
- **CVE version intelligence** — detects 23 known vulnerable package versions in package.json
|
|
22
22
|
- **AI agent security** — detects MCP server vulnerabilities, excessive AI permissions, indirect prompt injection
|
|
23
|
-
- **Auto-fix suggestions** — `fix_code` tool returns concrete patches the AI agent can apply
|
|
23
|
+
- **Auto-fix suggestions** — `fix_code` tool returns concrete patches and structured edits the AI agent can apply mechanically. Coverage: hardcoded credentials → env-var migration; public-prefix LLM keys (`NEXT_PUBLIC_/VITE_/EXPO_PUBLIC_/REACT_APP_`) → prefix removal; CORS wildcards → env allowlist; `dangerouslyAllowBrowser` flags → drop; sandbox bypass flags (`unsafe`/`noSandbox`/`allowEval`) → drop; agent loops → add `maxSteps`; raw-HTML React props → `<ReactMarkdown>`; missing auth checks → insert auth guard; SQL injection → parameterized queries; missing rate limiters / CSRF / security headers → snippet templates.
|
|
24
24
|
- **Pre-commit hook** — block insecure code before it reaches your repo
|
|
25
25
|
- **CI/CD ready** — GitHub Actions workflow with SARIF upload to Security tab
|
|
26
26
|
- **Agent-friendly output** — JSON format for AI agents, Markdown for humans, SARIF for CI/CD
|
|
@@ -228,7 +228,7 @@ Malicious postinstall scripts, unpinned GitHub Actions, typosquat detection
|
|
|
228
228
|
| `verify_fix` | Verify a security fix was applied correctly — returns fixed/still_vulnerable/new_issues |
|
|
229
229
|
| `security_workflow` | Get recommended tool workflow for your current task (writing, pre-commit, PR review, etc.) |
|
|
230
230
|
| `auth_coverage` | **Auth coverage map** — enumerate routes, parse middleware matchers, detect auth guards, report coverage % |
|
|
231
|
-
| `deep_scan` | **LLM-powered deep analysis** — IDOR, business logic, race conditions,
|
|
231
|
+
| `deep_scan` | **LLM-powered deep analysis** — IDOR, business logic, race conditions, auth bypass. Defaults to Claude Haiku 4.5 (~cents/scan). Pass `model: 'sonnet'` for deeper analysis. CLI: `npx guardvibe deep-scan <file> --focus idor` |
|
|
232
232
|
| `full_audit` | **Single source of truth** — runs ALL checks in one call, returns PASS/FAIL/WARN verdict + score + coverage % + deterministic result hash |
|
|
233
233
|
| `remediation_plan` | **Remediation plan** — generates section-by-section fix checklist after audit |
|
|
234
234
|
| `verify_remediation` | **Remediation verification** — compares before/after audit, flags skipped sections |
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runDeepScan(args: string[]): Promise<void>;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI: guardvibe deep-scan <file>
|
|
3
|
+
* LLM-powered deep security analysis.
|
|
4
|
+
*/
|
|
5
|
+
import { readFileSync, statSync } from "node:fs";
|
|
6
|
+
import { resolve, extname } from "node:path";
|
|
7
|
+
import { parseArgs } from "./args.js";
|
|
8
|
+
import { buildDeepScanPrompt, callLLM, parseDeepScanResult, formatDeepScanFindings, DEFAULT_MAX_BYTES, } from "../tools/deep-scan.js";
|
|
9
|
+
const EXT_TO_LANG = {
|
|
10
|
+
".ts": "typescript", ".tsx": "typescript", ".mts": "typescript", ".cts": "typescript",
|
|
11
|
+
".js": "javascript", ".jsx": "javascript", ".mjs": "javascript", ".cjs": "javascript",
|
|
12
|
+
".py": "python", ".go": "go", ".rb": "ruby", ".java": "java",
|
|
13
|
+
".rs": "rust", ".php": "php", ".cs": "csharp",
|
|
14
|
+
};
|
|
15
|
+
const VALID_FOCUS = ["all", "idor", "business-logic", "auth-bypass", "race-condition"];
|
|
16
|
+
export async function runDeepScan(args) {
|
|
17
|
+
const { flags, positional } = parseArgs(args);
|
|
18
|
+
const file = positional[0];
|
|
19
|
+
if (!file) {
|
|
20
|
+
console.error(" [ERR] Please specify a file: npx guardvibe deep-scan <file>");
|
|
21
|
+
console.error("");
|
|
22
|
+
console.error(" Options:");
|
|
23
|
+
console.error(" --focus <area> all (default) | idor | business-logic | auth-bypass | race-condition");
|
|
24
|
+
console.error(" --model <model> haiku (default, ~cents/scan) | sonnet (deeper, more expensive)");
|
|
25
|
+
console.error(" --max-bytes <n> Truncate input to N bytes (default 10000)");
|
|
26
|
+
console.error(" --format <type> markdown (default) | json");
|
|
27
|
+
console.error("");
|
|
28
|
+
console.error(" Requires ANTHROPIC_API_KEY (or OPENAI_API_KEY) environment variable.");
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
const path = resolve(file);
|
|
32
|
+
let content;
|
|
33
|
+
try {
|
|
34
|
+
const stat = statSync(path);
|
|
35
|
+
if (!stat.isFile()) {
|
|
36
|
+
console.error(` [ERR] Not a file: ${path}`);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
content = readFileSync(path, "utf-8");
|
|
40
|
+
}
|
|
41
|
+
catch (e) {
|
|
42
|
+
console.error(` [ERR] Cannot read file: ${path}`);
|
|
43
|
+
console.error(` ${e.message}`);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
if (!process.env.ANTHROPIC_API_KEY && !process.env.OPENAI_API_KEY) {
|
|
47
|
+
console.error(" [ERR] No LLM API key. Set ANTHROPIC_API_KEY or OPENAI_API_KEY in your environment.");
|
|
48
|
+
console.error(" Default model is Claude Haiku 4.5 — typically ~cents per scan.");
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
const focusArg = flags.focus ?? "all";
|
|
52
|
+
if (!VALID_FOCUS.includes(focusArg)) {
|
|
53
|
+
console.error(` [ERR] Invalid --focus: ${focusArg}. Use one of: ${VALID_FOCUS.join(", ")}`);
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
const focus = focusArg;
|
|
57
|
+
const modelArg = flags.model ?? "haiku";
|
|
58
|
+
if (modelArg !== "haiku" && modelArg !== "sonnet") {
|
|
59
|
+
console.error(` [ERR] Invalid --model: ${modelArg}. Use haiku or sonnet.`);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
const model = modelArg;
|
|
63
|
+
const maxBytes = flags["max-bytes"] != null ? Number(flags["max-bytes"]) : DEFAULT_MAX_BYTES;
|
|
64
|
+
if (!Number.isFinite(maxBytes) || maxBytes < 500 || maxBytes > 50_000) {
|
|
65
|
+
console.error(` [ERR] --max-bytes must be 500..50000 (got ${flags["max-bytes"]})`);
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
const format = (flags.format === "json" ? "json" : "markdown");
|
|
69
|
+
const language = EXT_TO_LANG[extname(path).toLowerCase()] ?? "unknown";
|
|
70
|
+
const prompt = buildDeepScanPrompt(content, language, [], focus);
|
|
71
|
+
const llmResponse = await callLLM(prompt, { model, maxBytes });
|
|
72
|
+
if (llmResponse === null) {
|
|
73
|
+
console.error(" [ERR] LLM call failed — check API key validity and network.");
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
const findings = parseDeepScanResult(llmResponse);
|
|
77
|
+
const output = formatDeepScanFindings(findings, format);
|
|
78
|
+
console.log(output);
|
|
79
|
+
}
|
package/build/cli.js
CHANGED
|
@@ -28,6 +28,7 @@ function printUsage() {
|
|
|
28
28
|
npx guardvibe check-cmd "<cmd>" Check if a shell command is safe to execute
|
|
29
29
|
npx guardvibe auth-coverage [path] Auth coverage analysis (Next.js routes)
|
|
30
30
|
npx guardvibe compliance [path] Compliance report (--framework SOC2|GDPR|...)
|
|
31
|
+
npx guardvibe deep-scan <file> LLM-powered deep scan (IDOR, business logic, race conditions)
|
|
31
32
|
npx guardvibe init <platform> Setup MCP server configuration
|
|
32
33
|
npx guardvibe hook install Install pre-commit security hook
|
|
33
34
|
npx guardvibe hook uninstall Remove pre-commit security hook
|
|
@@ -152,6 +153,10 @@ async function main() {
|
|
|
152
153
|
const { runCompliance } = await import("./cli/compliance.js");
|
|
153
154
|
await runCompliance(subArgs);
|
|
154
155
|
}
|
|
156
|
+
else if (command === "deep-scan") {
|
|
157
|
+
const { runDeepScan } = await import("./cli/deep-scan.js");
|
|
158
|
+
await runDeepScan(subArgs);
|
|
159
|
+
}
|
|
155
160
|
else {
|
|
156
161
|
console.error(` Unknown command: ${command}`);
|
|
157
162
|
printUsage();
|
package/build/index.js
CHANGED
|
@@ -863,20 +863,24 @@ server.tool("auth_coverage", "Analyze authentication coverage across Next.js App
|
|
|
863
863
|
return { content: [{ type: "text", text: output }] };
|
|
864
864
|
});
|
|
865
865
|
// Tool 32: LLM-powered deep scan
|
|
866
|
-
server.tool("deep_scan", "LLM-powered deep security analysis for vulnerabilities that pattern-matching cannot detect: IDOR, business logic flaws, race conditions, stale auth, mass assignment, privilege escalation.
|
|
866
|
+
server.tool("deep_scan", "LLM-powered deep security analysis for vulnerabilities that pattern-matching cannot detect: IDOR, business logic flaws, race conditions, stale auth, mass assignment, privilege escalation. Defaults to Claude Haiku 4.5 (~cents per scan); pass `model: 'sonnet'` for deeper analysis at higher cost. Requires ANTHROPIC_API_KEY or OPENAI_API_KEY env var.", {
|
|
867
867
|
code: z.string().describe("Code to analyze"),
|
|
868
868
|
language: z.string().describe("Programming language"),
|
|
869
869
|
context: z.string().optional().describe("Additional context (e.g., 'This is a payment endpoint')"),
|
|
870
870
|
existingFindings: z.array(z.string()).default([]).describe("Already-detected findings to avoid duplicating"),
|
|
871
|
+
focus: z.enum(["all", "idor", "business-logic", "auth-bypass", "race-condition"]).default("all").describe("Focus area — narrows the prompt to a specific vulnerability class"),
|
|
872
|
+
model: z.enum(["haiku", "sonnet"]).default("haiku").describe("LLM model. haiku = fast & cheap (default), sonnet = deeper analysis"),
|
|
873
|
+
maxBytes: z.number().int().min(500).max(50_000).default(10_000).describe("Max prompt size in bytes — caps cost. Code over this limit is truncated."),
|
|
871
874
|
format: z.enum(["markdown", "json"]).default("markdown").describe("Output format"),
|
|
872
|
-
}, async ({ code, language, context, existingFindings, format }) => {
|
|
873
|
-
const prompt = buildDeepScanPrompt(code, language, existingFindings);
|
|
874
|
-
const
|
|
875
|
+
}, async ({ code, language, context, existingFindings, focus, model, maxBytes, format }) => {
|
|
876
|
+
const prompt = buildDeepScanPrompt(code, language, existingFindings, focus);
|
|
877
|
+
const fullPrompt = context ? `${prompt}\n\nAdditional context: ${context}` : prompt;
|
|
878
|
+
const llmResponse = await callLLM(fullPrompt, { model, maxBytes });
|
|
875
879
|
if (llmResponse === null) {
|
|
876
880
|
return {
|
|
877
881
|
content: [{
|
|
878
882
|
type: "text",
|
|
879
|
-
text: "## Deep Scan — Setup Required\n\nNo LLM API key found. Set one of:\n- `ANTHROPIC_API_KEY` — uses Claude\n- `OPENAI_API_KEY` — uses GPT-4o\n\nThe deep scan sends code to the LLM API for semantic vulnerability analysis.",
|
|
883
|
+
text: "## Deep Scan — Setup Required\n\nNo LLM API key found. Set one of:\n- `ANTHROPIC_API_KEY` — uses Claude (default: Haiku 4.5; pass `model: 'sonnet'` for deeper analysis)\n- `OPENAI_API_KEY` — uses GPT-4o-mini / GPT-4o\n\nThe deep scan sends code to the LLM API for semantic vulnerability analysis. Default cost is a few cents per scan with Haiku.",
|
|
880
884
|
}],
|
|
881
885
|
};
|
|
882
886
|
}
|
|
@@ -12,10 +12,14 @@ export interface DeepScanFinding {
|
|
|
12
12
|
location: string;
|
|
13
13
|
fix: string;
|
|
14
14
|
}
|
|
15
|
+
export type DeepScanFocus = "all" | "idor" | "business-logic" | "auth-bypass" | "race-condition";
|
|
16
|
+
export type DeepScanModel = "haiku" | "sonnet";
|
|
17
|
+
export declare const MODEL_IDS: Record<DeepScanModel, string>;
|
|
18
|
+
export declare const DEFAULT_MAX_BYTES = 10000;
|
|
15
19
|
/**
|
|
16
20
|
* Build a structured prompt for the LLM to analyze code.
|
|
17
21
|
*/
|
|
18
|
-
export declare function buildDeepScanPrompt(code: string, language: string, existingFindings: string[]): string;
|
|
22
|
+
export declare function buildDeepScanPrompt(code: string, language: string, existingFindings: string[], focus?: DeepScanFocus): string;
|
|
19
23
|
/**
|
|
20
24
|
* Parse LLM response into structured findings.
|
|
21
25
|
* Handles raw JSON, JSON in markdown code blocks, and malformed responses.
|
|
@@ -25,9 +29,16 @@ export declare function parseDeepScanResult(response: string): DeepScanFinding[]
|
|
|
25
29
|
* Format deep scan findings as markdown or JSON.
|
|
26
30
|
*/
|
|
27
31
|
export declare function formatDeepScanFindings(findings: DeepScanFinding[], format: "markdown" | "json"): string;
|
|
32
|
+
export interface CallLLMOptions {
|
|
33
|
+
model?: DeepScanModel;
|
|
34
|
+
maxBytes?: number;
|
|
35
|
+
}
|
|
28
36
|
/**
|
|
29
37
|
* Call an LLM API for deep analysis. Uses native fetch.
|
|
30
38
|
* Supports Anthropic (ANTHROPIC_API_KEY) or OpenAI (OPENAI_API_KEY).
|
|
31
39
|
* Returns null if no API key is available.
|
|
40
|
+
*
|
|
41
|
+
* Defaults to Haiku 4.5 for cost; pass `model: "sonnet"` for higher-quality analysis.
|
|
42
|
+
* `maxBytes` truncates the prompt to keep cost bounded (default 10 KB).
|
|
32
43
|
*/
|
|
33
|
-
export declare function callLLM(prompt: string): Promise<string | null>;
|
|
44
|
+
export declare function callLLM(prompt: string, options?: CallLLMOptions): Promise<string | null>;
|
package/build/tools/deep-scan.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Uses native fetch — no extra dependencies.
|
|
7
7
|
*/
|
|
8
|
-
const
|
|
8
|
+
const ALL_AREAS = [
|
|
9
9
|
"IDOR (Insecure Direct Object Reference) — can users access resources belonging to other users?",
|
|
10
10
|
"Business logic flaws — are there authorization bypasses, price manipulation, or state machine violations?",
|
|
11
11
|
"Race conditions — are there TOCTOU issues, double-spend, or concurrent mutation without locking?",
|
|
@@ -13,18 +13,50 @@ const FOCUS_AREAS = [
|
|
|
13
13
|
"Mass assignment — can users set fields they shouldn't (role, isAdmin, price)?",
|
|
14
14
|
"Privilege escalation — can a regular user perform admin actions through parameter manipulation?",
|
|
15
15
|
];
|
|
16
|
+
const FOCUS_AREAS = {
|
|
17
|
+
all: ALL_AREAS,
|
|
18
|
+
idor: [
|
|
19
|
+
"IDOR (Insecure Direct Object Reference) — can users access resources belonging to other users?",
|
|
20
|
+
"Missing ownership scope on database queries (where: { id } instead of { id, userId })",
|
|
21
|
+
"URL/path parameters used directly as DB keys without authorization gate",
|
|
22
|
+
],
|
|
23
|
+
"business-logic": [
|
|
24
|
+
"Authorization bypass via parameter manipulation (e.g., role/isAdmin in body)",
|
|
25
|
+
"Price/amount manipulation or coupon stacking",
|
|
26
|
+
"State machine violations (skip steps, replay completed actions)",
|
|
27
|
+
"Idempotency / replay protection on payment / order paths",
|
|
28
|
+
],
|
|
29
|
+
"auth-bypass": [
|
|
30
|
+
"Missing or insufficient auth check before sensitive operations",
|
|
31
|
+
"Stale tokens / sessions still accepted after revoke",
|
|
32
|
+
"Cookie / JWT validation skipped on a subset of routes",
|
|
33
|
+
"Privilege-elevation through parameter manipulation",
|
|
34
|
+
],
|
|
35
|
+
"race-condition": [
|
|
36
|
+
"TOCTOU between read and write (check-then-act without locking)",
|
|
37
|
+
"Concurrent rate-limit increments without atomic ops",
|
|
38
|
+
"Double-spend / double-grant via parallel requests",
|
|
39
|
+
"Optimistic-update races on shared mutable state",
|
|
40
|
+
],
|
|
41
|
+
};
|
|
42
|
+
export const MODEL_IDS = {
|
|
43
|
+
haiku: "claude-haiku-4-5-20251001",
|
|
44
|
+
sonnet: "claude-sonnet-4-6",
|
|
45
|
+
};
|
|
46
|
+
export const DEFAULT_MAX_BYTES = 10_000;
|
|
16
47
|
/**
|
|
17
48
|
* Build a structured prompt for the LLM to analyze code.
|
|
18
49
|
*/
|
|
19
|
-
export function buildDeepScanPrompt(code, language, existingFindings) {
|
|
50
|
+
export function buildDeepScanPrompt(code, language, existingFindings, focus = "all") {
|
|
51
|
+
const areas = FOCUS_AREAS[focus] ?? FOCUS_AREAS.all;
|
|
20
52
|
const lines = [
|
|
21
53
|
"You are a senior application security engineer performing a deep code review.",
|
|
22
54
|
"Analyze the following code for security vulnerabilities that automated pattern-matching scanners miss.",
|
|
23
55
|
"",
|
|
24
|
-
|
|
56
|
+
`## Focus Areas (${focus})`,
|
|
25
57
|
"",
|
|
26
58
|
];
|
|
27
|
-
for (const area of
|
|
59
|
+
for (const area of areas) {
|
|
28
60
|
lines.push(`- ${area}`);
|
|
29
61
|
}
|
|
30
62
|
lines.push("");
|
|
@@ -123,11 +155,20 @@ export function formatDeepScanFindings(findings, format) {
|
|
|
123
155
|
* Call an LLM API for deep analysis. Uses native fetch.
|
|
124
156
|
* Supports Anthropic (ANTHROPIC_API_KEY) or OpenAI (OPENAI_API_KEY).
|
|
125
157
|
* Returns null if no API key is available.
|
|
158
|
+
*
|
|
159
|
+
* Defaults to Haiku 4.5 for cost; pass `model: "sonnet"` for higher-quality analysis.
|
|
160
|
+
* `maxBytes` truncates the prompt to keep cost bounded (default 10 KB).
|
|
126
161
|
*/
|
|
127
|
-
export async function callLLM(prompt) {
|
|
162
|
+
export async function callLLM(prompt, options = {}) {
|
|
128
163
|
// guardvibe-ignore — API URLs are hardcoded trusted endpoints, not user-controlled
|
|
129
164
|
const anthropicKey = process.env.ANTHROPIC_API_KEY;
|
|
130
165
|
const openaiKey = process.env.OPENAI_API_KEY;
|
|
166
|
+
const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
167
|
+
const model = options.model ?? "haiku";
|
|
168
|
+
// Truncate prompt to keep token budget bounded
|
|
169
|
+
const trimmedPrompt = prompt.length > maxBytes
|
|
170
|
+
? prompt.slice(0, maxBytes) + "\n\n[truncated by GuardVibe to stay within budget]"
|
|
171
|
+
: prompt;
|
|
131
172
|
if (anthropicKey) {
|
|
132
173
|
const res = await fetch("https://api.anthropic.com/v1/messages", {
|
|
133
174
|
method: "POST",
|
|
@@ -137,9 +178,9 @@ export async function callLLM(prompt) {
|
|
|
137
178
|
"anthropic-version": "2023-06-01",
|
|
138
179
|
},
|
|
139
180
|
body: JSON.stringify({
|
|
140
|
-
model:
|
|
181
|
+
model: MODEL_IDS[model],
|
|
141
182
|
max_tokens: 2048,
|
|
142
|
-
messages: [{ role: "user", content:
|
|
183
|
+
messages: [{ role: "user", content: trimmedPrompt }],
|
|
143
184
|
}),
|
|
144
185
|
});
|
|
145
186
|
if (!res.ok)
|
|
@@ -155,9 +196,9 @@ export async function callLLM(prompt) {
|
|
|
155
196
|
"Authorization": `Bearer ${openaiKey}`,
|
|
156
197
|
},
|
|
157
198
|
body: JSON.stringify({
|
|
158
|
-
model: "gpt-4o",
|
|
199
|
+
model: model === "sonnet" ? "gpt-4o" : "gpt-4o-mini",
|
|
159
200
|
max_tokens: 2048,
|
|
160
|
-
messages: [{ role: "user", content:
|
|
201
|
+
messages: [{ role: "user", content: trimmedPrompt }],
|
|
161
202
|
}),
|
|
162
203
|
});
|
|
163
204
|
if (!res.ok)
|
package/build/tools/fix-code.js
CHANGED
|
@@ -205,6 +205,54 @@ function generatePatch(finding, sourceLine) {
|
|
|
205
205
|
// --- Server data leaked to client ---
|
|
206
206
|
if (rule.id === "VG407")
|
|
207
207
|
return "// Keep sensitive data server-side:\nexport default async function Page() {\n const secret = process.env.SECRET;\n const safeData = transform(secret);\n return <Client data={safeData} />;\n}";
|
|
208
|
+
// --- VG014: dynamic-code-execution → safe parser ---
|
|
209
|
+
if (rule.id === "VG014") {
|
|
210
|
+
return "// Replace dynamic code execution with a safe parser:\n// Before: dynamic-code-execution(jsonString)\n// After: JSON.parse(jsonString)\n// Validate the parsed shape with Zod before use.";
|
|
211
|
+
}
|
|
212
|
+
// --- VG010 / VG123: SQL injection → parameterized query ---
|
|
213
|
+
if (["VG010", "VG123"].includes(rule.id)) {
|
|
214
|
+
return "// Use parameterized queries — never string-interpolate user input into SQL:\n// pg: db.query('SELECT * FROM users WHERE id = $1', [userId])\n// mysql: db.query('SELECT * FROM users WHERE id = ?', [userId])\n// Prisma: prisma.user.findUnique({ where: { id: userId } })\n// Knex: db('users').where({ id: userId }).first()";
|
|
215
|
+
}
|
|
216
|
+
// --- VG155: missing CSRF → middleware ---
|
|
217
|
+
if (rule.id === "VG155") {
|
|
218
|
+
return "// Next.js App Router: SameSite=Lax cookies + Content-Type=application/json triggers CORS preflight; Bearer-token auth is not browser-attached. If you accept browser-form posts, add csrf-csrf:\nimport { csrfSync } from \"csrf-csrf\";\nconst { csrfSynchronisedProtection } = csrfSync({ getSecret: () => process.env.CSRF_SECRET! });\nexport function middleware(req) { return csrfSynchronisedProtection(req); }\n\n// Express:\nimport csurf from \"csurf\";\napp.use(csurf({ cookie: true }));";
|
|
219
|
+
}
|
|
220
|
+
// --- VG144 / VG145: missing security headers ---
|
|
221
|
+
if (["VG144", "VG145"].includes(rule.id)) {
|
|
222
|
+
return "// next.config.ts → headers():\nasync headers() {\n return [{ source: \"/(.*)\", headers: [\n { key: \"Strict-Transport-Security\", value: \"max-age=63072000; includeSubDomains; preload\" },\n { key: \"X-Content-Type-Options\", value: \"nosniff\" },\n { key: \"X-Frame-Options\", value: \"DENY\" },\n { key: \"Referrer-Policy\", value: \"strict-origin-when-cross-origin\" },\n { key: \"Permissions-Policy\", value: \"camera=(), microphone=(), geolocation=()\" },\n ]}];\n}";
|
|
223
|
+
}
|
|
224
|
+
// --- VG030: missing global rate limiting ---
|
|
225
|
+
if (rule.id === "VG030") {
|
|
226
|
+
return "// Express:\nimport rateLimit from \"express-rate-limit\";\napp.use(rateLimit({ windowMs: 60_000, max: 100 }));\n\n// Next.js + Upstash (per-route):\nimport { Ratelimit } from \"@upstash/ratelimit\";\nconst rl = new Ratelimit({ redis, limiter: Ratelimit.slidingWindow(20, \"60s\") });\nconst { success } = await rl.limit(req.ip ?? \"anon\");\nif (!success) return new Response(\"Too many requests\", { status: 429 });";
|
|
227
|
+
}
|
|
228
|
+
// --- VG101: open redirect → allowlist ---
|
|
229
|
+
if (rule.id === "VG101") {
|
|
230
|
+
return "const ALLOWED = [\"example.com\", \"app.example.com\"];\nconst u = new URL(target, request.url);\nif (!ALLOWED.includes(u.hostname)) return redirect(\"/\");\nredirect(u.toString());";
|
|
231
|
+
}
|
|
232
|
+
// --- VG042: path traversal → path.resolve + boundary ---
|
|
233
|
+
if (rule.id === "VG042") {
|
|
234
|
+
return "import path from \"path\";\nconst BASE = path.resolve(\"/data/safe\");\nconst resolved = path.resolve(BASE, userPath);\nif (!resolved.startsWith(BASE + path.sep)) throw new Error(\"Path traversal blocked\");\nconst content = await fs.readFile(resolved, \"utf-8\");";
|
|
235
|
+
}
|
|
236
|
+
// --- VG060: bcrypt for password hashing ---
|
|
237
|
+
if (rule.id === "VG060") {
|
|
238
|
+
return "import bcrypt from \"bcrypt\";\nconst SALT_ROUNDS = 12;\nconst hash = await bcrypt.hash(plainPassword, SALT_ROUNDS);\n// Verify:\nconst ok = await bcrypt.compare(plainPassword, hash);";
|
|
239
|
+
}
|
|
240
|
+
// --- VG003: hardcoded JWT secret ---
|
|
241
|
+
if (rule.id === "VG003") {
|
|
242
|
+
return "// Move secret to environment:\n// Before: const SECRET = \"hardcoded-string\"\n// After: const SECRET = process.env.JWT_SECRET!\n// Then rotate the leaked secret in your provider/key vault before deploying.";
|
|
243
|
+
}
|
|
244
|
+
// --- VG1012: MCP @latest → pin ---
|
|
245
|
+
if (rule.id === "VG1012") {
|
|
246
|
+
return "// Re-run `npx guardvibe init` to write pinned versions, OR manually:\n// Before: \"args\": [\"-y\", \"some-mcp-server@latest\"]\n// After: \"args\": [\"-y\", \"some-mcp-server@1.4.2\"]";
|
|
247
|
+
}
|
|
248
|
+
// --- VG1033: agent loop without maxSteps ---
|
|
249
|
+
if (rule.id === "VG1033") {
|
|
250
|
+
return "// Always cap agent tool roundtrips:\nawait generateText({\n model,\n tools: { /* ... */ },\n maxSteps: 8,\n});";
|
|
251
|
+
}
|
|
252
|
+
// --- VG1036: code-exec sandbox bypass ---
|
|
253
|
+
if (rule.id === "VG1036") {
|
|
254
|
+
return "// Drop sandbox-bypass flags. If you need network/fs, allowlist specific endpoints:\nawait Sandbox.create({\n timeoutMs: 5_000,\n network: { allow: [\"api.example.com\"] },\n});";
|
|
255
|
+
}
|
|
208
256
|
// --- Fallback: use fixCode from rule ---
|
|
209
257
|
if (rule.fixCode) {
|
|
210
258
|
return `// Secure alternative:\n${rule.fixCode}`;
|
|
@@ -232,13 +280,13 @@ function generateStructuredEdit(finding, sourceLine, _lines) {
|
|
|
232
280
|
};
|
|
233
281
|
}
|
|
234
282
|
}
|
|
235
|
-
// --- NEXT_PUBLIC_ exposure → remove prefix ---
|
|
236
|
-
if (["VG411", "VG604", "VG627", "VG631", "VG655", "VG671", "VG676", "VG755"].includes(rule.id)) {
|
|
237
|
-
const m = /(NEXT_PUBLIC_)(\w+)
|
|
283
|
+
// --- NEXT_PUBLIC_ / VITE_ / EXPO_PUBLIC_ / REACT_APP_ exposure → remove prefix ---
|
|
284
|
+
if (["VG411", "VG604", "VG627", "VG631", "VG655", "VG671", "VG676", "VG755", "VG1028"].includes(rule.id)) {
|
|
285
|
+
const m = sourceLine.match(/(NEXT_PUBLIC_|VITE_|EXPO_PUBLIC_|REACT_APP_|GATSBY_|NUXT_PUBLIC_|PUBLIC_)([A-Z][\w]+)/);
|
|
238
286
|
if (m) {
|
|
239
287
|
return {
|
|
240
288
|
startLine: line, endLine: line,
|
|
241
|
-
oldText: sourceLine, newText: sourceLine.replace(
|
|
289
|
+
oldText: sourceLine, newText: sourceLine.replace(`${m[1]}${m[2]}`, m[2]),
|
|
242
290
|
};
|
|
243
291
|
}
|
|
244
292
|
}
|
|
@@ -280,6 +328,43 @@ function generateStructuredEdit(finding, sourceLine, _lines) {
|
|
|
280
328
|
imports: ['import { auth } from "@clerk/nextjs/server"'],
|
|
281
329
|
};
|
|
282
330
|
}
|
|
331
|
+
// --- Browser-mode AI SDK init flag → drop ---
|
|
332
|
+
if (["VG874", "VG998", "VG1023"].includes(rule.id)) {
|
|
333
|
+
const stripped = sourceLine
|
|
334
|
+
.replace(/,?\s*dangerouslyAllowBrowser\s*:\s*true\s*,?/, "")
|
|
335
|
+
.replace(/,?\s*browser\s*:\s*true\s*,?/, "");
|
|
336
|
+
if (stripped !== sourceLine) {
|
|
337
|
+
return { startLine: line, endLine: line, oldText: sourceLine, newText: stripped };
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
// --- VG1033: append maxSteps to single-line generateText/etc tool call ---
|
|
341
|
+
if (rule.id === "VG1033" && /(?:generateText|streamText|generate|streamObject|invoke|run)\s*\(\s*\{[^}]*\btools\s*:/.test(sourceLine) && /\}\s*\)\s*;?\s*$/.test(sourceLine)) {
|
|
342
|
+
const newText = sourceLine.replace(/(\})(\s*\)\s*;?\s*)$/, ", maxSteps: 8 $1$2");
|
|
343
|
+
if (newText !== sourceLine) {
|
|
344
|
+
return { startLine: line, endLine: line, oldText: sourceLine, newText };
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
// --- VG1036: drop sandbox bypass flags ---
|
|
348
|
+
if (rule.id === "VG1036") {
|
|
349
|
+
const stripped = sourceLine.replace(/,?\s*(?:unsafe|noSandbox|allowEval|allowAsync|privileged|allowAllNetwork)\s*:\s*true\s*,?/g, "");
|
|
350
|
+
if (stripped !== sourceLine) {
|
|
351
|
+
return { startLine: line, endLine: line, oldText: sourceLine, newText: stripped };
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
// --- VG1031: AI message via raw-HTML React prop → ReactMarkdown ---
|
|
355
|
+
if (rule.id === "VG1031") {
|
|
356
|
+
const RAW_HTML_PROP_RE = new RegExp("(<\\w+)\\s+" + "dangerously" + "SetInnerHTML\\s*=\\s*\\{\\{\\s*__html\\s*:\\s*([^}]+?)\\s*\\}\\}\\s*(\\/?)>");
|
|
357
|
+
const m = sourceLine.match(RAW_HTML_PROP_RE);
|
|
358
|
+
if (m) {
|
|
359
|
+
const inner = m[2].trim();
|
|
360
|
+
return {
|
|
361
|
+
startLine: line, endLine: line,
|
|
362
|
+
oldText: sourceLine,
|
|
363
|
+
newText: sourceLine.replace(m[0], `<ReactMarkdown>{${inner}}</ReactMarkdown>`),
|
|
364
|
+
imports: ['import ReactMarkdown from "react-markdown"'],
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
}
|
|
283
368
|
return undefined;
|
|
284
369
|
}
|
|
285
370
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "guardvibe",
|
|
3
|
-
"version": "3.0
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"mcpName": "io.github.goklab/guardvibe",
|
|
5
5
|
"description": "Security MCP for vibe coding. 390 rules, 36 tools, CLI + doctor. Host security, auth coverage mapping, LLM-powered deep scan (IDOR/business logic), taint analysis, +25 AI-native rules (MCP supply-chain, RAG/vector poisoning, agent loop DoS, public-prefix LLM keys, sandbox bypass). Plus Next.js, Supabase, Clerk, Stripe, Prisma, tRPC, Hono, GraphQL, Convex, Turso, Uploadthing, AI SDK, and the full AI-generated stack.",
|
|
6
6
|
"type": "module",
|