@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 +1 -1
- package/dist/index.js +126 -367
- 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.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:
|
|
2415
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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());`
|
|
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
|
|
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
|
|
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
|
|
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
|
-
});`
|
|
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
|
|
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.
|
|
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
|
-
}`
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
4551
|
+
const recommendations = [];
|
|
4759
4552
|
if (missing.includes("auth")) {
|
|
4760
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: ${
|
|
4810
|
-
patch
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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.
|
|
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": {
|