@quantracode/vibecheck 0.3.3 → 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.js CHANGED
@@ -10,7 +10,7 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
10
10
  import { Command } from "commander";
11
11
 
12
12
  // src/commands/scan.ts
13
- import path8 from "path";
13
+ import path9 from "path";
14
14
 
15
15
  // ../schema/dist/schemas/claim.js
16
16
  import { z } from "zod";
@@ -296,189 +296,301 @@ var SupplyChainInfoSchema = z5.object({
296
296
  })
297
297
  });
298
298
 
299
- // ../schema/dist/schemas/artifact.js
299
+ // ../schema/dist/schemas/custom-rule.js
300
300
  import { z as z6 } from "zod";
301
+ var FileTypeSchema = z6.enum([
302
+ "ts",
303
+ "tsx",
304
+ "js",
305
+ "jsx",
306
+ "json",
307
+ "env",
308
+ "yaml",
309
+ "yml",
310
+ "md",
311
+ "config",
312
+ "any"
313
+ ]);
314
+ var MatchConditionSchema = z6.object({
315
+ /** String or regex pattern to search for */
316
+ contains: z6.string().optional(),
317
+ /** Pattern that should NOT be present (for checking missing security controls) */
318
+ not_contains: z6.string().optional(),
319
+ /** Regex pattern to match (advanced) */
320
+ regex: z6.string().optional(),
321
+ /** Require the match to be case-sensitive (default: false) */
322
+ case_sensitive: z6.boolean().optional().default(false),
323
+ /** Require ALL conditions to match (default: true for contains, false for not_contains) */
324
+ all_must_match: z6.boolean().optional()
325
+ });
326
+ var FileFilterSchema = z6.object({
327
+ /** File types to include (e.g., ["ts", "js"]) */
328
+ file_type: z6.array(FileTypeSchema).optional(),
329
+ /** Glob patterns to include */
330
+ include: z6.array(z6.string()).optional(),
331
+ /** Glob patterns to exclude */
332
+ exclude: z6.array(z6.string()).optional(),
333
+ /** Only match files in specific directories */
334
+ directories: z6.array(z6.string()).optional()
335
+ });
336
+ var ContextConditionSchema = z6.object({
337
+ /** Match only in specific function/handler types */
338
+ in_function: z6.array(z6.enum([
339
+ "GET",
340
+ "POST",
341
+ "PUT",
342
+ "PATCH",
343
+ "DELETE",
344
+ "route_handler",
345
+ "middleware",
346
+ "any"
347
+ ])).optional(),
348
+ /** Require presence of imports */
349
+ requires_import: z6.array(z6.string()).optional(),
350
+ /** Exclude if imports are present */
351
+ excludes_import: z6.array(z6.string()).optional(),
352
+ /** Only match if file contains certain keywords */
353
+ file_contains: z6.array(z6.string()).optional(),
354
+ /** Only match if file does NOT contain certain keywords */
355
+ file_not_contains: z6.array(z6.string()).optional()
356
+ });
357
+ var CustomRuleSchema = z6.object({
358
+ /** Unique rule identifier (e.g., "VC-CUSTOM-001" or "MY-RULE-001") */
359
+ id: z6.string().regex(/^[A-Z]+-[A-Z]+-\d{3,}$/, {
360
+ message: "Rule ID must match pattern XXX-XXX-000 (e.g., VC-CUSTOM-001)"
361
+ }),
362
+ /** Rule version for tracking updates */
363
+ version: z6.string().optional().default("1.0.0"),
364
+ /** Severity level */
365
+ severity: SeveritySchema,
366
+ /** Confidence score (0.0 - 1.0) */
367
+ confidence: z6.number().min(0).max(1).default(0.8),
368
+ /** Category */
369
+ category: CategorySchema,
370
+ /** Human-readable title */
371
+ title: z6.string().min(1),
372
+ /** Detailed description of the security issue */
373
+ description: z6.string().min(1),
374
+ /** File filters to limit where the rule applies */
375
+ files: FileFilterSchema.optional(),
376
+ /** Match conditions - what to look for */
377
+ match: MatchConditionSchema,
378
+ /** Context conditions for more sophisticated matching */
379
+ context: ContextConditionSchema.optional(),
380
+ /** Recommended fix explanation */
381
+ recommended_fix: z6.string(),
382
+ /** Optional code patch in unified diff format */
383
+ patch: z6.string().optional(),
384
+ /** Reference links */
385
+ links: z6.object({
386
+ owasp: z6.string().url().optional(),
387
+ cwe: z6.string().url().optional(),
388
+ documentation: z6.string().url().optional()
389
+ }).optional(),
390
+ /** Optional metadata */
391
+ metadata: z6.object({
392
+ /** Author of the rule */
393
+ author: z6.string().optional(),
394
+ /** Tags for categorization */
395
+ tags: z6.array(z6.string()).optional(),
396
+ /** Creation date */
397
+ created: z6.string().optional(),
398
+ /** Last updated date */
399
+ updated: z6.string().optional()
400
+ }).optional(),
401
+ /** Whether the rule is enabled (default: true) */
402
+ enabled: z6.boolean().optional().default(true)
403
+ });
404
+ var CustomRuleCollectionSchema = z6.object({
405
+ /** Schema version */
406
+ schema_version: z6.string().optional().default("1.0"),
407
+ /** Array of rules */
408
+ rules: z6.array(CustomRuleSchema)
409
+ });
410
+
411
+ // ../schema/dist/schemas/artifact.js
412
+ import { z as z7 } from "zod";
301
413
  var ARTIFACT_VERSION = "0.3";
302
414
  var SUPPORTED_VERSIONS = ["0.1", "0.2", "0.3"];
