guardvibe 3.0.16 → 3.0.18
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.
|
@@ -152,11 +152,22 @@ function runChecks(files, root) {
|
|
|
152
152
|
const apiRoutes = files.routeHandlers
|
|
153
153
|
.map(r => r.path.replace(resolve(root), "").replace(/\\/g, "/"))
|
|
154
154
|
.filter(p => p.includes("/api/"));
|
|
155
|
+
// Check which routes are NOT covered by middleware matcher
|
|
156
|
+
// But exclude routes that have in-handler auth (requireAdmin, requireAuth, etc.)
|
|
157
|
+
const authGuardPattern = /requireAdmin|requireAuth|checkAuth|withAuth|getServerSession|auth\(\)|clerkClient|currentUser/;
|
|
155
158
|
const unprotectedApiRoutes = apiRoutes.filter(route => {
|
|
156
|
-
|
|
159
|
+
// Check if middleware matcher covers this route
|
|
160
|
+
const coveredByMatcher = matcherPaths.some(pattern => {
|
|
157
161
|
const normalized = pattern.replace(/:path\*/, "").replace(/\(.*?\)/, "");
|
|
158
162
|
return route.startsWith(normalized) || route.includes(normalized);
|
|
159
163
|
});
|
|
164
|
+
if (coveredByMatcher)
|
|
165
|
+
return false;
|
|
166
|
+
// Check if the route handler has in-handler auth guard
|
|
167
|
+
const handler = files.routeHandlers.find(r => r.path.replace(resolve(root), "").replace(/\\/g, "/") === route);
|
|
168
|
+
if (handler && authGuardPattern.test(handler.content))
|
|
169
|
+
return false;
|
|
170
|
+
return true;
|
|
160
171
|
});
|
|
161
172
|
if (unprotectedApiRoutes.length > 0) {
|
|
162
173
|
issues.push({
|
|
@@ -193,11 +204,23 @@ function runChecks(files, root) {
|
|
|
193
204
|
});
|
|
194
205
|
}
|
|
195
206
|
// Check for secrets in .env files that are also in NEXT_PUBLIC_
|
|
207
|
+
// Known safe public keys that are DESIGNED to be in client bundles
|
|
208
|
+
const knownPublicKeys = new Set([
|
|
209
|
+
"NEXT_PUBLIC_SUPABASE_ANON_KEY",
|
|
210
|
+
"NEXT_PUBLIC_SUPABASE_URL",
|
|
211
|
+
"NEXT_PUBLIC_TURNSTILE_SITE_KEY",
|
|
212
|
+
"NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY",
|
|
213
|
+
"NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY",
|
|
214
|
+
"NEXT_PUBLIC_RECAPTCHA_SITE_KEY",
|
|
215
|
+
"NEXT_PUBLIC_GA_MEASUREMENT_ID",
|
|
216
|
+
"NEXT_PUBLIC_SITE_URL",
|
|
217
|
+
"NEXT_PUBLIC_APP_URL",
|
|
218
|
+
]);
|
|
196
219
|
for (const envFile of files.envFiles) {
|
|
197
220
|
const lines = envFile.content.split("\n");
|
|
198
221
|
for (const line of lines) {
|
|
199
222
|
const match = /^(NEXT_PUBLIC_\w*(?:SECRET|KEY|PASSWORD|TOKEN|PRIVATE|CREDENTIAL)\w*)\s*=/.exec(line);
|
|
200
|
-
if (match && !/PUBLISHABLE/i.test(match[1])) {
|
|
223
|
+
if (match && !/PUBLISHABLE/i.test(match[1]) && !knownPublicKeys.has(match[1])) {
|
|
201
224
|
issues.push({
|
|
202
225
|
id: "AC021", severity: "critical", category: "secrets",
|
|
203
226
|
title: `NEXT_PUBLIC_ exposes secret: ${match[1]}`,
|
|
@@ -280,6 +280,18 @@ const SINK_PATTERNS = [
|
|
|
280
280
|
{ pattern: /writeFileSync?\s*\(/g, type: "path-traversal" },
|
|
281
281
|
{ pattern: /readFileSync?\s*\(/g, type: "path-traversal" },
|
|
282
282
|
];
|
|
283
|
+
// Patterns that break the taint chain (validation/sanitization)
|
|
284
|
+
const SANITIZER_PATTERNS = [
|
|
285
|
+
/validate\w*\s*\(/i,
|
|
286
|
+
/sanitize\w*\s*\(/i,
|
|
287
|
+
/safeParse\s*\(/i,
|
|
288
|
+
/parseBody\s*\(/i,
|
|
289
|
+
/DOMPurify/i,
|
|
290
|
+
/encodeURIComponent\s*\(/i,
|
|
291
|
+
/\.hostname\s*!==?\s*/i,
|
|
292
|
+
/\.origin\s*!==?\s*/i,
|
|
293
|
+
/allowlist|whitelist|allowedHosts/i,
|
|
294
|
+
];
|
|
283
295
|
function checkParamFlowsToSink(paramName, body, startLine) {
|
|
284
296
|
const lines = body.split("\n");
|
|
285
297
|
const taintedNames = new Set([paramName]);
|
|
@@ -289,11 +301,20 @@ function checkParamFlowsToSink(paramName, body, startLine) {
|
|
|
289
301
|
if (m) {
|
|
290
302
|
for (const t of taintedNames) {
|
|
291
303
|
if (m[2].includes(t)) {
|
|
292
|
-
|
|
304
|
+
const isSanitized = SANITIZER_PATTERNS.some(p => p.test(m[2]));
|
|
305
|
+
if (!isSanitized) {
|
|
306
|
+
taintedNames.add(m[1]);
|
|
307
|
+
}
|
|
293
308
|
break;
|
|
294
309
|
}
|
|
295
310
|
}
|
|
296
311
|
}
|
|
312
|
+
// Break taint if value passes through validation
|
|
313
|
+
for (const t of taintedNames) {
|
|
314
|
+
if (line.includes(t) && SANITIZER_PATTERNS.some(p => p.test(line))) {
|
|
315
|
+
taintedNames.delete(t);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
297
318
|
}
|
|
298
319
|
for (let i = 0; i < lines.length; i++) {
|
|
299
320
|
const line = lines[i];
|
|
@@ -169,8 +169,14 @@ export async function runFullAudit(path, options) {
|
|
|
169
169
|
const secretsJson = scanSecrets(projectRoot, true, "json");
|
|
170
170
|
const parsed = safeJsonParse(secretsJson);
|
|
171
171
|
if (parsed) {
|
|
172
|
-
|
|
173
|
-
const
|
|
172
|
+
// Filter out gitignored secrets — they're local dev files, not a security risk
|
|
173
|
+
const rawFindings = parsed.findings ?? [];
|
|
174
|
+
const actionableFindings = rawFindings.filter((f) => f.gitStatus !== "ignored");
|
|
175
|
+
const ignoredCount = rawFindings.length - actionableFindings.length;
|
|
176
|
+
const actionableCritical = actionableFindings.filter((f) => f.severity === "critical").length;
|
|
177
|
+
const actionableHigh = actionableFindings.filter((f) => f.severity === "high").length;
|
|
178
|
+
const actionableMedium = actionableFindings.length - actionableCritical - actionableHigh;
|
|
179
|
+
const secretFindings = actionableFindings.map((f) => ({
|
|
174
180
|
ruleId: `SECRET:${(f.provider ?? "unknown")}`,
|
|
175
181
|
severity: (f.severity ?? "high"),
|
|
176
182
|
file: (f.file ?? ""),
|
|
@@ -179,8 +185,15 @@ export async function runFullAudit(path, options) {
|
|
|
179
185
|
description: (f.match ?? f.description ?? ""),
|
|
180
186
|
fix: "Move this secret to an environment variable and ensure the file is in .gitignore",
|
|
181
187
|
}));
|
|
182
|
-
|
|
183
|
-
|
|
188
|
+
const detailText = actionableFindings.length === 0
|
|
189
|
+
? (ignoredCount > 0 ? `${ignoredCount} secret(s) in .gitignore (safe)` : "No secrets found")
|
|
190
|
+
: `${actionableFindings.length} secret(s) detected${ignoredCount > 0 ? ` (${ignoredCount} in .gitignore excluded)` : ""}`;
|
|
191
|
+
sections.push({
|
|
192
|
+
name: "secrets", status: "ok",
|
|
193
|
+
findings: actionableFindings.length, critical: actionableCritical, high: actionableHigh, medium: actionableMedium,
|
|
194
|
+
details: detailText, sectionFindings: secretFindings,
|
|
195
|
+
});
|
|
196
|
+
for (const f of actionableFindings) {
|
|
184
197
|
allFindings.push({ ruleId: `SECRET:${f.provider ?? "unknown"}`, severity: f.severity, file: f.file ?? "", line: f.line ?? 0 });
|
|
185
198
|
}
|
|
186
199
|
}
|
|
@@ -381,60 +394,54 @@ function buildInlineRemediationPlan(result) {
|
|
|
381
394
|
priority: 1,
|
|
382
395
|
tool: "scan_secrets",
|
|
383
396
|
actions: [
|
|
384
|
-
"
|
|
385
|
-
"For
|
|
386
|
-
"
|
|
387
|
-
"MCP: Call scan_secrets_history — OR CLI: run `npx guardvibe scan --format json` to verify",
|
|
388
|
-
"Re-run the scan and confirm secret count dropped to 0. If it didn't, your fix didn't work — try again",
|
|
397
|
+
"Look at sectionFindings above — each has file, line, description",
|
|
398
|
+
"For each: move hardcoded secret to .env, add file to .gitignore",
|
|
399
|
+
"Verify: run `npx guardvibe audit --format json 2>&1 | head -1` — secrets count must be 0",
|
|
389
400
|
],
|
|
390
401
|
},
|
|
391
402
|
code: {
|
|
392
403
|
priority: 2,
|
|
393
404
|
tool: "scan_directory",
|
|
394
405
|
actions: [
|
|
395
|
-
"
|
|
396
|
-
"
|
|
397
|
-
"
|
|
398
|
-
"Re-run full scan to confirm total code findings dropped",
|
|
406
|
+
"Look at sectionFindings above — each has ruleId, file, line, fix",
|
|
407
|
+
"Read each file, apply the fix. Run `npx guardvibe explain <RULE_ID>` if unclear",
|
|
408
|
+
"Verify each file: `npx guardvibe check <file> --format json` — must show 0 findings",
|
|
399
409
|
],
|
|
400
410
|
},
|
|
401
411
|
dependencies: {
|
|
402
412
|
priority: 3,
|
|
403
413
|
tool: "scan_dependencies",
|
|
404
414
|
actions: [
|
|
405
|
-
"
|
|
406
|
-
"Run `npm
|
|
407
|
-
"
|
|
408
|
-
"Re-run `npx guardvibe audit` and confirm dependency findings dropped to 0",
|
|
415
|
+
"Look at sectionFindings above — each has package name and CVE",
|
|
416
|
+
"Run `npm update <package>` for each. If already latest, check for alternative package",
|
|
417
|
+
"Verify: re-run audit — dependencies count must drop",
|
|
409
418
|
],
|
|
410
419
|
},
|
|
411
420
|
config: {
|
|
412
421
|
priority: 4,
|
|
413
422
|
tool: "audit_config",
|
|
414
423
|
actions: [
|
|
415
|
-
"
|
|
416
|
-
"
|
|
417
|
-
"
|
|
418
|
-
"Re-run audit and confirm config findings dropped",
|
|
424
|
+
"Look at sectionFindings above — each has ruleId, file, title, fix",
|
|
425
|
+
"Run `npx guardvibe explain <RULE_ID>` for each finding to get exact fix code",
|
|
426
|
+
"Apply fixes to the listed files. Verify: re-run audit — config count must drop",
|
|
419
427
|
],
|
|
420
428
|
},
|
|
421
429
|
taint: {
|
|
422
430
|
priority: 5,
|
|
423
431
|
tool: "analyze_cross_file_dataflow",
|
|
424
432
|
actions: [
|
|
425
|
-
"
|
|
426
|
-
"
|
|
427
|
-
"
|
|
433
|
+
"Look at sectionFindings above — each shows source file:line → sink file:line",
|
|
434
|
+
"Add input validation (Zod) at source, or sanitization at sink",
|
|
435
|
+
"Verify: re-run audit — taint count must be 0",
|
|
428
436
|
],
|
|
429
437
|
},
|
|
430
438
|
"auth-coverage": {
|
|
431
439
|
priority: 6,
|
|
432
440
|
tool: "auth_coverage",
|
|
433
441
|
actions: [
|
|
434
|
-
"
|
|
435
|
-
"
|
|
436
|
-
"
|
|
437
|
-
"Re-run `npx guardvibe auth-coverage --format json` and confirm unprotected count matches your authExceptions count",
|
|
442
|
+
"Look at sectionFindings above — each shows unprotected route path and file",
|
|
443
|
+
"Add auth guard, or add to .guardviberc authExceptions: {\"path\": \"/route\", \"reason\": \"Public\"}",
|
|
444
|
+
"Verify: `npx guardvibe auth-coverage --format json` — unprotected must be 0",
|
|
438
445
|
],
|
|
439
446
|
},
|
|
440
447
|
};
|
|
@@ -471,14 +478,11 @@ export function formatAuditResult(result, format) {
|
|
|
471
478
|
...result,
|
|
472
479
|
remediationPlan: {
|
|
473
480
|
totalSectionsWithFindings: plan.length,
|
|
474
|
-
warning: `
|
|
481
|
+
warning: `FIX ALL ${plan.length} SECTIONS. Each section has sectionFindings with exact file, line, and fix. Process every finding. After ALL sections: re-run audit to verify.`,
|
|
475
482
|
rules: [
|
|
476
|
-
"
|
|
477
|
-
"
|
|
478
|
-
"
|
|
479
|
-
"Do NOT skip auth-coverage by saying routes are 'intentionally public' — either add auth guards or add them to .guardviberc authExceptions so the scanner recognizes them.",
|
|
480
|
-
"After fixing each section, re-run the section-specific tool to confirm the finding count dropped. If it didn't drop, your fix didn't work.",
|
|
481
|
-
"Only call verify_remediation AFTER you have worked through ALL sections and confirmed each one improved.",
|
|
483
|
+
"NEVER use 'npx guardvibe diff' — use 'npx guardvibe audit --format json 2>&1 | head -1'",
|
|
484
|
+
"Fix EVERY finding in sectionFindings — not just one section",
|
|
485
|
+
"After fixing, re-run audit and verify finding count dropped",
|
|
482
486
|
],
|
|
483
487
|
steps: plan,
|
|
484
488
|
},
|
|
@@ -554,14 +558,7 @@ export function formatAuditResult(result, format) {
|
|
|
554
558
|
lines.push(``);
|
|
555
559
|
lines.push(`## Mandatory Remediation Plan`);
|
|
556
560
|
lines.push(``);
|
|
557
|
-
lines.push(`> **
|
|
558
|
-
lines.push(``);
|
|
559
|
-
lines.push(`**Rules:**`);
|
|
560
|
-
lines.push(`- Do NOT dismiss findings as "false positive" without running the section-specific tool first`);
|
|
561
|
-
lines.push(`- Do NOT say "npm audit is clean" to skip dependencies — GuardVibe uses OSV which may detect CVEs npm misses`);
|
|
562
|
-
lines.push(`- Do NOT say "already in .gitignore" to skip secrets — if detected, they exist in scanned files`);
|
|
563
|
-
lines.push(`- After fixing each section, re-run the section tool to confirm finding count dropped`);
|
|
564
|
-
lines.push(`- Only call verify_remediation AFTER all sections show improvement`);
|
|
561
|
+
lines.push(`> **FIX ALL ${plan.length} SECTIONS. Each section's sectionFindings has exact file, line, and fix. Process every finding. Re-run audit after ALL sections to verify.**`);
|
|
565
562
|
lines.push(``);
|
|
566
563
|
for (const step of plan) {
|
|
567
564
|
lines.push(`### Step ${step.priority}: ${step.section} (${step.findings} findings — ${step.critical} critical, ${step.high} high)`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "guardvibe",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.18",
|
|
4
4
|
"mcpName": "io.github.goklab/guardvibe",
|
|
5
5
|
"description": "Security MCP for vibe coding. 335 rules, 36 tools, CLI + doctor. Host security, auth coverage mapping, LLM-powered deep scan (IDOR/business logic), taint analysis. 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",
|