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/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.2.0",
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 a 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.
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 often scaffold code with hardcoded API keys, JWT secrets, database connection strings, or third-party client keys directly inside configuration files or source code. Additionally, local environment files (`.env`) are frequently created without updating the project's `.gitignore` to prevent them from being committed to version control.
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
- [Placeholder: The user will fill in real-world examples and analysis of why this happens in AI-generated code.]
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
- [Placeholder: The user will fill in the correct fix and mitigation strategies.]
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, fake login buttons, or bypass logic (e.g. returning `true` or dummy user objects unconditionally) to make testing quick. Developers may unknowingly deploy this placeholder logic directly to production, leaving endpoints unprotected.
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
- [Placeholder: The user will fill in real-world examples and analysis of why this happens in AI-generated code.]
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
- [Placeholder: The user will fill in the correct fix and mitigation strategies.]
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
- Services like Supabase or Firebase Firestore rely on Row-Level Security (RLS) or security rules files to protect data access. AI assistants frequently scaffold projects with default allow-all rules (e.g. `allow read, write: if true;` or `CREATE POLICY ... TO public USING (true);`) or create tables in Supabase without enabling RLS, enabling any client with an anonymous key to query or modify all records.
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
- [Placeholder: The user will fill in real-world examples and analysis of why this happens in AI-generated code.]
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
- [Placeholder: The user will fill in the correct fix and mitigation strategies.]
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
- Broken Object-Level Authorization occurs when an application exposes a resource endpoint (e.g. `/api/orders/:id` or fetching data by a user-supplied ID parameter) and retrieves or updates the resource without verifying that the authenticated user owns or has permission to access that specific resource.
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
- [Placeholder: The user will fill in real-world examples and analysis of why this happens in AI-generated code.]
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
- [Placeholder: The user will fill in the correct fix and mitigation strategies.]
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 often lacks backend input validation, trusting that client-side forms are sufficient. This opens the door to SQL injection, Cross-Site Scripting (XSS), server-side crashes due to unexpected data formats, or logic bypasses.
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
- [Placeholder: The user will fill in real-world examples and analysis of why this happens in AI-generated code.]
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
- [Placeholder: The user will fill in the correct fix and mitigation strategies.]
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 tools often write payment flows where pricing calculations, transaction amounts, subscription statuses, or checkout processes are determined or validated entirely on the client side. Attackers can easily intercept and modify request bodies (e.g. setting a premium product price to `$0.00`) before sending it to a payment gateway API.
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
- [Placeholder: The user will fill in real-world examples and analysis of why this happens in AI-generated code.]
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
- [Placeholder: The user will fill in the correct fix and mitigation strategies.]
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
- # Permissive policies in Supabase / SQL schema definition
25
- SUPABASE_PERMISSIVE_POLICY = re.compile(r'(?i)create\s+policy\s+\w+\s+on\s+\w+\s+(?:for\s+\w+\s+)?to\s+(?:public|anon)\s+using\s*\(\s*true\s*\)')
26
- SUPABASE_PERMISSIVE_POLICY_SIMPLE = re.compile(r'(?i)using\s*\(\s*true\s*\)|with\s+check\s*\(\s*true\s*\)')
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 to avoid false matches (e.g. commented-out commands or code annotations)
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 (support schemas, quotes, etc.)
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 read/write via USING(true) / CHECK(true)"))
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 Firebase Firestore/Storage rules
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
- file_findings = scan_sql_rls(file_path)
141
- if file_findings:
142
- findings_by_file[rel_path] = file_findings
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
- file_path = os.path.join(root, file)
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
- if not secret_files_found:
122
- print("PASS: No local secret files (like .env) were detected in the project.")
123
- sys.exit(0)
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
- sys.exit(1)
132
- else:
133
- print("PASS: All detected local secret files are correctly listed in .gitignore:")
134
- for sf, ignored in secret_files_found:
135
- print(f" - {sf} (ignored)")
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()