guardvibe 3.0.6 → 3.0.8
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/audit.js +7 -2
- package/build/cli/auth-coverage.d.ts +5 -0
- package/build/cli/auth-coverage.js +74 -0
- package/build/cli/compliance.d.ts +5 -0
- package/build/cli/compliance.js +33 -0
- package/build/cli.js +10 -0
- package/build/tools/check-code.js +18 -0
- package/build/tools/full-audit.d.ts +2 -2
- package/build/tools/full-audit.js +55 -1
- package/package.json +1 -1
package/build/cli/audit.js
CHANGED
|
@@ -9,13 +9,18 @@ import { runFullAudit, formatAuditResult } from "../tools/full-audit.js";
|
|
|
9
9
|
export async function runAudit(args) {
|
|
10
10
|
const { flags, positional } = parseArgs(args);
|
|
11
11
|
const targetPath = resolve(positional[0] ?? ".");
|
|
12
|
-
const
|
|
12
|
+
const rawFormat = validateFormat(flags);
|
|
13
13
|
const outputFile = getOutputPath(flags);
|
|
14
14
|
const failOn = getStringFlag(flags, "fail-on") ?? "critical";
|
|
15
15
|
const skipDeps = flags["skip-deps"] === true;
|
|
16
16
|
const skipSecrets = flags["skip-secrets"] === true;
|
|
17
|
+
// Terminal format by default when outputting to TTY, unless --format is specified
|
|
18
|
+
const isTerminal = !outputFile && process.stdout.isTTY && !flags["format"];
|
|
19
|
+
const format = isTerminal ? "terminal" : rawFormat;
|
|
17
20
|
const result = await runFullAudit(targetPath, { skipDeps, skipSecrets });
|
|
18
21
|
const output = formatAuditResult(result, format);
|
|
22
|
+
// For shouldFail, always use JSON-parseable format
|
|
23
|
+
const failCheckOutput = formatAuditResult(result, "json");
|
|
19
24
|
if (outputFile) {
|
|
20
25
|
const dir = dirname(outputFile);
|
|
21
26
|
if (!existsSync(dir)) {
|
|
@@ -29,6 +34,6 @@ export async function runAudit(args) {
|
|
|
29
34
|
}
|
|
30
35
|
// Print result hash to stderr for CI piping
|
|
31
36
|
console.error(`result-hash: ${result.resultHash}`);
|
|
32
|
-
if (shouldFail(
|
|
37
|
+
if (shouldFail(failCheckOutput, failOn))
|
|
33
38
|
process.exit(1);
|
|
34
39
|
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI: guardvibe auth-coverage [path]
|
|
3
|
+
* Analyze authentication coverage across Next.js App Router routes.
|
|
4
|
+
*/
|
|
5
|
+
import { readdirSync, readFileSync, statSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
6
|
+
import { resolve, dirname } from "path";
|
|
7
|
+
import { parseArgs, validateFormat, getOutputPath } from "./args.js";
|
|
8
|
+
import { analyzeAuthCoverage, formatAuthCoverage } from "../tools/auth-coverage.js";
|
|
9
|
+
export async function runAuthCoverage(args) {
|
|
10
|
+
const { flags, positional } = parseArgs(args);
|
|
11
|
+
const targetPath = resolve(positional[0] ?? ".");
|
|
12
|
+
const format = validateFormat(flags);
|
|
13
|
+
const outputFile = getOutputPath(flags);
|
|
14
|
+
// Walk directory to discover route/page/layout/middleware files
|
|
15
|
+
const jsFiles = [];
|
|
16
|
+
const skip = new Set(["node_modules", ".git", ".next", "build", "dist", ".turbo", "coverage"]);
|
|
17
|
+
function walk(d) {
|
|
18
|
+
if (jsFiles.length >= 500)
|
|
19
|
+
return;
|
|
20
|
+
let entries;
|
|
21
|
+
try {
|
|
22
|
+
entries = readdirSync(d);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
for (const entry of entries) {
|
|
28
|
+
if (jsFiles.length >= 500)
|
|
29
|
+
return;
|
|
30
|
+
if (skip.has(entry))
|
|
31
|
+
continue;
|
|
32
|
+
const full = resolve(d, entry);
|
|
33
|
+
let stat;
|
|
34
|
+
try {
|
|
35
|
+
stat = statSync(full);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (stat.isDirectory()) {
|
|
41
|
+
walk(full);
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (!/\.(ts|tsx|js|jsx)$/.test(entry))
|
|
45
|
+
continue;
|
|
46
|
+
if (stat.size > 100_000)
|
|
47
|
+
continue;
|
|
48
|
+
try {
|
|
49
|
+
const content = readFileSync(full, "utf-8");
|
|
50
|
+
const relPath = full.replace(targetPath + "/", "");
|
|
51
|
+
jsFiles.push({ path: relPath, content });
|
|
52
|
+
}
|
|
53
|
+
catch { /* skip unreadable */ }
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
walk(targetPath);
|
|
57
|
+
const routeFiles = jsFiles.filter(f => /\/(route|page)\.(ts|tsx|js|jsx)$/.test(f.path));
|
|
58
|
+
const layoutFiles = jsFiles.filter(f => /\/layout\.(ts|tsx|js|jsx)$/.test(f.path));
|
|
59
|
+
const middlewareFile = jsFiles.find(f => /middleware\.(ts|js)$/.test(f.path));
|
|
60
|
+
const report = analyzeAuthCoverage(routeFiles, middlewareFile?.content ?? "", layoutFiles);
|
|
61
|
+
const formatArg = format === "json" ? "json" : "markdown";
|
|
62
|
+
const result = formatAuthCoverage(report, formatArg);
|
|
63
|
+
if (outputFile) {
|
|
64
|
+
const dir = dirname(outputFile);
|
|
65
|
+
if (!existsSync(dir)) {
|
|
66
|
+
mkdirSync(dir, { recursive: true });
|
|
67
|
+
}
|
|
68
|
+
writeFileSync(outputFile, result, "utf-8");
|
|
69
|
+
console.log(` [OK] Results written to ${outputFile}`);
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
console.log(result);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI: guardvibe compliance [path] --framework SOC2
|
|
3
|
+
* Generate compliance report mapping findings to framework controls.
|
|
4
|
+
*/
|
|
5
|
+
import { writeFileSync, existsSync, mkdirSync } from "fs";
|
|
6
|
+
import { resolve, dirname } from "path";
|
|
7
|
+
import { parseArgs, validateFormat, getOutputPath, getStringFlag } from "./args.js";
|
|
8
|
+
import { complianceReport } from "../tools/compliance-report.js";
|
|
9
|
+
const VALID_FRAMEWORKS = new Set(["SOC2", "PCI-DSS", "HIPAA", "GDPR", "ISO27001", "EUAIACT"]);
|
|
10
|
+
export async function runCompliance(args) {
|
|
11
|
+
const { flags, positional } = parseArgs(args);
|
|
12
|
+
const targetPath = resolve(positional[0] ?? ".");
|
|
13
|
+
const framework = getStringFlag(flags, "framework") ?? "SOC2";
|
|
14
|
+
const format = validateFormat(flags);
|
|
15
|
+
const outputFile = getOutputPath(flags);
|
|
16
|
+
if (!VALID_FRAMEWORKS.has(framework)) {
|
|
17
|
+
console.error(` [ERR] Invalid framework "${framework}". Use: ${[...VALID_FRAMEWORKS].join(", ")}`);
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
const formatArg = format === "json" ? "json" : "markdown";
|
|
21
|
+
const result = complianceReport(targetPath, framework, formatArg);
|
|
22
|
+
if (outputFile) {
|
|
23
|
+
const dir = dirname(outputFile);
|
|
24
|
+
if (!existsSync(dir)) {
|
|
25
|
+
mkdirSync(dir, { recursive: true });
|
|
26
|
+
}
|
|
27
|
+
writeFileSync(outputFile, result, "utf-8");
|
|
28
|
+
console.log(` [OK] Results written to ${outputFile}`);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
console.log(result);
|
|
32
|
+
}
|
|
33
|
+
}
|
package/build/cli.js
CHANGED
|
@@ -26,6 +26,8 @@ function printUsage() {
|
|
|
26
26
|
npx guardvibe explain <ruleId> Get detailed remediation guidance for a rule
|
|
27
27
|
npx guardvibe fix <file> Get security fix suggestions for a file
|
|
28
28
|
npx guardvibe check-cmd "<cmd>" Check if a shell command is safe to execute
|
|
29
|
+
npx guardvibe auth-coverage [path] Auth coverage analysis (Next.js routes)
|
|
30
|
+
npx guardvibe compliance [path] Compliance report (--framework SOC2|GDPR|...)
|
|
29
31
|
npx guardvibe init <platform> Setup MCP server configuration
|
|
30
32
|
npx guardvibe hook install Install pre-commit security hook
|
|
31
33
|
npx guardvibe hook uninstall Remove pre-commit security hook
|
|
@@ -142,6 +144,14 @@ async function main() {
|
|
|
142
144
|
const { runCheckCmd } = await import("./cli/check-cmd.js");
|
|
143
145
|
await runCheckCmd(subArgs);
|
|
144
146
|
}
|
|
147
|
+
else if (command === "auth-coverage") {
|
|
148
|
+
const { runAuthCoverage } = await import("./cli/auth-coverage.js");
|
|
149
|
+
await runAuthCoverage(subArgs);
|
|
150
|
+
}
|
|
151
|
+
else if (command === "compliance") {
|
|
152
|
+
const { runCompliance } = await import("./cli/compliance.js");
|
|
153
|
+
await runCompliance(subArgs);
|
|
154
|
+
}
|
|
145
155
|
else {
|
|
146
156
|
console.error(` Unknown command: ${command}`);
|
|
147
157
|
printUsage();
|
|
@@ -276,6 +276,7 @@ export function analyzeCode(code, language, framework, filePath, configDir, rule
|
|
|
276
276
|
const codeHasFilenameSanitization = /(?:\.replace\s*\(\s*\/\[?\^?[a-z0-9\\-_\]]*\]?\/?[gi]*\s*,|sanitize(?:File|Name|Path)|safeName|cleanName)/i.test(code) ||
|
|
277
277
|
/(?:Date\.now\(\)|timestamp|uuid|nanoid|crypto\.randomUUID)[\s\S]{0,80}?\.\s*(?:ext|split|pop)/i.test(code);
|
|
278
278
|
const isPeerDeps = /["']peerDependencies["']/i.test(code);
|
|
279
|
+
const codeHasAuthSession = /(?:supabase\.auth\.getUser|supabase\.auth\.getSession|getServerSession|auth\(\)|getSession\(\)|currentUser\(\))/i.test(code);
|
|
279
280
|
// Config: check custom auth function names from .guardviberc
|
|
280
281
|
if (!codeHasAuthGuard && config.authFunctions && config.authFunctions.length > 0) {
|
|
281
282
|
const customPattern = new RegExp(`(?:${config.authFunctions.join("|")})\\s*\\(`, "i");
|
|
@@ -544,6 +545,23 @@ export function analyzeCode(code, language, framework, filePath, configDir, rule
|
|
|
544
545
|
if (/(?:Count|Length|Balance|Map|List|Array|Index|Size|Total|Num|Id|Type|Name|Status|Data|Info|Error|Result|Response|Config|Option|Url|Path|Provider|Model|Limit|Quota|Rate|Max|Min)/i.test(varName))
|
|
545
546
|
continue;
|
|
546
547
|
}
|
|
548
|
+
// Skip VG1005 (.or() filter injection) when all interpolated variables are
|
|
549
|
+
// server-verified auth IDs (user.id, session.user.id, auth.uid, currentUser.id)
|
|
550
|
+
if (rule.id === "VG1005" && codeHasAuthSession) {
|
|
551
|
+
// The regex match ends at `${` — grab the full template literal from code
|
|
552
|
+
const orStart = match.index;
|
|
553
|
+
const backtickIdx = code.indexOf('`', orStart);
|
|
554
|
+
if (backtickIdx !== -1) {
|
|
555
|
+
const closingBacktick = code.indexOf('`', backtickIdx + 1);
|
|
556
|
+
if (closingBacktick !== -1) {
|
|
557
|
+
const fullTemplate = code.substring(backtickIdx, closingBacktick + 1);
|
|
558
|
+
const interpolations = [...fullTemplate.matchAll(/\$\{([^}]+)\}/g)].map(m => m[1].trim());
|
|
559
|
+
const safeAuthPattern = /^(?:user\.id|user\?\.id|session\.user\.id|session\.user\?\.id|currentUser\.id|currentUser\?\.id|auth\.uid|auth\?\.uid|session\.uid|session\?\.uid)$/;
|
|
560
|
+
if (interpolations.length > 0 && interpolations.every(v => safeAuthPattern.test(v)))
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
547
565
|
// Skip VG903 React version in peerDependencies sections
|
|
548
566
|
if (rule.id === "VG903") {
|
|
549
567
|
const beforeText = code.substring(0, match.index);
|
|
@@ -76,6 +76,6 @@ export declare function runFullAudit(path: string, options?: {
|
|
|
76
76
|
skipSecrets?: boolean;
|
|
77
77
|
}): Promise<AuditResult>;
|
|
78
78
|
/**
|
|
79
|
-
* Format audit result as markdown or
|
|
79
|
+
* Format audit result as markdown, JSON, or terminal-friendly output.
|
|
80
80
|
*/
|
|
81
|
-
export declare function formatAuditResult(result: AuditResult, format: "markdown" | "json"): string;
|
|
81
|
+
export declare function formatAuditResult(result: AuditResult, format: "markdown" | "json" | "terminal"): string;
|
|
@@ -293,12 +293,15 @@ export async function runFullAudit(path, options) {
|
|
|
293
293
|
}
|
|
294
294
|
// --- Formatter ---
|
|
295
295
|
/**
|
|
296
|
-
* Format audit result as markdown or
|
|
296
|
+
* Format audit result as markdown, JSON, or terminal-friendly output.
|
|
297
297
|
*/
|
|
298
298
|
export function formatAuditResult(result, format) {
|
|
299
299
|
if (format === "json") {
|
|
300
300
|
return JSON.stringify(result);
|
|
301
301
|
}
|
|
302
|
+
if (format === "terminal") {
|
|
303
|
+
return formatTerminal(result);
|
|
304
|
+
}
|
|
302
305
|
const verdictLabel = {
|
|
303
306
|
PASS: "PASS — Project verified secure",
|
|
304
307
|
WARN: "WARN — High severity issues found",
|
|
@@ -363,3 +366,54 @@ export function formatAuditResult(result, format) {
|
|
|
363
366
|
lines.push(`Result hash: \`${result.resultHash}\` (same code + same GuardVibe version = same hash)`);
|
|
364
367
|
return lines.join("\n");
|
|
365
368
|
}
|
|
369
|
+
// --- Terminal-friendly formatter ---
|
|
370
|
+
function formatTerminal(result) {
|
|
371
|
+
const R = "\x1b[31m"; // red
|
|
372
|
+
const G = "\x1b[32m"; // green
|
|
373
|
+
const Y = "\x1b[33m"; // yellow
|
|
374
|
+
const B = "\x1b[1m"; // bold
|
|
375
|
+
const D = "\x1b[2m"; // dim
|
|
376
|
+
const X = "\x1b[0m"; // reset
|
|
377
|
+
const verdictColor = result.verdict === "PASS" ? G : result.verdict === "WARN" ? Y : R;
|
|
378
|
+
const scoreBar = (() => {
|
|
379
|
+
const width = 20;
|
|
380
|
+
const filled = Math.round((result.score / 100) * width);
|
|
381
|
+
const bar = "\u2588".repeat(filled) + "\u2591".repeat(width - filled);
|
|
382
|
+
const color = result.score >= 75 ? G : result.score >= 50 ? Y : R;
|
|
383
|
+
return `${color}${bar}${X}`;
|
|
384
|
+
})();
|
|
385
|
+
const lines = [
|
|
386
|
+
``,
|
|
387
|
+
` ${B}GuardVibe Full Audit Report${X}`,
|
|
388
|
+
``,
|
|
389
|
+
` ${verdictColor}${B}${result.verdict}${X} ${verdictColor}${result.verdict === "PASS" ? "Project verified secure" : result.verdict === "WARN" ? "High severity issues found" : "Critical security issues detected"}${X}`,
|
|
390
|
+
``,
|
|
391
|
+
` Score ${scoreBar} ${B}${result.grade}${X} ${D}(${result.score}/100)${X}`,
|
|
392
|
+
``,
|
|
393
|
+
` ${B}Findings${X}`,
|
|
394
|
+
` ${R}${B}${result.summary.critical}${X} critical ${Y}${B}${result.summary.high}${X} high ${D}${result.summary.medium} medium${X} ${D}(${result.summary.totalFindings} total)${X}`,
|
|
395
|
+
``,
|
|
396
|
+
` ${B}Sections${X}`,
|
|
397
|
+
];
|
|
398
|
+
const sectionIcon = { ok: `${G}\u2714${X}`, error: `${R}\u2718${X}`, skipped: `${D}\u2500${X}` };
|
|
399
|
+
for (const s of result.sections) {
|
|
400
|
+
const icon = sectionIcon[s.status] ?? s.status;
|
|
401
|
+
const count = s.findings > 0 ? `${s.findings}` : `${D}0${X}`;
|
|
402
|
+
lines.push(` ${icon} ${s.name.padEnd(14)} ${count.padStart(4)} ${D}${s.details}${X}`);
|
|
403
|
+
}
|
|
404
|
+
lines.push(``);
|
|
405
|
+
lines.push(` ${B}Coverage${X}`);
|
|
406
|
+
lines.push(` ${result.coverage.filesScanned} files scanned ${D}${result.coverage.coveragePercent}% coverage ${result.coverage.rulesApplied} rules${X}`);
|
|
407
|
+
if (result.actionItems.length > 0) {
|
|
408
|
+
lines.push(``);
|
|
409
|
+
lines.push(` ${B}Action Items${X}`);
|
|
410
|
+
for (const item of result.actionItems) {
|
|
411
|
+
const color = item.includes("critical") ? R : item.includes("high") ? Y : D;
|
|
412
|
+
lines.push(` ${color}\u25B8${X} ${item}`);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
lines.push(``);
|
|
416
|
+
lines.push(` ${D}Hash: ${result.resultHash} | ${result.timestamp.slice(0, 19)}${X}`);
|
|
417
|
+
lines.push(``);
|
|
418
|
+
return lines.join("\n");
|
|
419
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "guardvibe",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.8",
|
|
4
4
|
"mcpName": "io.github.goklab/guardvibe",
|
|
5
5
|
"description": "Security MCP for vibe coding. 334 rules, 34 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",
|