@quantracode/vibecheck 0.4.0 → 0.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/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.1";
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.1";
663
663
 
664
664
  // src/utils/file-utils.ts
665
665
  import fs from "fs";
@@ -2333,6 +2333,61 @@ async function buildScanContext(repoRoot, options = {}) {
2333
2333
  };
2334
2334
  }
2335
2335
 
2336
+ // src/scanners/helpers/patch-generator.ts
2337
+ import { readFileSync as readFileSync4 } from "fs";
2338
+ function generateFunctionStartPatch(repoRoot, relPath, functionStartLine, codeToInsert, contextLines = 3) {
2339
+ try {
2340
+ const absPath = resolvePath(repoRoot, relPath);
2341
+ const content = readFileSync4(absPath, "utf-8");
2342
+ const lines = content.split("\n");
2343
+ let bodyStartLine = functionStartLine;
2344
+ for (let i = functionStartLine - 1; i < lines.length; i++) {
2345
+ if (lines[i].includes("{")) {
2346
+ bodyStartLine = i + 1;
2347
+ break;
2348
+ }
2349
+ }
2350
+ const contextBefore = lines.slice(
2351
+ Math.max(0, bodyStartLine - contextLines),
2352
+ bodyStartLine
2353
+ );
2354
+ const contextAfter = lines.slice(
2355
+ bodyStartLine,
2356
+ Math.min(lines.length, bodyStartLine + contextLines)
2357
+ );
2358
+ const firstLineAfterBrace = lines[bodyStartLine];
2359
+ const indentation = firstLineAfterBrace?.match(/^(\s*)/)?.[1] || " ";
2360
+ const insertLines = codeToInsert.split("\n").map((line) => {
2361
+ if (line.trim() === "") return "";
2362
+ return indentation + line;
2363
+ });
2364
+ const oldStartLine = bodyStartLine - contextBefore.length + 1;
2365
+ const oldLineCount = contextBefore.length + contextAfter.length;
2366
+ const newLineCount = oldLineCount + insertLines.length;
2367
+ const diffLines = [];
2368
+ diffLines.push(`--- a/${relPath.replace(/\\/g, "/")}`);
2369
+ diffLines.push(`+++ b/${relPath.replace(/\\/g, "/")}`);
2370
+ diffLines.push(
2371
+ `@@ -${oldStartLine},${oldLineCount} +${oldStartLine},${newLineCount} @@`
2372
+ );
2373
+ for (const line of contextBefore) {
2374
+ diffLines.push(" " + line);
2375
+ }
2376
+ for (const line of insertLines) {
2377
+ diffLines.push("+" + line);
2378
+ }
2379
+ if (contextAfter.length > 0 && contextAfter[0].trim() !== "") {
2380
+ diffLines.push("+");
2381
+ }
2382
+ for (const line of contextAfter) {
2383
+ diffLines.push(" " + line);
2384
+ }
2385
+ return diffLines.join("\n");
2386
+ } catch (error) {
2387
+ return "";
2388
+ }
2389
+ }
2390
+
2336
2391
  // src/scanners/auth/unprotected-api-route.ts
2337
2392
  var RULE_ID = "VC-AUTH-001";
2338
2393
  var STATE_CHANGING_METHODS = ["POST", "PUT", "PATCH", "DELETE"];
@@ -2396,6 +2451,19 @@ async function scanUnprotectedApiRoutes(context) {
2396
2451
  route: routePath
2397
2452
  });
2398
2453
  const sinkOperations = sinks.map((s) => `${s.kind}.${s.operation}`).join(", ");
