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.
- package/README.md +247 -44
- package/SKILL.md +405 -396
- package/adapters/AGENTS-SNIPPET.md +32 -0
- package/adapters/checkmyvibe.mdc +18 -0
- package/adapters/opencode-checkmyvibe.md +19 -0
- package/commands/checkmyvibe-auth.md +15 -9
- package/commands/checkmyvibe-backend.md +24 -14
- package/commands/checkmyvibe-db.md +32 -14
- package/commands/checkmyvibe-frontend.md +23 -9
- package/commands/checkmyvibe-payment.md +53 -14
- package/commands/checkmyvibe-secrets.md +22 -13
- package/commands/checkmyvibe.md +12 -11
- package/install.js +58 -0
- package/package.json +2 -1
- package/references/vibe_risk_patterns.md +65 -21
- package/scripts/check_auth_patterns.py +27 -2
- package/scripts/check_db_config.py +72 -23
- package/scripts/check_gitignore.py +92 -14
- package/scripts/check_payment_config.py +146 -0
- package/scripts/scan_secrets.py +43 -0
|
@@ -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()
|
package/scripts/scan_secrets.py
CHANGED
|
@@ -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
|