guardvibe 3.0.13 → 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.
- package/build/tools/full-audit.d.ts +11 -0
- package/build/tools/full-audit.js +76 -7
- package/package.json +1 -1
|
@@ -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,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
|
-
|
|
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.
|
|
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",
|