guardvibe 3.0.51 → 3.0.53

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.
@@ -7,7 +7,7 @@ export const coreRules = [
7
7
  severity: "critical",
8
8
  owasp: "A01:2025 Broken Access Control",
9
9
  description: "Hardcoded passwords, API keys, or secrets detected in source code.",
10
- pattern: /(?:secret_?key|api_?key|api_?secret|private_?key|access_?key|password|passwd|pwd|auth_?token|jwt_?secret|app_?secret|master_?key|signing_?key|encryption_?key)\w*\s*[:=]\s*['"][^'"]{3,}['"]/gi,
10
+ pattern: /(?:secret_?key|api_?key|api_?secret|private_?key|access_?key|password|passwd|pwd|auth_?token|jwt_?secret|app_?secret|master_?key|signing_?key|encryption_?key)\w*\s*[:=]\s*['"][^'"\n]{3,}['"]/gi,
11
11
  languages: ["javascript", "typescript", "python", "go"],
12
12
  fix: "Use environment variables (process.env.SECRET) or a secrets manager. Never commit credentials to source code.",
13
13
  fixCode: "// Use environment variables instead\nconst password = process.env.DB_PASSWORD;\nconst apiKey = process.env.API_KEY;",
@@ -212,7 +212,7 @@ export const coreRules = [
212
212
  severity: "critical",
213
213
  owasp: "A07:2025 Auth Failures",
214
214
  description: "Variable named secret, password, or apiKey assigned a string literal. Secrets should come from environment variables or a secrets manager, never hardcoded in source.",
215
- pattern: /(?:(?:const|let|var|export)\s+)?(?:secret|password|passwd|apiKey|api_key|privateKey|private_key|signingKey|signing_key|encryptionKey|encryption_key|masterKey|master_key|dbPassword|db_password)\s*(?::\s*string\s*)?=\s*["'][^"']{8,}["']/gi,
215
+ pattern: /(?:(?:const|let|var|export)\s+)?(?:secret|password|passwd|apiKey|api_key|privateKey|private_key|signingKey|signing_key|encryptionKey|encryption_key|masterKey|master_key|dbPassword|db_password)\s*(?::\s*string\s*)?=\s*["'][^"'\n]{8,}["']/gi,
216
216
  languages: ["javascript", "typescript", "python"],
217
217
  fix: "Use environment variables: const secret = process.env.MY_SECRET. Never hardcode secrets in source code.",
218
218
  fixCode: "// Use environment variables\nconst secret = process.env.JWT_SECRET;\nconst apiKey = process.env.API_KEY;\n\n// In .env.local (never commit this file)\nJWT_SECRET=your-secret-here",
@@ -648,6 +648,11 @@ export function analyzeCode(code, language, framework, filePath, configDir, rule
648
648
  if (rule.id === "VG001" || rule.id === "VG062") {
649
649
  if (isHumanReadableString(lines, lineNumber))
650
650
  continue;
651
+ // Translation/locale files and event-tracker key maps are not credential stores —
652
+ // values are UI strings (`password: "Heslo"`) or analytics event names
653
+ // (`forgot_password: "forgot_password_clicked"`).
654
+ if (filePath && /(?:\/i18n\/|\/locales?\/|\/translations?\/|\/event[-_]tracker\/|\/analytics\/events\/|\/messages\/[a-z]{2}(?:[-_][A-Z]{2})?\.[jt]sx?$)/i.test(filePath))
655
+ continue;
651
656
  }
652
657
  // Skip credential rules when the variable name signals test/example/mock intent.
653
658
  // e.g. `testingPassword`, `examplePassword`, `mockApiKey`, `placeholderSecret`.
@@ -655,6 +660,15 @@ export function analyzeCode(code, language, framework, filePath, configDir, rule
655
660
  const matchedLine = lines[lineNumber - 1] ?? "";
656
661
  if (/(?:^|\s|\b)(?:testing|example|mock|placeholder|sample|demo|fake|dummy|stub|fixture)[A-Z_]/.test(matchedLine))
657
662
  continue;
663
+ // Skip TypeScript string-enum stringification: `INLINE_PASSWORD = "INLINE_PASSWORD"`.
664
+ // No real credential has identical name and value — canonical TS enum-key pattern.
665
+ // Covers both SCREAMING_SNAKE (TS string enums) and snake_case (event-tracker key maps).
666
+ if (/\b([A-Za-z_][A-Za-z0-9_]*)\s*[:=]\s*["']\1["']/.test(matchedLine))
667
+ continue;
668
+ // Skip SCREAMING_SNAKE error/status codes whose value is digits-only.
669
+ // e.g. `INVALID_PASSWORD = "5020"` — error code, not a credential.
670
+ if (/\b[A-Z][A-Z0-9_]*\s*=\s*["']\d+["']/.test(matchedLine))
671
+ continue;
658
672
  }
659
673
  // Skip VG010 (SQL injection) on Angular HTTP service calls — http.get/post/etc.
660
674
  // are HTTP client methods, not SQL. The existing pattern's `get` keyword catches them.
@@ -667,9 +681,21 @@ export function analyzeCode(code, language, framework, filePath, configDir, rule
667
681
  continue;
668
682
  // Also skip when matched call has no SQL keyword anywhere on the line — covers fetch/axios template-literal URLs.
669
683
  const hasSqlKeyword = /\b(?:SELECT|INSERT|UPDATE|DELETE|FROM|WHERE|JOIN|UNION|DROP|TRUNCATE|ALTER|CREATE\s+TABLE)\b/i.test(matchedLine);
670
- const isFetchOrAxios = /(?:fetch|axios|got|ky|undici|request)\s*[.\(]|axios\.(?:get|post|put|delete|patch)/i.test(matchedLine);
684
+ // Python `requests.get/post` and `requests.request(...)` are HTTP, not SQL.
685
+ const isFetchOrAxios = /(?:fetch|axios|got|ky|undici|requests?)\s*[.\(]|axios\.(?:get|post|put|delete|patch)/i.test(matchedLine);
671
686
  if (isFetchOrAxios && !hasSqlKeyword)
672
687
  continue;
688
+ // Skip when the first call argument is a URL path (starts with `/`) and no SQL keyword is on the line.
689
+ // Covers service-class HTTP wrappers like `this.get(`/api/...${id}/...`)` where the receiver
690
+ // isn't named http/api/client. SQL queries never start with `/`; URL paths always do.
691
+ const firstArgIsUrlPath = /\(\s*[`'"]\s*\/[a-zA-Z0-9_\-/{}$]/.test(matchedLine);
692
+ if (firstArgIsUrlPath && !hasSqlKeyword)
693
+ continue;
694
+ // Service-class HTTP wrapper: `this.get/post/...` with first arg starting from a URL-base var
695
+ // like `${this.basePath}/...` or `${apiUrl}/...`. SQL queries don't use `this.<verb>` style.
696
+ const isServiceVerbCall = /(?:^|[\s=])(?:return\s+|await\s+)?this\.(?:get|post|put|delete|patch|head|options|fetch|request)\s*\(/i.test(matchedLine);
697
+ if (isServiceVerbCall && !hasSqlKeyword)
698
+ continue;
673
699
  }
674
700
  // Skip supply chain rules for known legitimate packages
675
701
  if (["VG872", "VG873"].includes(rule.id)) {
@@ -223,11 +223,30 @@ export async function runFullAudit(path, options) {
223
223
  }
224
224
  // --- Section 3: Dependencies ---
225
225
  if (!options?.skipDeps) {
226
- const manifestPath = resolve(projectRoot, "package.json");
226
+ // Prefer lockfiles for resolved (installed) versions over package.json spec ranges.
227
+ // package.json says `"@clerk/nextjs": "^7.0.1"` but the installed version may be 7.2.8.
228
+ // OSV needs the resolved version to give accurate vuln matches; spec lower bound
229
+ // produces false positives where the bug was already patched in a higher minor.
230
+ const lockCandidates = ["package-lock.json", "npm-shrinkwrap.json", "yarn.lock", "pnpm-lock.yaml"];
231
+ let manifestPath = resolve(projectRoot, "package.json");
232
+ for (const lock of lockCandidates) {
233
+ const lockPath = resolve(projectRoot, lock);
234
+ if (existsSync(lockPath)) {
235
+ manifestPath = lockPath;
236
+ break;
237
+ }
238
+ }
227
239
  if (existsSync(manifestPath)) {
228
240
  try {
229
241
  const depsJson = await scanDependencies(manifestPath, "json");
230
242
  const parsed = safeJsonParse(depsJson);
243
+ if (!parsed) {
244
+ // scanDependencies returned a markdown error (OSV API unreachable, parse failed, etc).
245
+ // Push a dependencies section so downstream consumers always see it; surface the
246
+ // error in `details` so users can troubleshoot instead of silently missing the section.
247
+ const errSnippet = (depsJson.match(/Error:[^\n]+/) || ["Scan failed"])[0];
248
+ sections.push({ name: "dependencies", status: "error", findings: 0, critical: 0, high: 0, medium: 0, details: errSnippet });
249
+ }
231
250
  if (parsed) {
232
251
  const vulnPackages = parsed.summary?.vulnerable ?? 0;
233
252
  const depFindings = [];
@@ -15,47 +15,51 @@ export async function queryOsv(name, version, ecosystem) {
15
15
  return data.vulns ?? [];
16
16
  }
17
17
  export async function queryOsvBatch(packages) {
18
- const queries = packages.map(pkg => ({
19
- package: { name: pkg.name, ecosystem: pkg.ecosystem },
20
- version: pkg.version,
21
- }));
22
- const response = await fetch("https://api.osv.dev/v1/querybatch", {
23
- method: "POST",
24
- headers: { "Content-Type": "application/json" },
25
- body: JSON.stringify({ queries }),
26
- signal: AbortSignal.timeout(10000),
27
- });
28
18
  const results = new Map();
29
- if (!response.ok) {
30
- throw new Error(`OSV batch API error: ${response.status} ${response.statusText}`);
31
- }
32
- const data = await response.json();
33
- // Batch API returns minimal vuln info (just id). Fetch full details for each.
34
- for (let i = 0; i < packages.length; i++) {
35
- const key = `${packages[i].name}@${packages[i].version}`;
36
- const batchVulns = data.results[i]?.vulns || [];
37
- if (batchVulns.length === 0) {
38
- results.set(key, []);
39
- continue;
19
+ // OSV batch can time out on large monorepo lockfiles (1000+ packages),
20
+ // and very-large requests can be rate-limited. Chunk into 500-pkg batches
21
+ // and process sequentially so the per-request timeout is generous.
22
+ const CHUNK_SIZE = 500;
23
+ for (let start = 0; start < packages.length; start += CHUNK_SIZE) {
24
+ const chunk = packages.slice(start, start + CHUNK_SIZE);
25
+ const queries = chunk.map(pkg => ({
26
+ package: { name: pkg.name, ecosystem: pkg.ecosystem },
27
+ version: pkg.version,
28
+ }));
29
+ const response = await fetch("https://api.osv.dev/v1/querybatch", {
30
+ method: "POST",
31
+ headers: { "Content-Type": "application/json" },
32
+ body: JSON.stringify({ queries }),
33
+ signal: AbortSignal.timeout(60000),
34
+ });
35
+ if (!response.ok) {
36
+ throw new Error(`OSV batch API error: ${response.status} ${response.statusText}`);
40
37
  }
41
- // Fetch full vulnerability details by ID
42
- const fullVulns = [];
43
- for (const bv of batchVulns) {
44
- try {
45
- const vulnResponse = await fetch(`https://api.osv.dev/v1/vulns/${bv.id}`, {
46
- signal: AbortSignal.timeout(5000),
47
- });
48
- if (vulnResponse.ok) {
49
- const vulnData = await vulnResponse.json();
50
- fullVulns.push(vulnData);
51
- }
38
+ const data = await response.json();
39
+ for (let i = 0; i < chunk.length; i++) {
40
+ const key = `${chunk[i].name}@${chunk[i].version}`;
41
+ const batchVulns = data.results[i]?.vulns || [];
42
+ if (batchVulns.length === 0) {
43
+ results.set(key, []);
44
+ continue;
52
45
  }
53
- catch {
54
- // If individual fetch fails, use minimal info
55
- fullVulns.push({ id: bv.id, summary: "Details unavailable" });
46
+ const fullVulns = [];
47
+ for (const bv of batchVulns) {
48
+ try {
49
+ const vulnResponse = await fetch(`https://api.osv.dev/v1/vulns/${bv.id}`, {
50
+ signal: AbortSignal.timeout(5000),
51
+ });
52
+ if (vulnResponse.ok) {
53
+ const vulnData = await vulnResponse.json();
54
+ fullVulns.push(vulnData);
55
+ }
56
+ }
57
+ catch {
58
+ fullVulns.push({ id: bv.id, summary: "Details unavailable" });
59
+ }
56
60
  }
61
+ results.set(key, fullVulns);
57
62
  }
58
- results.set(key, fullVulns);
59
63
  }
60
64
  return results;
61
65
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "guardvibe",
3
- "version": "3.0.51",
3
+ "version": "3.0.53",
4
4
  "mcpName": "io.github.goklab/guardvibe",
5
5
  "description": "Security MCP for vibe coding. 365 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",