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
package/install.js
CHANGED
|
@@ -10,6 +10,7 @@ const srcSkill = path.join(__dirname, 'SKILL.md');
|
|
|
10
10
|
const srcScripts = path.join(__dirname, 'scripts');
|
|
11
11
|
const srcReferences = path.join(__dirname, 'references');
|
|
12
12
|
const srcCommands = path.join(__dirname, 'commands');
|
|
13
|
+
const srcAdapters = path.join(__dirname, 'adapters');
|
|
13
14
|
|
|
14
15
|
// Destination selection
|
|
15
16
|
const cwd = process.cwd();
|
|
@@ -32,6 +33,48 @@ function copyRecursiveSync(src, dest) {
|
|
|
32
33
|
}
|
|
33
34
|
}
|
|
34
35
|
|
|
36
|
+
function installHarnessAdapters() {
|
|
37
|
+
const installed = [];
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
// Cursor rule -> .cursor/rules/
|
|
41
|
+
const cursorRulesDir = path.join(cwd, '.cursor', 'rules');
|
|
42
|
+
const mdcSrc = path.join(srcAdapters, 'checkmyvibe.mdc');
|
|
43
|
+
if (fs.existsSync(path.join(cwd, '.cursor')) && fs.existsSync(mdcSrc)) {
|
|
44
|
+
fs.mkdirSync(cursorRulesDir, { recursive: true });
|
|
45
|
+
fs.copyFileSync(mdcSrc, path.join(cursorRulesDir, 'checkmyvibe.mdc'));
|
|
46
|
+
installed.push(`Cursor rule -> ${path.relative(cwd, path.join(cursorRulesDir, 'checkmyvibe.mdc'))}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// opencode command -> .opencode/command/
|
|
50
|
+
const opencodeCmdDir = path.join(cwd, '.opencode', 'command');
|
|
51
|
+
const ocSrc = path.join(srcAdapters, 'opencode-checkmyvibe.md');
|
|
52
|
+
if (fs.existsSync(path.join(cwd, '.opencode')) && fs.existsSync(ocSrc)) {
|
|
53
|
+
fs.mkdirSync(opencodeCmdDir, { recursive: true });
|
|
54
|
+
fs.copyFileSync(ocSrc, path.join(opencodeCmdDir, 'checkmyvibe.md'));
|
|
55
|
+
installed.push(`opencode command -> ${path.relative(cwd, path.join(opencodeCmdDir, 'checkmyvibe.md'))}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// AGENTS.md -> append discovery snippet if not already present
|
|
59
|
+
const agentsPath = path.join(cwd, 'AGENTS.md');
|
|
60
|
+
const snippetSrc = path.join(srcAdapters, 'AGENTS-SNIPPET.md');
|
|
61
|
+
if (fs.existsSync(agentsPath) && fs.existsSync(snippetSrc)) {
|
|
62
|
+
let agentsContent = fs.readFileSync(agentsPath, 'utf8');
|
|
63
|
+
if (!agentsContent.includes('## checkmyvibe')) {
|
|
64
|
+
const snippet = fs.readFileSync(snippetSrc, 'utf8');
|
|
65
|
+
const snippetBody = snippet.split(/^---$/m).pop().trim();
|
|
66
|
+
agentsContent = agentsContent.trimEnd() + '\n\n' + snippetBody + '\n';
|
|
67
|
+
fs.writeFileSync(agentsPath, agentsContent);
|
|
68
|
+
installed.push('AGENTS.md -> appended checkmyvibe section');
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
} catch (err) {
|
|
72
|
+
console.log(` (adapter installation skipped: ${err.message})`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return installed;
|
|
76
|
+
}
|
|
77
|
+
|
|
35
78
|
function performInstall(skillDest, commandsDest) {
|
|
36
79
|
try {
|
|
37
80
|
console.log(`\nInstalling checkmyvibe skill to: ${skillDest}...`);
|
|
@@ -67,6 +110,14 @@ function performInstall(skillDest, commandsDest) {
|
|
|
67
110
|
});
|
|
68
111
|
}
|
|
69
112
|
|
|
113
|
+
// Copy adapters into the skill folder so they ship with the skill itself
|
|
114
|
+
if (fs.existsSync(srcAdapters)) {
|
|
115
|
+
copyRecursiveSync(srcAdapters, path.join(skillDest, 'adapters'));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Detect other agent harnesses in this project and install their adapters
|
|
119
|
+
const adapters = installHarnessAdapters();
|
|
120
|
+
|
|
70
121
|
console.log('\n=========================================');
|
|
71
122
|
console.log('🎉 checkmyvibe Agent Skill & Commands installed!');
|
|
72
123
|
console.log('=========================================');
|
|
@@ -80,6 +131,13 @@ function performInstall(skillDest, commandsDest) {
|
|
|
80
131
|
console.log(' /checkmyvibe backend (or /checkmyvibe-backend) -> Server-side (auth, db, IDOR, input, payment)');
|
|
81
132
|
console.log(' /checkmyvibe frontend (or /checkmyvibe-frontend) -> Client-side (bundle leaks, client auth, pricing)');
|
|
82
133
|
console.log(' /checkmyvibe payment (or /checkmyvibe-payment) -> Payment & pricing integrity');
|
|
134
|
+
console.log('=========================================');
|
|
135
|
+
if (adapters.length > 0) {
|
|
136
|
+
console.log('\nOther agent harnesses detected — adapters installed:');
|
|
137
|
+
adapters.forEach((line) => console.log(` ${line}`));
|
|
138
|
+
}
|
|
139
|
+
console.log('\nUsing Codex, Gemini CLI, Cursor, or another AGENTS.md-based agent?');
|
|
140
|
+
console.log(`Paste the snippet from ${path.join(skillDest, 'adapters', 'AGENTS-SNIPPET.md')} into your AGENTS.md.`);
|
|
83
141
|
console.log('=========================================\n');
|
|
84
142
|
} catch (err) {
|
|
85
143
|
console.error('Error installing skill:', err.message);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "checkmyvibe",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.3.0",
|
|
5
5
|
"description": "A structured production readiness and code quality workflow Agent Skill for AI coding assistants to catch common vibe-coded configuration issues.",
|
|
6
6
|
"main": "install.js",
|
|
7
7
|
"bin": {
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"commands/",
|
|
13
13
|
"scripts/",
|
|
14
14
|
"references/",
|
|
15
|
+
"adapters/",
|
|
15
16
|
"install.js",
|
|
16
17
|
"README.md",
|
|
17
18
|
"LICENSE"
|
|
@@ -1,81 +1,125 @@
|
|
|
1
1
|
# Vibe Readiness Risk Patterns
|
|
2
2
|
|
|
3
|
-
This document serves as
|
|
3
|
+
This document serves as an exhaustive reference for common readiness risk patterns identified in AI-generated ("vibe-coded") applications. AI coding assistants frequently introduce these configuration and design flaws because they prioritize speed, feature completion, and standalone component demonstration over robust production-ready setups.
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
## 1. Exposed Secrets
|
|
8
8
|
|
|
9
9
|
### Overview
|
|
10
|
-
AI assistants
|
|
10
|
+
AI assistants scaffold code with hardcoded API keys, JWT secrets, database connection strings, or third-party client keys directly inside configuration files or source code. Local environment files (`.env`) are frequently created without updating `.gitignore` or `.dockerignore`.
|
|
11
11
|
|
|
12
12
|
### Why it happens in AI-generated code
|
|
13
|
-
|
|
13
|
+
1. **Context-Window Convenience:** Models frequently inline secret strings (`const openai = new OpenAI({ apiKey: "sk-proj-..." })`) so code runs immediately without requiring manual `.env` file configuration by the user.
|
|
14
|
+
2. **Bundler Prefix Confusion:** AI models mix up server and client environments in Next.js/Vite, assigning server-only secrets to `NEXT_PUBLIC_*` or `VITE_*` variables.
|
|
15
|
+
3. **React Server Component (RSC) Prop Leaks:** In Next.js App Router, an AI generates a Server Component that imports an environment variable and passes it as a prop to a client component (`<ChatWidget apiKey={process.env.OPENAI_KEY} />`), serializing the private secret directly into the browser HTML.
|
|
16
|
+
4. **Docker Leaks:** Models create a `Dockerfile` with `COPY . .` but omit `.dockerignore`, baking local `.env` files into public container images.
|
|
14
17
|
|
|
15
18
|
### The Correct Fix
|
|
16
|
-
|
|
19
|
+
- Keep all secrets in server-side environment variables accessed strictly on the server: `process.env.OPENAI_API_KEY`.
|
|
20
|
+
- Ensure `.env*` is listed in both `.gitignore` and `.dockerignore`.
|
|
21
|
+
- Check git commit history (`git log --all --full-history -- .env`) and rotate any key that was ever committed.
|
|
17
22
|
|
|
18
23
|
---
|
|
19
24
|
|
|
20
25
|
## 2. Missing/Fake Authentication (Fake/Stubbed Auth)
|
|
21
26
|
|
|
22
27
|
### Overview
|
|
23
|
-
When generating authentication flows, AI tools often implement mock authentication functions,
|
|
28
|
+
When generating authentication flows, AI tools often implement mock authentication functions, dummy user objects, or bypass middleware to make manual testing seamless during development. Developers unknowingly deploy this placeholder logic directly to production.
|
|
24
29
|
|
|
25
30
|
### Why it happens in AI-generated code
|
|
26
|
-
|
|
31
|
+
1. **Scaffolding Shortcuts:** AI assistants generate middleware like:
|
|
32
|
+
```js
|
|
33
|
+
function authMiddleware(req, res, next) {
|
|
34
|
+
req.user = { id: "test-user", role: "admin" }; // TODO: replace with real JWT auth
|
|
35
|
+
next();
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
2. **Provider Route Matcher Gaps:** When integrating modern auth providers like Clerk or NextAuth, AI tools often write overly permissive route matchers in Next.js `middleware.ts` (e.g., omitting `/api/(.*)`), leaving backend routes unprotected.
|
|
39
|
+
3. **Supabase Server Session Trap:** Models frequently call `supabase.auth.getSession()` on server routes or Server Actions. Supabase warns that `getSession()` is insecure on the server because it does not cryptographically revalidate the token with the Supabase Auth server.
|
|
27
40
|
|
|
28
41
|
### The Correct Fix
|
|
29
|
-
|
|
42
|
+
- Eliminate all mock auth stubs from production code paths.
|
|
43
|
+
- On Supabase servers, always use `await supabase.auth.getUser()`.
|
|
44
|
+
- For Clerk, protect all sensitive API routes with `auth.protect()`.
|
|
45
|
+
- Store authentication tokens in `httpOnly`, `Secure`, `SameSite` cookies rather than browser `localStorage`.
|
|
30
46
|
|
|
31
47
|
---
|
|
32
48
|
|
|
33
49
|
## 3. Database Misconfiguration (Permissive Database Rules / RLS)
|
|
34
50
|
|
|
35
51
|
### Overview
|
|
36
|
-
|
|
52
|
+
Databases directly exposed to the browser (Supabase, Firebase, Convex) rely on Row-Level Security (RLS) or security rules to restrict data access. AI assistants frequently scaffold projects with default allow-all rules (`USING (true)` / `allow read, write: if true;`) or create tables without enabling RLS.
|
|
37
53
|
|
|
38
54
|
### Why it happens in AI-generated code
|
|
39
|
-
|
|
55
|
+
1. **Quick-Fixing Permission Errors:** When an AI model encounters permission errors while building a frontend prototype, its default reaction is often to run `CREATE POLICY "allow_all" ON <table> FOR ALL USING (true);`.
|
|
56
|
+
2. **Server-Backed ORM Confusion:** When transitioning between Supabase and server-backed ORMs (Prisma, Drizzle, Neon, local SQLite/Postgres), models often fail to recognize whether the database is private behind a server or public to the browser.
|
|
57
|
+
3. **Exposed SQLite Files:** In lightweight stacks, models place `data.sqlite` directly inside the `public/` directory, allowing any web visitor to download the entire database via a simple HTTP GET request.
|
|
58
|
+
4. **NoSQL Query Injection:** In MongoDB/Mongoose backends, models pass `req.body` directly to `User.findOne(req.body)`, allowing query selectors like `{ "$gt": "" }` to bypass password checks.
|
|
40
59
|
|
|
41
60
|
### The Correct Fix
|
|
42
|
-
|
|
61
|
+
- In Supabase, enable RLS on every table and bind policies to `auth.uid() = user_id`.
|
|
62
|
+
- Never place SQLite or database files in public web directories.
|
|
63
|
+
- In serverless Postgres (Neon), configure connection pooling (`@neondatabase/serverless`) to prevent connection pool exhaustion.
|
|
64
|
+
- Sanitize Mongoose inputs using schema validation or explicit field extraction instead of passing raw request objects.
|
|
43
65
|
|
|
44
66
|
---
|
|
45
67
|
|
|
46
68
|
## 4. Broken Object-Level Authorization (BOLA / IDOR)
|
|
47
69
|
|
|
48
70
|
### Overview
|
|
49
|
-
|
|
71
|
+
BOLA/IDOR occurs when an endpoint retrieves, modifies, or deletes a resource based on a client-supplied identifier (e.g. `/api/orders/:id`, Server Actions `deleteDocument(id)`) without verifying that the authenticated user owns or has permission to access that resource.
|
|
50
72
|
|
|
51
73
|
### Why it happens in AI-generated code
|
|
52
|
-
|
|
74
|
+
1. **Single-Table Query Scaffolding:** AI tools generate queries using only the primary key:
|
|
75
|
+
```js
|
|
76
|
+
const item = await db.query('SELECT * FROM items WHERE id = $1', [req.params.id]);
|
|
77
|
+
```
|
|
78
|
+
2. **Server Actions RPC Insecurity:** In Next.js App Router, models expose Server Actions (`'use server'`) without re-authenticating the session inside the action function body.
|
|
79
|
+
3. **Multi-Tenant Blindness:** In team/organization applications, models verify `WHERE organization_id = req.params.orgId` without checking whether the requesting user is actually a member of that organization.
|
|
53
80
|
|
|
54
81
|
### The Correct Fix
|
|
55
|
-
|
|
82
|
+
- Always scope database queries by both resource ID and the authenticated user/organization ID:
|
|
83
|
+
```js
|
|
84
|
+
const order = await db.query(
|
|
85
|
+
'SELECT * FROM orders WHERE id = $1 AND user_id = $2',
|
|
86
|
+
[req.params.id, req.user.id]
|
|
87
|
+
);
|
|
88
|
+
if (!order) return res.status(404).json({ error: 'Not found' });
|
|
89
|
+
```
|
|
56
90
|
|
|
57
91
|
---
|
|
58
92
|
|
|
59
|
-
## 5. Unvalidated Inputs
|
|
93
|
+
## 5. Unvalidated Inputs, SSRF, & DOM XSS
|
|
60
94
|
|
|
61
95
|
### Overview
|
|
62
|
-
AI code
|
|
96
|
+
AI-generated code frequently trusts client inputs, leading to Server-Side Request Forgery (SSRF), DOM XSS in AI markdown renderers, SQL/NoSQL injection, and mass assignment.
|
|
63
97
|
|
|
64
98
|
### Why it happens in AI-generated code
|
|
65
|
-
|
|
99
|
+
1. **SSRF in AI Link/Image Scrapers:** AI apps frequently take a user-supplied URL to scrape or summarize (e.g. `axios.get(req.body.url)`). Attackers pass `http://169.254.169.254/latest/meta-data/` to steal cloud metadata and IAM credentials.
|
|
100
|
+
2. **DOM XSS via AI Markdown:** AI chat interfaces render model responses using `dangerouslySetInnerHTML` or `react-markdown` without `rehype-sanitize` or DOMPurify, enabling persistent XSS attacks via prompt injection.
|
|
101
|
+
3. **Mass Assignment:** Models write `await prisma.user.update({ where: { id }, data: req.body })`, allowing callers to overwrite privileged fields like `role: "admin"` or `isVerified: true`.
|
|
66
102
|
|
|
67
103
|
### The Correct Fix
|
|
68
|
-
|
|
104
|
+
- Filter user-supplied URLs against private IP ranges (`127.0.0.1`, `169.254.169.254`, `10.*.*.*`) before executing server-side HTTP requests.
|
|
105
|
+
- Sanitize rendered markdown using `rehype-sanitize` or DOMPurify.
|
|
106
|
+
- Whitelist request body fields explicitly when updating database records.
|
|
69
107
|
|
|
70
108
|
---
|
|
71
109
|
|
|
72
|
-
## 6. Client-Side Payment Logic
|
|
110
|
+
## 6. Client-Side Payment Logic & Webhook Risks
|
|
73
111
|
|
|
74
112
|
### Overview
|
|
75
|
-
AI
|
|
113
|
+
AI assistants frequently write payment flows where prices are trusted from client payloads, webhook signatures are never verified, idempotency is ignored, and subscription cancellations are unhandled.
|
|
76
114
|
|
|
77
115
|
### Why it happens in AI-generated code
|
|
78
|
-
|
|
116
|
+
1. **Trusting Client Amounts:** When creating checkout sessions, models extract `price` from `req.body` (`const { price } = req.body; charge(price);`), allowing users to edit the price to $0.01 in browser dev tools.
|
|
117
|
+
2. **Omitted Webhook Signatures:** In Stripe, Lemon Squeezy, Dodo Payments, or Razorpay integrations, models read webhook events directly from `req.body` without verifying cryptographic signatures.
|
|
118
|
+
3. **Lack of Idempotency:** Gateways retry webhooks on timeouts. Without tracking `event.id` in the database, retried webhooks grant duplicate credits or double-ship orders.
|
|
119
|
+
4. **The Cancellation Trap:** Models implement `checkout.session.completed` to grant access, but completely forget to listen for `customer.subscription.deleted` or `subscription_cancelled`. Canceled users retain paid access indefinitely.
|
|
79
120
|
|
|
80
121
|
### The Correct Fix
|
|
81
|
-
|
|
122
|
+
- Always resolve prices server-side from your product catalog or database.
|
|
123
|
+
- Cryptographically verify webhook signatures using gateway SDKs or HMAC SHA-256 before acting on event payloads.
|
|
124
|
+
- Record processed webhook event IDs (`event.id`) to ensure idempotent execution.
|
|
125
|
+
- Implement subscription cancellation and payment failure listeners to revoke access when plans expire.
|
|
@@ -4,6 +4,10 @@ import sys
|
|
|
4
4
|
import re
|
|
5
5
|
import fnmatch
|
|
6
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
|
+
|
|
7
11
|
SKIP_DIRS = {
|
|
8
12
|
'node_modules', '.git', 'build', 'dist', '.next', '.svelte-kit',
|
|
9
13
|
'__pycache__', 'venv', '.venv', 'env', 'coverage', 'out',
|
|
@@ -23,7 +27,8 @@ SKIP_EXTS = {
|
|
|
23
27
|
'.gz', '.mp3', '.mp4', '.mov', '.db', '.sqlite', '.exe', '.dll',
|
|
24
28
|
'.so', '.dylib', '.woff', '.woff2', '.ttf', '.eot', '.lock',
|
|
25
29
|
'.package-lock.json', '.pnpm-lock.yaml', '.yarn.lock', '.map',
|
|
26
|
-
'.svg', '.css', '.scss', '.less', '.html', '.xml'
|
|
30
|
+
'.svg', '.css', '.scss', '.less', '.html', '.xml',
|
|
31
|
+
'.md', '.markdown', '.txt', '.rst'
|
|
27
32
|
}
|
|
28
33
|
|
|
29
34
|
# 1. Naming patterns for fake/mock/bypass authentication
|
|
@@ -50,7 +55,23 @@ STUB_PATTERNS = [
|
|
|
50
55
|
|
|
51
56
|
# Generic bypass function or property: e.g. "authBypass: true", "skipAuth: true"
|
|
52
57
|
(re.compile(r'(?i)\b(?:authBypass|skipAuth|disableAuth|bypassAuthentication|devAuth)\b\s*:\s*true\b'),
|
|
53
|
-
"Authentication bypass flag set to true")
|
|
58
|
+
"Authentication bypass flag set to true"),
|
|
59
|
+
|
|
60
|
+
# Hardcoded mock user object return in auth-related functions
|
|
61
|
+
(re.compile(r'(?i)(?:function\s+\w*(?:auth|login|user|session|account)\w*\s*\([^)]*\)|\b\w*(?:auth|login|user|session|account)\w*\s*=\s*(?:async\s*)?\([^)]*\)\s*=>)\s*(?:\{[^}]*return\s*\{|\(\s*\{)\s*[\'"`]?(?:id|user_id|userId|role|email)[\'"`]?\s*:'),
|
|
62
|
+
"Hardcoded mock user object returned from auth handler"),
|
|
63
|
+
|
|
64
|
+
# Middleware unconditionally calling next() without checks
|
|
65
|
+
(re.compile(r'(?i)(?:\b\w*(?:auth|protect|guard|requireUser|session)\w*\s*=\s*(?:async\s*)?\([^)]*next[^)]*\)\s*=>|function\s+\w*(?:auth|protect|guard|requireUser|session)\w*\s*\([^)]*next[^)]*\))\s*\{?\s*next\(\);?\s*\}?'),
|
|
66
|
+
"Auth middleware unconditionally calls next() with no validation"),
|
|
67
|
+
|
|
68
|
+
# Direct assignment of hardcoded req.user
|
|
69
|
+
(re.compile(r'(?i)req\.user\s*=\s*\{\s*[\'"`]?(?:id|user_id|userId|role)[\'"`]?\s*:\s*[\'"`]'),
|
|
70
|
+
"Hardcoded mock user assigned directly to req.user"),
|
|
71
|
+
|
|
72
|
+
# Supabase insecure getSession() on server
|
|
73
|
+
(re.compile(r'(?i)\bsupabase\.auth\.getSession\(\)'),
|
|
74
|
+
"Insecure supabase.auth.getSession() used (use supabase.auth.getUser() on the server to verify JWT authenticity)"),
|
|
54
75
|
]
|
|
55
76
|
|
|
56
77
|
def is_binary(file_path):
|
|
@@ -107,6 +128,10 @@ def main():
|
|
|
107
128
|
dirs[:] = [d for d in dirs if d not in SKIP_DIRS and not d.startswith('.')]
|
|
108
129
|
|
|
109
130
|
for file in files:
|
|
131
|
+
if file in ['check_auth_patterns.py', 'scan_secrets.py', 'check_db_config.py', 'check_gitignore.py', 'check_payment_config.py']:
|
|
132
|
+
continue
|
|
133
|
+
if 'checkmyvibe-report' in file.lower():
|
|
134
|
+
continue
|
|
110
135
|
ext = os.path.splitext(file)[1].lower()
|
|
111
136
|
if ext in SKIP_EXTS or is_test_file(file) or file.startswith('.'):
|
|
112
137
|
continue
|
|
@@ -4,10 +4,14 @@ import sys
|
|
|
4
4
|
import re
|
|
5
5
|
import fnmatch
|
|
6
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
|
+
|
|
7
11
|
SKIP_DIRS = {
|
|
8
|
-
'node_modules', '.git', 'dist', 'build',
|
|
12
|
+
'node_modules', '.git', 'dist', 'build', '.next', '.svelte-kit',
|
|
9
13
|
'__tests__', '__mocks__', 'tests', 'test', 'spec', 'fixtures',
|
|
10
|
-
'mocks', 'e2e', 'cypress'
|
|
14
|
+
'mocks', 'e2e', 'cypress', 'venv', '.venv'
|
|
11
15
|
}
|
|
12
16
|
|
|
13
17
|
TEST_FILE_PATTERNS = ['*.test.*', '*.spec.*', '*.mock.*']
|
|
@@ -21,9 +25,11 @@ FIREBASE_PERMISSIVE = re.compile(r'allow\s+[\w\s,]+:\s*if\s+true\s*;')
|
|
|
21
25
|
# Permissive Realtime Database rules
|
|
22
26
|
RTDB_PERMISSIVE = re.compile(r'"\.(read|write)"\s*:\s*"true"')
|
|
23
27
|
|
|
24
|
-
#
|
|
25
|
-
|
|
26
|
-
|
|
28
|
+
# Insecure DB connection configuration
|
|
29
|
+
INSECURE_DB_SSL = re.compile(r'(?i)(?:sslmode\s*=\s*disable|ssl\s*:\s*false|rejectUnauthorized\s*:\s*false)')
|
|
30
|
+
|
|
31
|
+
# NoSQL / MongoDB direct body injection
|
|
32
|
+
MONGO_INJECTION_PATTERN = re.compile(r'(?i)\b\w+\.(?:find|findOne|deleteMany|updateOne|updateMany)\s*\(\s*(?:req\.body|req\.query)\s*\)')
|
|
27
33
|
|
|
28
34
|
def scan_firebase_rules(file_path):
|
|
29
35
|
"""Scan Firestore/Storage rules for allow-all statements."""
|
|
@@ -58,18 +64,14 @@ def scan_sql_rls(file_path):
|
|
|
58
64
|
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
|
|
59
65
|
content = f.read()
|
|
60
66
|
|
|
61
|
-
# 1. Strip comments
|
|
62
|
-
# Strip single-line -- comments
|
|
67
|
+
# 1. Strip comments
|
|
63
68
|
content_no_comments = re.sub(r'--.*$', '', content, flags=re.M)
|
|
64
|
-
# Strip multiline /* */ comments
|
|
65
69
|
content_no_comments = re.sub(r'/\*.*?\*/', '', content_no_comments, flags=re.S)
|
|
66
70
|
|
|
67
|
-
# 2. Find all CREATE TABLE statements
|
|
71
|
+
# 2. Find all CREATE TABLE statements
|
|
68
72
|
created_tables = re.findall(r'(?i)create\s+table\s+(?:if\s+not\s+exists\s+)?([\w\.]+)', content_no_comments)
|
|
69
|
-
# Find all ENABLE ROW LEVEL SECURITY statements
|
|
70
73
|
enabled_rls = re.findall(r'(?i)alter\s+table\s+(?:if\s+exists\s+)?([\w\.]+)\s+enable\s+row\s+level\s+security', content_no_comments)
|
|
71
74
|
|
|
72
|
-
# Clean table names (strip quotes, normalize schemas)
|
|
73
75
|
def clean_table_name(t):
|
|
74
76
|
t = t.replace('"', '').replace("'", "")
|
|
75
77
|
if '.' in t:
|
|
@@ -82,15 +84,12 @@ def scan_sql_rls(file_path):
|
|
|
82
84
|
for idx, table in enumerate(created_tables):
|
|
83
85
|
clean_t = clean_table_name(table)
|
|
84
86
|
if clean_t not in enabled_clean:
|
|
85
|
-
# Find line number in original file
|
|
86
87
|
escaped_t = re.escape(table)
|
|
87
88
|
match = re.search(r'(?i)create\s+table\s+(?:if\s+not\s+exists\s+)?' + escaped_t, content)
|
|
88
89
|
line_num = content.count('\n', 0, match.start()) + 1 if match else 1
|
|
89
90
|
findings.append((line_num, f"Table '{table}' is created but Row Level Security (RLS) is not enabled"))
|
|
90
91
|
|
|
91
|
-
# 3. Find permissive policies
|
|
92
|
-
# Search for CREATE POLICY ... USING (true) or WITH CHECK (true)
|
|
93
|
-
# Handles multiline policy definitions and quoted policy names
|
|
92
|
+
# 3. Find permissive policies
|
|
94
93
|
policy_pattern = re.compile(
|
|
95
94
|
r'(?i)create\s+policy\s+("[^"]+"|[a-zA-Z_]\w*)\s+on\s+([\w\.]+)\s+[^;]+?(?:using|with\s+check)\s*\(\s*true\s*\)',
|
|
96
95
|
re.DOTALL
|
|
@@ -99,20 +98,41 @@ def scan_sql_rls(file_path):
|
|
|
99
98
|
for match in policy_pattern.finditer(content_no_comments):
|
|
100
99
|
policy_name = match.group(1)
|
|
101
100
|
table_name = match.group(2)
|
|
102
|
-
# Find the character position of this policy match in the original content
|
|
103
101
|
orig_match = re.search(r'(?i)create\s+policy\s+' + re.escape(policy_name), content)
|
|
104
102
|
line_num = content.count('\n', 0, orig_match.start()) + 1 if orig_match else 1
|
|
105
|
-
findings.append((line_num, f"Permissive policy {policy_name} on table {table_name} allows public
|
|
103
|
+
findings.append((line_num, f"Permissive policy {policy_name} on table {table_name} allows public access via USING(true) / CHECK(true)"))
|
|
106
104
|
|
|
107
105
|
except Exception:
|
|
108
106
|
pass
|
|
109
107
|
return findings
|
|
110
108
|
|
|
109
|
+
def detect_project_db_context(target_dir):
|
|
110
|
+
"""Detect if project uses client-facing BaaS (Supabase/Firebase) or server-only ORM (Prisma/Drizzle/Mongoose/Neon)."""
|
|
111
|
+
has_supabase = False
|
|
112
|
+
has_firebase = False
|
|
113
|
+
has_server_orm = False
|
|
114
|
+
|
|
115
|
+
for root, dirs, files in os.walk(target_dir):
|
|
116
|
+
dirs[:] = [d for d in dirs if d not in SKIP_DIRS and not d.startswith('.')]
|
|
117
|
+
if 'supabase' in root.lower() or any('supabase' in f.lower() for f in files):
|
|
118
|
+
has_supabase = True
|
|
119
|
+
if any(f in ['firestore.rules', 'firebase.json', 'database.rules.json'] for f in files):
|
|
120
|
+
has_firebase = True
|
|
121
|
+
if 'schema.prisma' in files or any(f.startswith('drizzle.config') for f in files):
|
|
122
|
+
has_server_orm = True
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
'has_supabase': has_supabase,
|
|
126
|
+
'has_firebase': has_firebase,
|
|
127
|
+
'has_server_orm': has_server_orm
|
|
128
|
+
}
|
|
129
|
+
|
|
111
130
|
def main():
|
|
112
131
|
target_dir = sys.argv[1] if len(sys.argv) > 1 else '.'
|
|
113
132
|
target_dir = os.path.abspath(target_dir)
|
|
114
133
|
|
|
115
134
|
findings_by_file = {}
|
|
135
|
+
context = detect_project_db_context(target_dir)
|
|
116
136
|
|
|
117
137
|
for root, dirs, files in os.walk(target_dir):
|
|
118
138
|
dirs[:] = [d for d in dirs if d not in SKIP_DIRS and not d.startswith('.')]
|
|
@@ -123,23 +143,52 @@ def main():
|
|
|
123
143
|
file_path = os.path.join(root, file)
|
|
124
144
|
rel_path = os.path.relpath(file_path, target_dir)
|
|
125
145
|
|
|
126
|
-
# Check for
|
|
146
|
+
# 1. Check for exposed SQLite files in public web directories
|
|
147
|
+
if any(file.lower().endswith(ext) for ext in ['.sqlite', '.sqlite3', '.db']):
|
|
148
|
+
rel_lower = rel_path.lower().replace('\\', '/')
|
|
149
|
+
if any(pub in rel_lower for pub in ['public/', 'static/', 'www/', 'dist/', 'assets/']):
|
|
150
|
+
findings_by_file.setdefault(rel_path, []).append(
|
|
151
|
+
(1, f"Exposed SQLite database file '{file}' in public directory (Critical: allows arbitrary database download)")
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# 2. Check for Firebase Firestore/Storage rules
|
|
127
155
|
if file in ['firestore.rules', 'storage.rules']:
|
|
128
156
|
file_findings = scan_firebase_rules(file_path)
|
|
129
157
|
if file_findings:
|
|
130
158
|
findings_by_file[rel_path] = file_findings
|
|
131
159
|
|
|
132
|
-
# Check for Firebase Realtime Database rules
|
|
160
|
+
# 3. Check for Firebase Realtime Database rules
|
|
133
161
|
elif file == 'database.rules.json':
|
|
134
162
|
file_findings = scan_rtdb_rules(file_path)
|
|
135
163
|
if file_findings:
|
|
136
164
|
findings_by_file[rel_path] = file_findings
|
|
137
165
|
|
|
138
|
-
# Check for SQL migrations/schema files
|
|
166
|
+
# 4. Check for SQL migrations/schema files
|
|
139
167
|
elif file.endswith('.sql'):
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
168
|
+
# In server-only ORM stacks (Prisma/Drizzle/Neon) without Supabase, suppress RLS missing warnings on standard migration tables
|
|
169
|
+
is_supabase_migration = 'supabase' in rel_path.lower()
|
|
170
|
+
if is_supabase_migration or context['has_supabase'] or not context['has_server_orm']:
|
|
171
|
+
file_findings = scan_sql_rls(file_path)
|
|
172
|
+
if file_findings:
|
|
173
|
+
findings_by_file[rel_path] = file_findings
|
|
174
|
+
|
|
175
|
+
# 5. Check JS/TS/Py files for NoSQL injection and insecure SSL configs
|
|
176
|
+
elif any(file.endswith(ext) for ext in ['.js', '.ts', '.jsx', '.tsx', '.py']):
|
|
177
|
+
if file in ['check_db_config.py', 'scan_secrets.py', 'check_auth_patterns.py', 'check_payment_config.py']:
|
|
178
|
+
continue
|
|
179
|
+
try:
|
|
180
|
+
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
|
|
181
|
+
for line_num, line in enumerate(f, 1):
|
|
182
|
+
if MONGO_INJECTION_PATTERN.search(line):
|
|
183
|
+
findings_by_file.setdefault(rel_path, []).append(
|
|
184
|
+
(line_num, "Potential NoSQL/MongoDB injection: unescaped request body passed directly to query")
|
|
185
|
+
)
|
|
186
|
+
if INSECURE_DB_SSL.search(line):
|
|
187
|
+
findings_by_file.setdefault(rel_path, []).append(
|
|
188
|
+
(line_num, "Insecure database connection: SSL is disabled or certificate verification is bypassed")
|
|
189
|
+
)
|
|
190
|
+
except Exception:
|
|
191
|
+
pass
|
|
143
192
|
|
|
144
193
|
# Output findings
|
|
145
194
|
if not findings_by_file:
|
|
@@ -2,6 +2,25 @@
|
|
|
2
2
|
import os
|
|
3
3
|
import sys
|
|
4
4
|
import fnmatch
|
|
5
|
+
import subprocess
|
|
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
|
+
def is_tracked_by_git(file_rel_path, repo_root):
|
|
12
|
+
"""Check if a file is actively tracked in git index even if listed in .gitignore."""
|
|
13
|
+
try:
|
|
14
|
+
res = subprocess.run(
|
|
15
|
+
['git', 'ls-files', '--error-unmatch', file_rel_path],
|
|
16
|
+
cwd=repo_root,
|
|
17
|
+
stdout=subprocess.DEVNULL,
|
|
18
|
+
stderr=subprocess.DEVNULL,
|
|
19
|
+
check=False
|
|
20
|
+
)
|
|
21
|
+
return res.returncode == 0
|
|
22
|
+
except Exception:
|
|
23
|
+
return False
|
|
5
24
|
|
|
6
25
|
# Potential local secret file patterns (glob format)
|
|
7
26
|
SECRET_PATTERNS = [
|
|
@@ -97,43 +116,102 @@ def main():
|
|
|
97
116
|
|
|
98
117
|
gitignore_path = os.path.join(target_dir, '.gitignore')
|
|
99
118
|
has_gitignore = os.path.exists(gitignore_path)
|
|
100
|
-
|
|
101
119
|
gitignore_patterns = parse_gitignore(gitignore_path) if has_gitignore else []
|
|
102
120
|
|
|
121
|
+
dockerignore_path = os.path.join(target_dir, '.dockerignore')
|
|
122
|
+
has_dockerfile = any(
|
|
123
|
+
os.path.exists(os.path.join(target_dir, f))
|
|
124
|
+
for f in ['Dockerfile', 'docker-compose.yml', 'compose.yaml', 'compose.yml']
|
|
125
|
+
)
|
|
126
|
+
dockerignore_patterns = parse_gitignore(dockerignore_path) if os.path.exists(dockerignore_path) else []
|
|
127
|
+
|
|
103
128
|
secret_files_found = []
|
|
104
129
|
unignored_secrets = []
|
|
130
|
+
docker_exposed_secrets = []
|
|
131
|
+
npmrc_findings = []
|
|
132
|
+
git_tracked_secrets = []
|
|
105
133
|
|
|
106
134
|
for root, dirs, files in os.walk(target_dir):
|
|
107
135
|
# Exclude directories
|
|
108
136
|
dirs[:] = [d for d in dirs if d not in {'node_modules', '.git', 'dist', 'build', '.next', '.svelte-kit'}]
|
|
109
137
|
|
|
138
|
+
# Check if current directory has its own .gitignore (monorepo support)
|
|
139
|
+
local_gi = os.path.join(root, '.gitignore')
|
|
140
|
+
active_patterns = list(gitignore_patterns)
|
|
141
|
+
if root != target_dir and os.path.exists(local_gi):
|
|
142
|
+
active_patterns.extend(parse_gitignore(local_gi))
|
|
143
|
+
|
|
110
144
|
for file in files:
|
|
145
|
+
file_path = os.path.join(root, file)
|
|
146
|
+
rel_path = os.path.relpath(file_path, target_dir)
|
|
147
|
+
|
|
148
|
+
# Check .npmrc for hardcoded auth tokens
|
|
149
|
+
if file == '.npmrc':
|
|
150
|
+
try:
|
|
151
|
+
with open(file_path, 'r', encoding='utf-8', errors='ignore') as nf:
|
|
152
|
+
for line in nf:
|
|
153
|
+
if '_authToken=' in line or '_auth=' in line:
|
|
154
|
+
npmrc_findings.append(f"{rel_path} contains hardcoded npm authToken")
|
|
155
|
+
break
|
|
156
|
+
except Exception:
|
|
157
|
+
pass
|
|
158
|
+
|
|
111
159
|
if is_secret_file(file):
|
|
112
|
-
|
|
113
|
-
rel_path = os.path.relpath(file_path, target_dir)
|
|
114
|
-
|
|
115
|
-
if is_ignored(rel_path, gitignore_patterns):
|
|
160
|
+
if is_ignored(rel_path, active_patterns):
|
|
116
161
|
secret_files_found.append((rel_path, True))
|
|
162
|
+
# Check if git is actively tracking it despite .gitignore
|
|
163
|
+
if is_tracked_by_git(rel_path, target_dir):
|
|
164
|
+
git_tracked_secrets.append(rel_path)
|
|
117
165
|
else:
|
|
118
166
|
secret_files_found.append((rel_path, False))
|
|
119
167
|
unignored_secrets.append(rel_path)
|
|
120
168
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
169
|
+
# If Dockerfile is present, ensure secrets are also in .dockerignore
|
|
170
|
+
if has_dockerfile:
|
|
171
|
+
if not os.path.exists(dockerignore_path) or not is_ignored(rel_path, dockerignore_patterns):
|
|
172
|
+
docker_exposed_secrets.append(rel_path)
|
|
173
|
+
|
|
174
|
+
has_issues = False
|
|
175
|
+
|
|
125
176
|
if unignored_secrets:
|
|
177
|
+
has_issues = True
|
|
126
178
|
print("FAIL: The following local secret files are NOT excluded by .gitignore:")
|
|
127
179
|
for sf in unignored_secrets:
|
|
128
180
|
print(f" - {sf}")
|
|
129
181
|
if not has_gitignore:
|
|
130
182
|
print(" (Note: No .gitignore file was found in the project root)")
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
183
|
+
|
|
184
|
+
if git_tracked_secrets:
|
|
185
|
+
has_issues = True
|
|
186
|
+
print("FAIL (Git Index Leak): The following secret files are listed in .gitignore but are STILL TRACKED by Git:")
|
|
187
|
+
for sf in git_tracked_secrets:
|
|
188
|
+
print(f" - {sf} (Fix: run 'git rm --cached {sf}')")
|
|
189
|
+
|
|
190
|
+
if docker_exposed_secrets:
|
|
191
|
+
has_issues = True
|
|
192
|
+
print("FAIL (Docker Risk): Dockerfile detected, but secret files are NOT excluded in .dockerignore:")
|
|
193
|
+
for sf in docker_exposed_secrets:
|
|
194
|
+
print(f" - {sf} (risk of baking secrets into container images)")
|
|
195
|
+
if not os.path.exists(dockerignore_path):
|
|
196
|
+
print(" (Note: No .dockerignore file was found in the project root)")
|
|
197
|
+
|
|
198
|
+
if npmrc_findings:
|
|
199
|
+
has_issues = True
|
|
200
|
+
print("FAIL (NPM Credential Leak):")
|
|
201
|
+
for hit in npmrc_findings:
|
|
202
|
+
print(f" - {hit}")
|
|
203
|
+
|
|
204
|
+
if not secret_files_found and not has_issues:
|
|
205
|
+
print("PASS: No local secret files (like .env) were detected in the project.")
|
|
136
206
|
sys.exit(0)
|
|
207
|
+
|
|
208
|
+
if has_issues:
|
|
209
|
+
sys.exit(1)
|
|
210
|
+
|
|
211
|
+
print("PASS: All detected local secret files are correctly listed in .gitignore and not tracked by Git:")
|
|
212
|
+
for sf, ignored in secret_files_found:
|
|
213
|
+
print(f" - {sf} (ignored)")
|
|
214
|
+
sys.exit(0)
|
|
137
215
|
|
|
138
216
|
if __name__ == '__main__':
|
|
139
217
|
main()
|