guardvibe 3.0.11 → 3.0.13
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/build/cli/auth-coverage.js +3 -1
- package/build/index.js +2 -1
- package/build/tools/auth-coverage.d.ts +4 -1
- package/build/tools/auth-coverage.js +17 -1
- package/build/tools/full-audit.js +39 -27
- package/build/utils/config.d.ts +7 -0
- package/build/utils/config.js +1 -0
- package/package.json +1 -1
|
@@ -6,6 +6,7 @@ import { readdirSync, readFileSync, statSync, writeFileSync, existsSync, mkdirSy
|
|
|
6
6
|
import { resolve, dirname } from "path";
|
|
7
7
|
import { parseArgs, validateFormat, getOutputPath } from "./args.js";
|
|
8
8
|
import { analyzeAuthCoverage, formatAuthCoverage } from "../tools/auth-coverage.js";
|
|
9
|
+
import { loadConfig } from "../utils/config.js";
|
|
9
10
|
export async function runAuthCoverage(args) {
|
|
10
11
|
const { flags, positional } = parseArgs(args);
|
|
11
12
|
const targetPath = resolve(positional[0] ?? ".");
|
|
@@ -57,7 +58,8 @@ export async function runAuthCoverage(args) {
|
|
|
57
58
|
const routeFiles = jsFiles.filter(f => /\/(route|page)\.(ts|tsx|js|jsx)$/.test(f.path));
|
|
58
59
|
const layoutFiles = jsFiles.filter(f => /\/layout\.(ts|tsx|js|jsx)$/.test(f.path));
|
|
59
60
|
const middlewareFile = jsFiles.find(f => /middleware\.(ts|js)$/.test(f.path));
|
|
60
|
-
const
|
|
61
|
+
const config = loadConfig(targetPath);
|
|
62
|
+
const report = analyzeAuthCoverage(routeFiles, middlewareFile?.content ?? "", layoutFiles, config.authExceptions);
|
|
61
63
|
const formatArg = format === "json" ? "json" : "markdown";
|
|
62
64
|
const result = formatAuthCoverage(report, formatArg);
|
|
63
65
|
if (outputFile) {
|
package/build/index.js
CHANGED
|
@@ -852,7 +852,8 @@ server.tool("auth_coverage", "Analyze authentication coverage across Next.js App
|
|
|
852
852
|
const routeFiles = jsFiles.filter(f => /\/(route|page)\.(ts|tsx|js|jsx)$/.test(f.path));
|
|
853
853
|
const layoutFiles = jsFiles.filter(f => /\/layout\.(ts|tsx|js|jsx)$/.test(f.path));
|
|
854
854
|
const middlewareFile = jsFiles.find(f => /middleware\.(ts|js)$/.test(f.path));
|
|
855
|
-
const
|
|
855
|
+
const cfg = loadConfig(path);
|
|
856
|
+
const report = analyzeAuthCoverage(routeFiles, middlewareFile?.content ?? "", layoutFiles, cfg.authExceptions);
|
|
856
857
|
const output = formatAuthCoverage(report, format);
|
|
857
858
|
return { content: [{ type: "text", text: output }] };
|
|
858
859
|
}
|
|
@@ -39,7 +39,10 @@ export interface AuthCoverageReport {
|
|
|
39
39
|
/**
|
|
40
40
|
* Analyze auth coverage across all route files.
|
|
41
41
|
*/
|
|
42
|
-
export declare function analyzeAuthCoverage(routeFiles: FileEntry[], middlewareContent: string, layoutFiles?: FileEntry[]
|
|
42
|
+
export declare function analyzeAuthCoverage(routeFiles: FileEntry[], middlewareContent: string, layoutFiles?: FileEntry[], authExceptions?: Array<{
|
|
43
|
+
path: string;
|
|
44
|
+
reason: string;
|
|
45
|
+
}>): AuthCoverageReport;
|
|
43
46
|
/**
|
|
44
47
|
* Format auth coverage report as markdown or JSON.
|
|
45
48
|
*/
|
|
@@ -143,7 +143,7 @@ function hasAuthGuard(code) {
|
|
|
143
143
|
/**
|
|
144
144
|
* Analyze auth coverage across all route files.
|
|
145
145
|
*/
|
|
146
|
-
export function analyzeAuthCoverage(routeFiles, middlewareContent, layoutFiles) {
|
|
146
|
+
export function analyzeAuthCoverage(routeFiles, middlewareContent, layoutFiles, authExceptions) {
|
|
147
147
|
const routes = enumerateRoutes(routeFiles);
|
|
148
148
|
const matchers = parseMiddlewareMatchers(middlewareContent);
|
|
149
149
|
const hasMiddleware = middlewareContent.length > 0;
|
|
@@ -204,6 +204,22 @@ export function analyzeAuthCoverage(routeFiles, middlewareContent, layoutFiles)
|
|
|
204
204
|
}
|
|
205
205
|
}
|
|
206
206
|
}
|
|
207
|
+
// Apply authExceptions from .guardviberc — mark excepted routes as protected
|
|
208
|
+
if (authExceptions && authExceptions.length > 0) {
|
|
209
|
+
for (const route of routes) {
|
|
210
|
+
if (route.hasAuthGuard || route.middlewareCovered)
|
|
211
|
+
continue;
|
|
212
|
+
const isExcepted = authExceptions.some(exc => {
|
|
213
|
+
const excPath = exc.path.replace(/\[[\w]+\]/g, "[^/]+");
|
|
214
|
+
const regex = new RegExp("^" + excPath.replace(/\//g, "\\/") + "$");
|
|
215
|
+
return regex.test(route.urlPath) || route.urlPath === exc.path || route.urlPath.startsWith(exc.path + "/");
|
|
216
|
+
});
|
|
217
|
+
if (isExcepted) {
|
|
218
|
+
route.hasAuthGuard = true;
|
|
219
|
+
route.protectionSource = "auth-guard"; // treated as intentionally public
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
207
223
|
const protectedRoutes = routes.filter(r => r.hasAuthGuard || r.middlewareCovered).length;
|
|
208
224
|
const unprotectedList = routes.filter(r => !r.hasAuthGuard && !r.middlewareCovered);
|
|
209
225
|
return {
|
|
@@ -16,6 +16,7 @@ import { auditConfig } from "./audit-config.js";
|
|
|
16
16
|
import { analyzeCrossFileTaint } from "./cross-file-taint.js";
|
|
17
17
|
import { analyzeAuthCoverage } from "./auth-coverage.js";
|
|
18
18
|
import { getRules } from "../utils/rule-registry.js";
|
|
19
|
+
import { loadConfig } from "../utils/config.js";
|
|
19
20
|
// --- Core Logic ---
|
|
20
21
|
/**
|
|
21
22
|
* Compute verdict: PASS (0 critical + 0 high), WARN (high > 0), FAIL (critical > 0)
|
|
@@ -138,8 +139,9 @@ export async function runFullAudit(path, options) {
|
|
|
138
139
|
filesScanned = parsed.metadata?.filesScanned ?? 0;
|
|
139
140
|
filesSkipped = parsed.metadata?.filesSkipped ?? 0;
|
|
140
141
|
score = parsed.summary?.score ?? 100;
|
|
141
|
-
|
|
142
|
-
|
|
142
|
+
const codeGrade = parsed.summary?.grade ?? "A";
|
|
143
|
+
const codeScore = parsed.summary?.score ?? 100;
|
|
144
|
+
sections.push({ name: "code", status: "ok", ...counts, details: `Code ${codeGrade} (${codeScore}/100)` });
|
|
143
145
|
for (const f of parsed.findings ?? []) {
|
|
144
146
|
allFindings.push({ ruleId: f.id ?? "unknown", severity: f.severity, file: f.file ?? "", line: f.line ?? 0 });
|
|
145
147
|
}
|
|
@@ -239,7 +241,8 @@ export async function runFullAudit(path, options) {
|
|
|
239
241
|
const layoutFiles = jsFiles.filter(f => /\/layout\.(ts|tsx|js|jsx)$/.test(f.path));
|
|
240
242
|
if (routeFiles.length > 0) {
|
|
241
243
|
const middlewareFile = jsFiles.find(f => /middleware\.(ts|js)$/.test(f.path));
|
|
242
|
-
const
|
|
244
|
+
const config = loadConfig(projectRoot);
|
|
245
|
+
const report = analyzeAuthCoverage(routeFiles, middlewareFile?.content ?? "", layoutFiles, config.authExceptions);
|
|
243
246
|
const unprotected = report.unprotectedRoutes;
|
|
244
247
|
sections.push({ name: "auth-coverage", status: "ok", findings: unprotected, critical: 0, high: unprotected > 0 ? unprotected : 0, medium: 0,
|
|
245
248
|
details: `${report.protectedRoutes}/${report.totalRoutes} routes protected (${report.middlewareCoveragePercent}% middleware)` });
|
|
@@ -252,6 +255,15 @@ export async function runFullAudit(path, options) {
|
|
|
252
255
|
const totalMedium = sections.reduce((s, sec) => s + sec.medium, 0);
|
|
253
256
|
const totalFindings = sections.reduce((s, sec) => s + sec.findings, 0);
|
|
254
257
|
const rulesApplied = rules.length > 0 ? rules.length : 335;
|
|
258
|
+
// Adjust score to reflect ALL sections, not just code
|
|
259
|
+
// Each critical finding deducts 5 points, high deducts 3, medium deducts 1
|
|
260
|
+
// Score from code scan is the baseline, other sections reduce it further
|
|
261
|
+
const nonCodeCritical = totalCritical - (sections.find(s => s.name === "code")?.critical ?? 0);
|
|
262
|
+
const nonCodeHigh = totalHigh - (sections.find(s => s.name === "code")?.high ?? 0);
|
|
263
|
+
const nonCodeMedium = totalMedium - (sections.find(s => s.name === "code")?.medium ?? 0);
|
|
264
|
+
const deduction = (nonCodeCritical * 5) + (nonCodeHigh * 3) + (nonCodeMedium * 1);
|
|
265
|
+
score = Math.max(0, score - deduction);
|
|
266
|
+
grade = score >= 90 ? "A" : score >= 75 ? "B" : score >= 50 ? "C" : score >= 25 ? "D" : "F";
|
|
255
267
|
const verdict = computeVerdict(totalCritical, totalHigh, totalMedium);
|
|
256
268
|
const coverage = computeCoverage(filesScanned, filesSkipped, rulesApplied);
|
|
257
269
|
const resultHash = computeResultHash(allFindings);
|
|
@@ -297,60 +309,60 @@ function buildInlineRemediationPlan(result) {
|
|
|
297
309
|
priority: 1,
|
|
298
310
|
tool: "scan_secrets",
|
|
299
311
|
actions: [
|
|
300
|
-
"Call scan_secrets with format: json
|
|
301
|
-
"For EACH secret: move to environment variable,
|
|
302
|
-
"Rotate any API keys/tokens that were committed —
|
|
303
|
-
"Call scan_secrets_history
|
|
304
|
-
"Re-run
|
|
312
|
+
"MCP: Call scan_secrets with format: json — OR CLI: run `npx guardvibe scan --format json` and look for SECRET: findings in the output",
|
|
313
|
+
"For EACH secret found: (a) if it's in a .env file, ensure .env* is in .gitignore, (b) if it's hardcoded in source code, move it to an environment variable, (c) NEVER dismiss secrets as 'already in .gitignore' — if GuardVibe found it, the FILE exists and contains real values",
|
|
314
|
+
"Rotate any API keys/tokens that were committed to git history — run `git log --all -p -S 'YOUR_KEY_PREFIX' -- '*.env*'` to check",
|
|
315
|
+
"MCP: Call scan_secrets_history — OR CLI: run `npx guardvibe scan --format json` to verify",
|
|
316
|
+
"Re-run the scan and confirm secret count dropped to 0. If it didn't, your fix didn't work — try again",
|
|
305
317
|
],
|
|
306
318
|
},
|
|
307
319
|
code: {
|
|
308
320
|
priority: 2,
|
|
309
321
|
tool: "scan_directory",
|
|
310
322
|
actions: [
|
|
311
|
-
"Call scan_directory with format: json to get full finding list
|
|
312
|
-
"Fix ALL critical and high severity findings
|
|
313
|
-
"Call
|
|
314
|
-
"Re-run
|
|
323
|
+
"MCP: Call scan_directory with format: json — OR CLI: run `npx guardvibe scan --format json` to get full finding list",
|
|
324
|
+
"Fix ALL critical and high severity findings. Read each flagged file, understand the vulnerability, apply the fix suggested in the finding",
|
|
325
|
+
"After fixing each file, re-scan it: MCP: Call scan_file — OR CLI: run `npx guardvibe check <file> --format json` to confirm the finding is gone",
|
|
326
|
+
"Re-run full scan to confirm total code findings dropped",
|
|
315
327
|
],
|
|
316
328
|
},
|
|
317
329
|
dependencies: {
|
|
318
330
|
priority: 3,
|
|
319
331
|
tool: "scan_dependencies",
|
|
320
332
|
actions: [
|
|
321
|
-
"Call scan_dependencies with format: json
|
|
322
|
-
"Run npm audit fix
|
|
323
|
-
"If a package
|
|
324
|
-
"Re-run
|
|
333
|
+
"MCP: Call scan_dependencies with format: json — OR CLI: run `npx guardvibe scan --format json` and check dependency findings, also run `npm audit`",
|
|
334
|
+
"Run `npm audit fix` to auto-fix what's possible. If that doesn't work, run `npm update <package>` for each vulnerable package",
|
|
335
|
+
"If a package can't be updated (breaking changes), find an alternative or pin to a patched version",
|
|
336
|
+
"Re-run `npx guardvibe audit` and confirm dependency findings dropped to 0",
|
|
325
337
|
],
|
|
326
338
|
},
|
|
327
339
|
config: {
|
|
328
340
|
priority: 4,
|
|
329
341
|
tool: "audit_config",
|
|
330
342
|
actions: [
|
|
331
|
-
"Call audit_config with format: json
|
|
332
|
-
"
|
|
333
|
-
"
|
|
334
|
-
"Re-run
|
|
343
|
+
"MCP: Call audit_config with format: json — OR CLI: run `npx guardvibe audit --format json` and parse the config section details",
|
|
344
|
+
"Common config fixes: add missing security headers in next.config.ts (CSP, HSTS, X-Frame-Options, Referrer-Policy, Permissions-Policy), set poweredByHeader: false, configure CORS properly",
|
|
345
|
+
"MCP: Call explain_remediation for each rule ID — OR CLI: run `npx guardvibe explain <RULE_ID>` to get specific fix guidance",
|
|
346
|
+
"Re-run audit and confirm config findings dropped",
|
|
335
347
|
],
|
|
336
348
|
},
|
|
337
349
|
taint: {
|
|
338
350
|
priority: 5,
|
|
339
351
|
tool: "analyze_cross_file_dataflow",
|
|
340
352
|
actions: [
|
|
341
|
-
"Call analyze_cross_file_dataflow
|
|
342
|
-
"
|
|
343
|
-
"Re-run
|
|
353
|
+
"MCP: Call analyze_cross_file_dataflow — OR CLI: run `npx guardvibe audit --format json` and parse the taint section. Look for user input (URL params, form data, req.body) flowing to dangerous sinks (SQL, HTML, file system)",
|
|
354
|
+
"Fix each tainted flow: add Zod/joi validation at the input source, use parameterized queries for SQL, use sanitizeUrl/DOMPurify for HTML output, validate file paths",
|
|
355
|
+
"Re-run audit and confirm taint findings dropped to 0",
|
|
344
356
|
],
|
|
345
357
|
},
|
|
346
358
|
"auth-coverage": {
|
|
347
359
|
priority: 6,
|
|
348
360
|
tool: "auth_coverage",
|
|
349
361
|
actions: [
|
|
350
|
-
"Call auth_coverage with format: json to list all unprotected routes",
|
|
351
|
-
"
|
|
352
|
-
"
|
|
353
|
-
"Re-run
|
|
362
|
+
"MCP: Call auth_coverage with format: json — OR CLI: run `npx guardvibe auth-coverage --format json` to list all unprotected routes",
|
|
363
|
+
"For each unprotected route: (a) if it needs auth, add middleware or auth guard (Clerk/NextAuth/Supabase), (b) if it's intentionally public (homepage, blog, about, etc.), add it to .guardviberc file under authExceptions with a reason",
|
|
364
|
+
"Create or update .guardviberc in project root: {\"authExceptions\": [{\"path\": \"/blog\", \"reason\": \"Public page\"}]}",
|
|
365
|
+
"Re-run `npx guardvibe auth-coverage --format json` and confirm unprotected count matches your authExceptions count",
|
|
354
366
|
],
|
|
355
367
|
},
|
|
356
368
|
};
|
package/build/utils/config.d.ts
CHANGED
|
@@ -26,6 +26,13 @@ export interface GuardVibeConfig {
|
|
|
26
26
|
* e.g. ["requireAdmin", "verifyUser", "ensureLoggedIn"]
|
|
27
27
|
* These are added ON TOP of the built-in pattern-agnostic detection. */
|
|
28
28
|
authFunctions?: string[];
|
|
29
|
+
/** Routes that are intentionally public (no auth required).
|
|
30
|
+
* e.g. [{"path": "/blog", "reason": "Public page"}]
|
|
31
|
+
* These are excluded from auth-coverage unprotected count. */
|
|
32
|
+
authExceptions?: Array<{
|
|
33
|
+
path: string;
|
|
34
|
+
reason: string;
|
|
35
|
+
}>;
|
|
29
36
|
}
|
|
30
37
|
export declare function loadConfig(dir?: string): GuardVibeConfig;
|
|
31
38
|
export declare function resetConfigCache(): void;
|
package/build/utils/config.js
CHANGED
|
@@ -68,6 +68,7 @@ export function loadConfig(dir) {
|
|
|
68
68
|
requiredControls: Array.isArray(parsed.compliance.requiredControls) ? parsed.compliance.requiredControls : undefined,
|
|
69
69
|
} : undefined,
|
|
70
70
|
authFunctions: Array.isArray(parsed.authFunctions) ? parsed.authFunctions : undefined,
|
|
71
|
+
authExceptions: Array.isArray(parsed.authExceptions) ? parsed.authExceptions : undefined,
|
|
71
72
|
};
|
|
72
73
|
}
|
|
73
74
|
catch { }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "guardvibe",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.13",
|
|
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",
|