2454
+ const authCheckCode = `const session = await getServerSession(authOptions);
2455
+ if (!session) {
2456
+ return new Response(JSON.stringify({ error: "Unauthorized" }), {
2457
+ status: 401,
2458
+ headers: { "Content-Type": "application/json" }
2459
+ });
2460
+ }`;
2461
+ const patch = generateFunctionStartPatch(
2462
+ repoRoot,
2463
+ relPath,
2464
+ handler.startLine,
2465
+ authCheckCode
2466
+ );
2399
2467
  findings.push({
2400
2468
  id: generateFindingId({
2401
2469
  ruleId: RULE_ID,
@@ -2411,14 +2479,8 @@ async function scanUnprotectedApiRoutes(context) {
2411
2479
  evidence,
2412
2480
  remediation: {
2413
2481
  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
- }`
2482
+ patch: patch || void 0
2483
+ // Only include patch if generation succeeded
2422
2484
  },
2423
2485
  links: {
2424
2486
  owasp: "https://owasp.org/Top10/A01_2021-Broken_Access_Control/",
@@ -2521,19 +2583,8 @@ async function scanMiddlewareGap(context) {
2521
2583
  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
2584
  evidence,
2523
2585
  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
- };`
2586
+ 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"
2587
+ // No patch for file creation - apply-patches only handles modifications to existing files
2537
2588
  },
2538
2589
  links: {
2539
2590
  owasp: "https://owasp.org/Top10/A01_2021-Broken_Access_Control/"
@@ -2595,6 +2646,7 @@ export const config = {
2595
2646
  evidence,
2596
2647
  remediation: {
2597
2648
  recommendedFix: `Update the middleware matcher to include API routes. Example: matcher: ['/((?!_next/static|_next/image|favicon.ico).*)', '/api/:path*']`
2649
+ // No patch for middleware matcher updates - requires understanding the full matcher pattern and what should be included/excluded
2598
2650
  },
2599
2651
  links: {
2600
2652
  owasp: "https://owasp.org/Top10/A01_2021-Broken_Access_Control/"
@@ -2656,14 +2708,8 @@ async function scanIgnoredValidation(context) {
2656
2708
  category: "validation",
2657
2709
  evidence,
2658
2710
  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);`
2711
+ 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.`
2712
+ // No patch for validation fixes - requires knowing actual variable names and how to replace raw body usage
2667
2713
  },
2668
2714
  links: {
2669
2715
  cwe: "https://cwe.mitre.org/data/definitions/20.html"
@@ -2787,35 +2833,8 @@ async function scanClientSideOnlyValidation(context) {
2787
2833
  category: "validation",
2788
2834
  evidence,
2789
2835
  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
- }`
2836
+ 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.`
2837
+ // No patch for validation addition - requires knowing the validation schema structure and which fields to validate
2819
2838
  },
2820
2839
  links: {
2821
2840
  cwe: "https://cwe.mitre.org/data/definitions/20.html",
@@ -2885,12 +2904,8 @@ async function scanSensitiveLogging(context) {
2885
2904
  category: "privacy",
2886
2905
  evidence,
2887
2906
  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() });`
2907
+ 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.`
2908
+ // No patch for sensitive logging - requires understanding which data is needed for debugging vs which should be redacted
2894
2909
  },
2895
2910
  links: {
2896
2911
  cwe: "https://cwe.mitre.org/data/definitions/532.html",
@@ -2967,20 +2982,8 @@ async function scanOverBroadResponse(context) {
2967
2982
  category: "privacy",
2968
2983
  evidence,
2969
2984
  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
- });`
2985
+ 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.`
2986
+ // No patch for over-broad responses - requires understanding which fields are needed by the API consumer
2984
2987
  },
2985
2988
  links: {
2986
2989
  cwe: "https://cwe.mitre.org/data/definitions/359.html",
@@ -3078,17 +3081,8 @@ async function scanDebugFlags(context) {
3078
3081
  category: "config",
3079
3082
  evidence,
3080
3083
  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
- };`
3084
+ 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'.`
3085
+ // No patch for debug flag fixes - requires understanding config structure and which flags to guard
3092
3086
  },
3093
3087
  links: {
3094
3088
  cwe: "https://cwe.mitre.org/data/definitions/489.html",
@@ -3338,30 +3332,8 @@ async function scanSsrfProneFetch(context) {
3338
3332
  category: "network",
3339
3333
  evidence,
3340
3334
  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());`
3335
+ 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.`
3336
+ // No patch for SSRF validation - requires defining application-specific allowlist of permitted domains
3365
3337
  },
3366
3338
  links: {
3367
3339
  cwe: "https://cwe.mitre.org/data/definitions/918.html",
@@ -3420,22 +3392,8 @@ async function scanOpenRedirect(context) {
3420
3392
  category: "network",
3421
3393
  evidence,
3422
3394
  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));`
3395
+ 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)).`
3396
+ // No patch for redirect validation - requires defining application-specific allowlist of permitted paths/domains
3439
3397
  },
3440
3398
  links: {
3441
3399
  cwe: "https://cwe.mitre.org/data/definitions/601.html",
@@ -3493,26 +3451,8 @@ async function scanCorsMisconfiguration(context) {
3493
3451
  category: "network",
3494
3452
  evidence,
3495
3453
  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
- }));`
3454
+ 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 })`
3455
+ // No patch for CORS fixes - requires defining application-specific allowlist of trusted origins
3516
3456
  },
3517
3457
  links: {
3518
3458
  cwe: "https://cwe.mitre.org/data/definitions/942.html",
@@ -3569,28 +3509,8 @@ async function scanMissingTimeout(context) {
3569
3509
  category: "network",
3570
3510
  evidence,
3571
3511
  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
- });`
3512
+ 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.`
3513
+ // No patch for timeout fixes - implementation varies by method (fetch vs axios) and requires choosing appropriate timeout duration
3594
3514
  },
