guardvibe 3.0.14 → 3.0.16
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.
|
@@ -66,7 +66,7 @@ export const nextjsRules = [
|
|
|
66
66
|
severity: "medium",
|
|
67
67
|
owasp: "A05:2025 Security Misconfiguration",
|
|
68
68
|
description: "next.config is missing important security headers (Content-Security-Policy, Strict-Transport-Security, X-Frame-Options).",
|
|
69
|
-
pattern: /(?:async\s+)?headers\s*\(\s*\)(?![\s\S]*(?:X-Frame-Options|Strict-Transport-Security|Content-Security-Policy))/g,
|
|
69
|
+
pattern: /(?:async\s+)?headers\s*\(\s*\)\s*\{[\s\S]{0,20}return\s+\[(?![\s\S]*(?:X-Frame-Options|Strict-Transport-Security|Content-Security-Policy))/g,
|
|
70
70
|
languages: ["javascript", "typescript"],
|
|
71
71
|
fix: "Add security headers in next.config.ts headers() function.",
|
|
72
72
|
fixCode: '// next.config.ts\nasync headers() {\n return [{\n source: "/(.*)",\n headers: [\n { key: "X-Frame-Options", value: "DENY" },\n { key: "X-Content-Type-Options", value: "nosniff" },\n { key: "Strict-Transport-Security", value: "max-age=63072000; includeSubDomains" },\n ]\n }];\n}',
|
|
@@ -21,6 +21,15 @@ export interface FindingRef {
|
|
|
21
21
|
line: number;
|
|
22
22
|
[key: string]: unknown;
|
|
23
23
|
}
|
|
24
|
+
export interface SectionFinding {
|
|
25
|
+
ruleId: string;
|
|
26
|
+
severity: string;
|
|
27
|
+
file: string;
|
|
28
|
+
line: number;
|
|
29
|
+
name?: string;
|
|
30
|
+
description?: string;
|
|
31
|
+
fix?: string;
|
|
32
|
+
}
|
|
24
33
|
export interface AuditSection {
|
|
25
34
|
name: string;
|
|
26
35
|
status: "ok" | "error" | "skipped";
|
|
@@ -29,6 +38,8 @@ export interface AuditSection {
|
|
|
29
38
|
high: number;
|
|
30
39
|
medium: number;
|
|
31
40
|
details: string;
|
|
41
|
+
/** Individual findings for this section — enables AI to see exactly what to fix */
|
|
42
|
+
sectionFindings?: SectionFinding[];
|
|
32
43
|
}
|
|
33
44
|
export interface AuditResult {
|
|
34
45
|
verdict: AuditVerdict;
|
|
@@ -141,7 +141,15 @@ export async function runFullAudit(path, options) {
|
|
|
141
141
|
score = parsed.summary?.score ?? 100;
|
|
142
142
|
const codeGrade = parsed.summary?.grade ?? "A";
|
|
143
143
|
const codeScore = parsed.summary?.score ?? 100;
|
|
144
|
-
|
|
144
|
+
const codeSectionFindings = (parsed.findings ?? []).map((f) => ({
|
|
145
|
+
ruleId: (f.id ?? "unknown"),
|
|
146
|
+
severity: f.severity,
|
|
147
|
+
file: (f.file ?? ""),
|
|
148
|
+
line: (f.line ?? 0),
|
|
149
|
+
name: f.name,
|
|
150
|
+
fix: f.fix,
|
|
151
|
+
}));
|
|
152
|
+
sections.push({ name: "code", status: "ok", ...counts, details: `Code ${codeGrade} (${codeScore}/100)`, sectionFindings: codeSectionFindings });
|
|
145
153
|
for (const f of parsed.findings ?? []) {
|
|
146
154
|
allFindings.push({ ruleId: f.id ?? "unknown", severity: f.severity, file: f.file ?? "", line: f.line ?? 0 });
|
|
147
155
|
}
|
|
@@ -162,7 +170,16 @@ export async function runFullAudit(path, options) {
|
|
|
162
170
|
const parsed = safeJsonParse(secretsJson);
|
|
163
171
|
if (parsed) {
|
|
164
172
|
const counts = parseSectionCounts(parsed);
|
|
165
|
-
|
|
173
|
+
const secretFindings = (parsed.findings ?? []).map((f) => ({
|
|
174
|
+
ruleId: `SECRET:${(f.provider ?? "unknown")}`,
|
|
175
|
+
severity: (f.severity ?? "high"),
|
|
176
|
+
file: (f.file ?? ""),
|
|
177
|
+
line: (f.line ?? 0),
|
|
178
|
+
name: `Secret detected: ${(f.provider ?? "unknown")}`,
|
|
179
|
+
description: (f.match ?? f.description ?? ""),
|
|
180
|
+
fix: "Move this secret to an environment variable and ensure the file is in .gitignore",
|
|
181
|
+
}));
|
|
182
|
+
sections.push({ name: "secrets", status: "ok", ...counts, details: counts.findings === 0 ? "No secrets found" : `${counts.findings} secret(s) detected`, sectionFindings: secretFindings });
|
|
166
183
|
for (const f of parsed.findings ?? []) {
|
|
167
184
|
allFindings.push({ ruleId: `SECRET:${f.provider ?? "unknown"}`, severity: f.severity, file: f.file ?? "", line: f.line ?? 0 });
|
|
168
185
|
}
|
|
@@ -182,12 +199,23 @@ export async function runFullAudit(path, options) {
|
|
|
182
199
|
if (parsed) {
|
|
183
200
|
const vuln = parsed.summary?.vulnerable ?? 0;
|
|
184
201
|
const counts = { findings: vuln, critical: parsed.summary?.critical ?? 0, high: parsed.summary?.high ?? 0, medium: parsed.summary?.medium ?? 0 };
|
|
185
|
-
|
|
202
|
+
const depFindings = [];
|
|
186
203
|
for (const pkg of parsed.packages ?? []) {
|
|
187
204
|
for (const v of pkg.vulnerabilities ?? []) {
|
|
188
|
-
|
|
205
|
+
const vuln2 = v;
|
|
206
|
+
depFindings.push({
|
|
207
|
+
ruleId: `DEP:${(vuln2.id ?? "CVE")}`,
|
|
208
|
+
severity: (vuln2.severity ?? "high"),
|
|
209
|
+
file: "package.json",
|
|
210
|
+
line: 0,
|
|
211
|
+
name: `${pkg.name ?? "unknown"}: ${(vuln2.id ?? "CVE")}`,
|
|
212
|
+
description: (vuln2.summary ?? vuln2.details ?? ""),
|
|
213
|
+
fix: `Run: npm update ${pkg.name ?? ""}`,
|
|
214
|
+
});
|
|
215
|
+
allFindings.push({ ruleId: `DEP:${vuln2.id ?? "CVE"}`, severity: vuln2.severity, file: "package.json", line: 0 });
|
|
189
216
|
}
|
|
190
217
|
}
|
|
218
|
+
sections.push({ name: "dependencies", status: "ok", ...counts, details: vuln === 0 ? "No known CVEs" : `${vuln} vulnerable package(s)`, sectionFindings: depFindings });
|
|
191
219
|
}
|
|
192
220
|
}
|
|
193
221
|
catch {
|
|
@@ -204,9 +232,21 @@ export async function runFullAudit(path, options) {
|
|
|
204
232
|
const parsed = safeJsonParse(configJson);
|
|
205
233
|
if (parsed) {
|
|
206
234
|
const counts = parseSectionCounts(parsed);
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
235
|
+
// auditConfig uses "issues" key, not "findings"
|
|
236
|
+
const rawIssues = parsed.issues ?? parsed.findings ?? [];
|
|
237
|
+
const configFindings = rawIssues.map((f) => ({
|
|
238
|
+
ruleId: (f.id ?? f.ruleId ?? "CONFIG"),
|
|
239
|
+
severity: (f.severity ?? "medium"),
|
|
240
|
+
file: (Array.isArray(f.files) && f.files.length > 0 ? f.files[0] : (f.file ?? "")),
|
|
241
|
+
line: (f.line ?? 0),
|
|
242
|
+
name: (f.title ?? f.name ?? ""),
|
|
243
|
+
description: (f.description ?? ""),
|
|
244
|
+
fix: (f.fix ?? ""),
|
|
245
|
+
}));
|
|
246
|
+
sections.push({ name: "config", status: "ok", ...counts, details: counts.findings === 0 ? "Config secure" : `${counts.findings} config issue(s)`, sectionFindings: configFindings });
|
|
247
|
+
for (const f of rawIssues) {
|
|
248
|
+
const file = Array.isArray(f.files) && f.files.length > 0 ? f.files[0] : (f.file ?? "");
|
|
249
|
+
allFindings.push({ ruleId: f.id ?? f.ruleId ?? "CONFIG", severity: f.severity ?? "medium", file, line: f.line ?? 0 });
|
|
210
250
|
}
|
|
211
251
|
}
|
|
212
252
|
}
|
|
@@ -224,8 +264,31 @@ export async function runFullAudit(path, options) {
|
|
|
224
264
|
const taintCritical = crossFileFindings.filter(f => f.severity === "critical").length;
|
|
225
265
|
const taintHigh = crossFileFindings.filter(f => f.severity === "high").length;
|
|
226
266
|
const taintMedium = taintTotal - taintCritical - taintHigh;
|
|
267
|
+
const taintSectionFindings = crossFileFindings.map(f => ({
|
|
268
|
+
ruleId: `TAINT:${f.sink.type}`,
|
|
269
|
+
severity: f.severity,
|
|
270
|
+
file: f.source.file,
|
|
271
|
+
line: f.source.line,
|
|
272
|
+
name: `Tainted flow: ${f.source.type} → ${f.sink.type}`,
|
|
273
|
+
description: `User input from ${f.source.file}:${f.source.line} flows to ${f.sink.type} in ${f.sink.file}:${f.sink.line}`,
|
|
274
|
+
fix: `Add input validation at ${f.source.file}:${f.source.line} or output encoding at ${f.sink.file}:${f.sink.line}`,
|
|
275
|
+
}));
|
|
276
|
+
// Add per-file findings
|
|
277
|
+
for (const [file, findings] of perFileFindings) {
|
|
278
|
+
for (const pf of findings) {
|
|
279
|
+
taintSectionFindings.push({
|
|
280
|
+
ruleId: `TAINT:${pf.sink.type}`,
|
|
281
|
+
severity: "medium",
|
|
282
|
+
file,
|
|
283
|
+
line: pf.source.line,
|
|
284
|
+
name: `Tainted flow: ${pf.source.type} → ${pf.sink.type}`,
|
|
285
|
+
description: `${pf.source.type} (${pf.source.variable}) at line ${pf.source.line} flows to ${pf.sink.type} at line ${pf.sink.line}`,
|
|
286
|
+
fix: `Add validation/sanitization at line ${pf.source.line} before ${pf.sink.type} usage at line ${pf.sink.line}`,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
}
|
|
227
290
|
sections.push({ name: "taint", status: "ok", findings: taintTotal, critical: taintCritical, high: taintHigh, medium: taintMedium,
|
|
228
|
-
details: taintTotal === 0 ? "No tainted data flows" : `${taintTotal} tainted flow(s)
|
|
291
|
+
details: taintTotal === 0 ? "No tainted data flows" : `${taintTotal} tainted flow(s)`, sectionFindings: taintSectionFindings });
|
|
229
292
|
for (const f of crossFileFindings) {
|
|
230
293
|
allFindings.push({ ruleId: `TAINT:${f.sink.type}`, severity: f.severity, file: f.source.file, line: f.source.line });
|
|
231
294
|
}
|
|
@@ -244,8 +307,17 @@ export async function runFullAudit(path, options) {
|
|
|
244
307
|
const config = loadConfig(projectRoot);
|
|
245
308
|
const report = analyzeAuthCoverage(routeFiles, middlewareFile?.content ?? "", layoutFiles, config.authExceptions);
|
|
246
309
|
const unprotected = report.unprotectedRoutes;
|
|
310
|
+
const authFindings = report.unprotectedList.map(r => ({
|
|
311
|
+
ruleId: "AUTH:UNPROTECTED",
|
|
312
|
+
severity: "high",
|
|
313
|
+
file: r.filePath,
|
|
314
|
+
line: 0,
|
|
315
|
+
name: `Unprotected route: ${r.urlPath} (${r.method})`,
|
|
316
|
+
description: `Route ${r.urlPath} has no auth guard, middleware protection, or layout-level auth`,
|
|
317
|
+
fix: `Add auth guard to ${r.filePath}, or add {"path": "${r.urlPath}", "reason": "Public page"} to .guardviberc authExceptions`,
|
|
318
|
+
}));
|
|
247
319
|
sections.push({ name: "auth-coverage", status: "ok", findings: unprotected, critical: 0, high: unprotected > 0 ? unprotected : 0, medium: 0,
|
|
248
|
-
details: `${report.protectedRoutes}/${report.totalRoutes} routes protected (${report.middlewareCoveragePercent}% middleware)
|
|
320
|
+
details: `${report.protectedRoutes}/${report.totalRoutes} routes protected (${report.middlewareCoveragePercent}% middleware)`, sectionFindings: authFindings });
|
|
249
321
|
}
|
|
250
322
|
}
|
|
251
323
|
catch { /* auth coverage is optional */ }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "guardvibe",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.16",
|
|
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",
|