@quantracode/vibecheck 0.4.0 → 0.4.2

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/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  #!/usr/bin/env node
2
- declare const CLI_VERSION = "0.4.0";
2
+ declare const CLI_VERSION = "0.4.2";
3
3
 
4
4
  export { CLI_VERSION };
package/dist/index.js CHANGED
@@ -659,7 +659,7 @@ function validateArtifact(json) {
659
659
  }
660
660
 
661
661
  // src/constants.ts
662
- var CLI_VERSION = "0.4.0";
662
+ var CLI_VERSION = "0.4.2";
663
663
 
664
664
  // src/utils/file-utils.ts
665
665
  import fs from "fs";
@@ -865,6 +865,7 @@ async function applyPatches(findings, baseDir, options = {}) {
865
865
  applied: 0,
866
866
  failed: 0,
867
867
  skipped: 0,
868
+ noAutomatedPatch: 0,
868
869
  results: []
869
870
  };
870
871
  }
@@ -890,8 +891,12 @@ Found ${patchableFindings.length} finding(s) with patches.
890
891
  findingId: finding.id,
891
892
  file: targetFile,
892
893
  success: false,
893
- error: "Patch is not in unified diff format. Only standard git-style diffs are supported.",
894
- patch
894
+ error: "No automated patch available for this finding",
895
+ patch,
896
+ ruleId: finding.ruleId,
897
+ title: finding.title,
898
+ recommendedFix: finding.remediation.recommendedFix,
899
+ noAutomatedPatch: true
895
900
  });
896
901
  continue;
897
902
  }
@@ -965,13 +970,15 @@ Found ${patchableFindings.length} finding(s) with patches.
965
970
  }
966
971
  }
967
972
  const applied = results.filter((r) => r.success).length;
968
- const failed = results.filter((r) => !r.success && r.error !== "User declined").length;
973
+ const failed = results.filter((r) => !r.success && r.error !== "User declined" && !r.noAutomatedPatch).length;
969
974
  const skipped = results.filter((r) => r.error === "User declined").length;
975
+ const noAutomatedPatch = results.filter((r) => r.noAutomatedPatch).length;
970
976
  return {
971
977
  totalPatchable: patchableFindings.length,
972
978
  applied,
973
979
  failed,
974
980
  skipped,
981
+ noAutomatedPatch,
975
982
  results
976
983
  };
977
984
  }
@@ -2333,6 +2340,61 @@ async function buildScanContext(repoRoot, options = {}) {
2333
2340
  };
2334
2341
  }
2335
2342
 
2343
+ // src/scanners/helpers/patch-generator.ts
2344
+ import { readFileSync as readFileSync4 } from "fs";
2345
+ function generateFunctionStartPatch(repoRoot, relPath, functionStartLine, codeToInsert, contextLines = 3) {
2346
+ try {
2347
+ const absPath = resolvePath(repoRoot, relPath);
2348
+ const content = readFileSync4(absPath, "utf-8");
2349
+ const lines = content.split("\n");
2350
+ let bodyStartLine = functionStartLine;
2351
+ for (let i = functionStartLine - 1; i < lines.length; i++) {
2352
+ if (lines[i].includes("{")) {
2353
+ bodyStartLine = i + 1;
2354
+ break;
2355
+ }
2356
+ }
2357
+ const contextBefore = lines.slice(
2358
+ Math.max(0, bodyStartLine - contextLines),
2359
+ bodyStartLine
2360
+ );
2361
+ const contextAfter = lines.slice(
2362
+ bodyStartLine,
2363
+ Math.min(lines.length, bodyStartLine + contextLines)
2364
+ );
2365
+ const firstLineAfterBrace = lines[bodyStartLine];
2366
+ const indentation = firstLineAfterBrace?.match(/^(\s*)/)?.[1] || " ";
2367
+ const insertLines = codeToInsert.split("\n").map((line) => {
2368
+ if (line.trim() === "") return "";
2369
+ return indentation + line;
2370
+ });
2371
+ const oldStartLine = bodyStartLine - contextBefore.length + 1;
2372
+ const oldLineCount = contextBefore.length + contextAfter.length;
2373
+ const newLineCount = oldLineCount + insertLines.length;
2374
+ const diffLines = [];
2375
+ diffLines.push(`--- a/${relPath.replace(/\\/g, "/")}`);
2376
+ diffLines.push(`+++ b/${relPath.replace(/\\/g, "/")}`);
2377
+ diffLines.push(
2378
+ `@@ -${oldStartLine},${oldLineCount} +${oldStartLine},${newLineCount} @@`
2379
+ );
2380
+ for (const line of contextBefore) {
2381
+ diffLines.push(" " + line);
2382
+ }
2383
+ for (const line of insertLines) {
2384
+ diffLines.push("+" + line);
2385
+ }
2386
+ if (contextAfter.length > 0 && contextAfter[0].trim() !== "") {
2387
+ diffLines.push("+");
2388
+ }
2389
+ for (const line of contextAfter) {
2390
+ diffLines.push(" " + line);
2391
+ }
2392
+ return diffLines.join("\n");
2393
+ } catch (error) {
2394
+ return "";
2395
+ }
2396
+ }
2397
+
2336
2398
  // src/scanners/auth/unprotected-api-route.ts
2337
2399
  var RULE_ID = "VC-AUTH-001";
2338
2400
  var STATE_CHANGING_METHODS = ["POST", "PUT", "PATCH", "DELETE"];
@@ -2396,6 +2458,19 @@ async function scanUnprotectedApiRoutes(context) {
2396
2458
  route: routePath
2397
2459
  });
2398
2460
  const sinkOperations = sinks.map((s) => `${s.kind}.${s.operation}`).join(", ");
