guardvibe 3.0.14 → 3.0.15

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.
@@ -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,7 +232,16 @@ 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)` });
235
+ const configFindings = (parsed.findings ?? []).map((f) => ({
236
+ ruleId: (f.id ?? f.ruleId ?? "CONFIG"),
237
+ severity: (f.severity ?? "medium"),
238
+ file: (f.file ?? ""),
239
+ line: (f.line ?? 0),
240
+ name: (f.name ?? f.description ?? ""),
241
+ description: (f.description ?? f.details ?? ""),
242
+ fix: (f.fix ?? f.remediation ?? ""),
243
+ }));
244
+ sections.push({ name: "config", status: "ok", ...counts, details: counts.findings === 0 ? "Config secure" : `${counts.findings} config issue(s)`, sectionFindings: configFindings });
208
245
  for (const f of parsed.findings ?? []) {
209
246
  allFindings.push({ ruleId: f.id ?? f.ruleId ?? "CONFIG", severity: f.severity ?? "medium", file: f.file ?? "", line: f.line ?? 0 });
210
247
  }
@@ -224,8 +261,31 @@ export async function runFullAudit(path, options) {
224
261
  const taintCritical = crossFileFindings.filter(f => f.severity === "critical").length;
225
262
  const taintHigh = crossFileFindings.filter(f => f.severity === "high").length;
226
263
  const taintMedium = taintTotal - taintCritical - taintHigh;
264
+ const taintSectionFindings = crossFileFindings.map(f => ({
265
+ ruleId: `TAINT:${f.sink.type}`,
266
+ severity: f.severity,
267
+ file: f.source.file,
268
+ line: f.source.line,
269
+ name: `Tainted flow: ${f.source.type} → ${f.sink.type}`,
270
+ description: `User input from ${f.source.file}:${f.source.line} flows to ${f.sink.type} in ${f.sink.file}:${f.sink.line}`,
271
+ fix: `Add input validation at ${f.source.file}:${f.source.line} or output encoding at ${f.sink.file}:${f.sink.line}`,
272
+ }));
273
+ // Add per-file findings
274
+ for (const [file, findings] of perFileFindings) {
275
+ for (const pf of findings) {
276
+ taintSectionFindings.push({
277
+ ruleId: `TAINT:${pf.sink.type}`,
278
+ severity: "medium",
279
+ file,
280
+ line: pf.source.line,
281
+ name: `Tainted flow: ${pf.source.type} → ${pf.sink.type}`,
282
+ description: `${pf.source.type} (${pf.source.variable}) at line ${pf.source.line} flows to ${pf.sink.type} at line ${pf.sink.line}`,
283
+ fix: `Add validation/sanitization at line ${pf.source.line} before ${pf.sink.type} usage at line ${pf.sink.line}`,
284
+ });
285
+ }
286
+ }
227
287
  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)` });
288
+ details: taintTotal === 0 ? "No tainted data flows" : `${taintTotal} tainted flow(s)`, sectionFindings: taintSectionFindings });
229
289
  for (const f of crossFileFindings) {
230
290
  allFindings.push({ ruleId: `TAINT:${f.sink.type}`, severity: f.severity, file: f.source.file, line: f.source.line });
231
291
  }
@@ -244,8 +304,17 @@ export async function runFullAudit(path, options) {
244
304
  const config = loadConfig(projectRoot);
245
305
  const report = analyzeAuthCoverage(routeFiles, middlewareFile?.content ?? "", layoutFiles, config.authExceptions);
246
306
  const unprotected = report.unprotectedRoutes;
307
+ const authFindings = report.unprotectedList.map(r => ({
308
+ ruleId: "AUTH:UNPROTECTED",
309
+ severity: "high",
310
+ file: r.filePath,
311
+ line: 0,
312
+ name: `Unprotected route: ${r.urlPath} (${r.method})`,
313
+ description: `Route ${r.urlPath} has no auth guard, middleware protection, or layout-level auth`,
314
+ fix: `Add auth guard to ${r.filePath}, or add {"path": "${r.urlPath}", "reason": "Public page"} to .guardviberc authExceptions`,
315
+ }));
247
316
  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)` });
317
+ details: `${report.protectedRoutes}/${report.totalRoutes} routes protected (${report.middlewareCoveragePercent}% middleware)`, sectionFindings: authFindings });
249
318
  }
250
319
  }
251
320
  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.15",
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",