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
- sections.push({ name: "code", status: "ok", ...counts, details: `Code ${codeGrade} (${codeScore}/100)` });
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
- sections.push({ name: "secrets", status: "ok", ...counts, details: counts.findings === 0 ? "No secrets found" : `${counts.findings} secret(s) detected` });
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
- sections.push({ name: "dependencies", status: "ok", ...counts, details: vuln === 0 ? "No known CVEs" : `${vuln} vulnerable package(s)` });
202
+ const depFindings = [];
186
203
  for (const pkg of parsed.packages ?? []) {
187
204
  for (const v of pkg.vulnerabilities ?? []) {
188
- allFindings.push({ ruleId: `DEP:${v.id ?? "CVE"}`, severity: v.severity, file: "package.json", line: 0 });
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
- sections.push({ name: "config", status: "ok", ...counts, details: counts.findings === 0 ? "Config secure" : `${counts.findings} config issue(s)` });
208
- for (const f of parsed.findings ?? []) {
209
- allFindings.push({ ruleId: f.id ?? f.ruleId ?? "CONFIG", severity: f.severity ?? "medium", file: f.file ?? "", line: f.line ?? 0 });
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.14",
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",