guardvibe 3.0.0 → 3.0.3

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.
@@ -0,0 +1,365 @@
1
+ /**
2
+ * Full Audit — single source of truth for AI assistants.
3
+ * Orchestrates all security tools in one call, produces:
4
+ * - PASS/FAIL/WARN verdict
5
+ * - Unified report across code, secrets, deps, config, taint, auth
6
+ * - Deterministic result hash (same code = same hash)
7
+ * - Coverage metrics (files scanned, rules applied, %)
8
+ */
9
+ import { createHash } from "node:crypto";
10
+ import { resolve } from "node:path";
11
+ import { readdirSync, readFileSync, statSync, existsSync } from "node:fs";
12
+ import { scanDirectory } from "./scan-directory.js";
13
+ import { scanSecrets } from "./scan-secrets.js";
14
+ import { scanDependencies } from "./scan-dependencies.js";
15
+ import { auditConfig } from "./audit-config.js";
16
+ import { analyzeCrossFileTaint } from "./cross-file-taint.js";
17
+ import { analyzeAuthCoverage } from "./auth-coverage.js";
18
+ import { getRules } from "../utils/rule-registry.js";
19
+ // --- Core Logic ---
20
+ /**
21
+ * Compute verdict: PASS (0 critical + 0 high), WARN (high > 0), FAIL (critical > 0)
22
+ */
23
+ export function computeVerdict(critical, high, _medium) {
24
+ if (critical > 0)
25
+ return "FAIL";
26
+ if (high > 0)
27
+ return "WARN";
28
+ return "PASS";
29
+ }
30
+ /**
31
+ * Compute coverage metrics from scan results.
32
+ */
33
+ export function computeCoverage(filesScanned, filesSkipped, rulesApplied) {
34
+ const totalFiles = filesScanned + filesSkipped;
35
+ const coveragePercent = totalFiles > 0 ? Math.round((filesScanned / totalFiles) * 100) : 0;
36
+ return { filesScanned, filesSkipped, totalFiles, coveragePercent, rulesApplied };
37
+ }
38
+ /**
39
+ * Compute deterministic SHA256 hash of findings.
40
+ * Same findings (in any order) = same hash.
41
+ */
42
+ export function computeResultHash(findings) {
43
+ const normalized = findings
44
+ .map(f => `${f.ruleId}:${f.severity}:${f.file}:${f.line}`)
45
+ .sort()
46
+ .join("|");
47
+ return createHash("sha256").update(normalized).digest("hex").substring(0, 16);
48
+ }
49
+ // --- Orchestrator ---
50
+ function safeJsonParse(str) {
51
+ try {
52
+ return JSON.parse(str);
53
+ }
54
+ catch {
55
+ return null;
56
+ }
57
+ }
58
+ function parseSectionCounts(parsed) {
59
+ const s = parsed?.summary ?? {};
60
+ return {
61
+ findings: s.total ?? 0,
62
+ critical: s.critical ?? 0,
63
+ high: s.high ?? 0,
64
+ medium: s.medium ?? 0,
65
+ };
66
+ }
67
+ function collectJsFiles(dir, maxFiles = 200) {
68
+ const files = [];
69
+ const skip = new Set(["node_modules", ".git", ".next", "build", "dist", ".turbo", "coverage"]);
70
+ function walk(d) {
71
+ if (files.length >= maxFiles)
72
+ return;
73
+ let entries;
74
+ try {
75
+ entries = readdirSync(d);
76
+ }
77
+ catch {
78
+ return;
79
+ }
80
+ for (const entry of entries) {
81
+ if (files.length >= maxFiles)
82
+ return;
83
+ if (skip.has(entry))
84
+ continue;
85
+ const full = resolve(d, entry);
86
+ let stat;
87
+ try {
88
+ stat = statSync(full);
89
+ }
90
+ catch {
91
+ continue;
92
+ }
93
+ if (stat.isDirectory()) {
94
+ walk(full);
95
+ continue;
96
+ }
97
+ if (!/\.(ts|tsx|js|jsx|mts|cts)$/.test(entry))
98
+ continue;
99
+ if (stat.size > 100_000)
100
+ continue;
101
+ try {
102
+ const content = readFileSync(full, "utf-8");
103
+ const relPath = full.replace(resolve(dir) + "/", "");
104
+ files.push({ path: relPath, content });
105
+ }
106
+ catch { /* skip unreadable */ }
107
+ }
108
+ }
109
+ walk(resolve(dir));
110
+ return files;
111
+ }
112
+ /**
113
+ * Run a full security audit — single source of truth.
114
+ * Orchestrates code scan, secret scan, dependency scan, config audit,
115
+ * taint analysis, and auth coverage in one call.
116
+ */
117
+ export async function runFullAudit(path, options) {
118
+ const projectRoot = resolve(path);
119
+ const rules = getRules();
120
+ const allFindings = [];
121
+ const sections = [];
122
+ let filesScanned = 0;
123
+ let filesSkipped = 0;
124
+ let score = 100;
125
+ let grade = "A";
126
+ // Truncation tracking
127
+ let scanTruncated = false;
128
+ let scanTotalFindings = 0;
129
+ let scanMaxFindings = 50; // MAX_JSON_FINDINGS from scan-directory
130
+ let taintFilesProcessed = 0;
131
+ const taintFileCap = 200;
132
+ // --- Section 1: Code scan ---
133
+ try {
134
+ const codeJson = scanDirectory(projectRoot, true, [], "json", rules.length > 0 ? rules : undefined);
135
+ const parsed = safeJsonParse(codeJson);
136
+ if (parsed) {
137
+ const counts = parseSectionCounts(parsed);
138
+ filesScanned = parsed.metadata?.filesScanned ?? 0;
139
+ filesSkipped = parsed.metadata?.filesSkipped ?? 0;
140
+ score = parsed.summary?.score ?? 100;
141
+ grade = parsed.summary?.grade ?? "A";
142
+ sections.push({ name: "code", status: "ok", ...counts, details: `Grade ${grade} (${score}/100)` });
143
+ for (const f of parsed.findings ?? []) {
144
+ allFindings.push({ ruleId: f.id ?? "unknown", severity: f.severity, file: f.file ?? "", line: f.line ?? 0 });
145
+ }
146
+ if (parsed?.summary?.truncated) {
147
+ scanTruncated = true;
148
+ scanTotalFindings = parsed.summary.total ?? 0;
149
+ scanMaxFindings = parsed.summary.showing ?? 50;
150
+ }
151
+ }
152
+ }
153
+ catch {
154
+ sections.push({ name: "code", status: "error", findings: 0, critical: 0, high: 0, medium: 0, details: "Scan error" });
155
+ }
156
+ // --- Section 2: Secrets ---
157
+ if (!options?.skipSecrets) {
158
+ try {
159
+ const secretsJson = scanSecrets(projectRoot, true, "json");
160
+ const parsed = safeJsonParse(secretsJson);
161
+ if (parsed) {
162
+ const counts = parseSectionCounts(parsed);
163
+ sections.push({ name: "secrets", status: "ok", ...counts, details: counts.findings === 0 ? "No secrets found" : `${counts.findings} secret(s) detected` });
164
+ for (const f of parsed.findings ?? []) {
165
+ allFindings.push({ ruleId: `SECRET:${f.provider ?? "unknown"}`, severity: f.severity, file: f.file ?? "", line: f.line ?? 0 });
166
+ }
167
+ }
168
+ }
169
+ catch {
170
+ sections.push({ name: "secrets", status: "error", findings: 0, critical: 0, high: 0, medium: 0, details: "Scan error" });
171
+ }
172
+ }
173
+ // --- Section 3: Dependencies ---
174
+ if (!options?.skipDeps) {
175
+ const manifestPath = resolve(projectRoot, "package.json");
176
+ if (existsSync(manifestPath)) {
177
+ try {
178
+ const depsJson = await scanDependencies(manifestPath, "json");
179
+ const parsed = safeJsonParse(depsJson);
180
+ if (parsed) {
181
+ const vuln = parsed.summary?.vulnerable ?? 0;
182
+ const counts = { findings: vuln, critical: parsed.summary?.critical ?? 0, high: parsed.summary?.high ?? 0, medium: parsed.summary?.medium ?? 0 };
183
+ sections.push({ name: "dependencies", status: "ok", ...counts, details: vuln === 0 ? "No known CVEs" : `${vuln} vulnerable package(s)` });
184
+ for (const pkg of parsed.packages ?? []) {
185
+ for (const v of pkg.vulnerabilities ?? []) {
186
+ allFindings.push({ ruleId: `DEP:${v.id ?? "CVE"}`, severity: v.severity, file: "package.json", line: 0 });
187
+ }
188
+ }
189
+ }
190
+ }
191
+ catch {
192
+ sections.push({ name: "dependencies", status: "error", findings: 0, critical: 0, high: 0, medium: 0, details: "Scan error" });
193
+ }
194
+ }
195
+ else {
196
+ sections.push({ name: "dependencies", status: "skipped", findings: 0, critical: 0, high: 0, medium: 0, details: "No package.json found" });
197
+ }
198
+ }
199
+ // --- Section 4: Config audit ---
200
+ try {
201
+ const configJson = auditConfig(projectRoot, "json");
202
+ const parsed = safeJsonParse(configJson);
203
+ if (parsed) {
204
+ const counts = parseSectionCounts(parsed);
205
+ sections.push({ name: "config", status: "ok", ...counts, details: counts.findings === 0 ? "Config secure" : `${counts.findings} config issue(s)` });
206
+ for (const f of parsed.findings ?? []) {
207
+ allFindings.push({ ruleId: f.id ?? f.ruleId ?? "CONFIG", severity: f.severity ?? "medium", file: f.file ?? "", line: f.line ?? 0 });
208
+ }
209
+ }
210
+ }
211
+ catch {
212
+ sections.push({ name: "config", status: "error", findings: 0, critical: 0, high: 0, medium: 0, details: "No configs found" });
213
+ }
214
+ // --- Section 5: Taint analysis ---
215
+ try {
216
+ const jsFiles = collectJsFiles(projectRoot);
217
+ taintFilesProcessed = jsFiles.length;
218
+ if (jsFiles.length > 0) {
219
+ const { crossFileFindings, perFileFindings } = analyzeCrossFileTaint(jsFiles);
220
+ const perFileCount = Array.from(perFileFindings.values()).reduce((sum, f) => sum + f.length, 0);
221
+ const taintTotal = crossFileFindings.length + perFileCount;
222
+ const taintCritical = crossFileFindings.filter(f => f.severity === "critical").length;
223
+ const taintHigh = crossFileFindings.filter(f => f.severity === "high").length;
224
+ const taintMedium = taintTotal - taintCritical - taintHigh;
225
+ sections.push({ name: "taint", status: "ok", findings: taintTotal, critical: taintCritical, high: taintHigh, medium: taintMedium,
226
+ details: taintTotal === 0 ? "No tainted data flows" : `${taintTotal} tainted flow(s)` });
227
+ for (const f of crossFileFindings) {
228
+ allFindings.push({ ruleId: `TAINT:${f.sink.type}`, severity: f.severity, file: f.source.file, line: f.source.line });
229
+ }
230
+ }
231
+ }
232
+ catch {
233
+ sections.push({ name: "taint", status: "error", findings: 0, critical: 0, high: 0, medium: 0, details: "Analysis error" });
234
+ }
235
+ // --- Section 6: Auth coverage ---
236
+ try {
237
+ const jsFiles = collectJsFiles(projectRoot);
238
+ const routeFiles = jsFiles.filter(f => /\/(route|page)\.(ts|tsx|js|jsx)$/.test(f.path));
239
+ const layoutFiles = jsFiles.filter(f => /\/layout\.(ts|tsx|js|jsx)$/.test(f.path));
240
+ if (routeFiles.length > 0) {
241
+ const middlewareFile = jsFiles.find(f => /middleware\.(ts|js)$/.test(f.path));
242
+ const report = analyzeAuthCoverage(routeFiles, middlewareFile?.content ?? "", layoutFiles);
243
+ const unprotected = report.unprotectedRoutes;
244
+ sections.push({ name: "auth-coverage", status: "ok", findings: unprotected, critical: 0, high: unprotected > 0 ? unprotected : 0, medium: 0,
245
+ details: `${report.protectedRoutes}/${report.totalRoutes} routes protected (${report.middlewareCoveragePercent}% middleware)` });
246
+ }
247
+ }
248
+ catch { /* auth coverage is optional */ }
249
+ // --- Compute totals ---
250
+ const totalCritical = sections.reduce((s, sec) => s + sec.critical, 0);
251
+ const totalHigh = sections.reduce((s, sec) => s + sec.high, 0);
252
+ const totalMedium = sections.reduce((s, sec) => s + sec.medium, 0);
253
+ const totalFindings = sections.reduce((s, sec) => s + sec.findings, 0);
254
+ const rulesApplied = rules.length > 0 ? rules.length : 334;
255
+ const verdict = computeVerdict(totalCritical, totalHigh, totalMedium);
256
+ const coverage = computeCoverage(filesScanned, filesSkipped, rulesApplied);
257
+ const resultHash = computeResultHash(allFindings);
258
+ // Action items
259
+ const actionItems = [];
260
+ if (totalCritical > 0)
261
+ actionItems.push(`Fix ${totalCritical} critical finding(s) immediately`);
262
+ if (totalHigh > 0)
263
+ actionItems.push(`Address ${totalHigh} high severity finding(s)`);
264
+ const secretSection = sections.find(s => s.name === "secrets");
265
+ if (secretSection && secretSection.findings > 0)
266
+ actionItems.push("Rotate exposed secrets and add to .gitignore");
267
+ const depSection = sections.find(s => s.name === "dependencies");
268
+ if (depSection && depSection.findings > 0)
269
+ actionItems.push("Update vulnerable dependencies");
270
+ const authSection = sections.find(s => s.name === "auth-coverage");
271
+ if (authSection && authSection.findings > 0)
272
+ actionItems.push(`Add auth guards to ${authSection.findings} unprotected route(s)`);
273
+ if (verdict === "PASS")
274
+ actionItems.push("No action required — project verified secure");
275
+ return {
276
+ verdict,
277
+ score,
278
+ grade,
279
+ coverage,
280
+ resultHash,
281
+ timestamp: new Date().toISOString(),
282
+ sections,
283
+ truncation: {
284
+ truncated: scanTruncated || taintFilesProcessed >= taintFileCap,
285
+ maxFindings: scanMaxFindings,
286
+ totalFindings: scanTotalFindings || totalFindings,
287
+ taintFileCap,
288
+ taintFilesProcessed,
289
+ },
290
+ summary: { totalFindings, critical: totalCritical, high: totalHigh, medium: totalMedium },
291
+ actionItems,
292
+ };
293
+ }
294
+ // --- Formatter ---
295
+ /**
296
+ * Format audit result as markdown or JSON.
297
+ */
298
+ export function formatAuditResult(result, format) {
299
+ if (format === "json") {
300
+ return JSON.stringify(result);
301
+ }
302
+ const verdictLabel = {
303
+ PASS: "PASS — Project verified secure",
304
+ WARN: "WARN — High severity issues found",
305
+ FAIL: "FAIL — Critical security issues detected",
306
+ };
307
+ const lines = [
308
+ `# GuardVibe Full Audit Report`,
309
+ ``,
310
+ `## Verdict: ${result.verdict}`,
311
+ ``,
312
+ `**${verdictLabel[result.verdict]}**`,
313
+ ``,
314
+ `| Metric | Value |`,
315
+ `|--------|-------|`,
316
+ `| Score | ${result.grade} (${result.score}/100) |`,
317
+ `| Total findings | ${result.summary.totalFindings} |`,
318
+ `| Critical | ${result.summary.critical} |`,
319
+ `| High | ${result.summary.high} |`,
320
+ `| Medium | ${result.summary.medium} |`,
321
+ `| Result hash | \`${result.resultHash}\` |`,
322
+ ``,
323
+ `## Coverage`,
324
+ ``,
325
+ `| Metric | Value |`,
326
+ `|--------|-------|`,
327
+ `| Files scanned | ${result.coverage.filesScanned} |`,
328
+ `| Files skipped | ${result.coverage.filesSkipped} |`,
329
+ `| Coverage | ${result.coverage.coveragePercent}% |`,
330
+ `| Rules applied | ${result.coverage.rulesApplied} |`,
331
+ ``,
332
+ `## Sections`,
333
+ ``,
334
+ `| Section | Status | Findings | Critical | High | Medium | Details |`,
335
+ `|---------|--------|----------|----------|------|--------|---------|`,
336
+ ];
337
+ const statusIcon = { ok: "ok", error: "ERROR", skipped: "skipped" };
338
+ for (const s of result.sections) {
339
+ lines.push(`| ${s.name} | ${statusIcon[s.status] ?? s.status} | ${s.findings} | ${s.critical} | ${s.high} | ${s.medium} | ${s.details} |`);
340
+ }
341
+ if (result.truncation.truncated) {
342
+ lines.push(``);
343
+ lines.push(`## Truncation Notice`);
344
+ lines.push(``);
345
+ if (result.truncation.totalFindings > result.truncation.maxFindings) {
346
+ lines.push(`- Code scan: showing ${result.truncation.maxFindings} of ${result.truncation.totalFindings} findings (sorted by severity)`);
347
+ }
348
+ if (result.truncation.taintFilesProcessed >= result.truncation.taintFileCap) {
349
+ lines.push(`- Taint analysis: capped at ${result.truncation.taintFileCap} files (${result.truncation.taintFilesProcessed} found)`);
350
+ }
351
+ }
352
+ if (result.actionItems.length > 0) {
353
+ lines.push(``);
354
+ lines.push(`## Action Items`);
355
+ lines.push(``);
356
+ for (const item of result.actionItems) {
357
+ lines.push(`- ${item}`);
358
+ }
359
+ }
360
+ lines.push(``);
361
+ lines.push(`---`);
362
+ lines.push(`Timestamp: ${result.timestamp}`);
363
+ lines.push(`Result hash: \`${result.resultHash}\` (same code + same GuardVibe version = same hash)`);
364
+ return lines.join("\n");
365
+ }
@@ -49,11 +49,13 @@ export function scanDirectory(path, recursive = true, exclude = [], format = "ma
49
49
  const excludes = new Set([...DEFAULT_EXCLUDES, ...exclude, ...config.scan.exclude]);
50
50
  const maxSize = config.scan.maxFileSize;
51
51
  const filePaths = [];
52
- walkDirectory(scanRoot, recursive, excludes, filePaths);
52
+ const unsupportedFiles = [];
53
+ walkDirectory(scanRoot, recursive, excludes, filePaths, unsupportedFiles);
53
54
  const scanResults = [];
54
55
  const skippedFiles = [];
55
56
  const fileHashes = {};
56
57
  const effectiveRules = rules ?? [];
58
+ const unsupportedTypeCount = unsupportedFiles.length;
57
59
  for (const filePath of filePaths) {
58
60
  try {
59
61
  const stat = statSync(filePath);
@@ -145,7 +147,12 @@ export function scanDirectory(path, recursive = true, exclude = [], format = "ma
145
147
  low: allFindings.filter(f => f.rule.severity === "low").length,
146
148
  blocked: totalCritical > 0 || totalHigh > 0,
147
149
  grade, score,
148
- ...(truncated ? { truncated: true, showing: MAX_JSON_FINDINGS, message: `Showing top ${MAX_JSON_FINDINGS} of ${allFindings.length} findings (sorted by severity). Use scan_file on individual files for full details.` } : {}),
150
+ ...(truncated ? { truncated: true, showing: MAX_JSON_FINDINGS, totalBeforeTruncation: allFindings.length, message: `Showing top ${MAX_JSON_FINDINGS} of ${allFindings.length} findings (sorted by severity). Use scan_file on individual files for full details.` } : {}),
151
+ filesSkippedReasons: {
152
+ tooLarge: skippedFiles.filter(r => r.includes("too large")).length,
153
+ readError: skippedFiles.filter(r => r.includes("read error")).length,
154
+ unsupportedType: unsupportedTypeCount,
155
+ },
149
156
  },
150
157
  metadata,
151
158
  findings: limitedFindings.map(f => ({
@@ -255,8 +262,18 @@ export function scanDirectory(path, recursive = true, exclude = [], format = "ma
255
262
  else {
256
263
  lines.push(`## No Issues Found`, ``, `All files passed security checks.`);
257
264
  }
258
- if (skippedFiles.length > 0) {
259
- lines.push(``, `**Skipped files:** ${skippedFiles.length}`);
265
+ const totalSkipped = skippedFiles.length + unsupportedTypeCount;
266
+ if (totalSkipped > 0) {
267
+ const parts = [];
268
+ const tooLargeCount = skippedFiles.filter(r => r.includes("too large")).length;
269
+ const readErrorCount = skippedFiles.filter(r => r.includes("read error")).length;
270
+ if (tooLargeCount > 0)
271
+ parts.push(`${tooLargeCount} too large (>${Math.round(maxSize / 1024)}KB)`);
272
+ if (readErrorCount > 0)
273
+ parts.push(`${readErrorCount} read error`);
274
+ if (unsupportedTypeCount > 0)
275
+ parts.push(`${unsupportedTypeCount} unsupported type`);
276
+ lines.push(``, `**${totalSkipped} files skipped:** ${parts.join(", ")}`);
260
277
  }
261
278
  // ── Priority Summary Table (always at the end, visible in terminal) ──
262
279
  if (totalIssues > 0) {
@@ -42,6 +42,19 @@ const TAINT_SINKS = [
42
42
  description: "User input flows into file read path, enabling directory traversal and sensitive file access.",
43
43
  fix: "Validate file paths against an allowlist. Use path.resolve() and check prefix." },
44
44
  ];
45
+ // Known sanitizers that neutralize taint
46
+ const SANITIZERS = [
47
+ /DOMPurify\.sanitize\s*\(/,
48
+ /escapeHtml\s*\(/,
49
+ /encodeURIComponent\s*\(/,
50
+ /encodeURI\s*\(/,
51
+ /parseInt\s*\(/,
52
+ /Number\s*\(/,
53
+ /parseFloat\s*\(/,
54
+ /validator\.escape\s*\(/,
55
+ /sanitizeHtml\s*\(/,
56
+ /xss\s*\(/,
57
+ ];
45
58
  function extractAssignments(lines) {
46
59
  const assignments = [];
47
60
  const assignPattern = /(?:const|let|var)\s+([\w]+)\s*=\s*(.*)/;
@@ -51,14 +64,18 @@ function extractAssignments(lines) {
51
64
  continue;
52
65
  const varName = match[1];
53
66
  const value = match[2];
67
+ // Check if value is wrapped in a known sanitizer — if so, it's not tainted
68
+ const isSanitized = SANITIZERS.some(s => s.test(value));
54
69
  let tainted = false;
55
70
  let sourceType;
56
- for (const source of TAINT_SOURCES) {
57
- source.pattern.lastIndex = 0;
58
- if (source.pattern.test(value)) {
59
- tainted = true;
60
- sourceType = source.type;
61
- break;
71
+ if (!isSanitized) {
72
+ for (const source of TAINT_SOURCES) {
73
+ source.pattern.lastIndex = 0;
74
+ if (source.pattern.test(value)) {
75
+ tainted = true;
76
+ sourceType = source.type;
77
+ break;
78
+ }
62
79
  }
63
80
  }
64
81
  assignments.push({ name: varName, line: i + 1, tainted, sourceType });
@@ -68,7 +85,7 @@ function extractAssignments(lines) {
68
85
  function propagateTaint(assignments, lines) {
69
86
  let changed = true;
70
87
  let iterations = 0;
71
- while (changed && iterations < 10) {
88
+ while (changed && iterations < 25) {
72
89
  changed = false;
73
90
  iterations++;
74
91
  const taintedNames = new Set(assignments.filter(a => a.tainted).map(a => a.name));
@@ -76,6 +93,10 @@ function propagateTaint(assignments, lines) {
76
93
  if (assignment.tainted)
77
94
  continue;
78
95
  const lineContent = lines[assignment.line - 1] ?? "";
96
+ // Skip propagation if the value is wrapped in a sanitizer
97
+ const isSanitized = SANITIZERS.some(s => s.test(lineContent));
98
+ if (isSanitized)
99
+ continue;
79
100
  for (const name of taintedNames) {
80
101
  if (lineContent.includes(name) && name !== assignment.name) {
81
102
  assignment.tainted = true;
@@ -10,5 +10,6 @@
10
10
  * @param recursive - Whether to descend into subdirectories
11
11
  * @param excludes - Set of directory names to skip
12
12
  * @param results - Accumulator array (mutated in place)
13
+ * @param unsupportedResults - Optional accumulator for files with unsupported types
13
14
  */
14
- export declare function walkDirectory(dir: string, recursive: boolean, excludes: Set<string>, results: string[]): void;
15
+ export declare function walkDirectory(dir: string, recursive: boolean, excludes: Set<string>, results: string[], unsupportedResults?: string[]): void;
@@ -13,8 +13,9 @@ import { EXTENSION_MAP, CONFIG_FILE_MAP } from "./constants.js";
13
13
  * @param recursive - Whether to descend into subdirectories
14
14
  * @param excludes - Set of directory names to skip
15
15
  * @param results - Accumulator array (mutated in place)
16
+ * @param unsupportedResults - Optional accumulator for files with unsupported types
16
17
  */
17
- export function walkDirectory(dir, recursive, excludes, results) {
18
+ export function walkDirectory(dir, recursive, excludes, results, unsupportedResults) {
18
19
  let entries;
19
20
  try {
20
21
  entries = readdirSync(dir, { withFileTypes: true });
@@ -27,19 +28,26 @@ export function walkDirectory(dir, recursive, excludes, results) {
27
28
  continue;
28
29
  const fullPath = join(dir, entry.name);
29
30
  if (entry.isDirectory() && recursive) {
30
- walkDirectory(fullPath, recursive, excludes, results);
31
+ walkDirectory(fullPath, recursive, excludes, results, unsupportedResults);
31
32
  }
32
33
  else if (entry.isFile()) {
33
34
  const ext = extname(entry.name).toLowerCase();
35
+ let matched = false;
34
36
  if (EXTENSION_MAP[ext]) {
35
37
  results.push(fullPath);
38
+ matched = true;
36
39
  }
37
40
  if (entry.name.startsWith("Dockerfile") || entry.name.endsWith(".dockerfile")) {
38
41
  if (!results.includes(fullPath))
39
42
  results.push(fullPath);
43
+ matched = true;
40
44
  }
41
45
  if (CONFIG_FILE_MAP[entry.name] && !results.includes(fullPath)) {
42
46
  results.push(fullPath);
47
+ matched = true;
48
+ }
49
+ if (!matched && unsupportedResults) {
50
+ unsupportedResults.push(fullPath);
43
51
  }
44
52
  }
45
53
  }
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "guardvibe",
3
- "version": "3.0.0",
3
+ "version": "3.0.3",
4
4
  "mcpName": "io.github.goklab/guardvibe",
5
- "description": "Security MCP for vibe coding. 334 rules, 31 tools, CLI + doctor. Host security: CVE-2025-59536 hook injection, CVE-2026-21852 base URL hijack, MCP config audit, AI host hardening. Plus Next.js, Supabase, Clerk, Stripe, Prisma, tRPC, Hono, GraphQL, Convex, Turso, Uploadthing, AI SDK, and the full AI-generated stack.",
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",
7
7
  "bin": {
8
8
  "guardvibe": "build/cli.js",