checkmyvibe 1.2.0 → 1.3.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.
@@ -0,0 +1,146 @@
1
+ #!/usr/bin/env python3
2
+ import os
3
+ import sys
4
+ import re
5
+ import fnmatch
6
+
7
+ # Ensure stdout supports UTF-8 on Windows
8
+ if hasattr(sys.stdout, 'reconfigure'):
9
+ sys.stdout.reconfigure(encoding='utf-8', errors='replace')
10
+
11
+ SKIP_DIRS = {
12
+ 'node_modules', '.git', 'dist', 'build', '.next', '.svelte-kit',
13
+ '__tests__', '__mocks__', 'tests', 'test', 'spec', 'fixtures',
14
+ 'mocks', 'e2e', 'cypress', 'venv', '.venv'
15
+ }
16
+
17
+ TEST_FILE_PATTERNS = ['*.test.*', '*.spec.*', '*.mock.*']
18
+
19
+ def is_test_file(filename):
20
+ return any(fnmatch.fnmatch(filename, pat) for pat in TEST_FILE_PATTERNS)
21
+
22
+ # Patterns for client-controlled payment amounts
23
+ CLIENT_PRICE_PATTERNS = [
24
+ (re.compile(r'(?i)\b(?:amount|price|unit_amount|total)\s*:\s*(?:req\.body|req\.query)\.(?:amount|price|unit_amount|total)\b'),
25
+ "Charge amount or price is read directly from client request body/query (Critical: client price tampering)"),
26
+ (re.compile(r'(?i)const\s*\{\s*[^}]*(?:price|amount|total)[^}]*\}\s*=\s*req\.body'),
27
+ "Price or amount extracted directly from req.body (Critical: verify amount is fetched server-side from DB)"),
28
+ ]
29
+
30
+ # Patterns detecting payment webhook routes
31
+ WEBHOOK_ROUTE_PATTERN = re.compile(
32
+ r'(?i)\b(?:app|router)\s*\.\s*(?:post|all)\s*\(\s*[\'"`][^\'"`]*(?:webhook|stripe|lemonsqueezy|dodo|paddle|razorpay)[^\'"`]*[\'"`]'
33
+ )
34
+
35
+ # Provider signature verification patterns
36
+ STRIPE_SIG_PATTERN = re.compile(r'(?i)stripe\.webhooks\.constructEvent\s*\(')
37
+ LEMON_SIG_PATTERN = re.compile(r'(?i)(?:x-signature|crypto\.createHmac|createHmac\s*\(\s*[\'"]sha256[\'"])')
38
+ DODO_SIG_PATTERN = re.compile(r'(?i)(?:webhook-signature|dodo\.webhooks|verifyWebhookSignature)')
39
+ PADDLE_SIG_PATTERN = re.compile(r'(?i)(?:paddle\.webhooks|validateWebhookSignature|crypto\.createHmac)')
40
+ RAZORPAY_SIG_PATTERN = re.compile(r'(?i)(?:validateWebhookSignature|verifyPaymentSignature|crypto\.createHmac)')
41
+
42
+ # Subscription cancellation lifecycle patterns
43
+ SUB_CANCEL_PATTERNS = re.compile(
44
+ r'(?i)(?:customer\.subscription\.deleted|subscription_cancelled|subscription\.canceled|subscription\.expired|invoice\.payment_failed)'
45
+ )
46
+
47
+ # Billing portal IDOR
48
+ PORTAL_IDOR_PATTERN = re.compile(
49
+ r'(?i)\bbillingPortal\.sessions\.create\s*\(\s*\{[^}]*customer\s*:\s*req\.body\b'
50
+ )
51
+
52
+ def scan_file(file_path):
53
+ findings = []
54
+ try:
55
+ with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
56
+ content = f.read()
57
+ lines = content.splitlines()
58
+
59
+ # 1. Line by line pattern matching
60
+ for line_num, line in enumerate(lines, 1):
61
+ for pattern, desc in CLIENT_PRICE_PATTERNS:
62
+ if pattern.search(line):
63
+ findings.append((line_num, desc))
64
+
65
+ if PORTAL_IDOR_PATTERN.search(line):
66
+ findings.append((line_num, "Billing portal session created with customer ID from req.body (IDOR risk: user can manage another's subscription)"))
67
+
68
+ # 2. Webhook verification checks across webhook handlers
69
+ is_webhook_file = (
70
+ any(kw in file_path.lower() for kw in ['webhook', 'stripe', 'lemonsqueezy', 'dodo', 'paddle', 'razorpay'])
71
+ or WEBHOOK_ROUTE_PATTERN.search(content)
72
+ or 'webhook' in content.lower()
73
+ )
74
+
75
+ if is_webhook_file:
76
+ # Strip comments to prevent false negatives from keywords in comments
77
+ content_no_comments = re.sub(r'//.*$', '', content, flags=re.M)
78
+ content_no_comments = re.sub(r'/\*.*?\*/', '', content_no_comments, flags=re.S)
79
+ content_no_comments = re.sub(r'#.*$', '', content_no_comments, flags=re.M)
80
+
81
+ # Check Stripe webhook files
82
+ if 'stripe' in content.lower() and ('event' in content.lower() or 'webhook' in content.lower()):
83
+ if not STRIPE_SIG_PATTERN.search(content_no_comments) and ('checkout.session' in content or 'payment_intent' in content):
84
+ findings.append((1, "Stripe webhook handler detected without stripe.webhooks.constructEvent signature verification (Critical)"))
85
+
86
+ # Check Lemon Squeezy webhooks
87
+ if 'lemon' in content.lower() and ('signature' in content.lower() or 'event' in content.lower() or 'webhook' in content.lower()):
88
+ if not LEMON_SIG_PATTERN.search(content_no_comments):
89
+ findings.append((1, "Lemon Squeezy webhook handler missing HMAC SHA-256 signature verification via x-signature header (Critical)"))
90
+
91
+ # Check Dodo Payments webhooks
92
+ if 'dodo' in content.lower() and ('webhook' in content.lower() or 'event' in content.lower()):
93
+ if not DODO_SIG_PATTERN.search(content_no_comments):
94
+ findings.append((1, "Dodo Payments webhook handler missing signature verification (Critical)"))
95
+
96
+ # Check Razorpay webhooks
97
+ if 'razorpay' in content.lower() and ('webhook' in content.lower() or 'payment' in content.lower()):
98
+ if not RAZORPAY_SIG_PATTERN.search(content_no_comments):
99
+ findings.append((1, "Razorpay payment/webhook handler missing signature verification check (Critical)"))
100
+
101
+ # Check for subscription cancellation handling in subscription webhook files
102
+ if ('subscription' in content.lower() or 'checkout.session.completed' in content.lower()) and ('user' in content.lower() or 'plan' in content.lower()):
103
+ if not SUB_CANCEL_PATTERNS.search(content):
104
+ findings.append((1, "Subscription webhook handler handles setup/checkout but lacks subscription cancellation/failure handling (customer.subscription.deleted / subscription_cancelled) - allows revoked users to retain access"))
105
+
106
+ except Exception:
107
+ pass
108
+
109
+ return findings
110
+
111
+ def main():
112
+ target_dir = sys.argv[1] if len(sys.argv) > 1 else '.'
113
+ target_dir = os.path.abspath(target_dir)
114
+
115
+ findings_by_file = {}
116
+
117
+ for root, dirs, files in os.walk(target_dir):
118
+ dirs[:] = [d for d in dirs if d not in SKIP_DIRS and not d.startswith('.')]
119
+
120
+ for file in files:
121
+ if is_test_file(file):
122
+ continue
123
+ if file in ['check_payment_config.py', 'check_db_config.py', 'scan_secrets.py', 'check_auth_patterns.py', 'check_gitignore.py']:
124
+ continue
125
+ if not any(file.endswith(ext) for ext in ['.js', '.ts', '.jsx', '.tsx', '.py', '.php', '.go']):
126
+ continue
127
+
128
+ file_path = os.path.join(root, file)
129
+ rel_path = os.path.relpath(file_path, target_dir)
130
+
131
+ file_findings = scan_file(file_path)
132
+ if file_findings:
133
+ findings_by_file[rel_path] = file_findings
134
+
135
+ if not findings_by_file:
136
+ print("PASS: No payment integrity or webhook security flaws detected.")
137
+ sys.exit(0)
138
+
139
+ print("FAIL: The following payment integrity issues were found:")
140
+ for rel_path, file_findings in findings_by_file.items():
141
+ for line_num, desc in file_findings:
142
+ print(f"{rel_path}:{line_num}: {desc}")
143
+ sys.exit(1)
144
+
145
+ if __name__ == '__main__':
146
+ main()
@@ -5,6 +5,10 @@ import re
5
5
  import math
