getdoorman 1.0.9 → 1.1.1

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.
@@ -259,7 +259,7 @@ export default rules;
259
259
 
260
260
  // SEC-CRY-011: Hardcoded encryption key
261
261
  rules.push({
262
- id: 'SEC-CRY-011', category: 'security', severity: 'critical', confidence: 'definite',
262
+ id: 'SEC-CRY-011', category: 'security', severity: 'high', confidence: 'definite',
263
263
  title: 'Hardcoded encryption key or IV',
264
264
  check({ files }) {
265
265
  const findings = [];
@@ -467,7 +467,7 @@ rules.push({
467
467
 
468
468
  // SEC-CRY-021: Hardcoded salt
469
469
  rules.push({
470
- id: 'SEC-CRY-021', category: 'security', severity: 'critical', confidence: 'definite',
470
+ id: 'SEC-CRY-021', category: 'security', severity: 'high', confidence: 'definite',
471
471
  title: 'Hardcoded salt for password hashing',
472
472
  check({ files }) {
473
473
  const findings = [];
@@ -132,7 +132,7 @@ const rules = [
132
132
  {
133
133
  id: 'SEC-CS-006',
134
134
  category: 'security',
135
- severity: 'critical',
135
+ severity: 'high',
136
136
  confidence: 'definite',
137
137
  title: 'Insecure Deserialization: BinaryFormatter',
138
138
  description: 'BinaryFormatter is inherently insecure and can execute arbitrary code during deserialization.',
@@ -603,7 +603,7 @@ const rules = [
603
603
  {
604
604
  id: 'SEC-CS-035',
605
605
  category: 'security',
606
- severity: 'critical',
606
+ severity: 'high',
607
607
  confidence: 'definite',
608
608
  title: 'CORS: Credentials with Any Origin',
609
609
  description: 'AllowCredentials with AllowAnyOrigin is a severe misconfiguration.',
@@ -731,7 +731,7 @@ const rules = [
731
731
  {
732
732
  id: 'SEC-CS-043',
733
733
  category: 'security',
734
- severity: 'critical',
734
+ severity: 'high',
735
735
  confidence: 'definite',
736
736
  title: 'TLS Certificate Validation Disabled',
737
737
  description: 'Disabling certificate validation allows man-in-the-middle attacks.',
@@ -34,7 +34,7 @@ const rules = [
34
34
 
35
35
  // SEC-CSRF-002
36
36
  {
37
- id: 'SEC-CSRF-002', category: 'security', severity: 'critical', confidence: 'definite',
37
+ id: 'SEC-CSRF-002', category: 'security', severity: 'high', confidence: 'definite',
38
38
  title: 'CSRF token not validated on server',
39
39
  check({ files }) {
40
40
  const findings = [];
@@ -95,7 +95,7 @@ const rules = [
95
95
 
96
96
  // SEC-UPL-005
97
97
  {
98
- id: 'SEC-UPL-005', category: 'security', severity: 'critical', confidence: 'definite',
98
+ id: 'SEC-UPL-005', category: 'security', severity: 'high', confidence: 'definite',
99
99
  title: 'Executable file upload allowed',
100
100
  check({ files }) {
101
101
  const findings = [];
@@ -258,7 +258,7 @@ const rules = [
258
258
  {
259
259
  id: 'SEC-GO-014',
260
260
  category: 'security',
261
- severity: 'critical',
261
+ severity: 'high',
262
262
  confidence: 'definite',
263
263
  title: 'Insecure TLS: Certificate Verification Disabled',
264
264
  description: 'Setting InsecureSkipVerify to true disables TLS certificate validation, enabling MITM attacks.',
@@ -450,7 +450,7 @@ const rules = [
450
450
  {
451
451
  id: 'SEC-GO-026',
452
452
  category: 'security',
453
- severity: 'critical',
453
+ severity: 'high',
454
454
  confidence: 'definite',
455
455
  title: 'CORS Allow Credentials with Wildcard Origin',
456
456
  description: 'Allowing credentials with wildcard origin is a severe misconfiguration enabling credential theft.',
@@ -482,7 +482,7 @@ const rules = [
482
482
  {
483
483
  id: 'SEC-GO-028',
484
484
  category: 'security',
485
- severity: 'critical',
485
+ severity: 'high',
486
486
  confidence: 'definite',
487
487
  title: 'JWT None Algorithm Accepted',
488
488
  description: 'Not validating the JWT signing method allows attackers to use the "none" algorithm to forge tokens.',
@@ -59,7 +59,7 @@ const rules = [
59
59
  {
60
60
  id: 'SEC-MISC-001',
61
61
  category: 'security',
62
- severity: 'critical',
62
+ severity: 'high',
63
63
  confidence: 'definite',
64
64
  title: 'Public S3 bucket configuration detected',
65
65
  check({ files }) {
@@ -82,7 +82,7 @@ const rules = [
82
82
  {
83
83
  id: 'SEC-MISC-002',
84
84
  category: 'security',
85
- severity: 'critical',
85
+ severity: 'high',
86
86
  confidence: 'definite',
87
87
  title: 'Database bound to all network interfaces',
88
88
  check({ files }) {
@@ -268,7 +268,7 @@ const rules = [
268
268
  {
269
269
  id: 'SEC-MISC-007',
270
270
  category: 'security',
271
- severity: 'critical',
271
+ severity: 'high',
272
272
  confidence: 'definite',
273
273
  title: 'Permissive database security rules',
274
274
  check({ files }) {
@@ -592,7 +592,7 @@ const rules = [
592
592
  {
593
593
  id: 'SEC-MISC-015',
594
594
  category: 'security',
595
- severity: 'critical',
595
+ severity: 'high',
596
596
  confidence: 'definite',
597
597
  title: 'Overly permissive IAM policy detected',
598
598
  check({ files }) {
@@ -39,7 +39,7 @@ const rules = [
39
39
  {
40
40
  id: 'SEC-JWT-001',
41
41
  category: 'security',
42
- severity: 'critical',
42
+ severity: 'high',
43
43
  confidence: 'definite',
44
44
  title: 'JWT Algorithm "none" Allowed',
45
45
  description:
@@ -108,7 +108,7 @@ const rules = [
108
108
  {
109
109
  id: 'SEC-JWT-004',
110
110
  category: 'security',
111
- severity: 'critical',
111
+ severity: 'high',
112
112
  confidence: 'definite',
113
113
  title: 'Weak JWT Signing Secret',
114
114
  description:
@@ -241,10 +241,13 @@ const rules = [
241
241
  const findings = [];
242
242
  const callbackPattern = /(?:\/callback|\/oauth\/callback|\/auth\/callback)/;
243
243
  const tokenExchange = /(?:getToken|requestToken|exchangeCode|code.*token|grant_type.*authorization_code)/;
244
+ // Check if ANY file in the project validates OAuth state (cross-file check)
245
+ const projectHasStateValidation = [...files.values()].some(c => /(?:state|csrf|nonce).*(?:verify|validate|check|compare|match|===)/i.test(c));
244
246
  for (const [path, content] of files) {
245
247
  if (SKIP_PATH.test(path)) continue;
246
248
  if (!isJS(path)) continue;
247
249
  if (callbackPattern.test(content) && tokenExchange.test(content)) {
250
+ if (projectHasStateValidation) continue; // State is validated somewhere in the project
248
251
  if (!/(?:state|csrf|nonce)/.test(content)) {
249
252
  findings.push({
250
253
  ruleId: this.id,
@@ -39,7 +39,7 @@ const rules = [
39
39
  {
40
40
  id: 'SEC-PP-001',
41
41
  category: 'security',
42
- severity: 'critical',
42
+ severity: 'high',
43
43
  confidence: 'definite',
44
44
  title: 'Direct __proto__ Property Access',
45
45
  description:
@@ -108,7 +108,7 @@ const rules = [
108
108
  {
109
109
  id: 'SEC-PP-004',
110
110
  category: 'security',
111
- severity: 'critical',
111
+ severity: 'high',
112
112
  confidence: 'definite',
113
113
  title: 'Constructor Prototype Access Pattern',
114
114
  description:
@@ -30,7 +30,7 @@ const rules = [
30
30
 
31
31
  // SEC-RL-002
32
32
  {
33
- id: 'SEC-RL-002', category: 'security', severity: 'critical', confidence: 'definite',
33
+ id: 'SEC-RL-002', category: 'security', severity: 'high', confidence: 'definite',
34
34
  title: 'No rate limiting on login endpoint',
35
35
  check({ files, stack }) {
36
36
  const findings = [];
@@ -274,7 +274,7 @@ const rules = [
274
274
  {
275
275
  id: 'SEC-RS-015',
276
276
  category: 'security',
277
- severity: 'critical',
277
+ severity: 'high',
278
278
  confidence: 'definite',
279
279
  title: 'TLS Certificate Validation Disabled',
280
280
  description: 'Disabling TLS certificate validation allows man-in-the-middle attacks.',
@@ -290,7 +290,7 @@ const rules = [
290
290
  {
291
291
  id: 'SEC-RS-016',
292
292
  category: 'security',
293
- severity: 'critical',
293
+ severity: 'high',
294
294
  confidence: 'definite',
295
295
  title: 'TLS Hostname Validation Disabled',
296
296
  description: 'Disabling hostname validation in TLS allows certificate substitution attacks.',
@@ -549,7 +549,7 @@ const rules = [
549
549
  {
550
550
  id: 'SEC-RS-032',
551
551
  category: 'security',
552
- severity: 'critical',
552
+ severity: 'high',
553
553
  confidence: 'definite',
554
554
  title: 'Hardcoded Private Key',
555
555
  description: 'Private keys embedded in source code can be extracted and used to impersonate the service.',
@@ -96,7 +96,7 @@ const rules = [
96
96
  {
97
97
  id: 'SEC-SSRF-003',
98
98
  category: 'security',
99
- severity: 'critical',
99
+ severity: 'high',
100
100
  confidence: 'definite',
101
101
  title: 'Cloud Metadata Endpoint URL in Code',
102
102
  description:
@@ -105,14 +105,18 @@ const rules = [
105
105
  check({ files }) {
106
106
  const findings = [];
107
107
  const pattern = /169\.254\.169\.254|metadata\.google\.internal|metadata\.azure\.com/;
108
- const blocklistContext = /block|deny|forbidden|not.?allowed|invalid|reject|blacklist|safelist|denylist|disallow|banned|BLOCKED/i;
108
+ const safeContext = /block|deny|forbidden|not.?allowed|invalid|reject|blacklist|safelist|denylist|disallow|banned|BLOCKED|isAllowed|validate|security|guard|check|filter|protect/i;
109
109
  for (const [path, content] of files) {
110
110
  if (SKIP_PATH.test(path)) continue;
111
111
  if (isJS(path)) {
112
112
  const lines = content.split('\n');
113
113
  for (let i = 0; i < lines.length; i++) {
114
- if (pattern.test(lines[i]) && !blocklistContext.test(lines[i])) {
115
- findings.push({ ruleId: this.id, category: this.category, severity: this.severity, title: this.title, description: this.description, confidence: this.confidence, file: path, line: i + 1, fix: this.fix });
114
+ if (pattern.test(lines[i])) {
115
+ // Check ±5 lines for security/validation context
116
+ const ctx = lines.slice(Math.max(0, i - 5), i + 5).join(' ');
117
+ if (!safeContext.test(ctx)) {
118
+ findings.push({ ruleId: this.id, category: this.category, severity: this.severity, title: this.title, description: this.description, confidence: this.confidence, file: path, line: i + 1, fix: this.fix });
119
+ }
116
120
  }
117
121
  }
118
122
  }
@@ -40,7 +40,7 @@ const rules = [
40
40
  {
41
41
  id: 'SEC-SCA-001',
42
42
  category: 'security',
43
- severity: 'critical',
43
+ severity: 'high',
44
44
  confidence: 'definite',
45
45
  title: 'Postinstall Script Executes Binary or Shell Command',
46
46
  description:
@@ -77,7 +77,7 @@ const rules = [
77
77
  {
78
78
  id: 'SEC-SCA-002',
79
79
  category: 'security',
80
- severity: 'critical',
80
+ severity: 'high',
81
81
  confidence: 'definite',
82
82
  title: 'Install Hook Downloads and Executes Remote Code',
83
83
  description:
@@ -230,7 +230,7 @@ const rules = [
230
230
  {
231
231
  id: 'SEC-XSS-007',
232
232
  category: 'security',
233
- severity: 'critical',
233
+ severity: 'high',
234
234
  confidence: 'definite',
235
235
  title: 'javascript: protocol in href/src attributes',
236
236
  check({ files }) {
@@ -498,7 +498,7 @@ const rules = [
498
498
  },
499
499
  },
500
500
  // SEC-XSS-013: Server-side reflected XSS via res.send/res.write
501
- { id: 'SEC-XSS-013', category: 'security', severity: 'critical', confidence: 'definite', title: 'Server-Side Reflected XSS via res.send()',
501
+ { id: 'SEC-XSS-013', category: 'security', severity: 'high', confidence: 'definite', title: 'Server-Side Reflected XSS via res.send()',
502
502
  check({ files }) {
503
503
  const findings = [];
504
504
  for (const [fp, c] of files) {
@@ -0,0 +1,257 @@
1
+ /**
2
+ * Doorman Simple Checks — 10 things that matter, zero false positives.
3
+ * Each check returns { pass: boolean, message: string, file?: string, line?: number }
4
+ */
5
+
6
+ const CHECKS = [
7
+ {
8
+ id: 'leaked-keys',
9
+ name: 'Leaked API Keys',
10
+ icon: '🔑',
11
+ pass: 'No leaked API keys found',
12
+ fail: 'API key hardcoded in source code',
13
+ check(files) {
14
+ const patterns = [
15
+ { name: 'AWS Access Key', regex: /(?:^|[^A-Z0-9])AKIA[0-9A-Z]{16}(?:[^A-Z0-9]|$)/ },
16
+ { name: 'Stripe Secret Key', regex: /sk_live_[0-9a-zA-Z]{24,}/ },
17
+ { name: 'OpenAI API Key', regex: /sk-[a-zA-Z0-9]{20,}T3BlbkFJ/ },
18
+ { name: 'Anthropic API Key', regex: /sk-ant-[a-zA-Z0-9-]{20,}/ },
19
+ { name: 'GitHub Token', regex: /ghp_[0-9a-zA-Z]{36}/ },
20
+ { name: 'Supabase Service Key', regex: /eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9\.[a-zA-Z0-9_-]{50,}/ },
21
+ { name: 'Google API Key', regex: /AIza[0-9A-Za-z_-]{35}/ },
22
+ { name: 'Slack Token', regex: /xoxb-[0-9]{11,}-[0-9a-zA-Z]{24,}/ },
23
+ ];
24
+ const findings = [];
25
+ for (const [fp, content] of files) {
26
+ if (fp.endsWith('.example') || fp.endsWith('.sample') || fp.endsWith('.template')) continue;
27
+ if (/test|spec|mock|fixture|__test__|\.test\.|\.spec\./i.test(fp)) continue;
28
+ const lines = content.split('\n');
29
+ for (let i = 0; i < lines.length; i++) {
30
+ if (/^\s*(\/\/|#|\*|\/\*)/.test(lines[i])) continue; // skip comments
31
+ for (const p of patterns) {
32
+ if (p.regex.test(lines[i])) {
33
+ findings.push({ message: `${p.name} found`, file: fp, line: i + 1 });
34
+ }
35
+ }
36
+ }
37
+ }
38
+ return findings;
39
+ },
40
+ },
41
+ {
42
+ id: 'env-exposed',
43
+ name: '.env File Exposed',
44
+ icon: '📄',
45
+ pass: '.env is safe (in .gitignore)',
46
+ fail: '.env file may be committed to git',
47
+ check(files) {
48
+ const hasEnv = [...files.keys()].some(f => f === '.env' || f.match(/^\.env\.[^.]+$/) && !f.endsWith('.example') && !f.endsWith('.sample'));
49
+ const hasGitignore = files.has('.gitignore');
50
+ const gitignoreContent = files.get('.gitignore') || '';
51
+ const envInGitignore = /^\.env$/m.test(gitignoreContent) || /^\.env\b/m.test(gitignoreContent);
52
+
53
+ if (hasEnv && !envInGitignore) {
54
+ return [{ message: '.env file found but not in .gitignore' }];
55
+ }
56
+ return [];
57
+ },
58
+ },
59
+ {
60
+ id: 'sql-injection',
61
+ name: 'SQL Injection',
62
+ icon: '💉',
63
+ pass: 'No SQL injection patterns found',
64
+ fail: 'SQL query built with user input',
65
+ check(files) {
66
+ const findings = [];
67
+ const sqlPattern = /(?:query|execute|raw)\s*\(\s*`[^`]*(?:SELECT|INSERT|UPDATE|DELETE|FROM|WHERE)\b[^`]*\$\{/i;
68
+ for (const [fp, content] of files) {
69
+ if (/test|spec|mock/i.test(fp)) continue;
70
+ if (!fp.match(/\.(js|ts|jsx|tsx|py|rb|go|java|php)$/)) continue;
71
+ const lines = content.split('\n');
72
+ for (let i = 0; i < lines.length; i++) {
73
+ const block = lines.slice(i, i + 3).join(' ');
74
+ if (sqlPattern.test(block)) {
75
+ findings.push({ message: 'SQL query built with template literal', file: fp, line: i + 1 });
76
+ }
77
+ }
78
+ }
79
+ return findings;
80
+ },
81
+ },
82
+ {
83
+ id: 'no-error-handling',
84
+ name: 'Missing Error Handling',
85
+ icon: '💥',
86
+ pass: 'API routes have error handling',
87
+ fail: 'API route will crash on error',
88
+ check(files) {
89
+ const findings = [];
90
+ for (const [fp, content] of files) {
91
+ if (/test|spec|mock/i.test(fp)) continue;
92
+ if (!fp.match(/\.(js|ts|jsx|tsx)$/)) continue;
93
+ if (!fp.match(/api|route|handler|controller/i)) continue;
94
+ const lines = content.split('\n');
95
+ for (let i = 0; i < lines.length; i++) {
96
+ if (/export\s+(?:async\s+)?function\s+(?:GET|POST|PUT|DELETE|PATCH)\b/.test(lines[i])) {
97
+ const body = lines.slice(i, Math.min(lines.length, i + 30)).join('\n');
98
+ if (!/try\s*\{|\.catch\s*\(/.test(body)) {
99
+ findings.push({ message: `${fp.split('/').pop()} has no error handling`, file: fp, line: i + 1 });
100
+ break; // one per file
101
+ }
102
+ }
103
+ }
104
+ }
105
+ return findings;
106
+ },
107
+ },
108
+ {
109
+ id: 'hardcoded-secrets',
110
+ name: 'Hardcoded Secrets',
111
+ icon: '🔒',
112
+ pass: 'No hardcoded passwords or secrets',
113
+ fail: 'Password or secret hardcoded in source',
114
+ check(files) {
115
+ const findings = [];
116
+ const pattern = /(?:password|passwd|pwd|secret|secret_key|jwt_secret)\s*[:=]\s*['"][^'"]{8,}['"]/i;
117
+ for (const [fp, content] of files) {
118
+ if (/test|spec|mock|example|sample|\.env/i.test(fp)) continue;
119
+ if (!fp.match(/\.(js|ts|jsx|tsx|py|rb|go|java|php|yml|yaml|json)$/)) continue;
120
+ const lines = content.split('\n');
121
+ for (let i = 0; i < lines.length; i++) {
122
+ if (/^\s*(\/\/|#|\*|\/\*)/.test(lines[i])) continue;
123
+ if (pattern.test(lines[i]) && !/process\.env|os\.environ|ENV\[|config\.|getenv/i.test(lines[i])) {
124
+ findings.push({ message: 'Hardcoded secret', file: fp, line: i + 1 });
125
+ }
126
+ }
127
+ }
128
+ return findings;
129
+ },
130
+ },
131
+ {
132
+ id: 'eval-danger',
133
+ name: 'Code Execution',
134
+ icon: '⚠️',
135
+ pass: 'No dangerous eval() or exec() calls',
136
+ fail: 'eval() or exec() with dynamic input',
137
+ check(files) {
138
+ const findings = [];
139
+ for (const [fp, content] of files) {
140
+ if (/test|spec|mock/i.test(fp)) continue;
141
+ if (!fp.match(/\.(js|ts|jsx|tsx)$/)) continue;
142
+ const lines = content.split('\n');
143
+ for (let i = 0; i < lines.length; i++) {
144
+ if (/^\s*(\/\/|#|\*|\/\*)/.test(lines[i])) continue;
145
+ // Only flag eval() with variable input, not eval('literal')
146
+ if (/\beval\s*\(\s*(?!['"`])/.test(lines[i]) || /new\s+Function\s*\(\s*(?!['"`])/.test(lines[i])) {
147
+ findings.push({ message: 'eval() with dynamic input', file: fp, line: i + 1 });
148
+ }
149
+ }
150
+ }
151
+ return findings;
152
+ },
153
+ },
154
+ {
155
+ id: 'ai-cost-waste',
156
+ name: 'AI Cost Waste',
157
+ icon: '💰',
158
+ pass: 'AI API calls look efficient',
159
+ fail: 'AI API called without caching or limits',
160
+ check(files) {
161
+ const findings = [];
162
+ for (const [fp, content] of files) {
163
+ if (/test|spec|mock/i.test(fp)) continue;
164
+ if (!fp.match(/\.(js|ts|jsx|tsx)$/)) continue;
165
+ // Check for AI API calls without max_tokens
166
+ if (/openai|anthropic|chat\.completions\.create|messages\.create/i.test(content)) {
167
+ if (!/max_tokens|maxTokens|max_output_tokens/i.test(content)) {
168
+ findings.push({ message: 'AI API call without token limit', file: fp });
169
+ }
170
+ }
171
+ }
172
+ return findings;
173
+ },
174
+ },
175
+ {
176
+ id: 'debug-in-prod',
177
+ name: 'Debug Code Left In',
178
+ icon: '🐛',
179
+ pass: 'No debug code in production files',
180
+ fail: 'console.log or debug code left in',
181
+ check(files) {
182
+ const findings = [];
183
+ let count = 0;
184
+ for (const [fp, content] of files) {
185
+ if (/test|spec|mock/i.test(fp)) continue;
186
+ if (!fp.match(/\.(js|ts|jsx|tsx)$/)) continue;
187
+ if (fp.includes('logger') || fp.includes('debug') || fp.includes('config')) continue;
188
+ const lines = content.split('\n');
189
+ for (let i = 0; i < lines.length; i++) {
190
+ if (/console\.log\(/.test(lines[i]) && !/\/\//.test(lines[i].split('console')[0])) {
191
+ count++;
192
+ if (count <= 3) findings.push({ message: 'console.log left in code', file: fp, line: i + 1 });
193
+ }
194
+ }
195
+ }
196
+ if (count > 3) findings.push({ message: `...and ${count - 3} more console.log calls` });
197
+ return findings;
198
+ },
199
+ },
200
+ {
201
+ id: 'open-database',
202
+ name: 'Database Security',
203
+ icon: '🗄️',
204
+ pass: 'Database configuration looks secure',
205
+ fail: 'Database may be publicly accessible',
206
+ check(files) {
207
+ const findings = [];
208
+ for (const [fp, content] of files) {
209
+ if (/test|spec|mock/i.test(fp)) continue;
210
+ // MongoDB without auth
211
+ if (/mongodb:\/\/(?:localhost|127\.0\.0\.1|0\.0\.0\.0)(?::\d+)?\/\w+['"]/.test(content) && !/authSource|user.*password|mongodb\+srv/i.test(content)) {
212
+ findings.push({ message: 'MongoDB connection without authentication', file: fp });
213
+ }
214
+ // Binding to all interfaces
215
+ if (/\.listen\(\s*\d+\s*,\s*['"]0\.0\.0\.0['"]/.test(content)) {
216
+ findings.push({ message: 'Server bound to all network interfaces (0.0.0.0)', file: fp });
217
+ }
218
+ }
219
+ return findings;
220
+ },
221
+ },
222
+ {
223
+ id: 'outdated-deps',
224
+ name: 'Dangerous Dependencies',
225
+ icon: '📦',
226
+ pass: 'No known vulnerable packages',
227
+ fail: 'Package with known security issue',
228
+ check(files) {
229
+ const findings = [];
230
+ const dangerous = {
231
+ 'event-stream': 'Compromised package — cryptocurrency theft (2018)',
232
+ 'flatmap-stream': 'Malicious package — injected into event-stream',
233
+ 'ua-parser-js': 'Versions <0.7.31 — cryptocurrency miner injected',
234
+ 'colors': 'v1.4.1+ — intentional sabotage (infinite loop)',
235
+ 'faker': 'v6+ — intentional sabotage (random data)',
236
+ 'node-ipc': 'v10.1.1+ — intentional destructive code',
237
+ 'node-serialize': 'All versions — RCE via deserialization',
238
+ 'serialize-to-js': 'RCE via deserialization',
239
+ };
240
+ const pkgJson = files.get('package.json');
241
+ if (pkgJson) {
242
+ try {
243
+ const pkg = JSON.parse(pkgJson);
244
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
245
+ for (const [name, version] of Object.entries(allDeps)) {
246
+ if (dangerous[name]) {
247
+ findings.push({ message: `${name}: ${dangerous[name]}`, file: 'package.json' });
248
+ }
249
+ }
250
+ } catch {}
251
+ }
252
+ return findings;
253
+ },
254
+ },
255
+ ];
256
+
257
+ export default CHECKS;
@@ -0,0 +1,60 @@
1
+ import chalk from 'chalk';
2
+ import CHECKS from './simple-checks.js';
3
+
4
+ /**
5
+ * Run all simple checks and print clean output.
6
+ * Returns { passed, failed, issues }
7
+ */
8
+ export function runSimpleChecks(files) {
9
+ const results = [];
10
+
11
+ for (const check of CHECKS) {
12
+ const findings = check.check(files);
13
+ results.push({
14
+ ...check,
15
+ findings,
16
+ passed: findings.length === 0,
17
+ });
18
+ }
19
+
20
+ return results;
21
+ }
22
+
23
+ export function printSimpleReport(results) {
24
+ console.log('');
25
+ console.log(chalk.bold(' Doorman'));
26
+ console.log('');
27
+
28
+ let passCount = 0;
29
+ let failCount = 0;
30
+ const issues = [];
31
+
32
+ for (const r of results) {
33
+ if (r.passed) {
34
+ passCount++;
35
+ console.log(chalk.green(` ✓ ${r.name}`));
36
+ } else {
37
+ failCount++;
38
+ console.log(chalk.red(` ✗ ${r.name}`));
39
+ for (const f of r.findings.slice(0, 3)) {
40
+ const loc = f.file ? chalk.gray(` — ${f.file}${f.line ? ':' + f.line : ''}`) : '';
41
+ console.log(chalk.gray(` ${f.message}${loc}`));
42
+ issues.push(`${r.name}: ${f.message}${f.file ? ' in ' + f.file + (f.line ? ':' + f.line : '') : ''}`);
43
+ }
44
+ if (r.findings.length > 3) {
45
+ console.log(chalk.gray(` ...and ${r.findings.length - 3} more`));
46
+ }
47
+ }
48
+ }
49
+
50
+ console.log('');
51
+ if (failCount === 0) {
52
+ console.log(chalk.green.bold(' All clear. Ship it.'));
53
+ } else {
54
+ console.log(chalk.yellow(` ${failCount} issue${failCount === 1 ? '' : 's'} to fix before shipping.`));
55
+ console.log(chalk.gray(' Run `npx getdoorman fix` to generate a prompt for Claude/Codex.'));
56
+ }
57
+ console.log('');
58
+
59
+ return { passCount, failCount, issues };
60
+ }