3595
3515
  links: {
3596
3516
  cwe: "https://cwe.mitre.org/data/definitions/400.html"
@@ -3860,19 +3780,8 @@ async function scanNextAuthNotEnforced(context) {
3860
3780
  category: "auth",
3861
3781
  evidence,
3862
3782
  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
- };`
3783
+ 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"
3784
+ // No patch for file creation - apply-patches only handles modifications to existing files
3876
3785
  },
3877
3786
  links: {
3878
3787
  owasp: "https://owasp.org/Top10/A01_2021-Broken_Access_Control/"
@@ -3964,33 +3873,8 @@ async function scanMissingRateLimit(context) {
3964
3873
  category: "middleware",
3965
3874
  evidence,
3966
3875
  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
- }`
3876
+ 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.`
3877
+ // No patch for rate limiting - implementation varies by infrastructure (serverless vs traditional) and requires setup (Redis connection, identifier strategy)
3994
3878
  },
3995
3879
  links: {
3996
3880
  owasp: "https://cheatsheetseries.owasp.org/cheatsheets/Denial_of_Service_Cheat_Sheet.html",
@@ -4052,21 +3936,8 @@ async function scanMathRandomTokens(context) {
4052
3936
  category: "crypto",
4053
3937
  evidence,
4054
3938
  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');`
3939
+ 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.`
3940
+ // No patch for crypto random fixes - the correct replacement depends on the specific use case
4070
3941
  },
4071
3942
  links: {
4072
3943
  cwe: "https://cwe.mitre.org/data/definitions/338.html",
@@ -4121,26 +3992,8 @@ async function scanJwtDecodeUnverified(context) {
4121
3992
  category: "crypto",
4122
3993
  evidence,
4123
3994
  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`
3995
+ 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.`
3996
+ // No patch for JWT fixes - requires knowing secret location and error handling context
4144
3997
  },
4145
3998
  links: {
4146
3999
  cwe: "https://cwe.mitre.org/data/definitions/347.html",
@@ -4181,30 +4034,19 @@ async function scanWeakHashing(context) {
4181
4034
  });
4182
4035
  let title;
4183
4036
  let description;
4184
- let patch;
4037
+ let recommendedFix;
4185
4038
  if (usage.algorithm.startsWith("bcrypt")) {
4186
4039
  title = "Bcrypt with insufficient salt rounds";
4187
4040
  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);`;
4041
+ recommendedFix = "Increase bcrypt salt rounds to at least 10 (12 recommended): await bcrypt.hash(password, 12)";
4194
4042
  } else if (usage.algorithm === "md5" || usage.algorithm === "sha1") {
4195
4043
  title = `Weak hash algorithm (${usage.algorithm.toUpperCase()}) ${usage.isPasswordContext ? "for password" : "detected"}`;
4196
4044
  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');`;
4045
+ 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
4046
  } else {
4205
4047
  title = `Weak cryptographic configuration: ${usage.algorithm}`;
4206
4048
  description = `The cryptographic configuration "${usage.algorithm}" is considered weak and should be updated to current standards.`;
4207
- patch = `// Use modern, secure algorithms and configurations`;
4049
+ recommendedFix = "Use modern, secure algorithms and configurations";
4208
4050
  }
