guardvibe 2.9.0 → 2.9.4
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/README.md +3 -3
- package/build/cli.js +0 -0
- package/build/index.js +7 -7
- package/build/tools/check-code.js +70 -2
- package/build/tools/check-project.js +14 -26
- package/build/tools/compliance-report.js +18 -11
- package/build/tools/scan-directory.js +51 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -177,8 +177,8 @@ Dockerfile security, GitHub Actions CI/CD, Terraform (S3, IAM, RDS, security gro
|
|
|
177
177
|
### Secrets & Environment
|
|
178
178
|
API keys (AWS, GitHub, Stripe, OpenAI, Resend, Turso), .env management, .gitignore coverage, high-entropy detection, NEXT_PUBLIC exposure
|
|
179
179
|
|
|
180
|
-
### Compliance
|
|
181
|
-
SOC2, PCI-DSS, HIPAA, GDPR, ISO27001, EU AI Act (EUAIACT)
|
|
180
|
+
### Compliance Control Mapping
|
|
181
|
+
Maps security findings to SOC2, PCI-DSS, HIPAA, GDPR, ISO27001, and EU AI Act (EUAIACT) controls. Identifies which code-level vulnerabilities are relevant to specific compliance requirements. **Not a substitute for professional compliance audits.**
|
|
182
182
|
|
|
183
183
|
### Supply Chain
|
|
184
184
|
Malicious postinstall scripts, unpinned GitHub Actions, typosquat detection
|
|
@@ -195,7 +195,7 @@ Malicious postinstall scripts, unpinned GitHub Actions, typosquat detection
|
|
|
195
195
|
| `scan_secrets` | Detect leaked secrets, API keys, tokens |
|
|
196
196
|
| `check_dependencies` | Check individual packages against OSV |
|
|
197
197
|
| `check_package_health` | Typosquat detection, maintenance status, adoption metrics |
|
|
198
|
-
| `compliance_report` | SOC2
|
|
198
|
+
| `compliance_report` | Map security findings to compliance controls (SOC2, PCI-DSS, HIPAA, GDPR, ISO27001, EU AI Act) |
|
|
199
199
|
| `export_sarif` | SARIF v2.1.0 export for CI/CD integration |
|
|
200
200
|
| `get_security_docs` | Security best practices and guides |
|
|
201
201
|
| `fix_code` | **Auto-fix suggestions** with concrete patches for AI agents |
|
package/build/cli.js
CHANGED
|
File without changes
|
package/build/index.js
CHANGED
|
@@ -43,7 +43,7 @@ import { fixCode as fixCodeTool } from "./tools/fix-code.js";
|
|
|
43
43
|
const server = new McpServer({
|
|
44
44
|
name: "guardvibe",
|
|
45
45
|
version: pkg.version,
|
|
46
|
-
description: "Security MCP for vibe coding. 334 security rules and 31 tools covering OWASP, Next.js, Supabase, Stripe, Clerk, Prisma, Hono, AI SDK, MCP server security, and host environment hardening. Scans code, dependencies, secrets, configs, and git history.
|
|
46
|
+
description: "Security MCP for vibe coding. 334 security rules and 31 tools covering OWASP, Next.js, Supabase, Stripe, Clerk, Prisma, Hono, AI SDK, MCP server security, and host environment hardening. Scans code, dependencies, secrets, configs, and git history. Maps security findings to compliance controls (SOC2, PCI-DSS, HIPAA, GDPR, ISO27001, EU AI Act). Runs 100% locally with zero configuration. Note: GuardVibe is a security scanner, not a compliance auditor — it helps identify code-level issues relevant to compliance frameworks but does not replace professional compliance audits.",
|
|
47
47
|
});
|
|
48
48
|
// Tool 1: Analyze code for security vulnerabilities
|
|
49
49
|
server.tool("check_code", "Analyze code for security vulnerabilities (OWASP Top 10, XSS, SQL injection, insecure patterns). Use this when reviewing or writing code to catch security issues early.", {
|
|
@@ -235,7 +235,7 @@ server.tool("scan_staged", "Scan git-staged files for security vulnerabilities b
|
|
|
235
235
|
return { content: [{ type: "text", text: results + summary }] };
|
|
236
236
|
});
|
|
237
237
|
// Tool 9: Generate compliance-focused security report
|
|
238
|
-
server.tool("compliance_report", "
|
|
238
|
+
server.tool("compliance_report", "Map security scan findings to compliance framework controls (SOC2, PCI-DSS, HIPAA, GDPR, ISO27001, EUAIACT). Scans a directory and groups code-level security issues by the compliance control they relate to. Includes exploit scenarios and remediation evidence for each finding. Use mode=executive for a risk summary. Note: this maps code vulnerabilities to controls — it does not perform a compliance audit or replace professional assessment.", {
|
|
239
239
|
path: z.string().describe("Directory to scan"),
|
|
240
240
|
framework: z.enum(["SOC2", "PCI-DSS", "HIPAA", "GDPR", "ISO27001", "EUAIACT", "all"]).describe("Compliance framework"),
|
|
241
241
|
format: z.enum(["markdown", "json"]).default("markdown").describe("Output format: markdown (human) or json (machine-readable for agents)"),
|
|
@@ -590,7 +590,7 @@ server.tool("security_workflow", "Get the recommended GuardVibe security workflo
|
|
|
590
590
|
"pr_review",
|
|
591
591
|
"new_project",
|
|
592
592
|
"fix_vulnerabilities",
|
|
593
|
-
"
|
|
593
|
+
"compliance_mapping",
|
|
594
594
|
"dependency_check",
|
|
595
595
|
]).describe("What you are currently doing"),
|
|
596
596
|
}, async ({ task }) => {
|
|
@@ -642,11 +642,11 @@ server.tool("security_workflow", "Get the recommended GuardVibe security workflo
|
|
|
642
642
|
],
|
|
643
643
|
},
|
|
644
644
|
compliance_audit: {
|
|
645
|
-
task: "
|
|
646
|
-
description: "
|
|
645
|
+
task: "compliance_mapping",
|
|
646
|
+
description: "Map security findings to compliance framework controls. This identifies code-level issues relevant to specific controls — it does not replace professional compliance audits.",
|
|
647
647
|
steps: [
|
|
648
|
-
{ tool: "compliance_report", params: { path: ".", framework: "<SOC2|PCI-DSS|HIPAA|GDPR|ISO27001|EUAIACT>", format: "json" }, purpose: "
|
|
649
|
-
{ tool: "explain_remediation", params: { ruleId: "<id>" }, purpose: "Get
|
|
648
|
+
{ tool: "compliance_report", params: { path: ".", framework: "<SOC2|PCI-DSS|HIPAA|GDPR|ISO27001|EUAIACT>", format: "json" }, purpose: "Scan code and map findings to compliance controls." },
|
|
649
|
+
{ tool: "explain_remediation", params: { ruleId: "<id>" }, purpose: "Get remediation guidance and fix strategies for each finding.", condition: "for each finding" },
|
|
650
650
|
],
|
|
651
651
|
},
|
|
652
652
|
dependency_check: {
|
|
@@ -175,6 +175,37 @@ function hasRoleCheckPattern(code) {
|
|
|
175
175
|
return true;
|
|
176
176
|
return false;
|
|
177
177
|
}
|
|
178
|
+
/**
|
|
179
|
+
* Known legitimate npm packages with suspicious-looking prefixes.
|
|
180
|
+
* These are widely-used packages that trigger VG872/VG873 false positives.
|
|
181
|
+
*/
|
|
182
|
+
const LEGITIMATE_PREFIXED_PACKAGES = new Set([
|
|
183
|
+
"fast-glob", "fast-deep-equal", "fast-json-stable-stringify", "fast-json-stringify",
|
|
184
|
+
"fast-xml-parser", "fast-diff", "fast-levenshtein", "fast-redact", "fast-check",
|
|
185
|
+
"fast-uri", "fast-querystring", "fast-decode-uri-component", "fast-content-type-parse",
|
|
186
|
+
"safe-array-concat", "safe-stable-stringify", "safe-buffer", "safe-regex",
|
|
187
|
+
"safe-regex-test", "safe-push-apply",
|
|
188
|
+
"simple-git", "simple-update-notifier", "simple-swizzle", "simple-concat",
|
|
189
|
+
"native-promise-only", "native-url",
|
|
190
|
+
"pure-rand",
|
|
191
|
+
"clean-css", "clean-stack",
|
|
192
|
+
"modern-normalize", "modern-ahocorasick",
|
|
193
|
+
"enhanced-resolve",
|
|
194
|
+
"better-sqlite3", "better-opn",
|
|
195
|
+
"super-json",
|
|
196
|
+
"ultra-runner",
|
|
197
|
+
"core-js", "core-js-compat", "core-util-is", "core-js-pure",
|
|
198
|
+
"common-tags", "common-path-prefix",
|
|
199
|
+
"base-x", "base64-js",
|
|
200
|
+
"internal-slot", "internal-ip",
|
|
201
|
+
"shared-utils",
|
|
202
|
+
"original-url", "original-fs",
|
|
203
|
+
"secure-json-parse",
|
|
204
|
+
"native-run",
|
|
205
|
+
]);
|
|
206
|
+
function isLegitimatePackage(name) {
|
|
207
|
+
return LEGITIMATE_PREFIXED_PACKAGES.has(name);
|
|
208
|
+
}
|
|
178
209
|
/**
|
|
179
210
|
* Calculate confidence level for a finding based on file context and match quality.
|
|
180
211
|
*/
|
|
@@ -214,6 +245,13 @@ export function analyzeCode(code, language, framework, filePath, configDir, rule
|
|
|
214
245
|
// Pre-analyze: detect auth guards and role checks pattern-agnostically
|
|
215
246
|
let codeHasAuthGuard = hasAuthGuardPattern(code);
|
|
216
247
|
const codeHasRoleCheck = hasRoleCheckPattern(code);
|
|
248
|
+
// Pre-analyze: detect fix patterns to suppress false positives after remediation
|
|
249
|
+
const codeHasSanitization = /(?:DOMPurify\.sanitize|sanitize(?:Html|HTML)|xss\s*\(|purify\s*\(|escapeHtml|sanitizeHtml)\s*\(/i.test(code);
|
|
250
|
+
const codeHasUrlValidation = /(?:(?:validate|verify|check|safe|allowed)(?:Url|URL|Uri|URI)|(?:ALLOWED_(?:HOSTS|URLS|ORIGINS|DOMAINS))|(?:allowlist|whitelist|safelist)[\s\S]{0,50}?(?:includes|has|match))/i.test(code);
|
|
251
|
+
const codeHasUuidFilename = /(?:randomUUID|nanoid|uuidv4|v4\s*\(\)|crypto\.randomUUID)\s*\(/i.test(code);
|
|
252
|
+
const codeHasCronVerification = /(?:verify|validate|check)(?:Cron|Secret|Auth|Signature)\s*\(/i.test(code);
|
|
253
|
+
const isMigrationFile = filePath ? /(?:migrations?|supabase\/migrations|seeds?|fixtures)\//i.test(filePath) : false;
|
|
254
|
+
const isPeerDeps = /["']peerDependencies["']/i.test(code);
|
|
217
255
|
// Config: check custom auth function names from .guardviberc
|
|
218
256
|
if (!codeHasAuthGuard && config.authFunctions && config.authFunctions.length > 0) {
|
|
219
257
|
const customPattern = new RegExp(`(?:${config.authFunctions.join("|")})\\s*\\(`, "i");
|
|
@@ -264,9 +302,25 @@ export function analyzeCode(code, language, framework, filePath, configDir, rule
|
|
|
264
302
|
// Skip npm package rules (VG863/VG864/VG865): only apply to package.json files
|
|
265
303
|
if ((rule.id === "VG863" || rule.id === "VG864" || rule.id === "VG865") && filePath && !filePath.endsWith("package.json"))
|
|
266
304
|
continue;
|
|
267
|
-
// Skip destructive DDL rules (VG540-VG542) in migration directories
|
|
268
|
-
if (rule.id.startsWith("VG54")
|
|
305
|
+
// Skip destructive DDL rules (VG540-VG542) and view rules (VG439) in migration directories
|
|
306
|
+
if ((rule.id.startsWith("VG54") || rule.id === "VG439") && isMigrationFile)
|
|
307
|
+
continue;
|
|
308
|
+
// Skip innerHTML/XSS rules when DOMPurify or sanitization is present
|
|
309
|
+
if (codeHasSanitization && ["VG408", "VG012", "VG042"].includes(rule.id))
|
|
310
|
+
continue;
|
|
311
|
+
// Skip SSRF rules when URL validation/allowlist pattern is present
|
|
312
|
+
if (codeHasUrlValidation && ["VG120"].includes(rule.id))
|
|
313
|
+
continue;
|
|
314
|
+
// Skip filename rules when UUID-based filename generation is present
|
|
315
|
+
if (codeHasUuidFilename && rule.id === "VG993")
|
|
269
316
|
continue;
|
|
317
|
+
// Skip cron secret rules when custom verification function is present
|
|
318
|
+
if (codeHasCronVerification && ["VG968", "VG503"].includes(rule.id))
|
|
319
|
+
continue;
|
|
320
|
+
// Skip CVE version rules in peerDependencies (ranges, not actual versions)
|
|
321
|
+
if (isPeerDeps && rule.id === "VG903")
|
|
322
|
+
continue;
|
|
323
|
+
// VG872/VG873 legitimate package filtering is handled at match level below
|
|
270
324
|
// Skip server-only import rule (VG964) for files that are inherently server-only:
|
|
271
325
|
// Route Handlers (app/api/), middleware, instrumentation, next.config,
|
|
272
326
|
// lib/, utils/, tools/, server/, scripts/, CLI files, config files
|
|
@@ -324,6 +378,20 @@ export function analyzeCode(code, language, framework, filePath, configDir, rule
|
|
|
324
378
|
if (isHumanReadableString(lines, lineNumber))
|
|
325
379
|
continue;
|
|
326
380
|
}
|
|
381
|
+
// Skip supply chain rules for known legitimate packages
|
|
382
|
+
if (["VG872", "VG873"].includes(rule.id)) {
|
|
383
|
+
const pkgMatch = /"([\w@/-]+)"/.exec(match[0]);
|
|
384
|
+
if (pkgMatch && isLegitimatePackage(pkgMatch[1]))
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
// Skip VG903 React version in peerDependencies sections
|
|
388
|
+
if (rule.id === "VG903") {
|
|
389
|
+
const beforeText = code.substring(0, match.index);
|
|
390
|
+
const lastPeer = beforeText.lastIndexOf("peerDependencies");
|
|
391
|
+
const lastDeps = Math.max(beforeText.lastIndexOf('"dependencies"'), beforeText.lastIndexOf('"devDependencies"'));
|
|
392
|
+
if (lastPeer > lastDeps)
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
327
395
|
findings.push({
|
|
328
396
|
rule: effectiveRule,
|
|
329
397
|
match: match[0].substring(0, 80),
|
|
@@ -130,37 +130,25 @@ export function checkProject(files, format = "markdown", rules) {
|
|
|
130
130
|
});
|
|
131
131
|
}
|
|
132
132
|
lines.push(`---`, ``);
|
|
133
|
-
// Per-file details
|
|
133
|
+
// Per-file details (truncated to prevent MCP output overflow)
|
|
134
|
+
const MAX_DETAIL_FINDINGS = 30;
|
|
135
|
+
let detailCount = 0;
|
|
134
136
|
for (const r of results) {
|
|
137
|
+
if (detailCount >= MAX_DETAIL_FINDINGS) {
|
|
138
|
+
const remaining = totalIssues - detailCount;
|
|
139
|
+
lines.push(``, `> **${remaining} more findings omitted.** Use \`check_code\` or \`scan_file\` on individual files for full details.`, ``);
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
135
142
|
const fileIssueCount = r.findings.length;
|
|
136
143
|
lines.push(`## File: ${r.path} (${fileIssueCount} issues)`, ``);
|
|
137
|
-
// Group findings by rule.id to match check-code formatting
|
|
138
|
-
const grouped = new Map();
|
|
139
144
|
for (const finding of r.findings) {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
grouped.set(finding.rule.id, [finding]);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
const sortedGroups = Array.from(grouped.entries()).sort(([, aFindings], [, bFindings]) => {
|
|
149
|
-
return (severityOrder[aFindings[0].rule.severity] ?? 99) - (severityOrder[bFindings[0].rule.severity] ?? 99);
|
|
150
|
-
});
|
|
151
|
-
for (const [, groupFindings] of sortedGroups) {
|
|
152
|
-
const first = groupFindings[0];
|
|
153
|
-
const icon = first.rule.severity.toUpperCase();
|
|
154
|
-
if (groupFindings.length > 2) {
|
|
155
|
-
const lineList = groupFindings.map((f) => `~${f.line}`).join(", ");
|
|
156
|
-
lines.push(`## [${icon}] ${first.rule.name} (${first.rule.id})`, ``, `**OWASP:** ${first.rule.owasp}`, `**Occurrences:** ${groupFindings.length} (lines: ${lineList})`, `**Example match:** \`${first.match}\``, ``, first.rule.description, ``, `**Fix:** ${first.rule.fix}`, ...(first.rule.fixCode ? [``, `**Secure code:**`, `\`\`\``, first.rule.fixCode, `\`\`\``] : []), ``, `---`, ``);
|
|
157
|
-
}
|
|
158
|
-
else {
|
|
159
|
-
for (const finding of groupFindings) {
|
|
160
|
-
lines.push(`## [${icon}] ${finding.rule.name} (${finding.rule.id})`, ``, `**OWASP:** ${finding.rule.owasp}`, `**Line:** ~${finding.line}`, `**Match:** \`${finding.match}\``, ``, finding.rule.description, ``, `**Fix:** ${finding.rule.fix}`, ...(finding.rule.fixCode ? [``, `**Secure code:**`, `\`\`\``, finding.rule.fixCode, `\`\`\``] : []), ``, `---`, ``);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
145
|
+
if (detailCount >= MAX_DETAIL_FINDINGS)
|
|
146
|
+
break;
|
|
147
|
+
const icon = finding.rule.severity.toUpperCase();
|
|
148
|
+
lines.push(`### [${icon}] ${finding.rule.name} (${finding.rule.id})`, `**Line:** ~${finding.line} | **Match:** \`${finding.match}\``, `**Fix:** ${finding.rule.fix}`, ``);
|
|
149
|
+
detailCount++;
|
|
163
150
|
}
|
|
151
|
+
lines.push(`---`, ``);
|
|
164
152
|
}
|
|
165
153
|
}
|
|
166
154
|
else {
|
|
@@ -77,16 +77,18 @@ export function complianceReport(path, framework, format = "markdown", rules, mo
|
|
|
77
77
|
}
|
|
78
78
|
// --- FULL MODE ---
|
|
79
79
|
const lines = [
|
|
80
|
-
`# GuardVibe Compliance
|
|
80
|
+
`# GuardVibe Compliance Control Mapping`,
|
|
81
|
+
``,
|
|
82
|
+
`> This report maps code-level security findings to ${framework} controls. It identifies vulnerabilities relevant to compliance requirements but is not a substitute for professional compliance audits.`,
|
|
81
83
|
``,
|
|
82
84
|
`Framework: ${framework}`,
|
|
83
85
|
`Directory: ${scanRoot}`,
|
|
84
86
|
`Files scanned: ${filePaths.length}`,
|
|
85
|
-
`
|
|
87
|
+
`Security issues mapped to controls: ${relevant.length}`,
|
|
86
88
|
``,
|
|
87
89
|
];
|
|
88
90
|
if (controlMap.size === 0) {
|
|
89
|
-
lines.push(`## No
|
|
91
|
+
lines.push(`## No Issues Found`, ``, `No code-level security issues mapped to ${framework} controls were found. This does not constitute a compliance certification.`);
|
|
90
92
|
return lines.join("\n");
|
|
91
93
|
}
|
|
92
94
|
// Sort controls
|
|
@@ -97,17 +99,20 @@ export function complianceReport(path, framework, format = "markdown", rules, mo
|
|
|
97
99
|
}
|
|
98
100
|
lines.push(``);
|
|
99
101
|
lines.push(`---`, ``);
|
|
102
|
+
const MAX_COMPLIANCE_FINDINGS = 50;
|
|
103
|
+
let complianceCount = 0;
|
|
100
104
|
for (const [control, items] of sortedControls) {
|
|
105
|
+
if (complianceCount >= MAX_COMPLIANCE_FINDINGS) {
|
|
106
|
+
lines.push(``, `> **Additional findings omitted.** Use \`scan_file\` on individual files for full details.`, ``);
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
101
109
|
lines.push(`## ${control}`, ``);
|
|
102
110
|
for (const item of items) {
|
|
111
|
+
if (complianceCount >= MAX_COMPLIANCE_FINDINGS)
|
|
112
|
+
break;
|
|
103
113
|
const f = item.finding;
|
|
104
114
|
lines.push(`- **[${f.rule.severity.toUpperCase()}]** ${f.rule.name} (${f.rule.id}) in \`${f.filePath}\`:${f.line}`);
|
|
105
|
-
|
|
106
|
-
lines.push(` - **Exploit scenario:** ${f.rule.exploit}`);
|
|
107
|
-
}
|
|
108
|
-
if (f.rule.audit) {
|
|
109
|
-
lines.push(` - **Audit evidence:** ${f.rule.audit}`);
|
|
110
|
-
}
|
|
115
|
+
complianceCount++;
|
|
111
116
|
}
|
|
112
117
|
lines.push(``);
|
|
113
118
|
}
|
|
@@ -120,7 +125,9 @@ function formatExecutiveSummary(framework, scanRoot, filesScanned, relevant, con
|
|
|
120
125
|
const total = critical + high + medium;
|
|
121
126
|
const riskLevel = critical > 0 ? "HIGH" : high > 0 ? "MEDIUM" : total > 0 ? "LOW" : "MINIMAL";
|
|
122
127
|
const lines = [
|
|
123
|
-
`#
|
|
128
|
+
`# Security Findings — ${framework} Control Mapping`,
|
|
129
|
+
``,
|
|
130
|
+
`> Maps code-level security findings to ${framework} controls. Not a compliance audit or certification.`,
|
|
124
131
|
``,
|
|
125
132
|
`**Framework:** ${framework} | **Date:** ${new Date().toISOString().split("T")[0]}`,
|
|
126
133
|
`**Directory:** ${scanRoot}`,
|
|
@@ -167,7 +174,7 @@ function formatExecutiveSummary(framework, scanRoot, filesScanned, relevant, con
|
|
|
167
174
|
}
|
|
168
175
|
}
|
|
169
176
|
// Compliance coverage
|
|
170
|
-
lines.push(`##
|
|
177
|
+
lines.push(`## Controls with Findings`, ``, `| Control | Status | Issues |`, `|---------|--------|--------|`);
|
|
171
178
|
const sortedControls = [...controlMap.entries()].sort((a, b) => a[0].localeCompare(b[0]));
|
|
172
179
|
for (const [control, items] of sortedControls) {
|
|
173
180
|
const hasCritical = items.some(i => i.finding.rule.severity === "critical");
|
|
@@ -127,8 +127,17 @@ export function scanDirectory(path, recursive = true, exclude = [], format = "ma
|
|
|
127
127
|
// baseline file unreadable, skip comparison
|
|
128
128
|
}
|
|
129
129
|
}
|
|
130
|
+
// MCP output size limit — large projects can produce 300K+ characters which
|
|
131
|
+
// exceeds Claude Code's max allowed tokens for tool results.
|
|
132
|
+
const MAX_JSON_FINDINGS = 50;
|
|
133
|
+
const MAX_MD_FINDINGS = 30;
|
|
130
134
|
if (format === "json") {
|
|
131
135
|
const findingsWithFiles = scanResults.flatMap(r => r.findings.map(f => ({ ...f, rule: f.rule, file: r.path })));
|
|
136
|
+
// Sort by severity: critical first, then high, then medium
|
|
137
|
+
const severityRank = { critical: 0, high: 1, medium: 2, low: 3, info: 4 };
|
|
138
|
+
findingsWithFiles.sort((a, b) => (severityRank[a.rule.severity] ?? 4) - (severityRank[b.rule.severity] ?? 4));
|
|
139
|
+
const truncated = findingsWithFiles.length > MAX_JSON_FINDINGS;
|
|
140
|
+
const limitedFindings = findingsWithFiles.slice(0, MAX_JSON_FINDINGS);
|
|
132
141
|
const baseJson = {
|
|
133
142
|
summary: {
|
|
134
143
|
total: allFindings.length,
|
|
@@ -136,12 +145,13 @@ export function scanDirectory(path, recursive = true, exclude = [], format = "ma
|
|
|
136
145
|
low: allFindings.filter(f => f.rule.severity === "low").length,
|
|
137
146
|
blocked: totalCritical > 0 || totalHigh > 0,
|
|
138
147
|
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.` } : {}),
|
|
139
149
|
},
|
|
140
150
|
metadata,
|
|
141
|
-
findings:
|
|
151
|
+
findings: limitedFindings.map(f => ({
|
|
142
152
|
id: f.rule.id, name: f.rule.name, severity: f.rule.severity,
|
|
143
153
|
owasp: f.rule.owasp, line: f.line, match: f.match, file: f.file,
|
|
144
|
-
fix: f.rule.fix,
|
|
154
|
+
fix: f.rule.fix,
|
|
145
155
|
})),
|
|
146
156
|
baseline: findingsToBaseline(scanResults),
|
|
147
157
|
};
|
|
@@ -224,11 +234,20 @@ export function scanDirectory(path, recursive = true, exclude = [], format = "ma
|
|
|
224
234
|
lines.push(`${i + 1}. **[${item.rule.severity.toUpperCase()}] ${item.rule.name}** (${item.rule.id}) — ${item.count} ${item.count === 1 ? "occurrence" : "occurrences"} in ${fileLabel}`, ` ${item.rule.fix}`, ``);
|
|
225
235
|
});
|
|
226
236
|
lines.push(`---`, ``);
|
|
237
|
+
let findingsPrinted = 0;
|
|
227
238
|
for (const result of scanResults) {
|
|
239
|
+
if (findingsPrinted >= MAX_MD_FINDINGS) {
|
|
240
|
+
const remaining = allFindings.length - findingsPrinted;
|
|
241
|
+
lines.push(``, `> **${remaining} more findings omitted.** Use \`scan_file\` on individual files for full details.`, ``);
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
228
244
|
lines.push(`## File: ${result.path} (${result.findings.length} issues)`, ``);
|
|
229
245
|
for (const f of result.findings) {
|
|
246
|
+
if (findingsPrinted >= MAX_MD_FINDINGS)
|
|
247
|
+
break;
|
|
230
248
|
const icon = f.rule.severity.toUpperCase();
|
|
231
|
-
lines.push(`### [${icon}] ${f.rule.name} (${f.rule.id})`, `**Line:** ~${f.line} | **Match:** \`${f.match}\``,
|
|
249
|
+
lines.push(`### [${icon}] ${f.rule.name} (${f.rule.id})`, `**Line:** ~${f.line} | **Match:** \`${f.match}\``, `**Fix:** ${f.rule.fix}`, ``);
|
|
250
|
+
findingsPrinted++;
|
|
232
251
|
}
|
|
233
252
|
lines.push(`---`, ``);
|
|
234
253
|
}
|
|
@@ -237,9 +256,35 @@ export function scanDirectory(path, recursive = true, exclude = [], format = "ma
|
|
|
237
256
|
lines.push(`## No Issues Found`, ``, `All files passed security checks.`);
|
|
238
257
|
}
|
|
239
258
|
if (skippedFiles.length > 0) {
|
|
240
|
-
lines.push(``, `**Skipped files
|
|
241
|
-
|
|
242
|
-
|
|
259
|
+
lines.push(``, `**Skipped files:** ${skippedFiles.length}`);
|
|
260
|
+
}
|
|
261
|
+
// ── Priority Summary Table (always at the end, visible in terminal) ──
|
|
262
|
+
if (totalIssues > 0) {
|
|
263
|
+
const severityOrder = { critical: 0, high: 1, medium: 2, low: 3, info: 4 };
|
|
264
|
+
const ruleStats = new Map();
|
|
265
|
+
for (const r of scanResults) {
|
|
266
|
+
for (const f of r.findings) {
|
|
267
|
+
const existing = ruleStats.get(f.rule.id);
|
|
268
|
+
if (existing) {
|
|
269
|
+
existing.files.add(r.path);
|
|
270
|
+
existing.count++;
|
|
271
|
+
}
|
|
272
|
+
else
|
|
273
|
+
ruleStats.set(f.rule.id, { rule: f.rule, files: new Set([r.path]), count: 1 });
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
const sorted = Array.from(ruleStats.values())
|
|
277
|
+
.sort((a, b) => (severityOrder[a.rule.severity] ?? 99) - (severityOrder[b.rule.severity] ?? 99))
|
|
278
|
+
.slice(0, 10);
|
|
279
|
+
lines.push(``, `---`, `## Priority Summary`, ``, `| # | Severity | Rule | Issue | Files | Count |`, `|---|----------|------|-------|-------|-------|`);
|
|
280
|
+
sorted.forEach((item, i) => {
|
|
281
|
+
const sev = item.rule.severity.toUpperCase();
|
|
282
|
+
lines.push(`| ${i + 1} | ${sev} | ${item.rule.id} | ${item.rule.name} | ${item.files.size} | ${item.count} |`);
|
|
283
|
+
});
|
|
284
|
+
if (ruleStats.size > 10) {
|
|
285
|
+
lines.push(``, `*+ ${ruleStats.size - 10} more rule types not shown*`);
|
|
286
|
+
}
|
|
287
|
+
lines.push(``);
|
|
243
288
|
}
|
|
244
289
|
lines.push(securityBanner({ total: totalIssues, critical: totalCritical, high: totalHigh, medium: totalMedium, score, grade, filesScanned: metadata.filesScanned }));
|
|
245
290
|
return lines.join("\n");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "guardvibe",
|
|
3
|
-
"version": "2.9.
|
|
3
|
+
"version": "2.9.4",
|
|
4
4
|
"mcpName": "io.github.goklab/guardvibe",
|
|
5
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.",
|
|
6
6
|
"type": "module",
|