303
- var ArtifactVersionSchema = z6.enum(SUPPORTED_VERSIONS);
304
- var ToolInfoSchema = z6.object({
305
- name: z6.string(),
306
- version: z6.string()
415
+ var ArtifactVersionSchema = z7.enum(SUPPORTED_VERSIONS);
416
+ var ToolInfoSchema = z7.object({
417
+ name: z7.string(),
418
+ version: z7.string()
307
419
  });
308
- var GitInfoSchema = z6.object({
309
- branch: z6.string().optional(),
310
- commit: z6.string().optional(),
311
- remoteUrl: z6.string().optional(),
312
- isDirty: z6.boolean().optional()
420
+ var GitInfoSchema = z7.object({
421
+ branch: z7.string().optional(),
422
+ commit: z7.string().optional(),
423
+ remoteUrl: z7.string().optional(),
424
+ isDirty: z7.boolean().optional()
313
425
  });
314
- var RepoInfoSchema = z6.object({
315
- name: z6.string(),
316
- rootPathHash: z6.string(),
426
+ var RepoInfoSchema = z7.object({
427
+ name: z7.string(),
428
+ rootPathHash: z7.string(),
317
429
  git: GitInfoSchema.optional()
318
430
  });
319
- var SeverityCountsSchema = z6.object({
320
- critical: z6.number().int().nonnegative(),
321
- high: z6.number().int().nonnegative(),
322
- medium: z6.number().int().nonnegative(),
323
- low: z6.number().int().nonnegative(),
324
- info: z6.number().int().nonnegative()
431
+ var SeverityCountsSchema = z7.object({
432
+ critical: z7.number().int().nonnegative(),
433
+ high: z7.number().int().nonnegative(),
434
+ medium: z7.number().int().nonnegative(),
435
+ low: z7.number().int().nonnegative(),
436
+ info: z7.number().int().nonnegative()
325
437
  });
326
- var CategoryCountsSchema = z6.object({
327
- auth: z6.number().int().nonnegative(),
328
- validation: z6.number().int().nonnegative(),
329
- middleware: z6.number().int().nonnegative(),
330
- secrets: z6.number().int().nonnegative(),
331
- injection: z6.number().int().nonnegative(),
332
- privacy: z6.number().int().nonnegative(),
333
- config: z6.number().int().nonnegative(),
334
- network: z6.number().int().nonnegative(),
335
- crypto: z6.number().int().nonnegative(),
336
- uploads: z6.number().int().nonnegative(),
337
- hallucinations: z6.number().int().nonnegative(),
338
- abuse: z6.number().int().nonnegative(),
438
+ var CategoryCountsSchema = z7.object({
439
+ auth: z7.number().int().nonnegative(),
440
+ validation: z7.number().int().nonnegative(),
441
+ middleware: z7.number().int().nonnegative(),
442
+ secrets: z7.number().int().nonnegative(),
443
+ injection: z7.number().int().nonnegative(),
444
+ privacy: z7.number().int().nonnegative(),
445
+ config: z7.number().int().nonnegative(),
446
+ network: z7.number().int().nonnegative(),
447
+ crypto: z7.number().int().nonnegative(),
448
+ uploads: z7.number().int().nonnegative(),
449
+ hallucinations: z7.number().int().nonnegative(),
450
+ abuse: z7.number().int().nonnegative(),
339
451
  // Phase 4 categories
340
- correlation: z6.number().int().nonnegative(),
341
- authorization: z6.number().int().nonnegative(),
342
- lifecycle: z6.number().int().nonnegative(),
343
- "supply-chain": z6.number().int().nonnegative(),
344
- other: z6.number().int().nonnegative()
452
+ correlation: z7.number().int().nonnegative(),
453
+ authorization: z7.number().int().nonnegative(),
454
+ lifecycle: z7.number().int().nonnegative(),
455
+ "supply-chain": z7.number().int().nonnegative(),
456
+ other: z7.number().int().nonnegative()
345
457
  });
346
- var SummarySchema = z6.object({
347
- totalFindings: z6.number().int().nonnegative(),
458
+ var SummarySchema = z7.object({
459
+ totalFindings: z7.number().int().nonnegative(),
348
460
  bySeverity: SeverityCountsSchema,
349
461
  byCategory: CategoryCountsSchema
350
462
  });
351
- var RouteEntrySchema = z6.object({
352
- routeId: z6.string(),
353
- method: z6.string(),
354
- path: z6.string(),
355
- handler: z6.string().optional(),
356
- file: z6.string(),
357
- startLine: z6.number().int().positive().optional(),
358
- endLine: z6.number().int().positive().optional(),
359
- handlerSymbol: z6.string().optional(),
463
+ var RouteEntrySchema = z7.object({
464
+ routeId: z7.string(),
465
+ method: z7.string(),
466
+ path: z7.string(),
467
+ handler: z7.string().optional(),
468
+ file: z7.string(),
469
+ startLine: z7.number().int().positive().optional(),
470
+ endLine: z7.number().int().positive().optional(),
471
+ handlerSymbol: z7.string().optional(),
360
472
  // Deprecated: for backward compat with 0.1
361
- line: z6.number().int().positive().optional(),
362
- middleware: z6.array(z6.string()).optional()
473
+ line: z7.number().int().positive().optional(),
474
+ middleware: z7.array(z7.string()).optional()
363
475
  });
364
- var MiddlewareCoverageEntrySchema = z6.object({
365
- routeId: z6.string(),
366
- covered: z6.boolean(),
367
- reason: z6.string().optional()
476
+ var MiddlewareCoverageEntrySchema = z7.object({
477
+ routeId: z7.string(),
478
+ covered: z7.boolean(),
479
+ reason: z7.string().optional()
368
480
  });
369
- var MiddlewareEntrySchema = z6.object({
370
- name: z6.string().optional(),
371
- file: z6.string(),
372
- line: z6.number().int().positive().optional(),
373
- matcher: z6.array(z6.string()).optional(),
374
- appliesTo: z6.array(z6.string()).optional()
481
+ var MiddlewareEntrySchema = z7.object({
482
+ name: z7.string().optional(),
483
+ file: z7.string(),
484
+ line: z7.number().int().positive().optional(),
485
+ matcher: z7.array(z7.string()).optional(),
486
+ appliesTo: z7.array(z7.string()).optional()
375
487
  });
376
- var MiddlewareMapSchema = z6.object({
377
- middlewareFile: z6.string().optional(),
378
- matcher: z6.array(z6.string()),
379
- coverage: z6.array(MiddlewareCoverageEntrySchema)
488
+ var MiddlewareMapSchema = z7.object({
489
+ middlewareFile: z7.string().optional(),
490
+ matcher: z7.array(z7.string()),
491
+ coverage: z7.array(MiddlewareCoverageEntrySchema)
380
492
  });
381
- var IntentEntrySchema = z6.object({
382
- intentId: z6.string(),
493
+ var IntentEntrySchema = z7.object({
494
+ intentId: z7.string(),
383
495
  type: ClaimTypeSchema,
384
496
  scope: ClaimScopeSchema,
385
- targetRouteId: z6.string().optional(),
497
+ targetRouteId: z7.string().optional(),
386
498
  source: ClaimSourceSchema,
387
- location: z6.object({
388
- file: z6.string(),
389
- startLine: z6.number().int().positive(),
390
- endLine: z6.number().int().positive()
499
+ location: z7.object({
500
+ file: z7.string(),
501
+ startLine: z7.number().int().positive(),
502
+ endLine: z7.number().int().positive()
391
503
  }),
392
504
  strength: ClaimStrengthSchema,
393
- textEvidence: z6.string()
505
+ textEvidence: z7.string()
394
506
  });
395
- var IntentMapSchema = z6.object({
396
- intents: z6.array(IntentEntrySchema)
507
+ var IntentMapSchema = z7.object({
508
+ intents: z7.array(IntentEntrySchema)
397
509
  });
398
- var RouteMapSchema = z6.object({
399
- routes: z6.array(RouteEntrySchema)
510
+ var RouteMapSchema = z7.object({
511
+ routes: z7.array(RouteEntrySchema)
400
512
  });
401
- var CoverageMetricsSchema = z6.object({
402
- authCoverage: z6.object({
403
- totalStateChanging: z6.number().int().nonnegative(),
404
- protectedCount: z6.number().int().nonnegative(),
405
- unprotectedCount: z6.number().int().nonnegative()
513
+ var CoverageMetricsSchema = z7.object({
514
+ authCoverage: z7.object({
515
+ totalStateChanging: z7.number().int().nonnegative(),
516
+ protectedCount: z7.number().int().nonnegative(),
517
+ unprotectedCount: z7.number().int().nonnegative()
406
518
  }).optional(),
407
- validationCoverage: z6.object({
408
- totalStateChanging: z6.number().int().nonnegative(),
409
- validatedCount: z6.number().int().nonnegative()
519
+ validationCoverage: z7.object({
520
+ totalStateChanging: z7.number().int().nonnegative(),
521
+ validatedCount: z7.number().int().nonnegative()
410
522
  }).optional(),
411
- middlewareCoverage: z6.object({
412
- totalApiRoutes: z6.number().int().nonnegative(),
413
- coveredApiRoutes: z6.number().int().nonnegative()
523
+ middlewareCoverage: z7.object({
524
+ totalApiRoutes: z7.number().int().nonnegative(),
525
+ coveredApiRoutes: z7.number().int().nonnegative()
414
526
  }).optional()
415
527
  });
416
- var MetricsSchema = z6.object({
417
- filesScanned: z6.number().int().nonnegative(),
418
- linesOfCode: z6.number().int().nonnegative(),
419
- scanDurationMs: z6.number().nonnegative(),
420
- rulesExecuted: z6.number().int().nonnegative()
528
+ var MetricsSchema = z7.object({
529
+ filesScanned: z7.number().int().nonnegative(),
530
+ linesOfCode: z7.number().int().nonnegative(),
531
+ scanDurationMs: z7.number().nonnegative(),
532
+ rulesExecuted: z7.number().int().nonnegative()
421
533
  }).merge(CoverageMetricsSchema);
422
- var CIMetadataSchema = z6.object({
534
+ var CIMetadataSchema = z7.object({
423
535
  /** Security score (0-100) */
424
- securityScore: z6.number().int().min(0).max(100),
536
+ securityScore: z7.number().int().min(0).max(100),
425
537
  /** Overall status for CI badge */
426
- status: z6.enum(["pass", "warn", "fail"]),
538
+ status: z7.enum(["pass", "warn", "fail"]),
427
539
  /** Badge generation timestamp */
428
- badgeGeneratedAt: z6.string().datetime().optional(),
540
+ badgeGeneratedAt: z7.string().datetime().optional(),
429
541
  /** SHA-256 hash for determinism certification */
430
- artifactHash: z6.string().optional(),
542
+ artifactHash: z7.string().optional(),
431
543
  /** Whether artifact passed determinism certification */
432
- deterministicCertified: z6.boolean().optional()
544
+ deterministicCertified: z7.boolean().optional()
433
545
  });
434
- var CorrelationSummarySchema = z6.object({
546
+ var CorrelationSummarySchema = z7.object({
435
547
  /** Total number of correlation findings generated */
436
- totalCorrelations: z6.number().int().nonnegative(),
548
+ totalCorrelations: z7.number().int().nonnegative(),
437
549
  /** Count by correlation pattern */
438
- byPattern: z6.record(z6.string(), z6.number().int().nonnegative()),
550
+ byPattern: z7.record(z7.string(), z7.number().int().nonnegative()),
439
551
  /** Correlation pass duration in ms */
440
- correlationDurationMs: z6.number().nonnegative().optional()
552
+ correlationDurationMs: z7.number().nonnegative().optional()
441
553
  });
442
- var GraphNodeSchema = z6.object({
443
- id: z6.string(),
444
- type: z6.enum(["route", "middleware", "finding", "intent", "function"]),
445
- label: z6.string(),
446
- file: z6.string().optional(),
447
- line: z6.number().int().positive().optional(),
448
- metadata: z6.record(z6.string(), z6.unknown()).optional()
554
+ var GraphNodeSchema = z7.object({
555
+ id: z7.string(),
556
+ type: z7.enum(["route", "middleware", "finding", "intent", "function"]),
557
+ label: z7.string(),
558
+ file: z7.string().optional(),
559
+ line: z7.number().int().positive().optional(),
560
+ metadata: z7.record(z7.string(), z7.unknown()).optional()
449
561
  });
450
- var GraphEdgeSchema = z6.object({
451
- source: z6.string(),
452
- target: z6.string(),
453
- type: z6.enum(["calls", "protects", "validates", "correlates", "references"]),
454
- label: z6.string().optional()
562
+ var GraphEdgeSchema = z7.object({
563
+ source: z7.string(),
564
+ target: z7.string(),
565
+ type: z7.enum(["calls", "protects", "validates", "correlates", "references"]),
566
+ label: z7.string().optional()
455
567
  });
456
- var ProofTraceGraphSchema = z6.object({
457
- nodes: z6.array(GraphNodeSchema),
458
- edges: z6.array(GraphEdgeSchema)
568
+ var ProofTraceGraphSchema = z7.object({
569
+ nodes: z7.array(GraphNodeSchema),
570
+ edges: z7.array(GraphEdgeSchema)
459
571
  });
460
- var ScanArtifactSchema = z6.object({
572
+ var ScanArtifactSchema = z7.object({
461
573
  artifactVersion: ArtifactVersionSchema,
462
- generatedAt: z6.string().datetime(),
574
+ generatedAt: z7.string().datetime(),
463
575
  tool: ToolInfoSchema,
464
576
  repo: RepoInfoSchema.optional(),
465
577
  summary: SummarySchema,
466
- findings: z6.array(FindingSchema),
578
+ findings: z7.array(FindingSchema),
467
579
  // Phase 3: Enhanced maps
468
- routeMap: z6.union([
469
- z6.array(RouteEntrySchema),
580
+ routeMap: z7.union([
581
+ z7.array(RouteEntrySchema),
470
582
  // Legacy format (0.1)
471
583
  RouteMapSchema
472
584
  // New format (0.2)
473
585
  ]).optional(),
474
- middlewareMap: z6.union([
475
- z6.array(MiddlewareEntrySchema),
586
+ middlewareMap: z7.union([
587
+ z7.array(MiddlewareEntrySchema),
476
588
  // Legacy format (0.1)
477
589
  MiddlewareMapSchema
478
590
  // New format (0.2)
479
591
  ]).optional(),
480
592
  intentMap: IntentMapSchema.optional(),
481
- proofTraces: z6.record(z6.string(), ProofTraceSchema).optional(),
593
+ proofTraces: z7.record(z7.string(), ProofTraceSchema).optional(),
482
594
  metrics: MetricsSchema.optional(),
483
595
  // Phase 4: Supply chain analysis
484
596
  supplyChainInfo: SupplyChainInfoSchema.optional(),
@@ -547,7 +659,7 @@ function validateArtifact(json) {
547
659
  }
548
660
 
549
661
  // src/constants.ts
550
- var CLI_VERSION = "0.3.2";
662
+ var CLI_VERSION = "0.4.1";
551
663
 
552
664
  // src/utils/file-utils.ts
553
665
  import fs from "fs";
@@ -656,6 +768,601 @@ function getRepoName(cwd) {
656
768
  return path2.basename(cwd);
657
769
  }
658
770
 
771
+ // src/utils/apply-patches.ts
772
+ import { readFile, writeFile } from "fs/promises";
773
+ import { existsSync } from "fs";
774
+ import path3 from "path";
775
+ import readline from "readline";
776
+ function isUnifiedDiff(patch) {
777
+ const lines = patch.trim().split("\n");
778
+ return lines.some(
779
+ (line) => line.startsWith("--- ") || line.startsWith("+++ ") || line.startsWith("@@ ")
780
+ );
781
+ }
782
+ async function applyUnifiedDiff(filePath, patch, baseDir) {
783
+ try {
784
+ const absolutePath = path3.isAbsolute(filePath) ? filePath : path3.resolve(baseDir, filePath);
785
+ if (!existsSync(absolutePath)) {
786
+ return {
787
+ success: false,
788
+ error: `File not found: ${filePath}`
789
+ };
790
+ }
791
+ const content = await readFile(absolutePath, "utf-8");
792
+ const lines = content.split("\n");
793
+ const patchLines = patch.split("\n");
794
+ let currentLine = 0;
795
+ let inHunk = false;
796
+ let hunkStartLine = 0;
797
+ const newLines = [];
798
+ for (const patchLine of patchLines) {
799
+ if (patchLine.startsWith("--- ") || patchLine.startsWith("+++ ")) {
800
+ continue;
801
+ }
802
+ if (patchLine.startsWith("@@ ")) {
803
+ const match = patchLine.match(/@@ -(\d+),?\d* \+(\d+),?\d* @@/);
804
+ if (match) {
805
+ hunkStartLine = parseInt(match[1], 10) - 1;
806
+ while (currentLine < hunkStartLine) {
807
+ newLines.push(lines[currentLine]);
808
+ currentLine++;
809
+ }
810
+ inHunk = true;
811
+ }
812
+ continue;
813
+ }
814
+ if (inHunk) {
815
+ if (patchLine.startsWith("-")) {
816
+ currentLine++;
817
+ } else if (patchLine.startsWith("+")) {
818
+ newLines.push(patchLine.slice(1));
819
+ } else if (patchLine.startsWith(" ")) {
820
+ const contextLine = patchLine.slice(1);
821
+ if (lines[currentLine] !== contextLine) {
822
+ return {
823
+ success: false,
824
+ error: `Patch context mismatch at line ${currentLine + 1}. Expected: "${contextLine}", found: "${lines[currentLine]}"`
825
+ };
826
+ }
827
+ newLines.push(lines[currentLine]);
828
+ currentLine++;
829
+ }
830
+ }
831
+ }
832
+ while (currentLine < lines.length) {
833
+ newLines.push(lines[currentLine]);
834
+ currentLine++;
835
+ }
836
+ await writeFile(absolutePath, newLines.join("\n"), "utf-8");
837
+ return { success: true };
838
+ } catch (error) {
839
+ return {
840
+ success: false,
841
+ error: error instanceof Error ? error.message : String(error)
842
+ };
843
+ }
844
+ }
845
+ async function promptConfirmation(message) {
846
+ const rl = readline.createInterface({
847
+ input: process.stdin,
848
+ output: process.stdout
849
+ });
850
+ return new Promise((resolve3) => {
851
+ rl.question(`${message} (y/N): `, (answer) => {
852
+ rl.close();
853
+ resolve3(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
854
+ });
855
+ });
856
+ }
857
+ async function applyPatches(findings, baseDir, options = {}) {
858
+ const results = [];
859
+ const patchableFindings = findings.filter(
860
+ (f) => f.remediation?.patch && f.remediation.patch.trim().length > 0
861
+ );
862
+ if (patchableFindings.length === 0) {
863
+ return {
864
+ totalPatchable: 0,
865
+ applied: 0,
866
+ failed: 0,
867
+ skipped: 0,
868
+ results: []
869
+ };
870
+ }
871
+ console.log(`
872
+ Found ${patchableFindings.length} finding(s) with patches.
873
+ `);
874
+ for (const finding of patchableFindings) {
875
+ const patch = finding.remediation.patch;
876
+ const firstEvidence = finding.evidence[0];
877
+ if (!firstEvidence) {
878
+ results.push({
879
+ findingId: finding.id,
880
+ file: "unknown",
881
+ success: false,
882
+ error: "No evidence found to determine target file",
883
+ patch
884
+ });
885
+ continue;
886
+ }
887
+ const targetFile = firstEvidence.file;
888
+ if (!isUnifiedDiff(patch)) {
889
+ results.push({
890
+ findingId: finding.id,
891
+ file: targetFile,
892
+ success: false,
893
+ error: "Patch is not in unified diff format. Only standard git-style diffs are supported.",
894
+ patch
895
+ });
896
+ continue;
897
+ }
898
+ console.log(`\x1B[36m[${finding.ruleId}]\x1B[0m ${finding.title}`);
899
+ console.log(` File: ${targetFile}`);
900
+ console.log(` Severity: \x1B[33m${finding.severity.toUpperCase()}\x1B[0m`);
901
+ console.log(` Description: ${finding.description.slice(0, 100)}${finding.description.length > 100 ? "..." : ""}`);
902
+ console.log(`
903
+ Patch preview:`);
904
+ const patchLines = patch.split("\n").slice(0, 15);
905
+ for (const line of patchLines) {
906
+ if (line.startsWith("+")) {
907
+ console.log(` \x1B[32m${line}\x1B[0m`);
908
+ } else if (line.startsWith("-")) {
909
+ console.log(` \x1B[31m${line}\x1B[0m`);
910
+ } else {
911
+ console.log(` ${line}`);
912
+ }
913
+ }
914
+ if (patch.split("\n").length > 15) {
915
+ console.log(` \x1B[90m... (${patch.split("\n").length - 15} more lines)\x1B[0m`);
916
+ }
917
+ console.log("");
918
+ if (options.dryRun) {
919
+ console.log(` \x1B[90m[DRY RUN] Would apply patch to ${targetFile}\x1B[0m
920
+ `);
921
+ results.push({
922
+ findingId: finding.id,
923
+ file: targetFile,
924
+ success: true,
925
+ patch
926
+ });
927
+ continue;
928
+ }
929
+ let shouldApply = options.force ?? false;
930
+ if (!options.force) {
931
+ shouldApply = await promptConfirmation(" Apply this patch?");
932
+ }
933
+ if (!shouldApply) {
934
+ console.log(` \x1B[90mSkipped\x1B[0m
935
+ `);
936
+ results.push({
937
+ findingId: finding.id,
938
+ file: targetFile,
939
+ success: false,
940
+ error: "User declined",
941
+ patch
942
+ });
943
+ continue;
944
+ }
945
+ const result = await applyUnifiedDiff(targetFile, patch, baseDir);
946
+ if (result.success) {
947
+ console.log(` \x1B[32m\u2713 Patch applied successfully\x1B[0m
948
+ `);
949
+ results.push({
950
+ findingId: finding.id,
951
+ file: targetFile,
952
+ success: true,
953
+ patch
954
+ });
955
+ } else {
956
+ console.log(` \x1B[31m\u2717 Failed to apply patch: ${result.error}\x1B[0m
957
+ `);
958
+ results.push({
959
+ findingId: finding.id,
960
+ file: targetFile,
961
+ success: false,
962
+ error: result.error,
963
+ patch
964
+ });
965
+ }
966
+ }
967
+ const applied = results.filter((r) => r.success).length;
968
+ const failed = results.filter((r) => !r.success && r.error !== "User declined").length;
969
+ const skipped = results.filter((r) => r.error === "User declined").length;
970
+ return {
971
+ totalPatchable: patchableFindings.length,
972
+ applied,
973
+ failed,
974
+ skipped,
975
+ results
976
+ };
977
+ }
978
+
979
+ // src/utils/custom-rules-loader.ts
980
+ import { readFileSync as readFileSync2, statSync, readdirSync } from "fs";
981
+ import { join, extname } from "path";
982
+ import { parse as parseYAML } from "yaml";
983
+ function loadRuleFile(filePath) {
984
+ try {
985
+ const content = readFileSync2(filePath, "utf-8");
986
+ const parsed = parseYAML(content);
987
+ const collectionResult = CustomRuleCollectionSchema.safeParse(parsed);
988
+ if (collectionResult.success) {
989
+ return collectionResult.data.rules.filter((rule) => rule.enabled !== false);
990
+ }
991
+ const ruleResult = CustomRuleSchema.safeParse(parsed);
992
+ if (ruleResult.success) {
993
+ return ruleResult.data.enabled !== false ? [ruleResult.data] : [];
994
+ }
995
+ throw new Error(
996
+ `Invalid rule format in ${filePath}:
997
+ ${collectionResult.error?.message || ruleResult.error?.message}`
998
+ );
999
+ } catch (error) {
1000
+ if (error instanceof Error) {
1001
+ throw new Error(`Failed to load rule file ${filePath}: ${error.message}`);
1002
+ }
1003
+ throw error;
1004
+ }
1005
+ }
1006
+ function loadRulesFromDirectory(dirPath) {
1007
+ const rules = [];
1008
+ const seenIds = /* @__PURE__ */ new Set();
1009
+ try {
1010
+ const stat = statSync(dirPath);
1011
+ if (stat.isFile()) {
1012
+ const fileRules = loadRuleFile(dirPath);
1013
+ for (const rule of fileRules) {
1014
+ if (seenIds.has(rule.id)) {
1015
+ console.warn(
1016
+ `\x1B[33mWarning: Duplicate rule ID "${rule.id}" found, skipping duplicate\x1B[0m`
1017
+ );
1018
+ continue;
1019
+ }
1020
+ seenIds.add(rule.id);
1021
+ rules.push(rule);
1022
+ }
1023
+ return rules;
1024
+ }
1025
+ const files = readdirSync(dirPath);
1026
+ for (const file of files) {
1027
+ const ext = extname(file).toLowerCase();
1028
+ if (![".yaml", ".yml"].includes(ext)) {
1029
+ continue;
1030
+ }
1031
+ const filePath = join(dirPath, file);
1032
+ try {
1033
+ const fileRules = loadRuleFile(filePath);
1034
+ for (const rule of fileRules) {
1035
+ if (seenIds.has(rule.id)) {
1036
+ console.warn(
1037
+ `\x1B[33mWarning: Duplicate rule ID "${rule.id}" in ${file}, skipping duplicate\x1B[0m`
1038
+ );
1039
+ continue;
1040
+ }
1041
+ seenIds.add(rule.id);
1042
+ rules.push(rule);
1043
+ }
1044
+ } catch (error) {
1045
+ console.error(
1046
+ `\x1B[31mError loading ${file}: ${error instanceof Error ? error.message : String(error)}\x1B[0m`
1047
+ );
1048
+ }
1049
+ }
1050
+ return rules;
1051
+ } catch (error) {
1052
+ throw new Error(
1053
+ `Failed to load rules from ${dirPath}: ${error instanceof Error ? error.message : String(error)}`
1054
+ );
1055
+ }
1056
+ }
1057
+ function validateCustomRules(rules) {
1058
+ const valid = [];
1059
+ const errors = [];
1060
+ for (const rule of rules) {
1061
+ try {
1062
+ if (!rule.match.contains && !rule.match.not_contains && !rule.match.regex) {
1063
+ errors.push({
1064
+ ruleId: rule.id,
1065
+ error: "Rule must have at least one match condition (contains, not_contains, or regex)"
1066
+ });
1067
+ continue;
1068
+ }
1069
+ if (rule.match.regex) {
1070
+ try {
1071
+ new RegExp(rule.match.regex);
1072
+ } catch (e) {
1073
+ errors.push({
1074
+ ruleId: rule.id,
1075
+ error: `Invalid regex pattern: ${rule.match.regex}`
1076
+ });
1077
+ continue;
1078
+ }
1079
+ }
1080
+ if (rule.files?.file_type && rule.files.file_type.length === 0) {
1081
+ errors.push({
1082
+ ruleId: rule.id,
1083
+ error: "file_type array cannot be empty"
1084
+ });
1085
+ continue;
1086
+ }
1087
+ valid.push(rule);
1088
+ } catch (error) {
1089
+ errors.push({
1090
+ ruleId: rule.id,
1091
+ error: error instanceof Error ? error.message : String(error)
1092
+ });
1093
+ }
1094
+ }
1095
+ return { valid, errors };
1096
+ }
1097
+ function printRulesSummary(rules) {
1098
+ if (rules.length === 0) {
1099
+ console.log(" No custom rules loaded");
1100
+ return;
1101
+ }
1102
+ console.log(` Loaded ${rules.length} custom rule(s):`);
1103
+ const byCategory = /* @__PURE__ */ new Map();
1104
+ for (const rule of rules) {
1105
+ const existing = byCategory.get(rule.category) || [];
1106
+ existing.push(rule);
1107
+ byCategory.set(rule.category, existing);
1108
+ }
1109
+ for (const [category, categoryRules] of byCategory) {
1110
+ console.log(` \x1B[36m${category}\x1B[0m: ${categoryRules.length} rule(s)`);
1111
+ for (const rule of categoryRules) {
1112
+ const severityColor = rule.severity === "critical" || rule.severity === "high" ? "\x1B[31m" : rule.severity === "medium" ? "\x1B[33m" : "\x1B[36m";
1113
+ console.log(
1114
+ ` [${rule.id}] ${rule.title} (${severityColor}${rule.severity.toUpperCase()}\x1B[0m)`
1115
+ );
1116
+ }
1117
+ }
1118
+ }
1119
+
1120
+ // src/scanners/custom-rule-engine.ts
1121
+ import { readFileSync as readFileSync3 } from "fs";
1122
+ import { basename, extname as extname2 } from "path";
1123
+ import micromatch from "micromatch";
1124
+ function matchesFileFilter(filePath, filter) {
1125
+ if (!filter) {
1126
+ return true;
1127
+ }
1128
+ const ext = extname2(filePath).slice(1).toLowerCase();
1129
+ const fileName = basename(filePath);
1130
+ if (filter.file_type && filter.file_type.length > 0) {
1131
+ const hasAny = filter.file_type.includes("any");
1132
+ if (!hasAny) {
1133
+ const matchesType = filter.file_type.some((type) => {
1134
+ if (type === "config") {
1135
+ return /\.(config|rc)\.(js|ts|json|yaml|yml)$/.test(fileName) || /^\..*rc$/.test(fileName);
1136
+ }
1137
+ if (type === "env") {
1138
+ return /^\.env/.test(fileName);
1139
+ }
1140
+ return ext === type || type === "tsx" && ext === "ts" || type === "jsx" && ext === "js";
1141
+ });
1142
+ if (!matchesType) {
1143
+ return false;
1144
+ }
1145
+ }
1146
+ }
1147
+ if (filter.include && filter.include.length > 0) {
1148
+ if (!micromatch.isMatch(filePath, filter.include)) {
1149
+ return false;
1150
+ }
1151
+ }
1152
+ if (filter.exclude && filter.exclude.length > 0) {
1153
+ if (micromatch.isMatch(filePath, filter.exclude)) {
1154
+ return false;
1155
+ }
1156
+ }
1157
+ if (filter.directories && filter.directories.length > 0) {
1158
+ const matchesDir = filter.directories.some((dir) => {
1159
+ const normalizedDir = dir.replace(/\\/g, "/");
1160
+ const normalizedPath = filePath.replace(/\\/g, "/");
1161
+ return normalizedPath.includes(normalizedDir);
1162
+ });
1163
+ if (!matchesDir) {
1164
+ return false;
1165
+ }
1166
+ }
1167
+ return true;
1168
+ }
1169
+ function matchesContent(content, match, caseSensitive = false) {
1170
+ const lines = content.split("\n");
1171
+ const matchedLines = [];
1172
+ let snippet;
1173
+ const normalize = (str) => caseSensitive ? str : str.toLowerCase();
1174
+ if (match.contains) {
1175
+ const searchTerm = normalize(match.contains);
1176
+ let found = false;
1177
+ for (let i = 0; i < lines.length; i++) {
1178
+ if (normalize(lines[i]).includes(searchTerm)) {
1179
+ matchedLines.push(i + 1);
1180
+ if (!snippet) {
1181
+ snippet = lines[i].trim();
1182
+ }
1183
+ found = true;
1184
+ if (!match.all_must_match) {
1185
+ break;
1186
+ }
1187
+ }
1188
+ }
1189
+ if (!found) {
1190
+ return { matches: false, matchedLines: [] };
1191
+ }
1192
+ }
1193
+ if (match.not_contains) {
1194
+ const searchTerm = normalize(match.not_contains);
1195
+ const defaultAllMatch = match.all_must_match ?? true;
1196
+ for (let i = 0; i < lines.length; i++) {
1197
+ if (normalize(lines[i]).includes(searchTerm)) {
1198
+ if (defaultAllMatch) {
1199
+ return { matches: false, matchedLines: [] };
1200
+ }
1201
+ }
1202
+ }
1203
+ if (matchedLines.length === 0) {
1204
+ matchedLines.push(1);
1205
+ snippet = lines[0]?.trim() || "";
1206
+ }
1207
+ }
1208
+ if (match.regex) {
1209
+ try {
1210
+ const flags = caseSensitive ? "g" : "gi";
1211
+ const regex = new RegExp(match.regex, flags);
1212
+ let found = false;
1213
+ for (let i = 0; i < lines.length; i++) {
1214
+ if (regex.test(lines[i])) {
1215
+ matchedLines.push(i + 1);
1216
+ if (!snippet) {
1217
+ snippet = lines[i].trim();
1218
+ }
1219
+ found = true;
1220
+ if (!match.all_must_match) {
1221
+ break;
1222
+ }
1223
+ }
1224
+ }
1225
+ if (!found) {
1226
+ return { matches: false, matchedLines: [] };
1227
+ }
1228
+ } catch (e) {
1229
+ console.error(`Invalid regex: ${match.regex}`);
1230
+ return { matches: false, matchedLines: [] };
1231
+ }
1232
+ }
1233
+ return {
1234
+ matches: matchedLines.length > 0,
1235
+ matchedLines,
1236
+ snippet
1237
+ };
1238
+ }
1239
+ function matchesContext(content, sourceFile, context, helpers) {
1240
+ if (!context) {
1241
+ return true;
1242
+ }
1243
+ const normalizeContent = content.toLowerCase();
1244
+ if (context.requires_import && context.requires_import.length > 0) {
1245
+ const hasAllRequiredImports = context.requires_import.every((imp) => {
1246
+ return normalizeContent.includes(`from "${imp}"`) || normalizeContent.includes(`from '${imp}'`) || normalizeContent.includes(`require("${imp}")`) || normalizeContent.includes(`require('${imp}')`);
1247
+ });
1248
+ if (!hasAllRequiredImports) {
1249
+ return false;
1250
+ }
1251
+ }
1252
+ if (context.excludes_import && context.excludes_import.length > 0) {
1253
+ const hasExcludedImport = context.excludes_import.some((imp) => {
1254
+ return normalizeContent.includes(`from "${imp}"`) || normalizeContent.includes(`from '${imp}'`) || normalizeContent.includes(`require("${imp}")`) || normalizeContent.includes(`require('${imp}')`);
1255
+ });
1256
+ if (hasExcludedImport) {
1257
+ return false;
1258
+ }
1259
+ }
1260
+ if (context.file_contains && context.file_contains.length > 0) {
1261
+ const hasAll = context.file_contains.every(
1262
+ (term) => normalizeContent.includes(term.toLowerCase())
1263
+ );
1264
+ if (!hasAll) {
1265
+ return false;
1266
+ }
1267
+ }
1268
+ if (context.file_not_contains && context.file_not_contains.length > 0) {
1269
+ const hasAny = context.file_not_contains.some(
1270
+ (term) => normalizeContent.includes(term.toLowerCase())
1271
+ );
1272
+ if (hasAny) {
1273
+ return false;
1274
+ }
1275
+ }
1276
+ if (context.in_function && context.in_function.length > 0 && sourceFile) {
1277
+ try {
1278
+ const handlers = helpers.findRouteHandlers(sourceFile);
1279
+ if (handlers.length === 0) {
1280
+ return false;
1281
+ }
1282
+ const hasMatchingHandler = handlers.some((handler) => {
1283
+ return context.in_function?.includes(handler.method) || context.in_function?.includes("route_handler") || context.in_function?.includes("any");
1284
+ });
1285
+ if (!hasMatchingHandler) {
1286
+ return false;
1287
+ }
1288
+ } catch (e) {
1289
+ }
1290
+ }
1291
+ return true;
1292
+ }
1293
+ function createScannerFromRule(rule) {
1294
+ return async (context) => {
1295
+ const { repoRoot, fileIndex, helpers } = context;
1296
+ const findings = [];
1297
+ let filesToScan = fileIndex.allSourceFiles;
1298
+ filesToScan = filesToScan.filter(
1299
+ (relPath) => matchesFileFilter(relPath, rule.files)
1300
+ );
1301
+ for (const relPath of filesToScan) {
1302
+ const absPath = resolvePath(repoRoot, relPath);
1303
+ try {
1304
+ const content = readFileSync3(absPath, "utf-8");
1305
+ let sourceFile = null;
1306
+ if (/\.(ts|tsx|js|jsx)$/.test(relPath)) {
1307
+ sourceFile = helpers.parseFile(absPath);
1308
+ }
1309
+ if (!matchesContext(content, sourceFile, rule.context, helpers)) {
1310
+ continue;
1311
+ }
1312
+ const matchResult = matchesContent(
1313
+ content,
1314
+ rule.match,
1315
+ rule.match.case_sensitive
1316
+ );
1317
+ if (!matchResult.matches) {
1318
+ continue;
1319
+ }
1320
+ const evidence = matchResult.matchedLines.slice(0, 3).map((lineNum) => ({
1321
+ file: relPath,
1322
+ startLine: lineNum,
1323
+ endLine: lineNum,
1324
+ snippet: matchResult.snippet || content.split("\n")[lineNum - 1]?.trim() || "",
1325
+ label: `Match found`
1326
+ }));
1327
+ const fingerprint = generateFingerprint({
1328
+ ruleId: rule.id,
1329
+ file: relPath,
1330
+ line: matchResult.matchedLines[0] || 1,
1331
+ snippet: matchResult.snippet || ""
1332
+ });
1333
+ const finding = {
1334
+ id: generateFindingId({
1335
+ ruleId: rule.id,
1336
+ file: relPath,
1337
+ symbol: matchResult.snippet || "",
1338
+ startLine: matchResult.matchedLines[0] || 1
1339
+ }),
1340
+ ruleId: rule.id,
1341
+ title: rule.title,
1342
+ description: rule.description,
1343
+ severity: rule.severity,
1344
+ confidence: rule.confidence,
1345
+ category: rule.category,
1346
+ evidence,
1347
+ remediation: {
1348
+ recommendedFix: rule.recommended_fix,
1349
+ patch: rule.patch
1350
+ },
1351
+ links: rule.links,
1352
+ fingerprint
1353
+ };
1354
+ findings.push(finding);
1355
+ } catch (error) {
1356
+ continue;
1357
+ }
1358
+ }
1359
+ return findings;
1360
+ };
1361
+ }
1362
+ function createScannersFromRules(rules) {
1363
+ return rules.map(createScannerFromRule);
1364
+ }
1365
+
659
1366
  // src/scanners/types.ts
660
1367
  var SEVERITY_ORDER = {
661
1368
  critical: 4,
@@ -1626,6 +2333,61 @@ async function buildScanContext(repoRoot, options = {}) {
1626
2333
  };
1627
2334
  }
1628
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
+
1629
2391
  // src/scanners/auth/unprotected-api-route.ts
1630
2392
  var RULE_ID = "VC-AUTH-001";
1631
2393
  var STATE_CHANGING_METHODS = ["POST", "PUT", "PATCH", "DELETE"];
@@ -1689,6 +2451,19 @@ async function scanUnprotectedApiRoutes(context) {
1689
2451
  route: routePath
1690
2452
  });
1691
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
+ );
1692
2467
  findings.push({
1693
2468
  id: generateFindingId({
1694
2469
  ruleId: RULE_ID,
@@ -1704,14 +2479,8 @@ async function scanUnprotectedApiRoutes(context) {
1704
2479
  evidence,
1705
2480
  remediation: {
1706
2481
  recommendedFix: `Add authentication to the ${handler.method} handler. Check for a valid session using getServerSession(), auth(), or similar before performing database operations.`,
1707
- patch: `// Add at the start of your handler:
1708
- const session = await getServerSession(authOptions);
1709
- if (!session) {
1710
- return new Response(JSON.stringify({ error: "Unauthorized" }), {
1711
- status: 401,
1712
- headers: { "Content-Type": "application/json" }
1713
- });
1714
- }`
2482
+ patch: patch || void 0
2483
+ // Only include patch if generation succeeded
1715
2484
  },
1716
2485
  links: {
1717
2486
  owasp: "https://owasp.org/Top10/A01_2021-Broken_Access_Control/",
@@ -1814,19 +2583,8 @@ async function scanMiddlewareGap(context) {
1814
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.`,
1815
2584
  evidence,
1816
2585
  remediation: {
1817
- recommendedFix: "Create a middleware.ts file that checks authentication for protected routes. See: https://next-auth.js.org/configuration/nextjs#middleware",
1818
- patch: `// middleware.ts
1819
- import { withAuth } from "next-auth/middleware";
1820
-
1821
- export default withAuth({
1822
- callbacks: {
1823
- authorized: ({ token }) => !!token,
1824
- },
1825
- });
1826
-
1827
- export const config = {
1828
- matcher: ["/api/:path*", "/dashboard/:path*"],
1829
- };`
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
1830
2588
  },
1831
2589
  links: {
1832
2590
  owasp: "https://owasp.org/Top10/A01_2021-Broken_Access_Control/"
@@ -1888,6 +2646,7 @@ export const config = {
1888
2646
  evidence,
1889
2647
  remediation: {
1890
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
1891
2650
  },
1892
2651
  links: {
1893
2652
  owasp: "https://owasp.org/Top10/A01_2021-Broken_Access_Control/"
@@ -1949,14 +2708,8 @@ async function scanIgnoredValidation(context) {
1949
2708
  category: "validation",
1950
2709
  evidence,
1951
2710
  remediation: {
1952
- recommendedFix: `Assign the validation result to a variable and use the validated data instead of the raw input.`,
1953
- patch: usage.library === "zod" ? `// Instead of:
1954
- schema.parse(data);
1955
-
1956
- // Do:
1957
- const validatedData = schema.parse(data);
1958
- // Use validatedData for all subsequent operations` : `// Assign and use the validated result
1959
- 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
1960
2713
  },
1961
2714
  links: {
1962
2715
  cwe: "https://cwe.mitre.org/data/definitions/20.html"
@@ -2080,35 +2833,8 @@ async function scanClientSideOnlyValidation(context) {
2080
2833
  category: "validation",
2081
2834
  evidence,
2082
2835
  remediation: {
2083
- recommendedFix: `Add server-side validation using the same schema. Consider sharing schemas between frontend and backend.`,
2084
- patch: `// Share your Zod schema between frontend and backend:
2085
- // lib/schemas/user.ts
2086
- import { z } from "zod";
2087
-
2088
- export const createUserSchema = z.object({
2089
- name: z.string().min(1),
2090
- email: z.string().email(),
2091
- });
2092
-
2093
- // In your API route:
2094
- import { createUserSchema } from "@/lib/schemas/user";
2095
-
2096
- export async function POST(request: Request) {
2097
- const body = await request.json();
2098
-
2099
- // Server-side validation
2100
- const result = createUserSchema.safeParse(body);
2101
- if (!result.success) {
2102
- return Response.json(
2103
- { error: result.error.flatten() },
2104
- { status: 400 }
2105
- );
2106
- }
2107
-
2108
- // Use validated data
2109
- const { name, email } = result.data;
2110
- // ...
2111
- }`
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
2112
2838
  },
2113
2839
  links: {
2114
2840
  cwe: "https://cwe.mitre.org/data/definitions/20.html",
@@ -2178,12 +2904,8 @@ async function scanSensitiveLogging(context) {
2178
2904
  category: "privacy",
2179
2905
  evidence,
2180
2906
  remediation: {
2181
- recommendedFix: `Remove sensitive data from log statements. Only log non-sensitive identifiers like user IDs, timestamps, or action types.`,
2182
- patch: `// Instead of:
2183
- console.log("Login:", { email, password });
2184
-
2185
- // Do:
2186
- 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
2187
2909
  },
2188
2910
  links: {
2189
2911
  cwe: "https://cwe.mitre.org/data/definitions/532.html",
@@ -2260,20 +2982,8 @@ async function scanOverBroadResponse(context) {
2260
2982
  category: "privacy",
2261
2983
  evidence,
2262
2984
  remediation: {
2263
- 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.`,
2264
- patch: `// Instead of:
2265
- const users = await prisma.${query.model.toLowerCase()}.${query.operation}();
2266
-
2267
- // Use select to return only needed fields:
2268
- const users = await prisma.${query.model.toLowerCase()}.${query.operation}({
2269
- select: {
2270
- id: true,
2271
- name: true,
2272
- email: true,
2273
- createdAt: true,
2274
- // Explicitly omit: password, passwordHash, tokens, etc.
2275
- },
2276
- });`
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
2277
2987
  },
2278
2988
  links: {
2279
2989
  cwe: "https://cwe.mitre.org/data/definitions/359.html",
@@ -2371,17 +3081,8 @@ async function scanDebugFlags(context) {
2371
3081
  category: "config",
2372
3082
  evidence,
2373
3083
  remediation: {
2374
- recommendedFix: `Ensure debug flags are only enabled in development. Use environment variables or NODE_ENV checks.`,
2375
- patch: `// Guard debug flags with environment check:
2376
- const isDev = process.env.NODE_ENV === 'development';
2377
-
2378
- module.exports = {
2379
- // Only enable in development
2380
- ...(isDev && { debug: true }),
2381
-
2382
- // Or use environment variable:
2383
- debug: process.env.DEBUG === 'true',
2384
- };`
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
2385
3086
  },
2386
3087
  links: {
2387
3088
  cwe: "https://cwe.mitre.org/data/definitions/489.html",
@@ -2631,30 +3332,8 @@ async function scanSsrfProneFetch(context) {
2631
3332
  category: "network",
2632
3333
  evidence,
2633
3334
  remediation: {
2634
- 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.`,
2635
- patch: `// Validate URL before fetching
2636
- const url = new URL(userProvidedUrl);
2637
-
2638
- // Check against allowlist
2639
- const allowedHosts = ["api.example.com", "cdn.example.com"];
2640
- if (!allowedHosts.includes(url.hostname)) {
2641
- throw new Error("URL not allowed");
2642
- }
2643
-
2644
- // Block internal addresses
2645
- const blockedPatterns = [
2646
- /^localhost$/i,
2647
- /^127\\.\\d+\\.\\d+\\.\\d+$/,
2648
- /^10\\.\\d+\\.\\d+\\.\\d+$/,
2649
- /^172\\.(1[6-9]|2[0-9]|3[0-1])\\.\\d+\\.\\d+$/,
2650
- /^192\\.168\\.\\d+\\.\\d+$/,
2651
- /^169\\.254\\.169\\.254$/, // AWS metadata
2652
- ];
2653
- if (blockedPatterns.some(p => p.test(url.hostname))) {
2654
- throw new Error("URL not allowed");
2655
- }
2656
-
2657
- 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
2658
3337
  },
2659
3338
  links: {
2660
3339
  cwe: "https://cwe.mitre.org/data/definitions/918.html",
@@ -2713,22 +3392,8 @@ async function scanOpenRedirect(context) {
2713
3392
  category: "network",
2714
3393
  evidence,
2715
3394
  remediation: {
2716
- recommendedFix: `Validate the redirect URL against an allowlist of permitted paths or domains. Never redirect to arbitrary user-provided URLs without validation.`,
2717
- patch: `// Validate redirect URL before use
2718
- const allowedPaths = ['/dashboard', '/profile', '/settings'];
2719
- const redirectUrl = searchParams.get('next') || '/';
2720
-
2721
- // Option 1: Only allow relative paths
2722
- if (!redirectUrl.startsWith('/') || redirectUrl.startsWith('//')) {
2723
- return NextResponse.redirect(new URL('/', request.url));
2724
- }
2725
-
2726
- // Option 2: Allowlist check
2727
- if (!allowedPaths.some(path => redirectUrl.startsWith(path))) {
2728
- return NextResponse.redirect(new URL('/', request.url));
2729
- }
2730
-
2731
- 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
2732
3397
  },
2733
3398
  links: {
2734
3399
  cwe: "https://cwe.mitre.org/data/definitions/601.html",
@@ -2786,26 +3451,8 @@ async function scanCorsMisconfiguration(context) {
2786
3451
  category: "network",
2787
3452
  evidence,
2788
3453
  remediation: {
2789
- recommendedFix: `When using credentials, you must specify explicit allowed origins instead of "*". Use an allowlist of trusted domains.`,
2790
- patch: `// Instead of:
2791
- // cors({ origin: "*", credentials: true })
2792
-
2793
- // Use an allowlist:
2794
- const allowedOrigins = [
2795
- 'https://app.example.com',
2796
- 'https://admin.example.com',
2797
- ];
2798
-
2799
- app.use(cors({
2800
- origin: (origin, callback) => {
2801
- if (!origin || allowedOrigins.includes(origin)) {
2802
- callback(null, true);
2803
- } else {
2804
- callback(new Error('Not allowed by CORS'));
2805
- }
2806
- },
2807
- credentials: true,
2808
- }));`
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
2809
3456
  },
2810
3457
  links: {
2811
3458
  cwe: "https://cwe.mitre.org/data/definitions/942.html",
@@ -2862,28 +3509,8 @@ async function scanMissingTimeout(context) {
2862
3509
  category: "network",
2863
3510
  evidence,
2864
3511
  remediation: {
2865
- recommendedFix: `Add a timeout to prevent indefinite hangs. For fetch, use AbortController; for axios, use the timeout option.`,
2866
- patch: `// For fetch, use AbortController:
2867
- const controller = new AbortController();
2868
- const timeoutId = setTimeout(() => controller.abort(), 5000);
2869
-
2870
- try {
2871
- const response = await fetch(url, {
2872
- signal: controller.signal,
2873
- });
2874
- clearTimeout(timeoutId);
2875
- return response;
2876
- } catch (error) {
2877
- if (error.name === 'AbortError') {
2878
- throw new Error('Request timed out');
2879
- }
2880
- throw error;
2881
- }
2882
-
2883
- // For axios:
2884
- const response = await axios.get(url, {
2885
- timeout: 5000,
2886
- });`
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
2887
3514
  },
2888
3515
  links: {
2889
3516
  cwe: "https://cwe.mitre.org/data/definitions/400.html"
@@ -3153,19 +3780,8 @@ async function scanNextAuthNotEnforced(context) {
3153
3780
  category: "auth",
3154
3781
  evidence,
3155
3782
  remediation: {
3156
- recommendedFix: "Add authentication middleware or explicit auth checks to API routes. See https://next-auth.js.org/configuration/nextjs",
3157
- patch: `// middleware.ts
3158
- import { withAuth } from "next-auth/middleware";
3159
-
3160
- export default withAuth({
3161
- callbacks: {
3162
- authorized: ({ token }) => !!token,
3163
- },
3164
- });
3165
-
3166
- export const config = {
3167
- matcher: ["/api/:path*"],
3168
- };`
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
3169
3785
  },
3170
3786
  links: {
3171
3787
  owasp: "https://owasp.org/Top10/A01_2021-Broken_Access_Control/"
@@ -3257,33 +3873,8 @@ async function scanMissingRateLimit(context) {
3257
3873
  category: "middleware",
3258
3874
  evidence,
3259
3875
  remediation: {
3260
- recommendedFix: `Add rate limiting to protect against abuse. Consider using @upstash/ratelimit for serverless, or express-rate-limit for Express apps.`,
3261
- patch: `// Using @upstash/ratelimit with Vercel KV:
3262
- import { Ratelimit } from "@upstash/ratelimit";
3263
- import { kv } from "@vercel/kv";
3264
-
3265
- const ratelimit = new Ratelimit({
3266
- redis: kv,
3267
- limiter: Ratelimit.slidingWindow(10, "60 s"), // 10 requests per minute
3268
- });
3269
-
3270
- export async function POST(request: Request) {
3271
- const ip = request.headers.get("x-forwarded-for") ?? "127.0.0.1";
3272
- const { success, limit, reset, remaining } = await ratelimit.limit(ip);
3273
-
3274
- if (!success) {
3275
- return new Response("Too Many Requests", {
3276
- status: 429,
3277
- headers: {
3278
- "X-RateLimit-Limit": limit.toString(),
3279
- "X-RateLimit-Remaining": remaining.toString(),
3280
- "X-RateLimit-Reset": reset.toString(),
3281
- },
3282
- });
3283
- }
3284
-
3285
- // ... rest of handler
3286
- }`
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)
3287
3878
  },
3288
3879
  links: {
3289
3880
  owasp: "https://cheatsheetseries.owasp.org/cheatsheets/Denial_of_Service_Cheat_Sheet.html",
@@ -3345,21 +3936,8 @@ async function scanMathRandomTokens(context) {
3345
3936
  category: "crypto",
3346
3937
  evidence,
3347
3938
  remediation: {
3348
- recommendedFix: `Use crypto.randomBytes() or crypto.randomUUID() for security-sensitive random values.`,
3349
- patch: `import { randomBytes, randomUUID } from 'crypto';
3350
-
3351
- // For tokens/keys (hex string):
3352
- const token = randomBytes(32).toString('hex');
3353
-
3354
- // For session IDs (URL-safe base64):
3355
- const sessionId = randomBytes(24).toString('base64url');
3356
-
3357
- // For UUIDs:
3358
- const id = randomUUID();
3359
-
3360
- // For numbers in a range (e.g., 6-digit code):
3361
- const code = randomBytes(4).readUInt32BE() % 1000000;
3362
- 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
3363
3941
  },
3364
3942
  links: {
3365
3943
  cwe: "https://cwe.mitre.org/data/definitions/338.html",
@@ -3414,26 +3992,8 @@ async function scanJwtDecodeUnverified(context) {
3414
3992
  category: "crypto",
3415
3993
  evidence,
3416
3994
  remediation: {
3417
- recommendedFix: `Use jwt.verify() instead of jwt.decode() to ensure the token signature is valid.`,
3418
- patch: `import jwt from 'jsonwebtoken';
3419
-
3420
- // WRONG - doesn't verify signature:
3421
- // const payload = jwt.decode(token);
3422
-
3423
- // CORRECT - verifies signature:
3424
- try {
3425
- const payload = jwt.verify(token, process.env.JWT_SECRET);
3426
- // Token is valid, payload can be trusted
3427
- } catch (error) {
3428
- // Token is invalid or expired
3429
- throw new Error('Invalid token');
3430
- }
3431
-
3432
- // If you need to read claims before verification (e.g., to get kid for key lookup):
3433
- const header = jwt.decode(token, { complete: true })?.header;
3434
- const kid = header?.kid;
3435
- // ... look up the correct key ...
3436
- 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
3437
3997
  },
3438
3998
  links: {
3439
3999
  cwe: "https://cwe.mitre.org/data/definitions/347.html",
@@ -3474,30 +4034,19 @@ async function scanWeakHashing(context) {
3474
4034
  });
3475
4035
  let title;
3476
4036
  let description;
3477
- let patch;
4037
+ let recommendedFix;
3478
4038
  if (usage.algorithm.startsWith("bcrypt")) {
3479
4039
  title = "Bcrypt with insufficient salt rounds";
3480
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.`;
3481
- patch = `import bcrypt from 'bcrypt';
3482
-
3483
- // Use at least 10 salt rounds (12 recommended):
3484
- const SALT_ROUNDS = 12;
3485
-
3486
- 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)";
3487
4042
  } else if (usage.algorithm === "md5" || usage.algorithm === "sha1") {
3488
4043
  title = `Weak hash algorithm (${usage.algorithm.toUpperCase()}) ${usage.isPasswordContext ? "for password" : "detected"}`;
3489
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.`;
3490
- patch = `// For passwords, use bcrypt or argon2:
3491
- import bcrypt from 'bcrypt';
3492
- const hashedPassword = await bcrypt.hash(password, 12);
3493
-
3494
- // For non-password hashing (integrity checks, etc.):
3495
- import { createHash } from 'crypto';
3496
- 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')";
3497
4046
  } else {
3498
4047
  title = `Weak cryptographic configuration: ${usage.algorithm}`;
3499
4048
  description = `The cryptographic configuration "${usage.algorithm}" is considered weak and should be updated to current standards.`;
3500
- patch = `// Use modern, secure algorithms and configurations`;
4049
+ recommendedFix = "Use modern, secure algorithms and configurations";
3501
4050
  }
3502
4051
  findings.push({
3503
4052
  id: generateFindingId({
@@ -3514,8 +4063,8 @@ const hash = createHash('sha256').update(data).digest('hex');`;
3514
4063
  category: "crypto",
3515
4064
  evidence,
3516
4065
  remediation: {
3517
- 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.",
3518
- patch
4066
+ recommendedFix
4067
+ // No patch for crypto changes - too context-dependent
3519
4068
  },
3520
4069
  links: {
3521
4070
  cwe: "https://cwe.mitre.org/data/definitions/328.html",
@@ -3582,36 +4131,8 @@ async function scanMissingUploadConstraints(context) {
3582
4131
  category: "uploads",
3583
4132
  evidence,
3584
4133
  remediation: {
3585
- recommendedFix: `Add both size limits and file type validation to your upload handler.`,
3586
- patch: upload.uploadMethod === "multer" ? `// For multer, add limits and fileFilter:
3587
- const upload = multer({
3588
- limits: {
3589
- fileSize: 5 * 1024 * 1024, // 5MB limit
3590
- files: 1, // Single file
3591
- },
3592
- fileFilter: (req, file, cb) => {
3593
- const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
3594
- if (allowedTypes.includes(file.mimetype)) {
3595
- cb(null, true);
3596
- } else {
3597
- cb(new Error('Invalid file type'));
3598
- }
3599
- },
3600
- });` : `// For Next.js formData, validate the file:
3601
- const formData = await request.formData();
3602
- const file = formData.get('file') as File;
3603
-
3604
- // Check file size
3605
- const MAX_SIZE = 5 * 1024 * 1024; // 5MB
3606
- if (file.size > MAX_SIZE) {
3607
- return Response.json({ error: 'File too large' }, { status: 400 });
3608
- }
3609
-
3610
- // Check file type
3611
- const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
3612
- if (!allowedTypes.includes(file.type)) {
3613
- return Response.json({ error: 'Invalid file type' }, { status: 400 });
3614
- }`
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
3615
4136
  },
3616
4137
  links: {
3617
4138
  cwe: "https://cwe.mitre.org/data/definitions/434.html",
@@ -3668,29 +4189,8 @@ async function scanPublicUploadPath(context) {
3668
4189
  category: "uploads",
3669
4190
  evidence,
3670
4191
  remediation: {
3671
- recommendedFix: `Store uploads outside the public directory and serve them through a controlled API endpoint. Always sanitize and generate safe filenames.`,
3672
- patch: `// DON'T: Write directly to public folder
3673
- // fs.writeFile(path.join('public', filename), buffer);
3674
-
3675
- // DO: Store outside public with generated names
3676
- import { randomUUID } from 'crypto';
3677
- import path from 'path';
3678
-
3679
- // Generate safe filename
3680
- const ext = path.extname(originalFilename).toLowerCase();
3681
- const allowedExtensions = ['.jpg', '.jpeg', '.png', '.gif'];
3682
- if (!allowedExtensions.includes(ext)) {
3683
- throw new Error('Invalid file extension');
3684
- }
3685
-
3686
- const safeFilename = \`\${randomUUID()}\${ext}\`;
3687
- const uploadPath = path.join(process.cwd(), 'uploads', safeFilename);
3688
-
3689
- // Write to non-public directory
3690
- await fs.writeFile(uploadPath, buffer);
3691
-
3692
- // Serve through API route:
3693
- // 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
3694
4194
  },
3695
4195
  links: {
3696
4196
  cwe: "https://cwe.mitre.org/data/definitions/434.html",
@@ -4048,59 +4548,25 @@ function generateDescription(match, missing, costMultiplier, routePath) {
4048
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}.`;
4049
4549
  }
4050
4550
  function generateRemediation(category, missing) {
4051
- const patches = [];
4551
+ const recommendations = [];
4052
4552
  if (missing.includes("auth")) {
4053
- patches.push(`// Add authentication
4054
- const session = await getServerSession(authOptions);
4055
- if (!session) {
4056
- return Response.json({ error: "Unauthorized" }, { status: 401 });
4057
- }`);
4553
+ recommendations.push("authentication (e.g., getServerSession with 401 for unauthorized)");
4058
4554
  }
4059
4555
  if (missing.includes("rate_limit")) {
4060
- patches.push(`// Add rate limiting
4061
- import { Ratelimit } from "@upstash/ratelimit";
4062
- const ratelimit = new Ratelimit({
4063
- redis: kv,
4064
- limiter: Ratelimit.slidingWindow(10, "60 s"),
4065
- });
4066
- const { success } = await ratelimit.limit(userId);
4067
- if (!success) {
4068
- return Response.json({ error: "Rate limit exceeded" }, { status: 429 });
4069
- }`);
4556
+ recommendations.push("rate limiting (e.g., @upstash/ratelimit with sliding window)");
4070
4557
  }
4071
4558
  if (missing.includes("request_size_limit")) {
4072
- patches.push(`// Add request size limit
4073
- const body = await request.json();
4074
- const size = JSON.stringify(body).length;
4075
- if (size > 100 * 1024) { // 100KB limit
4076
- return Response.json({ error: "Request too large" }, { status: 413 });
4077
- }`);
4559
+ recommendations.push("request size limits (check JSON.stringify(body).length, reject if > threshold)");
4078
4560
  }
4079
4561
  if (missing.includes("timeout")) {
4080
- patches.push(`// Add timeout
4081
- const controller = new AbortController();
4082
- const timeout = setTimeout(() => controller.abort(), 30000); // 30s timeout
4083
- try {
4084
- const result = await expensiveOperation({ signal: controller.signal });
4085
- } finally {
4086
- clearTimeout(timeout);
4087
- }`);
4562
+ recommendations.push("timeout enforcement (use AbortController with setTimeout)");
4088
4563
  }
4089
4564
  if (missing.includes("input_validation")) {
4090
- patches.push(`// Add input validation
4091
- import { z } from "zod";
4092
- const schema = z.object({
4093
- prompt: z.string().max(4000),
4094
- model: z.enum(["gpt-4", "gpt-3.5-turbo"]),
4095
- });
4096
- const { success, data } = schema.safeParse(body);
4097
- if (!success) {
4098
- return Response.json({ error: "Invalid input" }, { status: 400 });
4099
- }`);
4565
+ recommendations.push("input validation (use Zod/Yup to validate and limit input size)");
4100
4566
  }
4101
4567
  return {
4102
- recommendedFix: `Add the following enforcement controls to protect against compute abuse: ${missing.map((m) => m.replace(/_/g, " ")).join(", ")}.`,
4103
- 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
4104
4570
  };
4105
4571
  }
4106
4572
 
@@ -6333,7 +6799,7 @@ var supplyChainPack = {
6333
6799
 
6334
6800
  // src/phase3/proof-trace-builder.ts
6335
6801
  import crypto3 from "crypto";
6336
- import path3 from "path";
6802
+ import path4 from "path";
6337
6803
  import { SyntaxKind as SyntaxKind2 } from "ts-morph";
6338
6804
  var MAX_TRACE_DEPTH = 2;
6339
6805
  function generateRouteId(routePath, method, file) {
@@ -6362,7 +6828,7 @@ function extractRoutePath6(routePart) {
6362
6828
  function buildRouteMap(ctx) {
6363
6829
  const routes = [];
6364
6830
  for (const routeFile of ctx.fileIndex.routeFiles) {
6365
- const absolutePath = path3.join(ctx.repoRoot, routeFile);
6831
+ const absolutePath = path4.join(ctx.repoRoot, routeFile);
6366
6832
  const sourceFile = ctx.helpers.parseFile(absolutePath);
6367
6833
  if (!sourceFile) continue;
6368
6834
  const handlers = ctx.helpers.findRouteHandlers(sourceFile);
@@ -6387,7 +6853,7 @@ function buildMiddlewareMap(ctx) {
6387
6853
  if (!ctx.fileIndex.middlewareFile) {
6388
6854
  return middlewareList;
6389
6855
  }
6390
- const absolutePath = path3.join(ctx.repoRoot, ctx.fileIndex.middlewareFile);
6856
+ const absolutePath = path4.join(ctx.repoRoot, ctx.fileIndex.middlewareFile);
6391
6857
  const sourceFile = ctx.helpers.parseFile(absolutePath);
6392
6858
  if (!sourceFile) return middlewareList;
6393
6859
  const relPath = ctx.fileIndex.middlewareFile.replace(/\\/g, "/");
@@ -6447,7 +6913,7 @@ function buildProofTrace(ctx, route) {
6447
6913
  let authProven = false;
6448
6914
  let validationProven = false;
6449
6915
  const sourceFile = ctx.helpers.parseFile(
6450
- path3.join(ctx.repoRoot, route.file)
6916
+ path4.join(ctx.repoRoot, route.file)
6451
6917
  );
6452
6918
  if (!sourceFile) {
6453
6919
  return {
@@ -6534,13 +7000,13 @@ function buildProofTrace(ctx, route) {
6534
7000
  }
6535
7001
  function getLocalImports(sourceFile, repoRoot, currentFile) {
6536
7002
  const imports = [];
6537
- const currentDir = path3.dirname(path3.join(repoRoot, currentFile));
7003
+ const currentDir = path4.dirname(path4.join(repoRoot, currentFile));
6538
7004
  for (const importDecl of sourceFile.getImportDeclarations()) {
6539
7005
  const moduleSpecifier = importDecl.getModuleSpecifierValue();
6540
7006
  if (!moduleSpecifier.startsWith(".")) {
6541
7007
  continue;
6542
7008
  }
6543
- let absolutePath = path3.resolve(currentDir, moduleSpecifier);
7009
+ let absolutePath = path4.resolve(currentDir, moduleSpecifier);
6544
7010
  const extensions = [".ts", ".tsx", ".js", ".jsx"];
6545
7011
  let resolved = false;
6546
7012
  for (const ext of extensions) {
@@ -6556,7 +7022,7 @@ function getLocalImports(sourceFile, repoRoot, currentFile) {
6556
7022
  }
6557
7023
  if (!resolved) {
6558
7024
  for (const ext of extensions) {
6559
- const indexPath = path3.join(absolutePath, `index${ext}`);
7025
+ const indexPath = path4.join(absolutePath, `index${ext}`);
6560
7026
  try {
6561
7027
  if (sourceFile.getProject().getSourceFile(indexPath)) {
6562
7028
  absolutePath = indexPath;
@@ -6596,7 +7062,7 @@ function traceImportedModule(ctx, importInfo, depth, needAuth, needValidation) {
6596
7062
  if (!sourceFile) {
6597
7063
  return result;
6598
7064
  }
6599
- const relPath = path3.relative(ctx.repoRoot, importInfo.absolutePath).replace(/\\/g, "/");
7065
+ const relPath = path4.relative(ctx.repoRoot, importInfo.absolutePath).replace(/\\/g, "/");
6600
7066
  if (needAuth) {
6601
7067
  sourceFile.forEachDescendant((node) => {
6602
7068
  if (result.authProven) return;
@@ -6701,7 +7167,7 @@ function buildAllProofTraces(ctx, routes) {
6701
7167
 
6702
7168
  // src/phase3/intent-miner.ts
6703
7169
  import crypto4 from "crypto";
6704
- import path4 from "path";
7170
+ import path5 from "path";
6705
7171
  import { SyntaxKind as SyntaxKind3 } from "ts-morph";
6706
7172
  var INTENT_PATTERNS = [
6707
7173
  // Auth patterns
@@ -6740,7 +7206,7 @@ function generateIntentId(type, file, line, evidence) {
6740
7206
  function mineIntentClaims(ctx, sourceFile, routes) {
6741
7207
  const claims = [];
6742
7208
  const filePath = sourceFile.getFilePath();
6743
- const relPath = path4.relative(ctx.repoRoot, filePath).replace(/\\/g, "/");
7209
+ const relPath = path5.relative(ctx.repoRoot, filePath).replace(/\\/g, "/");
6744
7210
  claims.push(...mineFromComments(sourceFile, relPath, routes));
6745
7211
  claims.push(...mineFromImports(sourceFile, relPath));
6746
7212
  claims.push(...mineFromIdentifiers(sourceFile, relPath, routes));
@@ -6912,7 +7378,7 @@ function truncateEvidence(text) {
6912
7378
  function mineAllIntentClaims(ctx, routes) {
6913
7379
  const allClaims = [];
6914
7380
  for (const file of ctx.fileIndex.allSourceFiles) {
6915
- const absolutePath = path4.join(ctx.repoRoot, file);
7381
+ const absolutePath = path5.join(ctx.repoRoot, file);
6916
7382
  const sourceFile = ctx.helpers.parseFile(absolutePath);
6917
7383
  if (!sourceFile) continue;
6918
7384
  const claims = mineIntentClaims(ctx, sourceFile, routes);
@@ -7212,7 +7678,7 @@ function generateFingerprint3(route) {
7212
7678
 
7213
7679
  // src/phase3/scanners/validation-claimed-missing.ts
7214
7680
  import crypto7 from "crypto";
7215
- import path5 from "path";
7681
+ import path6 from "path";
7216
7682
  var RULE_ID34 = "VC-HALL-012";
7217
7683
  async function scanValidationClaimedMissing(ctx) {
7218
7684
  const findings = [];
@@ -7229,7 +7695,7 @@ async function scanValidationClaimedMissing(ctx) {
7229
7695
  claimsByFile.set(claim.location.file, existing);
7230
7696
  }
7231
7697
  for (const [file, claims] of claimsByFile) {
7232
- const sourceFile = ctx.helpers.parseFile(path5.join(ctx.repoRoot, file));
7698
+ const sourceFile = ctx.helpers.parseFile(path6.join(ctx.repoRoot, file));
7233
7699
  if (!sourceFile) continue;
7234
7700
  const fileRoutes = routes.filter((r) => r.file === file);
7235
7701
  const handlers = ctx.helpers.findRouteHandlers(sourceFile);
@@ -7357,7 +7823,7 @@ function checkUnusedValidationImports(ctx, routes, claims) {
7357
7823
  );
7358
7824
  for (const claim of importClaims) {
7359
7825
  const sourceFile = ctx.helpers.parseFile(
7360
- path5.join(ctx.repoRoot, claim.location.file)
7826
+ path6.join(ctx.repoRoot, claim.location.file)
7361
7827
  );
7362
7828
  if (!sourceFile) continue;
7363
7829
  const hasSchemas = ctx.helpers.hasValidationSchemas(sourceFile);
@@ -7398,7 +7864,7 @@ function generateFingerprint4(route, issueType) {
7398
7864
 
7399
7865
  // src/phase3/scanners/auth-by-ui-server-gap.ts
7400
7866
  import crypto8 from "crypto";
7401
- import path6 from "path";
7867
+ import path7 from "path";
7402
7868
  var RULE_ID35 = "VC-AUTH-010";
7403
7869
  var CLIENT_AUTH_PATTERNS = [
7404
7870
  // React/Next.js session hooks
@@ -7439,7 +7905,7 @@ async function scanAuthByUiServerGap(ctx) {
7439
7905
  const sourceFile = ctx.helpers.parseFile(componentFile);
7440
7906
  if (!sourceFile) continue;
7441
7907
  const fullText = sourceFile.getFullText();
7442
- const relPath = path6.relative(ctx.repoRoot, componentFile).replace(/\\/g, "/");
7908
+ const relPath = path7.relative(ctx.repoRoot, componentFile).replace(/\\/g, "/");
7443
7909
  const hasClientAuth = CLIENT_AUTH_PATTERNS.some((p) => p.test(fullText));
7444
7910
  if (!hasClientAuth) continue;
7445
7911
  const apiCalls = findApiCalls(fullText, relPath, sourceFile);
@@ -8210,7 +8676,7 @@ function sarifToJson(sarif, pretty = true) {
8210
8676
  }
8211
8677
 
8212
8678
  // src/utils/progress.ts
8213
- import path7 from "path";
8679
+ import path8 from "path";
8214
8680
  var ANSI = {
8215
8681
  reset: "\x1B[0m",
8216
8682
  bold: "\x1B[1m",
@@ -8285,7 +8751,7 @@ var ScanProgress = class {
8285
8751
  const packNum = `[${String(this.currentPack + 1).padStart(2)}/${this.packs.length}]`;
8286
8752
  const filePercent = this.totalFiles > 0 ? Math.min(Math.round(this.filesProcessed / this.totalFiles * 100), 100) : 0;
8287
8753
  const progressBar = createProgressBar(this.filesProcessed, this.totalFiles, 20);
8288
- const fileName = this.currentFile ? path7.basename(this.currentFile) : "";
8754
+ const fileName = this.currentFile ? path8.basename(this.currentFile) : "";
8289
8755
  const truncatedFile = truncateEnd(fileName, 28);
8290
8756
  const statusText = `${ANSI.dim}${truncatedFile}${ANSI.reset} ${ANSI.cyan}${String(filePercent).padStart(3)}%${ANSI.reset}`;
8291
8757
  const packName = truncateEnd(pack.name, 32);
@@ -8362,13 +8828,13 @@ function normalizePath(p) {
8362
8828
  }
8363
8829
  function getOutputPath(basePath, format, targetDir) {
8364
8830
  let outputPath = basePath;
8365
- if (!path8.isAbsolute(outputPath)) {
8831
+ if (!path9.isAbsolute(outputPath)) {
8366
8832
  outputPath = resolvePath(targetDir, outputPath);
8367
8833
  }
8368
- const ext = path8.extname(outputPath);
8834
+ const ext = path9.extname(outputPath);
8369
8835
  if (!ext) {
8370
8836
  const filename = format === "sarif" ? "vibecheck-scan.sarif" : "vibecheck-scan.json";
8371
- outputPath = path8.join(outputPath, filename);
8837
+ outputPath = path9.join(outputPath, filename);
8372
8838
  } else if (format === "sarif" && ext === ".json") {
8373
8839
  outputPath = outputPath.replace(/\.json$/, ".sarif");
8374
8840
  } else if (format === "json" && ext === ".sarif") {
@@ -8376,11 +8842,19 @@ function getOutputPath(basePath, format, targetDir) {
8376
8842
  }
8377
8843
  return outputPath;
8378
8844
  }
8379
- async function runScannersWithProgress(baseContext, totalFiles) {
8845
+ async function runScannersWithProgress(baseContext, totalFiles, customScanners = []) {
8380
8846
  const allFindings = [];
8381
8847
  const seenFingerprints = /* @__PURE__ */ new Set();
8848
+ const packs = [...ALL_SCANNER_PACKS];
8849
+ if (customScanners.length > 0) {
8850
+ packs.push({
8851
+ id: "custom",
8852
+ name: "Custom Rules",
8853
+ scanners: customScanners
8854
+ });
8855
+ }
8382
8856
  const progress = new ScanProgress(
8383
- ALL_SCANNER_PACKS.map((pack) => ({
8857
+ packs.map((pack) => ({
8384
8858
  id: pack.id,
8385
8859
  name: pack.name,
8386
8860
  scannerCount: pack.scanners.length
@@ -8405,8 +8879,8 @@ async function runScannersWithProgress(baseContext, totalFiles) {
8405
8879
  prismaSchemaInfo: baseContext.prismaSchemaInfo
8406
8880
  });
8407
8881
  progress.start();
8408
- for (let packIndex = 0; packIndex < ALL_SCANNER_PACKS.length; packIndex++) {
8409
- const pack = ALL_SCANNER_PACKS[packIndex];
8882
+ for (let packIndex = 0; packIndex < packs.length; packIndex++) {
8883
+ const pack = packs[packIndex];
8410
8884
  progress.startPack(packIndex);
8411
8885
  for (let scannerIndex = 0; scannerIndex < pack.scanners.length; scannerIndex++) {
8412
8886
  const scanner = pack.scanners[scannerIndex];
@@ -8588,7 +9062,31 @@ async function executeScan(targetDir, options) {
8588
9062
  console.log(`\x1B[90m \u251C\u2500 API routes: ${initialContext.fileIndex.apiRouteFiles.length}\x1B[0m`);
8589
9063
  console.log(`\x1B[90m \u251C\u2500 Config files: ${initialContext.fileIndex.configFiles.length}\x1B[0m`);
8590
9064
  console.log(`\x1B[90m \u2514\u2500 Framework: ${initialContext.repoMeta.framework}\x1B[0m`);
8591
- const findings = await runScannersWithProgress(initialContext, totalFiles);
9065
+ let customScanners = [];
9066
+ if (options.rules) {
9067
+ const rulesSpinner = new Spinner("Loading custom rules");
9068
+ rulesSpinner.start();
9069
+ try {
9070
+ const customRules = loadRulesFromDirectory(options.rules);
9071
+ const validation = validateCustomRules(customRules);
9072
+ if (validation.errors.length > 0) {
9073
+ rulesSpinner.fail("Failed to validate custom rules");
9074
+ console.error("\n\x1B[31mCustom rule validation errors:\x1B[0m");
9075
+ for (const error of validation.errors) {
9076
+ console.error(` [${error.ruleId}] ${error.error}`);
9077
+ }
9078
+ return 1;
9079
+ }
9080
+ customScanners = createScannersFromRules(validation.valid);
9081
+ rulesSpinner.succeed(`Loaded ${validation.valid.length} custom rule(s)`);
9082
+ printRulesSummary(validation.valid);
9083
+ } catch (error) {
9084
+ rulesSpinner.fail("Failed to load custom rules");
9085
+ console.error(`\x1B[31m${error instanceof Error ? error.message : String(error)}\x1B[0m`);
9086
+ return 1;
9087
+ }
9088
+ }
9089
+ const findings = await runScannersWithProgress(initialContext, totalFiles, customScanners);
8592
9090
  const endTime = Date.now();
8593
9091
  const scanDurationMs = endTime - startTime;
8594
9092
  let phase3Data;
@@ -8712,13 +9210,13 @@ async function executeScan(targetDir, options) {
8712
9210
  const outputFiles = [];
8713
9211
  if (format === "json" || format === "both") {
8714
9212
  const jsonPath = getOutputPath(options.out, "json", absoluteTarget);
8715
- ensureDir(path8.dirname(jsonPath));
9213
+ ensureDir(path9.dirname(jsonPath));
8716
9214
  writeFileSync(jsonPath, JSON.stringify(artifact, null, 2));
8717
9215
  outputFiles.push(jsonPath);
8718
9216
  }
8719
9217
  if (format === "sarif" || format === "both") {
8720
9218
  const sarifPath = getOutputPath(options.out, "sarif", absoluteTarget);
8721
- ensureDir(path8.dirname(sarifPath));
9219
+ ensureDir(path9.dirname(sarifPath));
8722
9220
  const sarifLog = toSarif(artifact);
8723
9221
  writeFileSync(sarifPath, sarifToJson(sarifLog));
8724
9222
  outputFiles.push(sarifPath);
@@ -8730,6 +9228,39 @@ async function executeScan(targetDir, options) {
8730
9228
  }
8731
9229
  }
8732
9230
  printSummary(artifact, options);
9231
+ if (options.applyFixes) {
9232
+ console.log("\n" + "=".repeat(60));
9233
+ console.log("Applying Patches");
9234
+ console.log("=".repeat(60));
9235
+ const patchSummary = await applyPatches(
9236
+ artifact.findings,
9237
+ absoluteTarget,
9238
+ {
9239
+ force: options.force,
9240
+ dryRun: false
9241
+ }
9242
+ );
9243
+ console.log("\n" + "-".repeat(60));
9244
+ console.log("Patch Summary");
9245
+ console.log("-".repeat(60));
9246
+ console.log(`Total patchable findings: ${patchSummary.totalPatchable}`);
9247
+ console.log(`\x1B[32mSuccessfully applied: ${patchSummary.applied}\x1B[0m`);
9248
+ if (patchSummary.failed > 0) {
9249
+ console.log(`\x1B[31mFailed: ${patchSummary.failed}\x1B[0m`);
9250
+ }
9251
+ if (patchSummary.skipped > 0) {
9252
+ console.log(`\x1B[90mSkipped: ${patchSummary.skipped}\x1B[0m`);
9253
+ }
9254
+ if (patchSummary.failed > 0) {
9255
+ console.log("\nFailed patches:");
9256
+ for (const result of patchSummary.results) {
9257
+ if (!result.success && result.error !== "User declined") {
9258
+ console.log(` \x1B[31m\u2717\x1B[0m ${result.file}: ${result.error}`);
9259
+ }
9260
+ }
9261
+ }
9262
+ console.log("");
9263
+ }
8733
9264
  if (shouldFail(findings, failOn)) {
8734
9265
  console.log(
8735
9266
  `\x1B[31mFailing: Found findings with severity >= ${failOn}\x1B[0m`
@@ -8748,7 +9279,7 @@ function registerScanCommand(program2) {
8748
9279
  ).option(
8749
9280
  "-o, --out <path>",
8750
9281
  "Output file or directory path",
8751
- path8.join(DEFAULT_OUTPUT_DIR, DEFAULT_OUTPUT_FILE)
9282
+ path9.join(DEFAULT_OUTPUT_DIR, DEFAULT_OUTPUT_FILE)
8752
9283
  ).option(
8753
9284
  "-f, --format <format>",
8754
9285
  "Output format: json, sarif, or both",
@@ -8783,6 +9314,15 @@ function registerScanCommand(program2) {
8783
9314
  ).option(
8784
9315
  "--no-emit-traces",
8785
9316
  "Exclude proof traces from output"
9317
+ ).option(
9318
+ "--apply-fixes",
9319
+ "Apply patches from findings after scan (requires confirmation unless --force is used)"
9320
+ ).option(
9321
+ "--force",
9322
+ "Skip confirmation prompts when applying patches (use with --apply-fixes)"
9323
+ ).option(
9324
+ "-r, --rules <path>",
9325
+ "Path to directory containing custom YAML rules or a single YAML rule file"
8786
9326
  ).addHelpText(
8787
9327
  "after",
8788
9328
  `
@@ -8805,6 +9345,10 @@ Examples:
8805
9345
  $ vibecheck scan --include-tests Include test files
8806
9346
  $ vibecheck scan --no-emit-intents Skip intent mining
8807
9347
  $ vibecheck scan --no-emit-traces Skip proof trace building
9348
+ $ vibecheck scan --apply-fixes Apply patches from findings (with confirmation)
9349
+ $ vibecheck scan --apply-fixes --force Apply patches without confirmation prompts
9350
+ $ vibecheck scan --rules ./custom-rules Load custom YAML rules from directory
9351
+ $ vibecheck scan -r my-rule.yaml Load a single custom YAML rule file
8808
9352
 
8809
9353
  Windows-safe output paths:
8810
9354
  $ vibecheck scan --out vibecheck-scan.json Relative path (recommended)
@@ -8830,7 +9374,10 @@ Default excludes:
8830
9374
  emitTraces: cmdOptions.emitTraces !== false,
8831
9375
  // default true
8832
9376
  exclude: cmdOptions.exclude,
8833
- includeTests: Boolean(cmdOptions.includeTests)
9377
+ includeTests: Boolean(cmdOptions.includeTests),
9378
+ applyFixes: Boolean(cmdOptions.applyFixes),
9379
+ force: Boolean(cmdOptions.force),
9380
+ rules: cmdOptions.rules
8834
9381
  };
8835
9382
  const exitCode = await executeScan(targetDir, options);
8836
9383
  process.exit(exitCode);
@@ -9351,7 +9898,7 @@ function registerDemoArtifactCommand(program2) {
9351
9898
  }
9352
9899
 
9353
9900
  // src/commands/intent.ts
9354
- import path9 from "path";
9901
+ import path10 from "path";
9355
9902
  async function executeIntent(targetDir, options) {
9356
9903
  const absoluteTarget = resolvePath(targetDir);
9357
9904
  console.log(`Mining intent map: ${absoluteTarget}`);
@@ -9386,7 +9933,7 @@ Completed in ${endTime - startTime}ms`);
9386
9933
  options.repoName
9387
9934
  );
9388
9935
  let outputPath = options.out;
9389
- if (!path9.isAbsolute(outputPath)) {
9936
+ if (!path10.isAbsolute(outputPath)) {
9390
9937
  outputPath = resolvePath(absoluteTarget, outputPath);
9391
9938
  }
9392
9939
  if (options.format === "json") {
@@ -9512,63 +10059,63 @@ function registerIntentCommand(program2) {
9512
10059
  }
9513
10060
 
9514
10061
  // src/commands/evaluate.ts
9515
- import path10 from "path";
10062
+ import path11 from "path";
9516
10063
 
9517
10064
  // ../policy/dist/schemas/waiver.js
9518
- import { z as z7 } from "zod";
9519
- var WaiverMatchSchema = z7.object({
10065
+ import { z as z8 } from "zod";
10066
+ var WaiverMatchSchema = z8.object({
9520
10067
  /** Exact fingerprint match */
9521
- fingerprint: z7.string().optional(),
10068
+ fingerprint: z8.string().optional(),
9522
10069
  /** Rule ID (exact or prefix like "VC-AUTH-*") */
9523
- ruleId: z7.string().optional(),
10070
+ ruleId: z8.string().optional(),
9524
10071
  /** Path glob pattern for evidence file matching */
9525
- pathPattern: z7.string().optional()
10072
+ pathPattern: z8.string().optional()
9526
10073
  });
9527
- var WaiverSchema = z7.object({
10074
+ var WaiverSchema = z8.object({
9528
10075
  /** Unique waiver ID */
9529
- id: z7.string(),
10076
+ id: z8.string(),
9530
10077
  /** Match criteria */
9531
10078
  match: WaiverMatchSchema.refine((m) => m.fingerprint || m.ruleId, { message: "Waiver must specify fingerprint or ruleId" }),
9532
10079
  /** Justification for the waiver */
9533
- reason: z7.string().min(1),
10080
+ reason: z8.string().min(1),
9534
10081
  /** Who created this waiver */
9535
- createdBy: z7.string().min(1),
10082
+ createdBy: z8.string().min(1),
9536
10083
  /** When the waiver was created */
9537
- createdAt: z7.string().datetime(),
10084
+ createdAt: z8.string().datetime(),
9538
10085
  /** Optional expiration date */
9539
- expiresAt: z7.string().datetime().optional(),
10086
+ expiresAt: z8.string().datetime().optional(),
9540
10087
  /** Optional ticket/issue reference */
9541
- ticketRef: z7.string().optional()
10088
+ ticketRef: z8.string().optional()
9542
10089
  });
9543
- var WaiversFileSchema = z7.object({
10090
+ var WaiversFileSchema = z8.object({
9544
10091
  /** Schema version */
9545
- version: z7.literal("0.1"),
10092
+ version: z8.literal("0.1"),
9546
10093
  /** List of waivers */
9547
- waivers: z7.array(WaiverSchema)
10094
+ waivers: z8.array(WaiverSchema)
9548
10095
  });
9549
10096
 
9550
10097
  // ../policy/dist/schemas/policy-config.js
9551
- import { z as z8 } from "zod";
9552
- var ProfileNameSchema = z8.enum(["startup", "strict", "compliance-lite"]);
9553
- var ThresholdsSchema = z8.object({
10098
+ import { z as z9 } from "zod";
10099
+ var ProfileNameSchema = z9.enum(["startup", "strict", "compliance-lite"]);
10100
+ var ThresholdsSchema = z9.object({
9554
10101
  /** Minimum severity to trigger FAIL status */
9555
10102
  failOnSeverity: SeveritySchema.default("high"),
9556
10103
  /** Minimum severity to trigger WARN status */
9557
10104
  warnOnSeverity: SeveritySchema.default("medium"),
9558
10105
  /** Minimum confidence (0-1) for a finding to trigger FAIL */
9559
- minConfidenceForFail: z8.number().min(0).max(1).default(0.7),
10106
+ minConfidenceForFail: z9.number().min(0).max(1).default(0.7),
9560
10107
  /** Minimum confidence (0-1) for a finding to trigger WARN */
9561
- minConfidenceForWarn: z8.number().min(0).max(1).default(0.5),
10108
+ minConfidenceForWarn: z9.number().min(0).max(1).default(0.5),
9562
10109
  /** Special lower confidence threshold for critical findings */
9563
- minConfidenceCritical: z8.number().min(0).max(1).default(0.5),
10110
+ minConfidenceCritical: z9.number().min(0).max(1).default(0.5),
9564
10111
  /** Maximum number of findings before auto-fail (0 = unlimited) */
9565
- maxFindings: z8.number().int().min(0).default(0),
10112
+ maxFindings: z9.number().int().min(0).default(0),
9566
10113
  /** Maximum number of critical findings before auto-fail (0 = unlimited) */
9567
- maxCritical: z8.number().int().min(0).default(0),
10114
+ maxCritical: z9.number().int().min(0).default(0),
9568
10115
  /** Maximum number of high findings before auto-fail (0 = unlimited) */
9569
- maxHigh: z8.number().int().min(0).default(0)
10116
+ maxHigh: z9.number().int().min(0).default(0)
9570
10117
  });
9571
- var OverrideActionSchema = z8.enum([
10118
+ var OverrideActionSchema = z9.enum([
9572
10119
  "ignore",
9573
10120
  // Skip this finding entirely
9574
10121
  "downgrade",
@@ -9580,62 +10127,62 @@ var OverrideActionSchema = z8.enum([
9580
10127
  "fail"
9581
10128
  // Always fail on this
9582
10129
  ]);
9583
- var OverrideSchema = z8.object({
10130
+ var OverrideSchema = z9.object({
9584
10131
  /** Rule ID pattern (exact or prefix like "VC-AUTH-*") */
9585
- ruleId: z8.string().optional(),
10132
+ ruleId: z9.string().optional(),
9586
10133
  /** Category to match */
9587
10134
  category: CategorySchema.optional(),
9588
10135
  /** Path pattern for evidence file matching (glob-like) */
9589
- pathPattern: z8.string().optional(),
10136
+ pathPattern: z9.string().optional(),
9590
10137
  /** Action to take when matched */
9591
10138
  action: OverrideActionSchema,
9592
10139
  /** Override severity when action is downgrade/upgrade */
9593
10140
  severity: SeveritySchema.optional(),
9594
10141
  /** Comment explaining the override */
9595
- comment: z8.string().optional()
10142
+ comment: z9.string().optional()
9596
10143
  });
9597
- var RegressionPolicySchema = z8.object({
10144
+ var RegressionPolicySchema = z9.object({
9598
10145
  /** Fail on any new high/critical findings */
9599
- failOnNewHighCritical: z8.boolean().default(true),
10146
+ failOnNewHighCritical: z9.boolean().default(true),
9600
10147
  /** Fail on any severity regression (e.g., medium became high) */
9601
- failOnSeverityRegression: z8.boolean().default(false),
10148
+ failOnSeverityRegression: z9.boolean().default(false),
9602
10149
  /** Fail on net increase in findings */
9603
- failOnNetIncrease: z8.boolean().default(false),
10150
+ failOnNetIncrease: z9.boolean().default(false),
9604
10151
  /** Warn on any new findings */
9605
- warnOnNewFindings: z8.boolean().default(true),
10152
+ warnOnNewFindings: z9.boolean().default(true),
9606
10153
  /** Fail on protection removed from routes (auth/validation coverage decreased) */
9607
- failOnProtectionRemoved: z8.boolean().default(false),
10154
+ failOnProtectionRemoved: z9.boolean().default(false),
9608
10155
  /** Warn on protection removed from routes */
9609
- warnOnProtectionRemoved: z8.boolean().default(true),
10156
+ warnOnProtectionRemoved: z9.boolean().default(true),
9610
10157
  /** Fail on any semantic regression (coverage decrease, severity group increase) */
9611
- failOnSemanticRegression: z8.boolean().default(false)
10158
+ failOnSemanticRegression: z9.boolean().default(false)
9612
10159
  });
9613
- var PolicyConfigSchema = z8.object({
10160
+ var PolicyConfigSchema = z9.object({
9614
10161
  /** Profile name for presets */
9615
10162
  profile: ProfileNameSchema.optional(),
9616
10163
  /** Threshold configuration */
9617
10164
  thresholds: ThresholdsSchema.default({}),
9618
10165
  /** Override rules */
9619
- overrides: z8.array(OverrideSchema).default([]),
10166
+ overrides: z9.array(OverrideSchema).default([]),
9620
10167
  /** Regression policy */
9621
10168
  regression: RegressionPolicySchema.default({})
9622
10169
  });
9623
- var ConfigFileSchema = z8.object({
10170
+ var ConfigFileSchema = z9.object({
9624
10171
  /** Policy configuration */
9625
10172
  policy: PolicyConfigSchema.optional(),
9626
10173
  /** Path to waivers file */
9627
- waiversPath: z8.string().optional()
10174
+ waiversPath: z9.string().optional()
9628
10175
  });
9629
10176
 
9630
10177
  // ../policy/dist/schemas/policy-report.js
9631
- import { z as z9 } from "zod";
10178
+ import { z as z10 } from "zod";
9632
10179
  var POLICY_REPORT_VERSION = "0.1";
9633
- var PolicyStatusSchema = z9.enum(["pass", "warn", "fail"]);
9634
- var PolicyReasonSchema = z9.object({
10180
+ var PolicyStatusSchema = z10.enum(["pass", "warn", "fail"]);
10181
+ var PolicyReasonSchema = z10.object({
9635
10182
  /** The status this reason contributes to */
9636
10183
  status: PolicyStatusSchema,
9637
10184
  /** Machine-readable reason code */
9638
- code: z9.enum([
10185
+ code: z10.enum([
9639
10186
  "severity_threshold",
9640
10187
  "confidence_threshold",
9641
10188
  "count_threshold",
@@ -9651,47 +10198,47 @@ var PolicyReasonSchema = z9.object({
9651
10198
  "semantic_regression"
9652
10199
  ]),
9653
10200
  /** Human-readable description */
9654
- message: z9.string(),
10201
+ message: z10.string(),
9655
10202
  /** Optional finding IDs that triggered this */
9656
- findingIds: z9.array(z9.string()).optional(),
10203
+ findingIds: z10.array(z10.string()).optional(),
9657
10204
  /** Optional details */
9658
- details: z9.record(z9.unknown()).optional()
10205
+ details: z10.record(z10.unknown()).optional()
9659
10206
  });
9660
- var PolicySummaryCountsSchema = z9.object({
10207
+ var PolicySummaryCountsSchema = z10.object({
9661
10208
  /** Total findings after filtering */
9662
- total: z9.number().int(),
10209
+ total: z10.number().int(),
9663
10210
  /** By severity */
9664
- bySeverity: z9.object({
9665
- critical: z9.number().int(),
9666
- high: z9.number().int(),
9667
- medium: z9.number().int(),
9668
- low: z9.number().int(),
9669
- info: z9.number().int()
10211
+ bySeverity: z10.object({
10212
+ critical: z10.number().int(),
10213
+ high: z10.number().int(),
10214
+ medium: z10.number().int(),
10215
+ low: z10.number().int(),
10216
+ info: z10.number().int()
9670
10217
  }),
9671
10218
  /** By category */
9672
- byCategory: z9.record(CategorySchema, z9.number().int()),
10219
+ byCategory: z10.record(CategorySchema, z10.number().int()),
9673
10220
  /** Count of waived findings */
9674
- waived: z9.number().int(),
10221
+ waived: z10.number().int(),
9675
10222
  /** Count of ignored by override */
9676
- ignored: z9.number().int()
10223
+ ignored: z10.number().int()
9677
10224
  });
9678
- var ProtectionRegressionSchema = z9.object({
10225
+ var ProtectionRegressionSchema = z10.object({
9679
10226
  /** Route identifier (e.g., "/api/users:POST") */
9680
- routeId: z9.string(),
10227
+ routeId: z10.string(),
9681
10228
  /** File containing the route */
9682
- file: z9.string(),
10229
+ file: z10.string(),
9683
10230
  /** HTTP method */
9684
- method: z9.string(),
10231
+ method: z10.string(),
9685
10232
  /** Type of protection that was removed */
9686
- protectionType: z9.enum(["auth", "validation", "rate-limit", "middleware"]),
10233
+ protectionType: z10.enum(["auth", "validation", "rate-limit", "middleware"]),
9687
10234
  /** Description of what changed */
9688
- description: z9.string(),
10235
+ description: z10.string(),
9689
10236
  /** Related finding fingerprints */
9690
- relatedFingerprints: z9.array(z9.string()).optional()
10237
+ relatedFingerprints: z10.array(z10.string()).optional()
9691
10238
  });
9692
- var SemanticRegressionSchema = z9.object({
10239
+ var SemanticRegressionSchema = z10.object({
9693
10240
  /** Type of semantic regression */
9694
- type: z9.enum([
10241
+ type: z10.enum([
9695
10242
  "protection_removed",
9696
10243
  // Auth/validation removed from route
9697
10244
  "coverage_decreased",
@@ -9702,68 +10249,68 @@ var SemanticRegressionSchema = z9.object({
9702
10249
  /** Severity of this regression */
9703
10250
  severity: SeveritySchema,
9704
10251
  /** Human-readable description */
9705
- description: z9.string(),
10252
+ description: z10.string(),
9706
10253
  /** Route or fingerprint group affected */
9707
- affectedId: z9.string(),
10254
+ affectedId: z10.string(),
9708
10255
  /** Detailed evidence */
9709
- details: z9.record(z9.unknown()).optional()
10256
+ details: z10.record(z10.unknown()).optional()
9710
10257
  });
9711
- var RegressionSummarySchema = z9.object({
10258
+ var RegressionSummarySchema = z10.object({
9712
10259
  /** Baseline artifact path or identifier */
9713
- baselineId: z9.string(),
10260
+ baselineId: z10.string(),
9714
10261
  /** When baseline was generated */
9715
- baselineGeneratedAt: z9.string(),
10262
+ baselineGeneratedAt: z10.string(),
9716
10263
  /** New findings (not in baseline) */
9717
- newFindings: z9.array(z9.object({
9718
- findingId: z9.string(),
9719
- fingerprint: z9.string(),
10264
+ newFindings: z10.array(z10.object({
10265
+ findingId: z10.string(),
10266
+ fingerprint: z10.string(),
9720
10267
  severity: SeveritySchema,
9721
- ruleId: z9.string(),
9722
- title: z9.string()
10268
+ ruleId: z10.string(),
10269
+ title: z10.string()
9723
10270
  })),
9724
10271
  /** Resolved findings (in baseline but not current) */
9725
- resolvedFindings: z9.array(z9.object({
9726
- fingerprint: z9.string(),
10272
+ resolvedFindings: z10.array(z10.object({
10273
+ fingerprint: z10.string(),
9727
10274
  severity: SeveritySchema,
9728
- ruleId: z9.string(),
9729
- title: z9.string()
10275
+ ruleId: z10.string(),
10276
+ title: z10.string()
9730
10277
  })),
9731
10278
  /** Persisting findings (in both) */
9732
- persistingCount: z9.number().int(),
10279
+ persistingCount: z10.number().int(),
9733
10280
  /** Severity regressions (same fingerprint but higher severity) */
9734
- severityRegressions: z9.array(z9.object({
9735
- fingerprint: z9.string(),
9736
- ruleId: z9.string(),
10281
+ severityRegressions: z10.array(z10.object({
10282
+ fingerprint: z10.string(),
10283
+ ruleId: z10.string(),
9737
10284
  previousSeverity: SeveritySchema,
9738
10285
  currentSeverity: SeveritySchema,
9739
- title: z9.string()
10286
+ title: z10.string()
9740
10287
  })),
9741
10288
  /** Net change in finding count */
9742
- netChange: z9.number().int(),
10289
+ netChange: z10.number().int(),
9743
10290
  /** Protection regressions (auth/validation removed from routes) */
9744
- protectionRegressions: z9.array(ProtectionRegressionSchema).optional(),
10291
+ protectionRegressions: z10.array(ProtectionRegressionSchema).optional(),
9745
10292
  /** Semantic regressions (abstract security property degradations) */
9746
- semanticRegressions: z9.array(SemanticRegressionSchema).optional()
10293
+ semanticRegressions: z10.array(SemanticRegressionSchema).optional()
9747
10294
  });
9748
- var WaivedFindingSchema = z9.object({
10295
+ var WaivedFindingSchema = z10.object({
9749
10296
  /** The finding that was waived */
9750
- finding: z9.object({
9751
- id: z9.string(),
9752
- fingerprint: z9.string(),
9753
- ruleId: z9.string(),
10297
+ finding: z10.object({
10298
+ id: z10.string(),
10299
+ fingerprint: z10.string(),
10300
+ ruleId: z10.string(),
9754
10301
  severity: SeveritySchema,
9755
- title: z9.string()
10302
+ title: z10.string()
9756
10303
  }),
9757
10304
  /** The waiver that matched */
9758
10305
  waiver: WaiverSchema,
9759
10306
  /** Whether waiver is expired */
9760
- expired: z9.boolean()
10307
+ expired: z10.boolean()
9761
10308
  });
9762
- var PolicyReportSchema = z9.object({
10309
+ var PolicyReportSchema = z10.object({
9763
10310
  /** Report schema version */
9764
- policyVersion: z9.literal(POLICY_REPORT_VERSION),
10311
+ policyVersion: z10.literal(POLICY_REPORT_VERSION),
9765
10312
  /** When evaluation was performed */
9766
- evaluatedAt: z9.string().datetime(),
10313
+ evaluatedAt: z10.string().datetime(),
9767
10314
  /** Profile name used */
9768
10315
  profileName: ProfileNameSchema.nullable(),
9769
10316
  /** Final status */
@@ -9771,36 +10318,36 @@ var PolicyReportSchema = z9.object({
9771
10318
  /** Thresholds applied */
9772
10319
  thresholds: ThresholdsSchema,
9773
10320
  /** Overrides applied */
9774
- overrides: z9.array(OverrideSchema),
10321
+ overrides: z10.array(OverrideSchema),
9775
10322
  /** Regression policy applied */
9776
10323
  regressionPolicy: RegressionPolicySchema,
9777
10324
  /** Summary counts */
9778
10325
  summary: PolicySummaryCountsSchema,
9779
10326
  /** Reasons for the status */
9780
- reasons: z9.array(PolicyReasonSchema),
10327
+ reasons: z10.array(PolicyReasonSchema),
9781
10328
  /** Regression summary (if baseline provided) */
9782
10329
  regression: RegressionSummarySchema.optional(),
9783
10330
  /** Waived findings */
9784
- waivedFindings: z9.array(WaivedFindingSchema),
10331
+ waivedFindings: z10.array(WaivedFindingSchema),
9785
10332
  /** Active (non-waived, non-ignored) findings included in evaluation */
9786
- activeFindings: z9.array(z9.object({
9787
- id: z9.string(),
9788
- fingerprint: z9.string(),
9789
- ruleId: z9.string(),
10333
+ activeFindings: z10.array(z10.object({
10334
+ id: z10.string(),
10335
+ fingerprint: z10.string(),
10336
+ ruleId: z10.string(),
9790
10337
  severity: SeveritySchema,
9791
10338
  originalSeverity: SeveritySchema.optional(),
9792
- confidence: z9.number(),
9793
- title: z9.string(),
10339
+ confidence: z10.number(),
10340
+ title: z10.string(),
9794
10341
  category: CategorySchema,
9795
- evidencePaths: z9.array(z9.string())
10342
+ evidencePaths: z10.array(z10.string())
9796
10343
  })),
9797
10344
  /** Recommended exit code (0 = pass/warn, 1 = fail) */
9798
- exitCode: z9.union([z9.literal(0), z9.literal(1)]),
10345
+ exitCode: z10.union([z10.literal(0), z10.literal(1)]),
9799
10346
  /** Source artifact info */
9800
- artifact: z9.object({
9801
- path: z9.string().optional(),
9802
- generatedAt: z9.string(),
9803
- repoName: z9.string().optional()
10347
+ artifact: z10.object({
10348
+ path: z10.string().optional(),
10349
+ generatedAt: z10.string(),
10350
+ repoName: z10.string().optional()
9804
10351
  })
9805
10352
  });
9806
10353
 
@@ -9928,7 +10475,7 @@ var PROFILE_DESCRIPTIONS = {
9928
10475
  };
9929
10476
 
9930
10477
  // ../policy/dist/waivers.js
9931
- import micromatch from "micromatch";
10478
+ import micromatch2 from "micromatch";
9932
10479
  function matchRuleId(ruleId, pattern) {
9933
10480
  if (pattern.endsWith("*")) {
9934
10481
  const prefix = pattern.slice(0, -1);
@@ -9937,7 +10484,7 @@ function matchRuleId(ruleId, pattern) {
9937
10484
  return ruleId === pattern;
9938
10485
  }
9939
10486
  function matchPathPattern(evidencePaths, pattern) {
9940
- return evidencePaths.some((path14) => micromatch.isMatch(path14, pattern));
10487
+ return evidencePaths.some((path15) => micromatch2.isMatch(path15, pattern));
9941
10488
  }
9942
10489
  function isWaiverExpired(waiver, now = /* @__PURE__ */ new Date()) {
9943
10490
  if (!waiver.expiresAt) {
@@ -10748,7 +11295,7 @@ async function executeEvaluate(options) {
10748
11295
  });
10749
11296
  if (options.out) {
10750
11297
  const outPath = resolvePath(options.out);
10751
- ensureDir(path10.dirname(outPath));
11298
+ ensureDir(path11.dirname(outPath));
10752
11299
  writeFileSync(outPath, JSON.stringify(report, null, 2));
10753
11300
  if (!options.quiet) {
10754
11301
  console.log(`Policy report written to: ${outPath}`);
@@ -10826,7 +11373,7 @@ Examples:
10826
11373
  }
10827
11374
 
10828
11375
  // src/commands/waivers.ts
10829
- import path11 from "path";
11376
+ import path12 from "path";
10830
11377
  var DEFAULT_WAIVERS_FILE = "vibecheck-waivers.json";
10831
11378
  function loadWaiversFile(filepath) {
10832
11379
  const absolutePath = resolvePath(filepath);
@@ -10846,7 +11393,7 @@ function loadWaiversFile(filepath) {
10846
11393
  }
10847
11394
  function saveWaiversFile(filepath, file) {
10848
11395
  const absolutePath = resolvePath(filepath);
10849
- ensureDir(path11.dirname(absolutePath));
11396
+ ensureDir(path12.dirname(absolutePath));
10850
11397
  writeFileSync(absolutePath, JSON.stringify(file, null, 2));
10851
11398
  }
10852
11399
  function executeWaiversInit(filepath, force) {
@@ -11019,13 +11566,13 @@ Examples:
11019
11566
  }
11020
11567
 
11021
11568
  // src/commands/view.ts
11022
- import { existsSync as existsSync3 } from "fs";
11023
- import { resolve as resolve2, join as join3 } from "path";
11569
+ import { existsSync as existsSync4 } from "fs";
11570
+ import { resolve as resolve2, join as join4 } from "path";
11024
11571
 
11025
11572
  // src/utils/static-server.ts
11026
11573
  import { createServer } from "http";
11027
- import { existsSync, readFileSync as readFileSync2, statSync, readdirSync } from "fs";
11028
- import { join, extname } from "path";
11574
+ import { existsSync as existsSync2, readFileSync as readFileSync5, statSync as statSync2, readdirSync as readdirSync2 } from "fs";
11575
+ import { join as join2, extname as extname3 } from "path";
11029
11576
  var MIME_TYPES = {
11030
11577
  ".html": "text/html; charset=utf-8",
11031
11578
  ".js": "application/javascript; charset=utf-8",
@@ -11050,7 +11597,7 @@ var MIME_TYPES = {
11050
11597
  ".map": "application/json"
11051
11598
  };
11052
11599
  function getMimeType(filePath) {
11053
- const ext = extname(filePath).toLowerCase();
11600
+ const ext = extname3(filePath).toLowerCase();
11054
11601
  return MIME_TYPES[ext] || "application/octet-stream";
11055
11602
  }
11056
11603
  function resolveFilePath(staticDir, urlPath) {
@@ -11064,49 +11611,49 @@ function resolveFilePath(staticDir, urlPath) {
11064
11611
  if (queryIndex !== -1) {
11065
11612
  decodedPath = decodedPath.substring(0, queryIndex);
11066
11613
  }
11067
- let filePath = join(staticDir, decodedPath === "/" ? "index.html" : decodedPath);
11068
- const normalizedPath = join(filePath);
11614
+ let filePath = join2(staticDir, decodedPath === "/" ? "index.html" : decodedPath);
11615
+ const normalizedPath = join2(filePath);
11069
11616
  if (!normalizedPath.startsWith(staticDir)) {
11070
11617
  return null;
11071
11618
  }
11072
- if (existsSync(filePath)) {
11073
- const stat = statSync(filePath);
11619
+ if (existsSync2(filePath)) {
11620
+ const stat = statSync2(filePath);
11074
11621
  if (stat.isDirectory()) {
11075
- filePath = join(filePath, "index.html");
11076
- if (!existsSync(filePath)) {
11622
+ filePath = join2(filePath, "index.html");
11623
+ if (!existsSync2(filePath)) {
11077
11624
  return null;
11078
11625
  }
11079
11626
  }
11080
11627
  return filePath;
11081
11628
  }
11082
- if (existsSync(filePath + ".html")) {
11629
+ if (existsSync2(filePath + ".html")) {
11083
11630
  return filePath + ".html";
11084
11631
  }
11085
11632
  const segments = decodedPath.split("/").filter(Boolean);
11086
11633
  if (segments.length >= 2) {
11087
11634
  const parentPath = segments.slice(0, -1).join("/");
11088
- const parentDir = join(staticDir, parentPath);
11089
- if (existsSync(parentDir) && statSync(parentDir).isDirectory()) {
11635
+ const parentDir = join2(staticDir, parentPath);
11636
+ if (existsSync2(parentDir) && statSync2(parentDir).isDirectory()) {
11090
11637
  try {
11091
- const files = readdirSync(parentDir);
11638
+ const files = readdirSync2(parentDir);
11092
11639
  const htmlFile = files.find((f) => f.endsWith(".html"));
11093
11640
  if (htmlFile) {
11094
- return join(parentDir, htmlFile);
11641
+ return join2(parentDir, htmlFile);
11095
11642
  }
11096
11643
  } catch {
11097
11644
  }
11098
11645
  }
11099
- const parentHtml = join(staticDir, parentPath + ".html");
11100
- if (existsSync(parentHtml)) {
11646
+ const parentHtml = join2(staticDir, parentPath + ".html");
11647
+ if (existsSync2(parentHtml)) {
11101
11648
  return parentHtml;
11102
11649
  }
11103
- const parentIndexHtml = join(staticDir, parentPath, "index.html");
11104
- if (existsSync(parentIndexHtml)) {
11650
+ const parentIndexHtml = join2(staticDir, parentPath, "index.html");
11651
+ if (existsSync2(parentIndexHtml)) {
11105
11652
  return parentIndexHtml;
11106
11653
  }
11107
11654
  }
11108
- const indexPath = join(staticDir, "index.html");
11109
- if (existsSync(indexPath)) {
11655
+ const indexPath = join2(staticDir, "index.html");
11656
+ if (existsSync2(indexPath)) {
11110
11657
  return indexPath;
11111
11658
  }
11112
11659
  return null;
@@ -11120,7 +11667,7 @@ function handleRequest(staticDir, req, res) {
11120
11667
  return;
11121
11668
  }
11122
11669
  try {
11123
- const content = readFileSync2(filePath);
11670
+ const content = readFileSync5(filePath);
11124
11671
  const contentType = getMimeType(filePath);
11125
11672
  res.writeHead(200, {
11126
11673
  "Content-Type": contentType,
@@ -11140,9 +11687,9 @@ async function startStaticServer(options) {
11140
11687
  const server = createServer((req, res) => {
11141
11688
  const urlPath = req.url || "/";
11142
11689
  if (urlPath === "/__vibecheck__/artifact" || urlPath === "/__vibecheck__/artifact.json") {
11143
- if (artifactPath && existsSync(artifactPath)) {
11690
+ if (artifactPath && existsSync2(artifactPath)) {
11144
11691
  try {
11145
- const content = readFileSync2(artifactPath);
11692
+ const content = readFileSync5(artifactPath);
11146
11693
  res.writeHead(200, {
11147
11694
  "Content-Type": "application/json",
11148
11695
  "Content-Length": content.length,
@@ -11222,22 +11769,22 @@ function isPortAvailable(port) {
11222
11769
  }
11223
11770
 
11224
11771
  // src/utils/viewer-cache.ts
11225
- import { existsSync as existsSync2, mkdirSync, writeFileSync as writeFileSync3, readFileSync as readFileSync3, rmSync } from "fs";
11226
- import { join as join2 } from "path";
11772
+ import { existsSync as existsSync3, mkdirSync, writeFileSync as writeFileSync3, readFileSync as readFileSync6, rmSync } from "fs";
11773
+ import { join as join3 } from "path";
11227
11774
  import { homedir } from "os";
11228
11775
  import { execSync as execSync2 } from "child_process";
11229
11776
  import { extract } from "tar";
11230
- var CACHE_DIR = join2(homedir(), ".vibecheck");
11231
- var VIEWER_DIR = join2(CACHE_DIR, "viewer");
11232
- var VIEWER_DIST_DIR = join2(VIEWER_DIR, "dist");
11233
- var VERSION_FILE = join2(VIEWER_DIR, ".version");
11777
+ var CACHE_DIR = join3(homedir(), ".vibecheck");
11778
+ var VIEWER_DIR = join3(CACHE_DIR, "viewer");
11779
+ var VIEWER_DIST_DIR = join3(VIEWER_DIR, "dist");
11780
+ var VERSION_FILE = join3(VIEWER_DIR, ".version");
11234
11781
  var VIEWER_PACKAGE = "@quantracode/vibecheck-viewer";
11235
11782
  function getInstalledVersion() {
11236
- if (!existsSync2(VERSION_FILE)) {
11783
+ if (!existsSync3(VERSION_FILE)) {
11237
11784
  return null;
11238
11785
  }
11239
11786
  try {
11240
- return readFileSync3(VERSION_FILE, "utf-8").trim();
11787
+ return readFileSync6(VERSION_FILE, "utf-8").trim();
11241
11788
  } catch {
11242
11789
  return null;
11243
11790
  }
@@ -11266,7 +11813,7 @@ async function downloadPackage(version) {
11266
11813
  stdio: ["pipe", "pipe", "pipe"]
11267
11814
  }
11268
11815
  ).trim();
11269
- if (existsSync2(VIEWER_DIR)) {
11816
+ if (existsSync3(VIEWER_DIR)) {
11270
11817
  rmSync(VIEWER_DIR, { recursive: true, force: true });
11271
11818
  }
11272
11819
  mkdirSync(VIEWER_DIR, { recursive: true });
@@ -11274,7 +11821,7 @@ async function downloadPackage(version) {
11274
11821
  if (!response.ok) {
11275
11822
  throw new Error(`Failed to download viewer: ${response.statusText}`);
11276
11823
  }
11277
- const tarPath = join2(CACHE_DIR, "viewer.tgz");
11824
+ const tarPath = join3(CACHE_DIR, "viewer.tgz");
11278
11825
  const buffer = Buffer.from(await response.arrayBuffer());
11279
11826
  writeFileSync3(tarPath, buffer);
11280
11827
  await extract({
@@ -11292,7 +11839,7 @@ async function ensureViewer(forceUpdate = false) {
11292
11839
  try {
11293
11840
  latestVersion = await getLatestVersion();
11294
11841
  } catch (error) {
11295
- if (installedVersion && existsSync2(join2(VIEWER_DIST_DIR, "index.html"))) {
11842
+ if (installedVersion && existsSync3(join3(VIEWER_DIST_DIR, "index.html"))) {
11296
11843
  return {
11297
11844
  path: VIEWER_DIST_DIR,
11298
11845
  version: installedVersion
@@ -11300,7 +11847,7 @@ async function ensureViewer(forceUpdate = false) {
11300
11847
  }
11301
11848
  throw error;
11302
11849
  }
11303
- const needsUpdate = forceUpdate || !installedVersion || installedVersion !== latestVersion || !existsSync2(join2(VIEWER_DIST_DIR, "index.html"));
11850
+ const needsUpdate = forceUpdate || !installedVersion || installedVersion !== latestVersion || !existsSync3(join3(VIEWER_DIST_DIR, "index.html"));
11304
11851
  if (needsUpdate) {
11305
11852
  console.log(
11306
11853
  installedVersion ? `Updating viewer from ${installedVersion} to ${latestVersion}...` : `Installing viewer v${latestVersion}...`
@@ -11315,7 +11862,7 @@ async function ensureViewer(forceUpdate = false) {
11315
11862
  };
11316
11863
  }
11317
11864
  function clearViewerCache() {
11318
- if (existsSync2(VIEWER_DIR)) {
11865
+ if (existsSync3(VIEWER_DIR)) {
11319
11866
  rmSync(VIEWER_DIR, { recursive: true, force: true });
11320
11867
  console.log("Viewer cache cleared.");
11321
11868
  } else {
@@ -11425,7 +11972,7 @@ Check your network connection and try again.`);
11425
11972
  `);
11426
11973
  process.exit(1);
11427
11974
  }
11428
- if (!existsSync3(join3(viewerInfo.path, "index.html"))) {
11975
+ if (!existsSync4(join4(viewerInfo.path, "index.html"))) {
11429
11976
  console.error(`\x1B[31mError: Viewer files not found.\x1B[0m`);
11430
11977
  console.error(`Try reinstalling: vibecheck view --update
11431
11978
  `);
@@ -11434,7 +11981,7 @@ Check your network connection and try again.`);
11434
11981
  let artifactPath;
11435
11982
  if (options.artifact) {
11436
11983
  artifactPath = resolve2(options.artifact);
11437
- if (!existsSync3(artifactPath)) {
11984
+ if (!existsSync4(artifactPath)) {
11438
11985
  console.log(
11439
11986
  `\x1B[33mWarning: Artifact file not found: ${options.artifact}\x1B[0m
11440
11987
  `
@@ -11450,7 +11997,7 @@ Check your network connection and try again.`);
11450
11997
  ];
11451
11998
  for (const p of commonPaths) {
11452
11999
  const fullPath = resolve2(p);
11453
- if (existsSync3(fullPath)) {
12000
+ if (existsSync4(fullPath)) {
11454
12001
  artifactPath = fullPath;
11455
12002
  console.log(`\x1B[90mAuto-detected artifact: ${p}\x1B[0m`);
11456
12003
  break;
@@ -11515,11 +12062,11 @@ Workflow:
11515
12062
  }
11516
12063
 
11517
12064
  // src/commands/license.ts
11518
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync4, existsSync as existsSync4, mkdirSync as mkdirSync2, unlinkSync } from "fs";
12065
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync4, existsSync as existsSync5, mkdirSync as mkdirSync2, unlinkSync } from "fs";
11519
12066
  import { homedir as homedir2 } from "os";
11520
- import { join as join4 } from "path";
12067
+ import { join as join5 } from "path";
11521
12068
  import chalk from "chalk";
11522
- import readline from "readline";
12069
+ import readline2 from "readline";
11523
12070
 
11524
12071
  // ../license/dist/types.js
11525
12072
  var PLAN_FEATURES = {
@@ -11733,17 +12280,17 @@ function inspectLicense(licenseKey) {
11733
12280
  }
11734
12281
 
11735
12282
  // src/commands/license.ts
11736
- var CONFIG_DIR = join4(homedir2(), ".vibecheck");
11737
- var LICENSE_FILE = join4(CONFIG_DIR, "license.key");
12283
+ var CONFIG_DIR = join5(homedir2(), ".vibecheck");
12284
+ var LICENSE_FILE = join5(CONFIG_DIR, "license.key");
11738
12285
  function ensureConfigDir() {
11739
- if (!existsSync4(CONFIG_DIR)) {
12286
+ if (!existsSync5(CONFIG_DIR)) {
11740
12287
  mkdirSync2(CONFIG_DIR, { recursive: true });
11741
12288
  }
11742
12289
  }
11743
12290
  function getStoredLicenseKey() {
11744
12291
  try {
11745
- if (existsSync4(LICENSE_FILE)) {
11746
- return readFileSync4(LICENSE_FILE, "utf-8").trim();
12292
+ if (existsSync5(LICENSE_FILE)) {
12293
+ return readFileSync7(LICENSE_FILE, "utf-8").trim();
11747
12294
  }
11748
12295
  } catch {
11749
12296
  }
@@ -11755,7 +12302,7 @@ function storeLicenseKey(licenseKey) {
11755
12302
  }
11756
12303
  function clearLicenseKey() {
11757
12304
  try {
11758
- if (existsSync4(LICENSE_FILE)) {
12305
+ if (existsSync5(LICENSE_FILE)) {
11759
12306
  unlinkSync(LICENSE_FILE);
11760
12307
  return true;
11761
12308
  }
@@ -11764,7 +12311,7 @@ function clearLicenseKey() {
11764
12311
  return false;
11765
12312
  }
11766
12313
  function promptInput(question) {
11767
- const rl = readline.createInterface({
12314
+ const rl = readline2.createInterface({
11768
12315
  input: process.stdin,
11769
12316
  output: process.stdout
11770
12317
  });
@@ -11935,11 +12482,11 @@ async function createLicenseAction(options) {
11935
12482
  }
11936
12483
  let privateKey;
11937
12484
  if (options.key) {
11938
- if (!existsSync4(options.key)) {
12485
+ if (!existsSync5(options.key)) {
11939
12486
  console.error(chalk.red(`Error: Key file not found: ${options.key}`));
11940
12487
  process.exit(1);
11941
12488
  }
11942
- privateKey = readFileSync4(options.key, "utf-8").trim();
12489
+ privateKey = readFileSync7(options.key, "utf-8").trim();
11943
12490
  } else if (process.env[options.keyEnv]) {
11944
12491
  privateKey = process.env[options.keyEnv];
11945
12492
  }
@@ -12111,7 +12658,7 @@ async function keygenAction() {
12111
12658
  }
12112
12659
 
12113
12660
  // src/commands/verify-determinism.ts
12114
- import path12 from "path";
12661
+ import path13 from "path";
12115
12662
  import crypto9 from "crypto";
12116
12663
  function normalizeArtifact(artifact) {
12117
12664
  const normalized = JSON.parse(JSON.stringify(artifact));
@@ -12371,8 +12918,8 @@ async function executeVerifyDeterminism(targetDir, options) {
12371
12918
  printCertificate(certificate, verbose);
12372
12919
  if (out) {
12373
12920
  const outPath = resolvePath(out);
12374
- ensureDir(path12.dirname(outPath));
12375
- const certPath = outPath.endsWith(".json") ? outPath : path12.join(outPath, "determinism-cert.json");
12921
+ ensureDir(path13.dirname(outPath));
12922
+ const certPath = outPath.endsWith(".json") ? outPath : path13.join(outPath, "determinism-cert.json");
12376
12923
  writeFileSync(certPath, JSON.stringify(certificate, null, 2));
12377
12924
  console.log(`Certificate written to: ${certPath}
12378
12925
  `);
@@ -12418,7 +12965,7 @@ What it does:
12418
12965
  }
12419
12966
 
12420
12967
  // src/commands/badge.ts
12421
- import path13 from "path";
12968
+ import path14 from "path";
12422
12969
  var COLORS = {
12423
12970
  brightgreen: "#4c1",
12424
12971
  green: "#97ca00",
@@ -12653,7 +13200,7 @@ async function executeBadge(options) {
12653
13200
  const writtenFiles = [];
12654
13201
  for (const badge of badges) {
12655
13202
  const svg = generateSvgBadge(badge, style);
12656
- const filePath = path13.join(absoluteOutPath, badge.filename);
13203
+ const filePath = path14.join(absoluteOutPath, badge.filename);
12657
13204
  writeFileSync(filePath, svg);
12658
13205
  writtenFiles.push(filePath);
12659
13206
  console.log(` \x1B[32m\u2713\x1B[0m ${badge.filename} (${badge.label}: ${badge.message})`);
@@ -12662,8 +13209,8 @@ async function executeBadge(options) {
12662
13209
  \x1B[32m${writtenFiles.length} badges generated\x1B[0m
12663
13210
  `);
12664
13211
  console.log(`\x1B[90mUsage in README.md:\x1B[0m`);
12665
- console.log(` ![VibeCheck Status](${path13.basename(absoluteOutPath)}/vibecheck-status.svg)`);
12666
- console.log(` ![VibeCheck Score](${path13.basename(absoluteOutPath)}/vibecheck-score.svg)
13212
+ console.log(` ![VibeCheck Status](${path14.basename(absoluteOutPath)}/vibecheck-status.svg)`);
13213
+ console.log(` ![VibeCheck Score](${path14.basename(absoluteOutPath)}/vibecheck-score.svg)
12667
13214
  `);
12668
13215
  return 0;
12669
13216
  }