4209
4051
  findings.push({
4210
4052
  id: generateFindingId({
@@ -4221,8 +4063,8 @@ const hash = createHash('sha256').update(data).digest('hex');`;
4221
4063
  category: "crypto",
4222
4064
  evidence,
4223
4065
  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
4066
+ recommendedFix
4067
+ // No patch for crypto changes - too context-dependent
4226
4068
  },
4227
4069
  links: {
4228
4070
  cwe: "https://cwe.mitre.org/data/definitions/328.html",
@@ -4289,36 +4131,8 @@ async function scanMissingUploadConstraints(context) {
4289
4131
  category: "uploads",
4290
4132
  evidence,
4291
4133
  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
- }`
4134
+ 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.`
4135
+ // No patch for upload constraints - requires knowing appropriate size limits and allowed file types for the specific use case
4322
4136
  },
4323
4137
  links: {
4324
4138
  cwe: "https://cwe.mitre.org/data/definitions/434.html",
@@ -4375,29 +4189,8 @@ async function scanPublicUploadPath(context) {
4375
4189
  category: "uploads",
4376
4190
  evidence,
4377
4191
  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`
4192
+ 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.`
4193
+ // No patch for upload path fixes - requires restructuring file handling, creating API routes, and implementing authorization logic
4401
4194
  },
4402
4195
  links: {
4403
4196
  cwe: "https://cwe.mitre.org/data/definitions/434.html",
@@ -4755,59 +4548,25 @@ function generateDescription(match, missing, costMultiplier, routePath) {
4755
4548
  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
4549
  }
4757
4550
  function generateRemediation(category, missing) {
4758
- const patches = [];
4551
+ const recommendations = [];
4759
4552
  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
- }`);
4553
+ recommendations.push("authentication (e.g., getServerSession with 401 for unauthorized)");
4765
4554
  }
4766
4555
  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
- }`);
4556
+ recommendations.push("rate limiting (e.g., @upstash/ratelimit with sliding window)");
4777
4557
  }
4778
4558
  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
- }`);
4559
+ recommendations.push("request size limits (check JSON.stringify(body).length, reject if > threshold)");
4785
4560
  }
4786
4561
  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
- }`);
4562
+ recommendations.push("timeout enforcement (use AbortController with setTimeout)");
4795
4563
  }
4796
4564
  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
- }`);
4565
+ recommendations.push("input validation (use Zod/Yup to validate and limit input size)");
4807
4566
  }
4808
4567
  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")
4568
+ recommendedFix: `Add the following enforcement controls to protect against compute abuse: ${recommendations.join("; ")}.`
4569
+ // No patch for compute abuse fixes - each requires different implementation based on the specific operation and infrastructure
4811
4570
  };
4812
4571
  }
4813
4572
 
@@ -11812,7 +11571,7 @@ import { resolve as resolve2, join as join4 } from "path";
11812
11571
 
11813
11572
  // src/utils/static-server.ts
11814
11573
  import { createServer } from "http";
