ship-safe 3.1.0 → 3.2.0

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.
@@ -101,6 +101,21 @@ export function finding(file, line, patternName, severity, matched, description,
101
101
  console.log(` ${chalk.gray('Why:')} ${description}`);
102
102
  }
103
103
 
104
+ /**
105
+ * Print a vulnerability finding (code issue — show matched code, not masked)
106
+ */
107
+ export function vulnerabilityFinding(file, line, patternName, severity, matched, description) {
108
+ const color = severityColors[severity] || chalk.white;
109
+ const icon = severityIcons[severity] || '';
110
+ const snippet = matched.length > 80 ? matched.slice(0, 80) + '…' : matched;
111
+
112
+ console.log();
113
+ console.log(chalk.white.bold(`${file}:${line}`));
114
+ console.log(` ${icon}${color(`[${severity.toUpperCase()}]`)} ${chalk.white(patternName)}`);
115
+ console.log(` ${chalk.gray('Code:')} ${chalk.cyan(snippet)}`);
116
+ console.log(` ${chalk.gray('Why:')} ${description}`);
117
+ }
118
+
104
119
  /**
105
120
  * Mask the middle of a secret for safe display
106
121
  */
@@ -113,15 +128,27 @@ export function maskSecret(secret) {
113
128
 
114
129
  /**
115
130
  * Print a summary box
131
+ *
132
+ * stats can include:
133
+ * total, critical, high, medium, filesScanned
134
+ * secretsTotal (optional), vulnsTotal (optional)
116
135
  */
117
136
  export function summary(stats) {
118
137
  console.log();
119
138
  console.log(chalk.cyan('='.repeat(60)));
120
139
 
121
140
  if (stats.total === 0) {
122
- console.log(chalk.green.bold(' \u2714 No secrets detected!'));
141
+ console.log(chalk.green.bold(' \u2714 No issues detected!'));
123
142
  } else {
124
- console.log(chalk.red.bold(` \u26a0 Found ${stats.total} potential secret(s)`));
143
+ const secretsTotal = stats.secretsTotal ?? stats.total;
144
+ const vulnsTotal = stats.vulnsTotal ?? 0;
145
+
146
+ if (secretsTotal > 0) {
147
+ console.log(chalk.red.bold(` \u26a0 Found ${secretsTotal} secret(s)`));
148
+ }
149
+ if (vulnsTotal > 0) {
150
+ console.log(chalk.yellow.bold(` \u26a0 Found ${vulnsTotal} code vulnerability/vulnerabilities`));
151
+ }
125
152
 
126
153
  if (stats.critical > 0) {
127
154
  console.log(chalk.red(` \u2022 Critical: ${stats.critical}`));
@@ -138,6 +165,19 @@ export function summary(stats) {
138
165
  console.log(chalk.cyan('='.repeat(60)));
139
166
  }
140
167
 
168
+ /**
169
+ * Print recommended actions after finding code vulnerabilities
170
+ */
171
+ export function vulnRecommendations() {
172
+ console.log();
173
+ console.log(chalk.yellow.bold('Code Vulnerability Actions:'));
174
+ console.log();
175
+ console.log(chalk.white('1.') + ' Fix the flagged code patterns (see "Why" descriptions above)');
176
+ console.log(chalk.white('2.') + ' Use # ship-safe-ignore on lines that are safe (e.g. internal tools, controlled input)');
177
+ console.log(chalk.white('3.') + ' Run npx ship-safe checklist for a full launch-day security review');
178
+ console.log();
179
+ }
180
+
141
181
  /**
142
182
  * Print recommended actions after finding secrets
143
183
  */
@@ -392,6 +392,52 @@ export const SECRET_PATTERNS = [
392
392
  severity: 'medium',
393
393
  description: 'Supabase anon keys. Safe for frontend but verify RLS is enabled.'
394
394
  },
395
+ {
396
+ name: 'Stytch Secret Key',
397
+ pattern: /secret-(?:live|test)-[a-zA-Z0-9]{30,}/g,
398
+ severity: 'critical',
399
+ description: 'Stytch secret keys grant full access to your authentication system.'
400
+ },
401
+ {
402
+ name: 'Okta API Token',
403
+ pattern: /(?:okta|OKTA)[_-]?(?:api[_-]?)?token["']?\s*[:=]\s*["']?(00[a-zA-Z0-9_-]{38})["']?/gi,
404
+ severity: 'high',
405
+ description: 'Okta API tokens can manage your identity provider and user directory.'
406
+ },
407
+
408
+ // =========================================================================
409
+ // CRITICAL: Additional Cloud/Infra
410
+ // =========================================================================
411
+ {
412
+ name: 'Azure Storage Connection String',
413
+ pattern: /DefaultEndpointsProtocol=https;AccountName=[^;]{1,50};AccountKey=[a-zA-Z0-9+/=]{44,}/g,
414
+ severity: 'critical',
415
+ description: 'Azure Storage connection strings contain account keys with full storage access.'
416
+ },
417
+ {
418
+ name: 'AWS Session Token',
419
+ pattern: /(?:aws_session_token|aws_security_token)[\s]*[=:][\s]*["']?([A-Za-z0-9/+=]{100,})["']?/gi,
420
+ severity: 'critical',
421
+ description: 'AWS session tokens are temporary credentials that still grant account access.'
422
+ },
423
+ {
424
+ name: 'PlanetScale Service Token',
425
+ pattern: /pscale_tkn_[a-zA-Z0-9_-]{32,}/g,
426
+ severity: 'critical',
427
+ description: 'PlanetScale service tokens grant programmatic database branch access.'
428
+ },
429
+ {
430
+ name: 'Shopify Admin API Access Token',
431
+ pattern: /shpat_[a-fA-F0-9]{32}/g,
432
+ severity: 'critical',
433
+ description: 'Shopify admin tokens grant full store management access including orders and customers.'
434
+ },
435
+ {
436
+ name: 'Shopify Custom App Access Token',
437
+ pattern: /shpca_[a-fA-F0-9]{32}/g,
438
+ severity: 'critical',
439
+ description: 'Shopify custom app tokens provide scoped store admin access.'
440
+ },
395
441
 
396
442
  // =========================================================================
397
443
  // HIGH: Productivity & SaaS
@@ -421,6 +467,66 @@ export const SECRET_PATTERNS = [
421
467
  description: 'Figma PATs can access your design files and projects.'
422
468
  },
423
469
 
470
+ // =========================================================================
471
+ // HIGH: AI/ML Providers (2025-2026 additions)
472
+ // =========================================================================
473
+ {
474
+ name: 'xAI (Grok) API Key',
475
+ pattern: /xai-[A-Za-z0-9]{52,}/g,
476
+ severity: 'high',
477
+ description: 'xAI API keys grant access to Grok models and incur usage charges on your account.'
478
+ },
479
+ {
480
+ name: 'Tavily API Key',
481
+ pattern: /tvly-[a-zA-Z0-9]{32,}/g,
482
+ severity: 'high',
483
+ description: 'Tavily API keys grant access to their AI-powered search service.'
484
+ },
485
+ {
486
+ name: 'Cerebras API Key',
487
+ pattern: /csk-[a-zA-Z0-9]{48,}/g,
488
+ severity: 'high',
489
+ description: 'Cerebras API keys provide access to fast AI inference.'
490
+ },
491
+ {
492
+ name: 'Pinecone API Key',
493
+ pattern: /pcsk_[a-zA-Z0-9]{47}_[a-zA-Z0-9]{47}/g,
494
+ severity: 'high',
495
+ description: 'Pinecone API keys grant access to your vector database indexes.'
496
+ },
497
+ {
498
+ name: 'ElevenLabs API Key',
499
+ pattern: /(?:elevenlabs|ELEVENLABS)[_-]?(?:api[_-]?)?key["']?\s*[:=]\s*["']?([a-fA-F0-9]{32})["']?/gi,
500
+ severity: 'high',
501
+ description: 'ElevenLabs API keys grant access to their voice AI cloning and synthesis service.'
502
+ },
503
+ {
504
+ name: 'DeepSeek API Key',
505
+ pattern: /(?:deepseek|DEEPSEEK)[_-]?(?:api[_-]?)?key["']?\s*[:=]\s*["']?(sk-[a-zA-Z0-9]{32,})["']?/gi,
506
+ severity: 'high',
507
+ description: 'DeepSeek API keys grant access to their language models.'
508
+ },
509
+ {
510
+ name: 'Voyage AI API Key',
511
+ pattern: /(?:voyage|VOYAGE)[_-]?(?:ai[_-]?)?(?:api[_-]?)?key["']?\s*[:=]\s*["']?([a-zA-Z0-9]{32,})["']?/gi,
512
+ severity: 'high',
513
+ requiresEntropyCheck: true,
514
+ description: 'Voyage AI API keys provide access to their embedding and reranking models.'
515
+ },
516
+ {
517
+ name: 'Fireworks AI API Key',
518
+ pattern: /(?:fireworks|FIREWORKS)[_-]?(?:api[_-]?)?key["']?\s*[:=]\s*["']?([a-zA-Z0-9]{32,})["']?/gi,
519
+ severity: 'high',
520
+ requiresEntropyCheck: true,
521
+ description: 'Fireworks AI API keys grant access to fast open-source model inference.'
522
+ },
523
+ {
524
+ name: 'Anyscale API Key',
525
+ pattern: /esecret_[a-zA-Z0-9]{32,}/g,
526
+ severity: 'high',
527
+ description: 'Anyscale API keys grant access to their managed Ray and LLM endpoints.'
528
+ },
529
+
424
530
  // =========================================================================
425
531
  // HIGH: Payments (Additional)
426
532
  // =========================================================================
@@ -454,6 +560,99 @@ export const SECRET_PATTERNS = [
454
560
  severity: 'high',
455
561
  description: 'Paddle API keys can manage your subscriptions and payments.'
456
562
  },
563
+ {
564
+ name: 'Stripe Restricted Key',
565
+ pattern: /rk_(?:live|test)_[a-zA-Z0-9]{24,}/g,
566
+ severity: 'high',
567
+ description: 'Stripe restricted keys have scoped permissions but still grant API access.'
568
+ },
569
+ {
570
+ name: 'Square Access Token',
571
+ pattern: /EAAAl[0-9a-zA-Z_-]{50,}/g,
572
+ severity: 'high',
573
+ description: 'Square access tokens can process payments and manage store data.'
574
+ },
575
+ {
576
+ name: 'Square OAuth Token',
577
+ pattern: /sq0[a-z]tp-[a-zA-Z0-9_-]{22}/g,
578
+ severity: 'high',
579
+ description: 'Square OAuth tokens authorize Square API access for a merchant account.'
580
+ },
581
+ {
582
+ name: 'Shopify Shared Secret',
583
+ pattern: /shpss_[a-fA-F0-9]{32}/g,
584
+ severity: 'high',
585
+ description: 'Shopify shared secrets validate webhook payload signatures.'
586
+ },
587
+ {
588
+ name: 'Braintree Access Token',
589
+ pattern: /access_token\$(?:production|sandbox)\$[a-z0-9]{16}\$[a-f0-9]{32}/g,
590
+ severity: 'critical',
591
+ description: 'Braintree access tokens grant full payment processing access.'
592
+ },
593
+
594
+ // =========================================================================
595
+ // HIGH: Realtime & Messaging
596
+ // =========================================================================
597
+ {
598
+ name: 'Pusher App Secret',
599
+ pattern: /(?:pusher|PUSHER)[_-]?(?:app[_-]?)?secret["']?\s*[:=]\s*["']?([a-f0-9]{32})["']?/gi,
600
+ severity: 'high',
601
+ description: 'Pusher app secrets authenticate private and presence channel subscriptions.'
602
+ },
603
+ {
604
+ name: 'Ably API Key',
605
+ pattern: /(?:ably|ABLY)[_-]?(?:api[_-]?)?key["']?\s*[:=]\s*["']?([a-zA-Z0-9_-]{8}\.[a-zA-Z0-9_-]{6}:[a-zA-Z0-9+/=_-]{43,})["']?/gi,
606
+ severity: 'high',
607
+ description: 'Ably API keys grant full publish and subscribe access to your realtime channels.'
608
+ },
609
+ {
610
+ name: 'Mapbox Access Token',
611
+ pattern: /pk\.eyJ1[a-zA-Z0-9._-]{40,}/g,
612
+ severity: 'medium',
613
+ description: 'Mapbox tokens can incur charges if abused. Restrict token scope and allowed URLs.'
614
+ },
615
+
616
+ // =========================================================================
617
+ // HIGH: DevOps & CI/CD
618
+ // =========================================================================
619
+ {
620
+ name: 'CircleCI Personal API Token',
621
+ pattern: /CCIPAT_[a-zA-Z0-9]{40,}/g,
622
+ severity: 'high',
623
+ description: 'CircleCI API tokens can trigger builds, read logs, and access pipeline data.'
624
+ },
625
+ {
626
+ name: 'Sentry Auth Token',
627
+ pattern: /sntrys_[a-zA-Z0-9_]{64,}/g,
628
+ severity: 'high',
629
+ description: 'Sentry auth tokens provide full API access to your error data and project settings.'
630
+ },
631
+ {
632
+ name: 'Terraform Cloud Token',
633
+ pattern: /(?:terraform|TFC)[_-]?(?:api[_-]?)?token["']?\s*[:=]\s*["']?([a-zA-Z0-9]{14}\.atlasv1\.[a-zA-Z0-9_-]{67,})["']?/gi,
634
+ severity: 'high',
635
+ description: 'Terraform Cloud tokens can read and apply infrastructure state.'
636
+ },
637
+ {
638
+ name: 'Cloudinary API Secret',
639
+ pattern: /(?:cloudinary|CLOUDINARY)[_-]?(?:api[_-]?)?secret["']?\s*[:=]\s*["']?([a-zA-Z0-9_-]{27,})["']?/gi,
640
+ severity: 'high',
641
+ requiresEntropyCheck: true,
642
+ description: 'Cloudinary API secrets grant access to your media library and transformations.'
643
+ },
644
+ {
645
+ name: 'Algolia Admin API Key',
646
+ pattern: /(?:algolia|ALGOLIA)[_-]?(?:admin[_-]?)?(?:api[_-]?)?key["']?\s*[:=]\s*["']?([a-f0-9]{32})["']?/gi,
647
+ severity: 'high',
648
+ description: 'Algolia admin keys can modify indices and change search configuration.'
649
+ },
650
+ {
651
+ name: 'LaunchDarkly SDK Key',
652
+ pattern: /sdk-[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/g,
653
+ severity: 'high',
654
+ description: 'LaunchDarkly SDK keys can read all feature flags and user data.'
655
+ },
457
656
 
458
657
  // =========================================================================
459
658
  // HIGH: Analytics & Monitoring
@@ -566,7 +765,24 @@ export const SKIP_DIRS = new Set([
566
765
  'jspm_packages',
567
766
  '.vercel',
568
767
  '.netlify',
569
- '.serverless'
768
+ '.serverless',
769
+ // Additional build/tooling output
770
+ '.yarn',
771
+ 'storybook-static',
772
+ 'playwright-report',
773
+ '.playwright',
774
+ '.gradle',
775
+ 'target', // Maven/Gradle build output
776
+ '.pytest_cache',
777
+ '.mypy_cache',
778
+ '.ruff_cache',
779
+ '.tox',
780
+ 'site-packages',
781
+ '.pnpm',
782
+ 'jspm_packages',
783
+ '.expo',
784
+ '.docusaurus',
785
+ '.storybook',
570
786
  ]);
571
787
 
572
788
  export const SKIP_EXTENSIONS = new Set([
@@ -593,6 +809,182 @@ export const SKIP_EXTENSIONS = new Set([
593
809
  // Maximum file size to scan (1MB)
594
810
  export const MAX_FILE_SIZE = 1_000_000;
595
811
 
812
+ // =============================================================================
813
+ // SECURITY VULNERABILITY PATTERNS
814
+ // =============================================================================
815
+ //
816
+ // These patterns detect insecure code patterns (OWASP Top 10, misconfigs, etc.)
817
+ // They are distinct from SECRET_PATTERNS:
818
+ // - Secrets → move to env vars, rotate if exposed
819
+ // - Vulns → fix the code pattern, can't just rotate
820
+ //
821
+ // Each pattern includes category: 'vulnerability' to separate output sections.
822
+
823
+ export const SECURITY_PATTERNS = [
824
+
825
+ // =========================================================================
826
+ // XSS — Cross-Site Scripting
827
+ // =========================================================================
828
+ {
829
+ name: 'XSS: dangerouslySetInnerHTML',
830
+ pattern: /dangerouslySetInnerHTML\s*=\s*\{\s*\{/g,
831
+ severity: 'high',
832
+ category: 'vulnerability',
833
+ description: 'dangerouslySetInnerHTML can introduce XSS if the value contains user input. Sanitize with DOMPurify or restructure to avoid it.'
834
+ },
835
+ {
836
+ name: 'XSS: innerHTML Assignment',
837
+ pattern: /\.innerHTML\s*=/g,
838
+ severity: 'medium',
839
+ category: 'vulnerability',
840
+ description: 'innerHTML set to user-controlled data leads to XSS. Use textContent for plain text or DOMPurify to sanitize HTML.'
841
+ },
842
+ {
843
+ name: 'XSS: document.write',
844
+ pattern: /\bdocument\.write\s*\(/g,
845
+ severity: 'medium',
846
+ category: 'vulnerability',
847
+ description: 'document.write() is deprecated and can introduce XSS. Use DOM manipulation (createElement, appendChild) instead.'
848
+ },
849
+
850
+ // =========================================================================
851
+ // Code Injection
852
+ // =========================================================================
853
+ {
854
+ name: 'Code Injection: eval()',
855
+ pattern: /\beval\s*\(/g,
856
+ severity: 'high',
857
+ category: 'vulnerability',
858
+ description: 'eval() executes arbitrary JavaScript and is a serious attack vector. Replace with JSON.parse(), Function calls, or safer alternatives.'
859
+ },
860
+ {
861
+ name: 'Code Injection: new Function()',
862
+ pattern: /\bnew\s+Function\s*\(/g,
863
+ severity: 'high',
864
+ category: 'vulnerability',
865
+ description: 'new Function() is functionally equivalent to eval() and can execute arbitrary code. Avoid dynamic code generation.'
866
+ },
867
+
868
+ // =========================================================================
869
+ // SQL Injection
870
+ // =========================================================================
871
+ {
872
+ name: 'SQL Injection: Template Literal Query',
873
+ pattern: /`(?:SELECT|INSERT|UPDATE|DELETE|DROP\s+TABLE|ALTER\s+TABLE)[^`]*\$\{/gi,
874
+ severity: 'critical',
875
+ category: 'vulnerability',
876
+ description: 'SQL queries with interpolated template variables are vulnerable to injection. Use parameterized queries or a query builder.'
877
+ },
878
+ {
879
+ name: 'SQL Injection: String Concatenation Query',
880
+ pattern: /["'](?:SELECT|INSERT|UPDATE|DELETE)\s+[^"']{4,}["']\s*\+/gi,
881
+ severity: 'high',
882
+ category: 'vulnerability',
883
+ description: 'Building SQL with string concatenation is vulnerable to SQL injection. Use parameterized queries (?, $1) or an ORM.'
884
+ },
885
+
886
+ // =========================================================================
887
+ // Command Injection
888
+ // =========================================================================
889
+ {
890
+ name: 'Command Injection: exec with Template Literal',
891
+ pattern: /\bexec(?:Sync)?\s*\(\s*`[^`]*\$\{/g,
892
+ severity: 'critical',
893
+ category: 'vulnerability',
894
+ description: 'Running shell commands with interpolated values can lead to command injection. Validate all inputs or use execFile() with argument arrays.'
895
+ },
896
+ {
897
+ name: 'Command Injection: shell: true',
898
+ pattern: /\bspawn(?:Sync)?\s*\([^)]*\bshell\s*:\s*true/g,
899
+ severity: 'high',
900
+ category: 'vulnerability',
901
+ description: 'shell: true in spawn/spawnSync enables shell expansion and can lead to command injection. Remove shell: true and pass arguments as an array.'
902
+ },
903
+
904
+ // =========================================================================
905
+ // Weak Cryptography
906
+ // =========================================================================
907
+ {
908
+ name: 'Weak Crypto: MD5',
909
+ pattern: /createHash\s*\(\s*['"]md5['"]\s*\)/gi,
910
+ severity: 'medium',
911
+ category: 'vulnerability',
912
+ description: 'MD5 is cryptographically broken and must not be used for security purposes. Use SHA-256 (createHash("sha256")) or SHA-3.'
913
+ },
914
+ {
915
+ name: 'Weak Crypto: SHA-1',
916
+ pattern: /createHash\s*\(\s*['"]sha1['"]\s*\)/gi,
917
+ severity: 'medium',
918
+ category: 'vulnerability',
919
+ description: 'SHA-1 is cryptographically weak and collision-prone. Use SHA-256 (createHash("sha256")) or SHA-3 instead.'
920
+ },
921
+
922
+ // =========================================================================
923
+ // TLS / SSL Bypass
924
+ // =========================================================================
925
+ {
926
+ name: 'TLS Bypass: NODE_TLS_REJECT_UNAUTHORIZED=0',
927
+ pattern: /NODE_TLS_REJECT_UNAUTHORIZED\s*[=:]\s*['"]?0['"]?/g,
928
+ severity: 'critical',
929
+ category: 'vulnerability',
930
+ description: 'Setting NODE_TLS_REJECT_UNAUTHORIZED=0 disables TLS certificate validation and exposes your app to MITM attacks. Never use in production.'
931
+ },
932
+ {
933
+ name: 'TLS Bypass: rejectUnauthorized false',
934
+ pattern: /\brejectUnauthorized\s*:\s*false\b/g,
935
+ severity: 'high',
936
+ category: 'vulnerability',
937
+ description: 'rejectUnauthorized: false disables TLS certificate checking and enables man-in-the-middle attacks. Remove it or use a proper CA bundle.'
938
+ },
939
+ {
940
+ name: 'TLS Bypass: verify=False (Python)',
941
+ pattern: /\brequests\.\w+\s*\([^)]*\bverify\s*=\s*False\b/g,
942
+ severity: 'high',
943
+ category: 'vulnerability',
944
+ description: 'verify=False in Python requests disables SSL certificate verification. Remove this or pass verify="/path/to/ca-bundle.crt".'
945
+ },
946
+
947
+ // =========================================================================
948
+ // Unsafe Deserialization
949
+ // =========================================================================
950
+ {
951
+ name: 'Unsafe Deserialization: pickle.loads',
952
+ pattern: /\bpickle\.loads?\s*\(/g,
953
+ severity: 'high',
954
+ category: 'vulnerability',
955
+ description: 'pickle.loads() on untrusted data can execute arbitrary Python code (RCE). Use JSON or another safe format for data from untrusted sources.'
956
+ },
957
+ {
958
+ name: 'Unsafe Deserialization: yaml.load',
959
+ pattern: /\byaml\.load\s*\(/g,
960
+ severity: 'medium',
961
+ category: 'vulnerability',
962
+ description: 'yaml.load() can execute arbitrary code with certain YAML tags. Use yaml.safe_load() for untrusted input.'
963
+ },
964
+
965
+ // =========================================================================
966
+ // Security Misconfigurations
967
+ // =========================================================================
968
+ {
969
+ name: 'Security Config: CORS Wildcard',
970
+ pattern: /\borigin\s*:\s*['"]?\*['"]?/g,
971
+ severity: 'medium',
972
+ category: 'vulnerability',
973
+ description: 'CORS wildcard (*) allows any origin to make credentialed requests to your API. Use a specific allowlist of trusted origins.'
974
+ },
975
+
976
+ // =========================================================================
977
+ // Deprecated / Insecure Node.js APIs
978
+ // =========================================================================
979
+ {
980
+ name: 'Deprecated API: new Buffer()',
981
+ pattern: /\bnew\s+Buffer\s*\(/g,
982
+ severity: 'medium',
983
+ category: 'vulnerability',
984
+ description: 'new Buffer() is deprecated since Node.js 6 and has security implications. Use Buffer.from(), Buffer.alloc(), or Buffer.allocUnsafe().'
985
+ },
986
+ ];
987
+
596
988
  // =============================================================================
597
989
  // TEST FILE PATTERNS (skipped by default, override with --include-tests)
598
990
  // =============================================================================
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ship-safe",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "description": "Security toolkit for vibe coders and indie hackers. Secure your MVP in 5 minutes.",
5
5
  "main": "cli/index.js",
6
6
  "bin": {
@@ -57,7 +57,7 @@
57
57
  "dependencies": {
58
58
  "chalk": "^5.3.0",
59
59
  "commander": "^12.1.0",
60
- "glob": "^10.3.10",
60
+ "fast-glob": "^3.3.3",
61
61
  "ora": "^8.0.1",
62
62
  "write-file-atomic": "^7.0.0"
63
63
  }