6
6
  import fnmatch
7
7
 
8
+ # Ensure stdout supports UTF-8 on Windows
9
+ if hasattr(sys.stdout, 'reconfigure'):
10
+ sys.stdout.reconfigure(encoding='utf-8', errors='replace')
11
+
8
12
  # Directories to skip during scanning
9
13
  SKIP_DIRS = {
10
14
  'node_modules', '.git', 'build', 'dist', '.next', '.svelte-kit',
@@ -30,16 +34,46 @@ SKIP_EXTS = {
30
34
  '.svg', '.css', '.scss', '.less', '.html', '.xml'
31
35
  }
32
36
 
37
+ import base64
38
+ import json
39
+
33
40
  # Regexes for known API key formats (live keys are Critical; test keys are Should Fix)
34
41
  API_KEY_PATTERNS = [
35
42
  (re.compile(r'AIza[0-9A-Za-z-_]{35}'), 'Google API Key'),
36
43
  (re.compile(r'AKIA[0-9A-Z]{16}'), 'AWS Access Key ID'),
37
44
  (re.compile(r'ghp_[a-zA-Z0-9]{36,40}'), 'GitHub Personal Access Token'),
45
+ (re.compile(r'github_pat_[0-9a-zA-Z_]{82}'), 'GitHub Fine-Grained Personal Access Token (Critical)'),
38
46
  (re.compile(r'sk_live_[0-9a-zA-Z]{24,96}'), 'Stripe LIVE Secret Key (Critical: charges real cards)'),
39
47
  (re.compile(r'sk_test_[0-9a-zA-Z]{24,96}'), 'Stripe TEST Secret Key (Should Fix: not production-critical but must not be committed)'),
48
+ (re.compile(r'whsec_[0-9a-zA-Z]{32,64}'), 'Stripe Webhook Secret (Critical: allows forging payment events)'),
40
49
  (re.compile(r'xox[bapr]-[0-9]{12}-[0-9]{12}-[0-9]{12}-[a-z0-9]{32}'), 'Slack Token'),
50
+ # Modern AI and Developer Providers
51
+ (re.compile(r'sk-(?:proj|None)-[a-zA-Z0-9_-]{48,160}'), 'OpenAI Secret API Key (Critical: charges API usage)'),
52
+ (re.compile(r'sk-ant-api03-[a-zA-Z0-9_-]{80,120}'), 'Anthropic Claude API Key (Critical: charges API usage)'),
53
+ (re.compile(r'gsk_[a-zA-Z0-9]{52}'), 'Groq API Key'),
54
+ (re.compile(r're_[a-zA-Z0-9]{32,48}'), 'Resend Email API Key (Critical: email sending/spam abuse)'),
55
+ (re.compile(r'SG\.[a-zA-Z0-9_-]{22}\.[a-zA-Z0-9_-]{43}'), 'SendGrid API Key (Critical: email sending/spam abuse)'),
56
+ (re.compile(r'npm_[0-9a-zA-Z]{36}'), 'NPM Access Token (Critical: package publishing)'),
57
+ (re.compile(r'phx_[a-zA-Z0-9]{40,64}'), 'PostHog Personal API Key (Critical)'),
58
+ (re.compile(r'pplx-[a-zA-Z0-9]{48}'), 'Perplexity API Key'),
59
+ (re.compile(r'hf_[a-zA-Z0-9]{34}'), 'HuggingFace Access Token'),
60
+ (re.compile(r'(?:postgres|postgresql|mysql|mongodb(?:\+srv)?|redis):\/\/[a-zA-Z0-9_.-]+:[^@\s]+@[a-zA-Z0-9_.-]+'), 'Hardcoded Database Connection URI with password (Critical)'),
41
61
  ]
42
62
 
63
+ JWT_PATTERN = re.compile(r'\beyJ[a-zA-Z0-9_-]+\.([a-zA-Z0-9_-]+)\.[a-zA-Z0-9_-]+\b')
64
+
65
+ def inspect_jwt_payload(token_payload_b64):
66
+ """Inspect base64 JWT payload for sensitive roles like Supabase service_role."""
67
+ try:
68
+ padded = token_payload_b64 + '=' * (-len(token_payload_b64) % 4)
69
+ payload_bytes = base64.urlsafe_b64decode(padded.encode('utf-8'))
70
+ payload = json.loads(payload_bytes.decode('utf-8', errors='ignore'))
71
+ if payload.get('role') == 'service_role':
72
+ return 'Supabase Service Role JWT (Critical: bypasses all Row Level Security)'
73
+ except Exception:
74
+ pass
75
+ return None
76
+
43
77
  # Regex for variable assignments with potential hardcoded secrets
44
78
  # Matches: name = "string" or "name": "string", where name contains key, secret, token, password, credential, auth
45
79
  ASSIGNMENT_PATTERN = re.compile(
@@ -81,6 +115,12 @@ def scan_file(file_path):
81
115
  if pattern.search(line):
82
116
  findings.append((line_num, f"Exposed {name}"))
83
117
 
118
+ # Check for JWT tokens (e.g. Supabase service_role)
119
+ for jwt_match in JWT_PATTERN.finditer(line):
120
+ jwt_desc = inspect_jwt_payload(jwt_match.group(1))
121
+ if jwt_desc:
122
+ findings.append((line_num, f"Exposed {jwt_desc}"))
123
+
84
124
  # 2. Check variable assignments
85
125
  for match in ASSIGNMENT_PATTERN.finditer(line):
86
126
  var_name, quote, val = match.groups()
@@ -144,6 +184,9 @@ def main():
144
184
 
145
185
  for file in files:
146
186
  ext = os.path.splitext(file)[1].lower()
187
+ # Skip checkmyvibe audit reports so reported secrets don't self-flag
188
+ if 'checkmyvibe-report' in file.lower():
189
+ continue
147
190
  # Skip hidden files unless they are .env configuration files
148
191
  if ext in SKIP_EXTS or is_test_file(file):
149
192
  continue