2461
+ const authCheckCode = `const session = await getServerSession(authOptions);
2462
+ if (!session) {
2463
+ return new Response(JSON.stringify({ error: "Unauthorized" }), {
2464
+ status: 401,
2465
+ headers: { "Content-Type": "application/json" }
2466
+ });
2467
+ }`;
2468
+ const patch = generateFunctionStartPatch(
2469
+ repoRoot,
2470
+ relPath,
2471
+ handler.startLine,
2472
+ authCheckCode
2473
+ );
2399
2474
  findings.push({
2400
2475
  id: generateFindingId({
2401
2476
  ruleId: RULE_ID,
@@ -2411,14 +2486,8 @@ async function scanUnprotectedApiRoutes(context) {
2411
2486
  evidence,
2412
2487
  remediation: {
2413
2488
  recommendedFix: `Add authentication to the ${handler.method} handler. Check for a valid session using getServerSession(), auth(), or similar before performing database operations.`,
2414
- patch: `// Add at the start of your handler:
2415
- const session = await getServerSession(authOptions);
2416
- if (!session) {
2417
- return new Response(JSON.stringify({ error: "Unauthorized" }), {
2418
- status: 401,
2419
- headers: { "Content-Type": "application/json" }
2420
- });
2421
- }`
2489
+ patch: patch || void 0
2490
+ // Only include patch if generation succeeded
2422
2491
  },
2423
2492
  links: {
2424
2493
  owasp: "https://owasp.org/Top10/A01_2021-Broken_Access_Control/",
@@ -2521,19 +2590,8 @@ async function scanMiddlewareGap(context) {
2521
2590
  description: `This Next.js project uses next-auth but has no middleware.ts file. API routes (${fileIndex.apiRouteFiles.length} found) may lack server-side authentication enforcement. While next-auth provides session management, middleware is recommended for edge-level protection.`,
2522
2591
  evidence,
2523
2592
  remediation: {
2524
- recommendedFix: "Create a middleware.ts file that checks authentication for protected routes. See: https://next-auth.js.org/configuration/nextjs#middleware",
2525
- patch: `// middleware.ts
2526
- import { withAuth } from "next-auth/middleware";
2527
-
2528
- export default withAuth({
2529
- callbacks: {
2530
- authorized: ({ token }) => !!token,
2531
- },
2532
- });
2533
-
2534
- export const config = {
2535
- matcher: ["/api/:path*", "/dashboard/:path*"],
2536
- };`
2593
+ recommendedFix: "Create a middleware.ts file that checks authentication for protected routes. Use next-auth's withAuth helper with a matcher config for /api/:path* and other protected routes. See: https://next-auth.js.org/configuration/nextjs#middleware"
2594
+ // No patch for file creation - apply-patches only handles modifications to existing files
2537
2595
  },
2538
2596
  links: {
2539
2597
  owasp: "https://owasp.org/Top10/A01_2021-Broken_Access_Control/"
@@ -2595,6 +2653,7 @@ export const config = {
2595
2653
  evidence,
2596
2654
  remediation: {
2597
2655
  recommendedFix: `Update the middleware matcher to include API routes. Example: matcher: ['/((?!_next/static|_next/image|favicon.ico).*)', '/api/:path*']`
2656
+ // No patch for middleware matcher updates - requires understanding the full matcher pattern and what should be included/excluded
2598
2657
  },
2599
2658
  links: {
2600
2659
  owasp: "https://owasp.org/Top10/A01_2021-Broken_Access_Control/"
@@ -2656,14 +2715,8 @@ async function scanIgnoredValidation(context) {
2656
2715
  category: "validation",
2657
2716
  evidence,
2658
2717
  remediation: {
2659
- recommendedFix: `Assign the validation result to a variable and use the validated data instead of the raw input.`,
2660
- patch: usage.library === "zod" ? `// Instead of:
2661
- schema.parse(data);
2662
-
2663
- // Do:
2664
- const validatedData = schema.parse(data);
2665
- // Use validatedData for all subsequent operations` : `// Assign and use the validated result
2666
- const validatedData = await schema.validate(data);`
2718
+ recommendedFix: `Assign the validation result to a variable and use the validated data instead of the raw input. For Zod: const validatedData = schema.parse(data). For Yup/Joi: const validatedData = await schema.validate(data). Then use validatedData for all operations.`
2719
+ // No patch for validation fixes - requires knowing actual variable names and how to replace raw body usage
2667
2720
  },
2668
2721
  links: {
2669
2722
  cwe: "https://cwe.mitre.org/data/definitions/20.html"
@@ -2787,35 +2840,8 @@ async function scanClientSideOnlyValidation(context) {
2787
2840
  category: "validation",
2788
2841
  evidence,
2789
2842
  remediation: {
2790
- recommendedFix: `Add server-side validation using the same schema. Consider sharing schemas between frontend and backend.`,
2791
- patch: `// Share your Zod schema between frontend and backend:
2792
- // lib/schemas/user.ts
2793
- import { z } from "zod";
2794
-
2795
- export const createUserSchema = z.object({
2796
- name: z.string().min(1),
2797
- email: z.string().email(),
2798
- });
2799
-
2800
- // In your API route:
2801
- import { createUserSchema } from "@/lib/schemas/user";
2802
-
2803
- export async function POST(request: Request) {
2804
- const body = await request.json();
2805
-
2806
- // Server-side validation
2807
- const result = createUserSchema.safeParse(body);
2808
- if (!result.success) {
2809
- return Response.json(
2810
- { error: result.error.flatten() },
2811
- { status: 400 }
2812
- );
2813
- }
2814
-
2815
- // Use validated data
2816
- const { name, email } = result.data;
2817
- // ...
2818
- }`
2843
+ recommendedFix: `Add server-side validation using the same schema. Consider sharing schemas between frontend and backend. Example with Zod: const result = schema.safeParse(body); if (!result.success) return Response.json({ error: result.error }, { status: 400 }); then use result.data for validated values.`
2844
+ // No patch for validation addition - requires knowing the validation schema structure and which fields to validate
2819
2845
  },
2820
2846
  links: {
2821
2847
  cwe: "https://cwe.mitre.org/data/definitions/20.html",
@@ -2885,12 +2911,8 @@ async function scanSensitiveLogging(context) {
2885
2911
  category: "privacy",
2886
2912
  evidence,
2887
2913
  remediation: {
2888
- recommendedFix: `Remove sensitive data from log statements. Only log non-sensitive identifiers like user IDs, timestamps, or action types.`,
2889
- patch: `// Instead of:
2890
- console.log("Login:", { email, password });
2891
-
2892
- // Do:
2893
- console.log("Login attempt:", { email, timestamp: Date.now() });`
2914
+ recommendedFix: `Remove sensitive data from log statements. Only log non-sensitive identifiers like user IDs, timestamps, or action types. Never log passwords, tokens, auth headers, secrets, or API keys.`
2915
+ // No patch for sensitive logging - requires understanding which data is needed for debugging vs which should be redacted
2894
2916
  },
2895
2917
  links: {
2896
2918
  cwe: "https://cwe.mitre.org/data/definitions/532.html",
@@ -2967,20 +2989,8 @@ async function scanOverBroadResponse(context) {
2967
2989
  category: "privacy",
2968
2990
  evidence,
2969
2991
  remediation: {
2970
- recommendedFix: `Use Prisma's \`select\` clause to explicitly choose which fields to return. Never expose password hashes, tokens, or other sensitive data in API responses.`,
2971
- patch: `// Instead of:
2972
- const users = await prisma.${query.model.toLowerCase()}.${query.operation}();
2973
-
2974
- // Use select to return only needed fields:
2975
- const users = await prisma.${query.model.toLowerCase()}.${query.operation}({
2976
- select: {
2977
- id: true,
2978
- name: true,
2979
- email: true,
2980
- createdAt: true,
2981
- // Explicitly omit: password, passwordHash, tokens, etc.
2982
- },
2983
- });`
2992
+ recommendedFix: `Use Prisma's \`select\` clause to explicitly choose which fields to return. Never expose password hashes, tokens, or other sensitive data in API responses. Example: prisma.user.findMany({ select: { id: true, name: true, email: true } }) - only include fields the API consumer needs.`
2993
+ // No patch for over-broad responses - requires understanding which fields are needed by the API consumer
2984
2994
  },
2985
2995
  links: {
2986
2996
  cwe: "https://cwe.mitre.org/data/definitions/359.html",
@@ -3078,17 +3088,8 @@ async function scanDebugFlags(context) {
3078
3088
  category: "config",
3079
3089
  evidence,
3080
3090
  remediation: {
3081
- recommendedFix: `Ensure debug flags are only enabled in development. Use environment variables or NODE_ENV checks.`,
3082
- patch: `// Guard debug flags with environment check:
3083
- const isDev = process.env.NODE_ENV === 'development';
3084
-
3085
- module.exports = {
3086
- // Only enable in development
3087
- ...(isDev && { debug: true }),
3088
-
3089
- // Or use environment variable:
3090
- debug: process.env.DEBUG === 'true',
3091
- };`
3091
+ recommendedFix: `Ensure debug flags are only enabled in development. Use environment variables or NODE_ENV checks. Example: ...(process.env.NODE_ENV === 'development' && { debug: true }) or debug: process.env.DEBUG === 'true'.`
3092
+ // No patch for debug flag fixes - requires understanding config structure and which flags to guard
3092
3093
  },
3093
3094
  links: {
3094
3095
  cwe: "https://cwe.mitre.org/data/definitions/489.html",
@@ -3338,30 +3339,8 @@ async function scanSsrfProneFetch(context) {
3338
3339
  category: "network",
3339
3340
  evidence,
3340
3341
  remediation: {
3341
- recommendedFix: `Validate and sanitize the URL before use. Use an allowlist of permitted domains or URL patterns. Never allow requests to internal networks, localhost, or cloud metadata endpoints.`,
3342
- patch: `// Validate URL before fetching
3343
- const url = new URL(userProvidedUrl);
3344
-
3345
- // Check against allowlist
3346
- const allowedHosts = ["api.example.com", "cdn.example.com"];
3347
- if (!allowedHosts.includes(url.hostname)) {
3348
- throw new Error("URL not allowed");
3349
- }
3350
-
3351
- // Block internal addresses
3352
- const blockedPatterns = [
3353
- /^localhost$/i,
3354
- /^127\\.\\d+\\.\\d+\\.\\d+$/,
3355
- /^10\\.\\d+\\.\\d+\\.\\d+$/,
3356
- /^172\\.(1[6-9]|2[0-9]|3[0-1])\\.\\d+\\.\\d+$/,
3357
- /^192\\.168\\.\\d+\\.\\d+$/,
3358
- /^169\\.254\\.169\\.254$/, // AWS metadata
3359
- ];
3360
- if (blockedPatterns.some(p => p.test(url.hostname))) {
3361
- throw new Error("URL not allowed");
3362
- }
3363
-
3364
- const response = await fetch(url.toString());`
3342
+ recommendedFix: `Validate and sanitize the URL before use. Use an allowlist of permitted domains or URL patterns. Block requests to internal networks (10.x.x.x, 172.16-31.x.x, 192.168.x.x), localhost (127.x.x.x), and cloud metadata endpoints (169.254.169.254). Parse with new URL() and validate url.hostname.`
3343
+ // No patch for SSRF validation - requires defining application-specific allowlist of permitted domains
3365
3344
  },
3366
3345
  links: {
3367
3346
  cwe: "https://cwe.mitre.org/data/definitions/918.html",
@@ -3420,22 +3399,8 @@ async function scanOpenRedirect(context) {
3420
3399
  category: "network",
3421
3400
  evidence,
3422
3401
  remediation: {
3423
- recommendedFix: `Validate the redirect URL against an allowlist of permitted paths or domains. Never redirect to arbitrary user-provided URLs without validation.`,
3424
- patch: `// Validate redirect URL before use
3425
- const allowedPaths = ['/dashboard', '/profile', '/settings'];
3426
- const redirectUrl = searchParams.get('next') || '/';
3427
-
3428
- // Option 1: Only allow relative paths
3429
- if (!redirectUrl.startsWith('/') || redirectUrl.startsWith('//')) {
3430
- return NextResponse.redirect(new URL('/', request.url));
3431
- }
3432
-
3433
- // Option 2: Allowlist check
3434
- if (!allowedPaths.some(path => redirectUrl.startsWith(path))) {
3435
- return NextResponse.redirect(new URL('/', request.url));
3436
- }
3437
-
3438
- return NextResponse.redirect(new URL(redirectUrl, request.url));`
3402
+ recommendedFix: `Validate the redirect URL against an allowlist of permitted paths or domains. Never redirect to arbitrary user-provided URLs without validation. Options: (1) Only allow relative paths: check !url.startsWith('/') || url.startsWith('//'), (2) Use allowlist: check allowedPaths.some(path => url.startsWith(path)).`
3403
+ // No patch for redirect validation - requires defining application-specific allowlist of permitted paths/domains
3439
3404
  },
3440
3405
  links: {
3441
3406
  cwe: "https://cwe.mitre.org/data/definitions/601.html",
@@ -3493,26 +3458,8 @@ async function scanCorsMisconfiguration(context) {
3493
3458
  category: "network",
3494
3459
  evidence,
3495
3460
  remediation: {
3496
- recommendedFix: `When using credentials, you must specify explicit allowed origins instead of "*". Use an allowlist of trusted domains.`,
3497
- patch: `// Instead of:
3498
- // cors({ origin: "*", credentials: true })
3499
-
3500
- // Use an allowlist:
3501
- const allowedOrigins = [
3502
- 'https://app.example.com',
3503
- 'https://admin.example.com',
3504
- ];
3505
-
3506
- app.use(cors({
3507
- origin: (origin, callback) => {
3508
- if (!origin || allowedOrigins.includes(origin)) {
3509
- callback(null, true);
3510
- } else {
3511
- callback(new Error('Not allowed by CORS'));
3512
- }
3513
- },
3514
- credentials: true,
3515
- }));`
3461
+ recommendedFix: `When using credentials, you must specify explicit allowed origins instead of "*". Use an allowlist of trusted domains. Example: cors({ origin: (origin, callback) => { if (allowedOrigins.includes(origin)) callback(null, true); else callback(new Error('Not allowed')); }, credentials: true })`
3462
+ // No patch for CORS fixes - requires defining application-specific allowlist of trusted origins
3516
3463
  },
3517
3464
  links: {
3518
3465
  cwe: "https://cwe.mitre.org/data/definitions/942.html",
@@ -3569,28 +3516,8 @@ async function scanMissingTimeout(context) {
3569
3516
  category: "network",
3570
3517
  evidence,
3571
3518
  remediation: {
3572
- recommendedFix: `Add a timeout to prevent indefinite hangs. For fetch, use AbortController; for axios, use the timeout option.`,
3573
- patch: `// For fetch, use AbortController:
3574
- const controller = new AbortController();
3575
- const timeoutId = setTimeout(() => controller.abort(), 5000);
3576
-
3577
- try {
3578
- const response = await fetch(url, {
3579
- signal: controller.signal,
3580
- });
3581
- clearTimeout(timeoutId);
3582
- return response;
3583
- } catch (error) {
3584
- if (error.name === 'AbortError') {
3585
- throw new Error('Request timed out');
3586
- }
3587
- throw error;
3588
- }
3589
-
3590
- // For axios:
3591
- const response = await axios.get(url, {
3592
- timeout: 5000,
3593
- });`
3519
+ recommendedFix: `Add a timeout to prevent indefinite hangs. For fetch, use AbortController with setTimeout(() => controller.abort(), 5000) and handle AbortError. For axios, add timeout: 5000 to options. Choose timeout duration based on expected response times.`
3520
+ // No patch for timeout fixes - implementation varies by method (fetch vs axios) and requires choosing appropriate timeout duration
3594
3521
  },
3595
3522
  links: {
3596
3523
  cwe: "https://cwe.mitre.org/data/definitions/400.html"
@@ -3860,19 +3787,8 @@ async function scanNextAuthNotEnforced(context) {
3860
3787
  category: "auth",
3861
3788
  evidence,
3862
3789
  remediation: {
3863
- recommendedFix: "Add authentication middleware or explicit auth checks to API routes. See https://next-auth.js.org/configuration/nextjs",
3864
- patch: `// middleware.ts
3865
- import { withAuth } from "next-auth/middleware";
3866
-
3867
- export default withAuth({
3868
- callbacks: {
3869
- authorized: ({ token }) => !!token,
3870
- },
3871
- });
3872
-
3873
- export const config = {
3874
- matcher: ["/api/:path*"],
3875
- };`
3790
+ recommendedFix: "Add authentication middleware or explicit auth checks to API routes. Create middleware.ts using next-auth's withAuth helper with matcher for /api/:path*. See https://next-auth.js.org/configuration/nextjs"
3791
+ // No patch for file creation - apply-patches only handles modifications to existing files
3876
3792
  },
3877
3793
  links: {
3878
3794
  owasp: "https://owasp.org/Top10/A01_2021-Broken_Access_Control/"
@@ -3964,33 +3880,8 @@ async function scanMissingRateLimit(context) {
3964
3880
  category: "middleware",
3965
3881
  evidence,
3966
3882
  remediation: {
3967
- recommendedFix: `Add rate limiting to protect against abuse. Consider using @upstash/ratelimit for serverless, or express-rate-limit for Express apps.`,
3968
- patch: `// Using @upstash/ratelimit with Vercel KV:
3969
- import { Ratelimit } from "@upstash/ratelimit";
3970
- import { kv } from "@vercel/kv";
3971
-
3972
- const ratelimit = new Ratelimit({
3973
- redis: kv,
3974
- limiter: Ratelimit.slidingWindow(10, "60 s"), // 10 requests per minute
3975
- });
3976
-
3977
- export async function POST(request: Request) {
3978
- const ip = request.headers.get("x-forwarded-for") ?? "127.0.0.1";
3979
- const { success, limit, reset, remaining } = await ratelimit.limit(ip);
3980
-
3981
- if (!success) {
3982
- return new Response("Too Many Requests", {
3983
- status: 429,
3984
- headers: {
3985
- "X-RateLimit-Limit": limit.toString(),
3986
- "X-RateLimit-Remaining": remaining.toString(),
3987
- "X-RateLimit-Reset": reset.toString(),
3988
- },
3989
- });
3990
- }
3991
-
3992
- // ... rest of handler
3993
- }`
3883
+ recommendedFix: `Add rate limiting to protect against abuse. For serverless: use @upstash/ratelimit with Ratelimit.slidingWindow(10, "60 s") and check success before proceeding. For Express: use express-rate-limit middleware. Limit by IP address or user ID.`
3884
+ // No patch for rate limiting - implementation varies by infrastructure (serverless vs traditional) and requires setup (Redis connection, identifier strategy)
3994
3885
  },
3995
3886
  links: {
3996
3887
  owasp: "https://cheatsheetseries.owasp.org/cheatsheets/Denial_of_Service_Cheat_Sheet.html",
@@ -4052,21 +3943,8 @@ async function scanMathRandomTokens(context) {
4052
3943
  category: "crypto",
4053
3944
  evidence,
4054
3945
  remediation: {
4055
- recommendedFix: `Use crypto.randomBytes() or crypto.randomUUID() for security-sensitive random values.`,
4056
- patch: `import { randomBytes, randomUUID } from 'crypto';
4057
-
4058
- // For tokens/keys (hex string):
4059
- const token = randomBytes(32).toString('hex');
4060
-
4061
- // For session IDs (URL-safe base64):
4062
- const sessionId = randomBytes(24).toString('base64url');
4063
-
4064
- // For UUIDs:
4065
- const id = randomUUID();
4066
-
4067
- // For numbers in a range (e.g., 6-digit code):
4068
- const code = randomBytes(4).readUInt32BE() % 1000000;
4069
- const resetCode = code.toString().padStart(6, '0');`
3946
+ recommendedFix: `Use crypto.randomBytes() or crypto.randomUUID() for security-sensitive random values. For tokens/keys: randomBytes(32).toString('hex'). For session IDs: randomBytes(24).toString('base64url'). For UUIDs: randomUUID(). For numeric codes: randomBytes(4).readUInt32BE() % range.`
3947
+ // No patch for crypto random fixes - the correct replacement depends on the specific use case
4070
3948
  },
4071
3949
  links: {
4072
3950
  cwe: "https://cwe.mitre.org/data/definitions/338.html",
@@ -4121,26 +3999,8 @@ async function scanJwtDecodeUnverified(context) {
4121
3999
  category: "crypto",
4122
4000
  evidence,
4123
4001
  remediation: {
4124
- recommendedFix: `Use jwt.verify() instead of jwt.decode() to ensure the token signature is valid.`,
4125
- patch: `import jwt from 'jsonwebtoken';
4126
-
4127
- // WRONG - doesn't verify signature:
4128
- // const payload = jwt.decode(token);
4129
-
4130
- // CORRECT - verifies signature:
4131
- try {
4132
- const payload = jwt.verify(token, process.env.JWT_SECRET);
4133
- // Token is valid, payload can be trusted
4134
- } catch (error) {
4135
- // Token is invalid or expired
4136
- throw new Error('Invalid token');
4137
- }
4138
-
4139
- // If you need to read claims before verification (e.g., to get kid for key lookup):
4140
- const header = jwt.decode(token, { complete: true })?.header;
4141
- const kid = header?.kid;
4142
- // ... look up the correct key ...
4143
- const payload = jwt.verify(token, key); // MUST verify before trusting`
4002
+ recommendedFix: `Use jwt.verify() instead of jwt.decode() to ensure the token signature is valid. Example: jwt.verify(token, process.env.JWT_SECRET) with proper error handling. If you need to read claims before verification (e.g., to get kid for key lookup), decode the header first, look up the correct key, then verify with that key.`
4003
+ // No patch for JWT fixes - requires knowing secret location and error handling context
4144
4004
  },
4145
4005
  links: {
4146
4006
  cwe: "https://cwe.mitre.org/data/definitions/347.html",
@@ -4181,30 +4041,19 @@ async function scanWeakHashing(context) {
4181
4041
  });
4182
4042
  let title;
4183
4043
  let description;
4184
- let patch;
4044
+ let recommendedFix;
4185
4045
  if (usage.algorithm.startsWith("bcrypt")) {
4186
4046
  title = "Bcrypt with insufficient salt rounds";
4187
4047
  description = `Bcrypt is configured with low salt rounds (${usage.algorithm}). The cost factor should be at least 10 (ideally 12+) to provide adequate protection against brute-force attacks. Lower values make password cracking significantly faster.`;
4188
- patch = `import bcrypt from 'bcrypt';
4189
-
4190
- // Use at least 10 salt rounds (12 recommended):
4191
- const SALT_ROUNDS = 12;
4192
-
4193
- const hashedPassword = await bcrypt.hash(password, SALT_ROUNDS);`;
4048
+ recommendedFix = "Increase bcrypt salt rounds to at least 10 (12 recommended): await bcrypt.hash(password, 12)";
4194
4049
  } else if (usage.algorithm === "md5" || usage.algorithm === "sha1") {
4195
4050
  title = `Weak hash algorithm (${usage.algorithm.toUpperCase()}) ${usage.isPasswordContext ? "for password" : "detected"}`;
4196
4051
  description = `${usage.algorithm.toUpperCase()} is cryptographically broken and should not be used for security purposes${usage.isPasswordContext ? ", especially for passwords" : ""}. MD5 can be brute-forced or attacked with rainbow tables in seconds. SHA1 has known collision vulnerabilities.`;
4197
- patch = `// For passwords, use bcrypt or argon2:
4198
- import bcrypt from 'bcrypt';
4199
- const hashedPassword = await bcrypt.hash(password, 12);
4200
-
4201
- // For non-password hashing (integrity checks, etc.):
4202
- import { createHash } from 'crypto';
4203
- const hash = createHash('sha256').update(data).digest('hex');`;
4052
+ recommendedFix = usage.isPasswordContext ? "For passwords, use bcrypt: await bcrypt.hash(password, 12)" : "For non-password hashing, use SHA-256: createHash('sha256').update(data).digest('hex')";
4204
4053
  } else {
4205
4054
  title = `Weak cryptographic configuration: ${usage.algorithm}`;
4206
4055
  description = `The cryptographic configuration "${usage.algorithm}" is considered weak and should be updated to current standards.`;
4207
- patch = `// Use modern, secure algorithms and configurations`;
4056
+ recommendedFix = "Use modern, secure algorithms and configurations";
4208
4057
  }
4209
4058
  findings.push({
4210
4059
  id: generateFindingId({
@@ -4221,8 +4070,8 @@ const hash = createHash('sha256').update(data).digest('hex');`;
4221
4070
  category: "crypto",
4222
4071
  evidence,
4223
4072
  remediation: {
4224
- recommendedFix: usage.isPasswordContext ? "Use bcrypt with at least 10 salt rounds, or argon2id for new applications." : "Use SHA-256 or stronger for integrity checks. For passwords, use bcrypt or argon2.",
4225
- patch
4073
+ recommendedFix
4074
+ // No patch for crypto changes - too context-dependent
4226
4075
  },
4227
4076
  links: {
4228
4077
  cwe: "https://cwe.mitre.org/data/definitions/328.html",
@@ -4289,36 +4138,8 @@ async function scanMissingUploadConstraints(context) {
4289
4138
  category: "uploads",
4290
4139
  evidence,
4291
4140
  remediation: {
4292
- recommendedFix: `Add both size limits and file type validation to your upload handler.`,
4293
- patch: upload.uploadMethod === "multer" ? `// For multer, add limits and fileFilter:
4294
- const upload = multer({
4295
- limits: {
4296
- fileSize: 5 * 1024 * 1024, // 5MB limit
4297
- files: 1, // Single file
4298
- },
4299
- fileFilter: (req, file, cb) => {
4300
- const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
4301
- if (allowedTypes.includes(file.mimetype)) {
4302
- cb(null, true);
4303
- } else {
4304
- cb(new Error('Invalid file type'));
4305
- }
4306
- },
4307
- });` : `// For Next.js formData, validate the file:
4308
- const formData = await request.formData();
4309
- const file = formData.get('file') as File;
4310
-
4311
- // Check file size
4312
- const MAX_SIZE = 5 * 1024 * 1024; // 5MB
4313
- if (file.size > MAX_SIZE) {
4314
- return Response.json({ error: 'File too large' }, { status: 400 });
4315
- }
4316
-
4317
- // Check file type
4318
- const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
4319
- if (!allowedTypes.includes(file.type)) {
4320
- return Response.json({ error: 'Invalid file type' }, { status: 400 });
4321
- }`
4141
+ recommendedFix: `Add both size limits and file type validation to your upload handler. For multer: add limits.fileSize and fileFilter callback. For Next.js formData: check file.size and file.type. Define allowed MIME types based on your use case (images, documents, etc.) and appropriate size limits.`
4142
+ // No patch for upload constraints - requires knowing appropriate size limits and allowed file types for the specific use case
4322
4143
  },
4323
4144
  links: {
4324
4145
  cwe: "https://cwe.mitre.org/data/definitions/434.html",
@@ -4375,29 +4196,8 @@ async function scanPublicUploadPath(context) {
4375
4196
  category: "uploads",
4376
4197
  evidence,
4377
4198
  remediation: {
4378
- recommendedFix: `Store uploads outside the public directory and serve them through a controlled API endpoint. Always sanitize and generate safe filenames.`,
4379
- patch: `// DON'T: Write directly to public folder
4380
- // fs.writeFile(path.join('public', filename), buffer);
4381
-
4382
- // DO: Store outside public with generated names
4383
- import { randomUUID } from 'crypto';
4384
- import path from 'path';
4385
-
4386
- // Generate safe filename
4387
- const ext = path.extname(originalFilename).toLowerCase();
4388
- const allowedExtensions = ['.jpg', '.jpeg', '.png', '.gif'];
4389
- if (!allowedExtensions.includes(ext)) {
4390
- throw new Error('Invalid file extension');
4391
- }
4392
-
4393
- const safeFilename = \`\${randomUUID()}\${ext}\`;
4394
- const uploadPath = path.join(process.cwd(), 'uploads', safeFilename);
4395
-
4396
- // Write to non-public directory
4397
- await fs.writeFile(uploadPath, buffer);
4398
-
4399
- // Serve through API route:
4400
- // GET /api/files/[id] -> Stream file from uploads directory`
4199
+ recommendedFix: `Store uploads outside the public directory and serve them through a controlled API endpoint. Always sanitize and generate safe filenames using randomUUID() + validated extension. Store in a non-public directory (e.g., uploads/) and serve through an API route that handles authorization and content-type headers.`
4200
+ // No patch for upload path fixes - requires restructuring file handling, creating API routes, and implementing authorization logic
4401
4201
  },
4402
4202
  links: {
4403
4203
  cwe: "https://cwe.mitre.org/data/definitions/434.html",
@@ -4755,59 +4555,25 @@ function generateDescription(match, missing, costMultiplier, routePath) {
4755
4555
  return `This endpoint (${routePath}) performs ${categoryDescriptions[match.category]}. Without proper controls, attackers can abuse this endpoint causing significant financial damage or service degradation. Estimated cost amplification: ${costMultiplier}x per request. Missing enforcement: ${missingText}.`;
4756
4556
  }
4757
4557
  function generateRemediation(category, missing) {
4758
- const patches = [];
4558
+ const recommendations = [];
4759
4559
  if (missing.includes("auth")) {
4760
- patches.push(`// Add authentication
4761
- const session = await getServerSession(authOptions);
4762
- if (!session) {
4763
- return Response.json({ error: "Unauthorized" }, { status: 401 });
4764
- }`);
4560
+ recommendations.push("authentication (e.g., getServerSession with 401 for unauthorized)");
4765
4561
  }
4766
4562
  if (missing.includes("rate_limit")) {
4767
- patches.push(`// Add rate limiting
4768
- import { Ratelimit } from "@upstash/ratelimit";
4769
- const ratelimit = new Ratelimit({
4770
- redis: kv,
4771
- limiter: Ratelimit.slidingWindow(10, "60 s"),
4772
- });
4773
- const { success } = await ratelimit.limit(userId);
4774
- if (!success) {
4775
- return Response.json({ error: "Rate limit exceeded" }, { status: 429 });
4776
- }`);
4563
+ recommendations.push("rate limiting (e.g., @upstash/ratelimit with sliding window)");
4777
4564
  }
4778
4565
  if (missing.includes("request_size_limit")) {
4779
- patches.push(`// Add request size limit
4780
- const body = await request.json();
4781
- const size = JSON.stringify(body).length;
4782
- if (size > 100 * 1024) { // 100KB limit
4783
- return Response.json({ error: "Request too large" }, { status: 413 });
4784
- }`);
4566
+ recommendations.push("request size limits (check JSON.stringify(body).length, reject if > threshold)");
4785
4567
  }
4786
4568
  if (missing.includes("timeout")) {
4787
- patches.push(`// Add timeout
4788
- const controller = new AbortController();
4789
- const timeout = setTimeout(() => controller.abort(), 30000); // 30s timeout
4790
- try {
4791
- const result = await expensiveOperation({ signal: controller.signal });
4792
- } finally {
4793
- clearTimeout(timeout);
4794
- }`);
4569
+ recommendations.push("timeout enforcement (use AbortController with setTimeout)");
4795
4570
  }
4796
4571
  if (missing.includes("input_validation")) {
4797
- patches.push(`// Add input validation
4798
- import { z } from "zod";
4799
- const schema = z.object({
4800
- prompt: z.string().max(4000),
4801
- model: z.enum(["gpt-4", "gpt-3.5-turbo"]),
4802
- });
4803
- const { success, data } = schema.safeParse(body);
4804
- if (!success) {
4805
- return Response.json({ error: "Invalid input" }, { status: 400 });
4806
- }`);
4572
+ recommendations.push("input validation (use Zod/Yup to validate and limit input size)");
4807
4573
  }
4808
4574
  return {
4809
- recommendedFix: `Add the following enforcement controls to protect against compute abuse: ${missing.map((m) => m.replace(/_/g, " ")).join(", ")}.`,
4810
- patch: patches.join("\n\n")
4575
+ recommendedFix: `Add the following enforcement controls to protect against compute abuse: ${recommendations.join("; ")}.`
4576
+ // No patch for compute abuse fixes - each requires different implementation based on the specific operation and infrastructure
4811
4577
  };
4812
4578
  }
4813
4579
 
@@ -9492,14 +9258,45 @@ async function executeScan(targetDir, options) {
9492
9258
  if (patchSummary.skipped > 0) {
9493
9259
  console.log(`\x1B[90mSkipped: ${patchSummary.skipped}\x1B[0m`);
9494
9260
  }
9261
+ if (patchSummary.noAutomatedPatch > 0) {
9262
+ console.log(`\x1B[33mNo automated patch: ${patchSummary.noAutomatedPatch}\x1B[0m`);
9263
+ }
9495
9264
  if (patchSummary.failed > 0) {
9496
9265
  console.log("\nFailed patches:");
9497
9266
  for (const result of patchSummary.results) {
9498
- if (!result.success && result.error !== "User declined") {
9267
+ if (!result.success && result.error !== "User declined" && !result.noAutomatedPatch) {
9499
9268
  console.log(` \x1B[31m\u2717\x1B[0m ${result.file}: ${result.error}`);
9500
9269
  }
9501
9270
  }
9502
9271
  }
9272
+ if (patchSummary.noAutomatedPatch > 0) {
9273
+ console.log("\nFindings without automated patches:");
9274
+ console.log(`\x1B[90mThese findings require manual review and fixing.\x1B[0m
9275
+ `);
9276
+ for (const result of patchSummary.results) {
9277
+ if (result.noAutomatedPatch) {
9278
+ console.log(` \x1B[33m\u25CF\x1B[0m \x1B[36m[${result.ruleId}]\x1B[0m ${result.title}`);
9279
+ console.log(` File: ${result.file}`);
9280
+ if (result.recommendedFix) {
9281
+ const maxWidth = 70;
9282
+ const words = result.recommendedFix.split(" ");
9283
+ let line = " ";
9284
+ for (const word of words) {
9285
+ if (line.length + word.length + 1 > maxWidth) {
9286
+ console.log(`\x1B[90m${line}\x1B[0m`);
9287
+ line = " " + word;
9288
+ } else {
9289
+ line += (line.length > 4 ? " " : "") + word;
9290
+ }
9291
+ }
9292
+ if (line.length > 4) {
9293
+ console.log(`\x1B[90m${line}\x1B[0m`);
9294
+ }
9295
+ }
9296
+ console.log("");
9297
+ }
9298
+ }
9299
+ }
9503
9300
  console.log("");
9504
9301
  }
9505
9302
  if (shouldFail(findings, failOn)) {
@@ -11812,7 +11609,7 @@ import { resolve as resolve2, join as join4 } from "path";
11812
11609
 
11813
11610
  // src/utils/static-server.ts
11814
11611
  import { createServer } from "http";
11815
- import { existsSync as existsSync2, readFileSync as readFileSync4, statSync as statSync2, readdirSync as readdirSync2 } from "fs";
11612
+ import { existsSync as existsSync2, readFileSync as readFileSync5, statSync as statSync2, readdirSync as readdirSync2 } from "fs";
11816
11613
  import { join as join2, extname as extname3 } from "path";
11817
11614
  var MIME_TYPES = {
11818
11615
  ".html": "text/html; charset=utf-8",
@@ -11908,7 +11705,7 @@ function handleRequest(staticDir, req, res) {
11908
11705
  return;
11909
11706
  }
11910
11707
  try {
11911
- const content = readFileSync4(filePath);
11708
+ const content = readFileSync5(filePath);
11912
11709
  const contentType = getMimeType(filePath);
11913
11710
  res.writeHead(200, {
11914
11711
  "Content-Type": contentType,
@@ -11930,7 +11727,7 @@ async function startStaticServer(options) {
11930
11727
  if (urlPath === "/__vibecheck__/artifact" || urlPath === "/__vibecheck__/artifact.json") {
11931
11728
  if (artifactPath && existsSync2(artifactPath)) {
11932
11729
  try {
11933
- const content = readFileSync4(artifactPath);
11730
+ const content = readFileSync5(artifactPath);
11934
11731
  res.writeHead(200, {
11935
11732
  "Content-Type": "application/json",
11936
11733
  "Content-Length": content.length,
@@ -12010,7 +11807,7 @@ function isPortAvailable(port) {
12010
11807
  }
12011
11808
 
12012
11809
  // src/utils/viewer-cache.ts
12013
- import { existsSync as existsSync3, mkdirSync, writeFileSync as writeFileSync3, readFileSync as readFileSync5, rmSync } from "fs";
11810
+ import { existsSync as existsSync3, mkdirSync, writeFileSync as writeFileSync3, readFileSync as readFileSync6, rmSync } from "fs";
12014
11811
  import { join as join3 } from "path";
12015
11812
  import { homedir } from "os";
12016
11813
  import { execSync as execSync2 } from "child_process";
@@ -12025,7 +11822,7 @@ function getInstalledVersion() {
12025
11822
  return null;
12026
11823
  }
12027
11824
  try {
12028
- return readFileSync5(VERSION_FILE, "utf-8").trim();
11825
+ return readFileSync6(VERSION_FILE, "utf-8").trim();
12029
11826
  } catch {
12030
11827
  return null;
12031
11828
  }
@@ -12303,7 +12100,7 @@ Workflow:
12303
12100
  }
12304
12101
 
12305
12102
  // src/commands/license.ts
12306
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync4, existsSync as existsSync5, mkdirSync as mkdirSync2, unlinkSync } from "fs";
12103
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync4, existsSync as existsSync5, mkdirSync as mkdirSync2, unlinkSync } from "fs";
12307
12104
  import { homedir as homedir2 } from "os";
12308
12105
  import { join as join5 } from "path";
12309
12106
  import chalk from "chalk";
@@ -12531,7 +12328,7 @@ function ensureConfigDir() {
12531
12328
  function getStoredLicenseKey() {
12532
12329
  try {
12533
12330
  if (existsSync5(LICENSE_FILE)) {
12534
- return readFileSync6(LICENSE_FILE, "utf-8").trim();
12331
+ return readFileSync7(LICENSE_FILE, "utf-8").trim();
12535
12332
  }
12536
12333
  } catch {
12537
12334
  }
@@ -12727,7 +12524,7 @@ async function createLicenseAction(options) {
12727
12524
  console.error(chalk.red(`Error: Key file not found: ${options.key}`));
12728
12525
  process.exit(1);
12729
12526
  }
12730
- privateKey = readFileSync6(options.key, "utf-8").trim();
12527
+ privateKey = readFileSync7(options.key, "utf-8").trim();
12731
12528
  } else if (process.env[options.keyEnv]) {
12732
12529
  privateKey = process.env[options.keyEnv];
12733
12530
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quantracode/vibecheck",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "Security scanner for modern web applications",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -58,8 +58,8 @@
58
58
  "@types/tar": "^6.1.13",
59
59
  "tsup": "^8.5.1",
60
60
  "vitest": "^2.1.8",
61
- "@vibecheck/policy": "0.0.1",
62
61
  "@vibecheck/license": "0.0.1",
62
+ "@vibecheck/policy": "0.0.1",
63
63
  "@vibecheck/schema": "0.0.1"
64
64
  },
65
65
  "scripts": {