vbguard 0.1.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,151 @@
1
+ // AI tools frequently scaffold permissive database and infra configs
2
+ // These are the configs that caused the Moltbook breach
3
+
4
+ function scanPermissiveConfigs(ctx) {
5
+ const { content, relativePath, ext, basename } = ctx;
6
+ const findings = [];
7
+
8
+ // === Supabase checks ===
9
+
10
+ // Detect Supabase client initialized without RLS awareness
11
+ if (['.js', '.ts', '.jsx', '.tsx'].includes(ext)) {
12
+ // Using supabase with service_role key in client code
13
+ const supabaseInit = content.match(
14
+ /createClient\s*\(\s*[^,]+,\s*(?:process\.env\.)?(?:NEXT_PUBLIC_)?SUPABASE_(?:SERVICE_ROLE|ANON)_KEY/i
15
+ );
16
+
17
+ // Check for direct table access without auth checks
18
+ const directTableAccess = content.match(
19
+ /supabase\s*\.from\s*\(\s*['"`]\w+['"`]\s*\)\s*\.(?:select|insert|update|delete|upsert)\s*\(/gi
20
+ );
21
+
22
+ if (directTableAccess && directTableAccess.length > 3) {
23
+ // If lots of direct table access without any auth/RLS mentions
24
+ if (!/rls|row.?level|policy|policies|auth\.uid|auth\.role/i.test(content)) {
25
+ findings.push({
26
+ rule: 'config/supabase-no-rls-awareness',
27
+ severity: 'high',
28
+ file: relativePath,
29
+ line: null,
30
+ message: 'Multiple direct Supabase table operations found with no mention of RLS policies. The Moltbook breach happened because RLS was not configured — data was publicly readable and writable.',
31
+ fix: 'Enable RLS on all tables in Supabase dashboard and create appropriate policies. Test with anon key to verify restrictions.',
32
+ });
33
+ }
34
+ }
35
+ }
36
+
37
+ // === Firebase rules check ===
38
+ if (
39
+ basename === 'firestore.rules' ||
40
+ basename === 'database.rules.json' ||
41
+ basename === 'storage.rules' ||
42
+ basename === 'firebase.json'
43
+ ) {
44
+ // Check for allow read, write: if true
45
+ const permissiveRegex = /allow\s+(?:read|write|get|list|create|update|delete)\s*(?:,\s*(?:read|write|get|list|create|update|delete)\s*)*\s*:\s*if\s+true/gi;
46
+ const permissiveRule = permissiveRegex.exec(content);
47
+ if (permissiveRule) {
48
+ const lineNum = content.substring(0, permissiveRule.index).split('\n').length;
49
+ findings.push({
50
+ rule: 'config/firebase-permissive-rules',
51
+ severity: 'critical',
52
+ file: relativePath,
53
+ line: lineNum,
54
+ message: 'Firebase rules allow unrestricted access (if true). Anyone can read and write your database.',
55
+ fix: 'Restrict rules to authenticated users: allow read, write: if request.auth != null; Add granular per-collection rules.',
56
+ });
57
+ }
58
+
59
+ // Check for wide-open rules
60
+ if (/['"]\s*\.read['"]\s*:\s*true|['"]\s*\.write['"]\s*:\s*true/i.test(content)) {
61
+ findings.push({
62
+ rule: 'config/firebase-rtdb-open',
63
+ severity: 'critical',
64
+ file: relativePath,
65
+ line: null,
66
+ message: 'Firebase Realtime Database rules set to public read/write.',
67
+ fix: 'Set ".read" and ".write" to "auth != null" at minimum.',
68
+ });
69
+ }
70
+ }
71
+
72
+ // === Docker checks ===
73
+ if (basename === 'Dockerfile') {
74
+ // Running as root
75
+ if (!/USER\s+(?!root)/i.test(content)) {
76
+ findings.push({
77
+ rule: 'config/docker-running-as-root',
78
+ severity: 'medium',
79
+ file: relativePath,
80
+ line: null,
81
+ message: 'Dockerfile does not set a non-root USER. Container will run as root, increasing blast radius of any exploit.',
82
+ fix: 'Add USER directive: RUN addgroup -S app && adduser -S app -G app\\nUSER app',
83
+ });
84
+ }
85
+
86
+ // Copying .env into image
87
+ if (/COPY\s+.*\.env/i.test(content)) {
88
+ const match = content.match(/COPY\s+.*\.env/i);
89
+ const lineNum = content.substring(0, match.index).split('\n').length;
90
+ findings.push({
91
+ rule: 'config/docker-copies-env',
92
+ severity: 'critical',
93
+ file: relativePath,
94
+ line: lineNum,
95
+ message: '.env file copied into Docker image. Secrets will be baked into the image layer and visible to anyone with access.',
96
+ fix: 'Use Docker secrets, --env-file at runtime, or environment variables in docker-compose.yml instead.',
97
+ });
98
+ }
99
+
100
+ // Using latest tag
101
+ if (/FROM\s+\w+:latest/i.test(content)) {
102
+ findings.push({
103
+ rule: 'config/docker-latest-tag',
104
+ severity: 'low',
105
+ file: relativePath,
106
+ line: null,
107
+ message: 'Using :latest tag in Dockerfile. Builds are not reproducible and may introduce unexpected changes.',
108
+ fix: 'Pin to a specific version: FROM node:20-alpine instead of FROM node:latest',
109
+ });
110
+ }
111
+ }
112
+
113
+ // === docker-compose checks ===
114
+ if (basename === 'docker-compose.yml' || basename === 'docker-compose.yaml') {
115
+ // Exposed database ports
116
+ const dbPortMatch = content.match(
117
+ /ports:\s*\n\s*-\s*['"]?(?:0\.0\.0\.0:)?(\d+):(?:5432|3306|27017|6379|9200)/m
118
+ );
119
+ if (dbPortMatch) {
120
+ const lineNum = content.substring(0, dbPortMatch.index).split('\n').length;
121
+ findings.push({
122
+ rule: 'config/docker-exposed-db-port',
123
+ severity: 'high',
124
+ file: relativePath,
125
+ line: lineNum,
126
+ message: 'Database port exposed to host. AI tools always expose DB ports for convenience. In production, databases should only be accessible within the Docker network.',
127
+ fix: 'Remove the ports mapping for databases, or bind to localhost: "127.0.0.1:5432:5432"',
128
+ });
129
+ }
130
+
131
+ // Hardcoded passwords in compose
132
+ const composePassMatch = content.match(
133
+ /(?:POSTGRES_PASSWORD|MYSQL_ROOT_PASSWORD|MONGO_INITDB_ROOT_PASSWORD|REDIS_PASSWORD)\s*[:=]\s*['"]?([^'"\s\n]+)/i
134
+ );
135
+ if (composePassMatch) {
136
+ const lineNum = content.substring(0, composePassMatch.index).split('\n').length;
137
+ findings.push({
138
+ rule: 'config/docker-hardcoded-password',
139
+ severity: 'high',
140
+ file: relativePath,
141
+ line: lineNum,
142
+ message: 'Database password hardcoded in docker-compose. AI tools always set simple passwords like "password" or "postgres".',
143
+ fix: 'Use environment variables: POSTGRES_PASSWORD=${DB_PASSWORD} and set in .env file.',
144
+ });
145
+ }
146
+ }
147
+
148
+ return findings;
149
+ }
150
+
151
+ module.exports = { scanPermissiveConfigs };
@@ -0,0 +1,178 @@
1
+ // Patterns specifically tuned for AI-generated code leaks
2
+ // AI tools commonly inline credentials instead of using env vars
3
+
4
+ const SECRET_PATTERNS = [
5
+ {
6
+ name: 'AWS Access Key',
7
+ regex: /(?:AKIA|ASIA)[0-9A-Z]{16}/g,
8
+ severity: 'critical',
9
+ fix: 'Move to environment variable AWS_ACCESS_KEY_ID and use aws-sdk credential provider.',
10
+ },
11
+ {
12
+ name: 'AWS Secret Key',
13
+ regex: /(?:aws)?(?:_?secret)?(?:_?access)?(?:_?key)\s*[:=]\s*['"`]([A-Za-z0-9/+=]{40})['"`]/gi,
14
+ severity: 'critical',
15
+ fix: 'Move to environment variable AWS_SECRET_ACCESS_KEY. Never hardcode AWS secrets.',
16
+ },
17
+ {
18
+ name: 'Stripe Secret Key',
19
+ regex: /sk_live_[0-9a-zA-Z]{24,}/g,
20
+ severity: 'critical',
21
+ fix: 'Move to environment variable STRIPE_SECRET_KEY. This key can charge real cards.',
22
+ },
23
+ {
24
+ name: 'Stripe Publishable Key in Backend',
25
+ regex: /pk_live_[0-9a-zA-Z]{24,}/g,
26
+ severity: 'medium',
27
+ backendOnly: true,
28
+ fix: 'Publishable keys are safe in frontend, but check this isn\'t a backend file leaking keys.',
29
+ },
30
+ {
31
+ name: 'OpenAI API Key',
32
+ regex: /sk-[a-zA-Z0-9]{20,}T3BlbkFJ[a-zA-Z0-9]{20,}/g,
33
+ severity: 'critical',
34
+ fix: 'Move to environment variable OPENAI_API_KEY.',
35
+ },
36
+ {
37
+ name: 'OpenAI API Key (new format)',
38
+ regex: /sk-(?:proj-)?[a-zA-Z0-9_-]{40,}/g,
39
+ severity: 'critical',
40
+ fix: 'Move to environment variable OPENAI_API_KEY.',
41
+ },
42
+ {
43
+ name: 'Anthropic API Key',
44
+ regex: /sk-ant-[a-zA-Z0-9_-]{40,}/g,
45
+ severity: 'critical',
46
+ fix: 'Move to environment variable ANTHROPIC_API_KEY.',
47
+ },
48
+ {
49
+ name: 'Supabase Service Role Key',
50
+ regex: /eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9\.[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+/g,
51
+ severity: 'critical',
52
+ jwtCheck: true,
53
+ fix: 'Supabase service_role key bypasses RLS. Never expose in client code. Use SUPABASE_SERVICE_ROLE_KEY env var server-side only.',
54
+ },
55
+ {
56
+ name: 'Generic JWT Token',
57
+ regex: /eyJ[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]{10,}/g,
58
+ severity: 'high',
59
+ fix: 'Hardcoded JWTs should be fetched at runtime, not embedded in source code.',
60
+ },
61
+ {
62
+ name: 'GitHub Token',
63
+ regex: /(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9_]{36,}/g,
64
+ severity: 'critical',
65
+ fix: 'Move to environment variable GITHUB_TOKEN.',
66
+ },
67
+ {
68
+ name: 'Google API Key',
69
+ regex: /AIza[0-9A-Za-z_-]{35}/g,
70
+ severity: 'high',
71
+ fix: 'Move to environment variable and restrict the key in Google Cloud Console.',
72
+ },
73
+ {
74
+ name: 'Slack Token',
75
+ regex: /xox[bpors]-[0-9]{10,}-[0-9a-zA-Z]{10,}/g,
76
+ severity: 'critical',
77
+ fix: 'Move to environment variable SLACK_TOKEN.',
78
+ },
79
+ {
80
+ name: 'Discord Bot Token',
81
+ regex: /[MN][A-Za-z\d]{23,}\.[\w-]{6}\.[\w-]{27,}/g,
82
+ severity: 'critical',
83
+ fix: 'Move to environment variable DISCORD_BOT_TOKEN.',
84
+ },
85
+ {
86
+ name: 'SendGrid API Key',
87
+ regex: /SG\.[a-zA-Z0-9_-]{22}\.[a-zA-Z0-9_-]{43}/g,
88
+ severity: 'critical',
89
+ fix: 'Move to environment variable SENDGRID_API_KEY.',
90
+ },
91
+ {
92
+ name: 'Twilio Auth Token',
93
+ regex: /(?:twilio)?(?:_?auth)?(?:_?token)\s*[:=]\s*['"`]([a-f0-9]{32})['"`]/gi,
94
+ severity: 'critical',
95
+ fix: 'Move to environment variable TWILIO_AUTH_TOKEN.',
96
+ },
97
+ {
98
+ name: 'Database Connection String',
99
+ regex: /(?:mongodb(?:\+srv)?|postgres(?:ql)?|mysql|redis|amqp|mssql):\/\/[^\s'"`,}{)]+/gi,
100
+ severity: 'critical',
101
+ fix: 'Move connection string to DATABASE_URL environment variable. Never hardcode credentials.',
102
+ },
103
+ {
104
+ name: 'Hardcoded Password Assignment',
105
+ regex: /(?:password|passwd|pwd|secret)\s*[:=]\s*['"`](?!.*\b(?:process\.env|os\.environ|getenv)\b)[^'"` \n]{4,}['"`]/gi,
106
+ severity: 'high',
107
+ fix: 'Use environment variables for passwords. AI tools commonly inline these for convenience.',
108
+ },
109
+ {
110
+ name: 'Private Key Block',
111
+ regex: /-----BEGIN (?:RSA |EC |DSA )?PRIVATE KEY-----/g,
112
+ severity: 'critical',
113
+ fix: 'Never commit private keys. Store in a secrets manager or environment variable.',
114
+ },
115
+ {
116
+ name: 'Firebase Config with API Key',
117
+ regex: /(?:firebase|firebaseConfig)\s*(?:=|:)\s*\{[^}]*apiKey\s*:\s*['"`][^'"`]+['"`]/gs,
118
+ severity: 'medium',
119
+ fix: 'Firebase config with API key is safe in frontend, but ensure Firestore/RTDB rules are locked down.',
120
+ },
121
+ ];
122
+
123
+ function scanSecrets(ctx) {
124
+ const { content, relativePath, ext } = ctx;
125
+ const findings = [];
126
+
127
+ // Skip .env files for secret scanning — those are supposed to have secrets
128
+ if (ctx.basename.startsWith('.env')) return findings;
129
+ // Skip lock files
130
+ if (ctx.basename === 'package-lock.json' || ctx.basename === 'yarn.lock') return findings;
131
+
132
+ const isBackend = /(?:server|api|backend|routes|controllers|middleware|lib|utils)/i.test(relativePath);
133
+ const lines = content.split('\n');
134
+
135
+ for (const pattern of SECRET_PATTERNS) {
136
+ // Reset regex
137
+ pattern.regex.lastIndex = 0;
138
+
139
+ let match;
140
+ while ((match = pattern.regex.exec(content)) !== null) {
141
+ // Find line number
142
+ const upToMatch = content.substring(0, match.index);
143
+ const lineNum = upToMatch.split('\n').length;
144
+ const line = lines[lineNum - 1] || '';
145
+
146
+ // Skip if it's in a comment
147
+ const trimmedLine = line.trim();
148
+ if (trimmedLine.startsWith('//') || trimmedLine.startsWith('#') || trimmedLine.startsWith('*')) {
149
+ // Still flag it — commented out secrets are still in git history
150
+ }
151
+
152
+ // Skip if it's referencing an env var
153
+ if (/process\.env|os\.environ|os\.getenv|ENV\[|getenv/i.test(line)) continue;
154
+
155
+ // Skip example/placeholder values
156
+ const matchStr = match[0].toLowerCase();
157
+ if (/example|placeholder|your[_-]?key|xxx|test|dummy|fake|sample/i.test(matchStr)) continue;
158
+ if (/example|placeholder|your[_-]?key|xxx|test|dummy|fake|sample/i.test(line)) continue;
159
+
160
+ findings.push({
161
+ rule: `secret/${pattern.name.toLowerCase().replace(/\s+/g, '-')}`,
162
+ severity: pattern.severity,
163
+ file: relativePath,
164
+ line: lineNum,
165
+ message: `Hardcoded ${pattern.name} detected. AI tools commonly inline credentials — this is a top cause of breaches in vibe-coded apps.`,
166
+ fix: pattern.fix,
167
+ snippet: line.trim().substring(0, 120),
168
+ });
169
+
170
+ // Only report first match per pattern per file
171
+ break;
172
+ }
173
+ }
174
+
175
+ return findings;
176
+ }
177
+
178
+ module.exports = { scanSecrets };