@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 +1 -1
- package/dist/index.js +168 -371
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
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.
|
|
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: "
|
|
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:
|
|
2415
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
3342
|
-
patch
|
|
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
|
|
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
|
|
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
|
|
3573
|
-
patch
|
|
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
|
|
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.
|
|
3968
|
-
patch
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
4558
|
+
const recommendations = [];
|
|
4759
4559
|
if (missing.includes("auth")) {
|
|
4760
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: ${
|
|
4810
|
-
patch
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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.
|
|
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": {
|