11815
- import { existsSync as existsSync2, readFileSync as readFileSync4, statSync as statSync2, readdirSync as readdirSync2 } from "fs";
11574
+ import { existsSync as existsSync2, readFileSync as readFileSync5, statSync as statSync2, readdirSync as readdirSync2 } from "fs";
11816
11575
  import { join as join2, extname as extname3 } from "path";
11817
11576
  var MIME_TYPES = {
11818
11577
  ".html": "text/html; charset=utf-8",
@@ -11908,7 +11667,7 @@ function handleRequest(staticDir, req, res) {
11908
11667
  return;
11909
11668
  }
11910
11669
  try {
11911
- const content = readFileSync4(filePath);
11670
+ const content = readFileSync5(filePath);
11912
11671
  const contentType = getMimeType(filePath);
11913
11672
  res.writeHead(200, {
11914
11673
  "Content-Type": contentType,
@@ -11930,7 +11689,7 @@ async function startStaticServer(options) {
11930
11689
  if (urlPath === "/__vibecheck__/artifact" || urlPath === "/__vibecheck__/artifact.json") {
11931
11690
  if (artifactPath && existsSync2(artifactPath)) {
11932
11691
  try {
11933
- const content = readFileSync4(artifactPath);
11692
+ const content = readFileSync5(artifactPath);
11934
11693
  res.writeHead(200, {
11935
11694
  "Content-Type": "application/json",
11936
11695
  "Content-Length": content.length,
@@ -12010,7 +11769,7 @@ function isPortAvailable(port) {
12010
11769
  }
12011
11770
 
12012
11771
  // src/utils/viewer-cache.ts
12013
- import { existsSync as existsSync3, mkdirSync, writeFileSync as writeFileSync3, readFileSync as readFileSync5, rmSync } from "fs";
11772
+ import { existsSync as existsSync3, mkdirSync, writeFileSync as writeFileSync3, readFileSync as readFileSync6, rmSync } from "fs";
12014
11773
  import { join as join3 } from "path";
12015
11774
  import { homedir } from "os";
12016
11775
  import { execSync as execSync2 } from "child_process";
@@ -12025,7 +11784,7 @@ function getInstalledVersion() {
12025
11784
  return null;
12026
11785
  }
12027
11786
  try {
12028
- return readFileSync5(VERSION_FILE, "utf-8").trim();
11787
+ return readFileSync6(VERSION_FILE, "utf-8").trim();
12029
11788
  } catch {
12030
11789
  return null;
12031
11790
  }
@@ -12303,7 +12062,7 @@ Workflow:
12303
12062
  }
12304
12063
 
12305
12064
  // src/commands/license.ts
12306
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync4, existsSync as existsSync5, mkdirSync as mkdirSync2, unlinkSync } from "fs";
12065
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync4, existsSync as existsSync5, mkdirSync as mkdirSync2, unlinkSync } from "fs";
12307
12066
  import { homedir as homedir2 } from "os";
12308
12067
  import { join as join5 } from "path";
12309
12068
  import chalk from "chalk";
@@ -12531,7 +12290,7 @@ function ensureConfigDir() {
12531
12290
  function getStoredLicenseKey() {
12532
12291
  try {
12533
12292
  if (existsSync5(LICENSE_FILE)) {
12534
- return readFileSync6(LICENSE_FILE, "utf-8").trim();
12293
+ return readFileSync7(LICENSE_FILE, "utf-8").trim();
12535
12294
  }
12536
12295
  } catch {
12537
12296
  }
@@ -12727,7 +12486,7 @@ async function createLicenseAction(options) {
12727
12486
  console.error(chalk.red(`Error: Key file not found: ${options.key}`));
12728
12487
  process.exit(1);
12729
12488
  }
12730
- privateKey = readFileSync6(options.key, "utf-8").trim();
12489
+ privateKey = readFileSync7(options.key, "utf-8").trim();
12731
12490
  } else if (process.env[options.keyEnv]) {
12732
12491
  privateKey = process.env[options.keyEnv];
12733
12492
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quantracode/vibecheck",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
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": {