getdoorman 1.1.1 → 1.2.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.
package/bin/doorman.js CHANGED
@@ -1,11 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { Command } from 'commander';
4
- import { check } from '../src/index.js';
5
- import { generateReport } from '../src/compliance.js';
6
3
  import { readFileSync } from 'fs';
7
4
  import { fileURLToPath } from 'url';
8
- import { dirname, join } from 'path';
5
+ import { dirname, join, resolve } from 'path';
6
+ import { Command } from 'commander';
9
7
 
10
8
  const __dirname = dirname(fileURLToPath(import.meta.url));
11
9
  const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
@@ -14,449 +12,52 @@ const program = new Command();
14
12
 
15
13
  program
16
14
  .name('getdoorman')
17
- .description('Find security issues. Tell your AI to fix them.')
15
+ .description('10 checks. Zero false positives. Ship with confidence.')
18
16
  .version(pkg.version);
19
17
 
20
18
  program
21
- .command('check')
22
- .alias('scan')
23
- .description('Scan your code for issues (always free)')
24
- .argument('[path]', 'Path to scan', '.')
25
- .option('--ci', 'CI mode — exit with code 1 if score below threshold')
26
- .option('--min-score <number>', 'Minimum score to pass in CI mode', '70')
19
+ .command('check', { isDefault: true })
20
+ .description('Check your code before shipping')
21
+ .argument('[path]', 'Path to check', '.')
27
22
  .option('--json', 'Output results as JSON')
28
- .option('--sarif [path]', 'Output SARIF 2.1.0 report (for GitHub Code Scanning, VS Code)')
29
- .option('--html [path]', 'Output standalone HTML report')
30
- .option('--category <categories>', 'Only run specific categories (comma-separated)')
31
- .option('--severity <level>', 'Minimum severity to report (critical,high,medium,low,info)', 'low')
32
- .option('--full', 'Force full scan (skip incremental)')
33
- .option('--no-cache', 'Disable file hash caching (force re-scan all files)')
34
- .option('--timeout <seconds>', 'Custom scan timeout in seconds', '60')
35
- .option('--verbose', 'Show all findings including suggestions')
36
- .option('--detail', 'Show all findings individually (disables smart summary)')
37
- .option('--strict', 'Show all severity levels including medium/low')
38
- .option('-q, --quiet', 'Only show one-line summary (no findings detail)')
39
- .option('--config <path>', 'Path to a custom config file')
40
- .option('--baseline [path]', 'Only show NEW findings (diff against baseline file)')
41
- .option('--save-baseline [path]', 'Save current findings as baseline for future diffs')
42
- .option('--profile', 'Show performance profile of slowest rules')
43
- .option('--share', 'Generate a shareable score card')
44
- .option('--deep', 'Run full 2,500+ rule scan (advanced)')
23
+ .option('--ci', 'Exit with code 1 if issues found')
45
24
  .action(async (path, options) => {
46
25
  try {
47
- // Default: simple 10-check mode. --deep for full scan.
48
- if (!options.deep && !options.json && !options.sarif && !options.html && !options.ci && !options.detail && !options.strict) {
49
- const { collectFiles } = await import('../src/scanner.js');
50
- const { runSimpleChecks, printSimpleReport } = await import('../src/simple-reporter.js');
51
- const files = await collectFiles(path, { silent: true });
52
- const results = runSimpleChecks(files);
26
+ const { collectFiles } = await import('../src/scanner.js');
27
+ const { runSimpleChecks, printSimpleReport } = await import('../src/simple-reporter.js');
28
+
29
+ const files = await collectFiles(resolve(path), { silent: true });
30
+ const results = runSimpleChecks(files);
31
+
32
+ if (options.json) {
33
+ const output = results.map(r => ({
34
+ check: r.name,
35
+ passed: r.passed,
36
+ issues: r.findings.map(f => ({ message: f.message, file: f.file, line: f.line })),
37
+ }));
38
+ console.log(JSON.stringify(output, null, 2));
39
+ } else {
53
40
  printSimpleReport(results);
41
+ }
54
42
 
55
- // Viral hooks on first scan
43
+ // On first scan: add doorman to AI tool configs
44
+ try {
56
45
  const { isFirstScan, installClaudeMd, installAgentsMd, installCursorRules, installClaudeHook } = await import('../src/hooks.js');
57
- const { resolve } = await import('path');
58
46
  const resolvedPath = resolve(path);
59
47
  if (isFirstScan(resolvedPath)) {
60
- const chalk = (await import('chalk')).default;
61
48
  installClaudeMd(resolvedPath);
62
49
  installAgentsMd(resolvedPath);
63
50
  installCursorRules(resolvedPath);
64
51
  installClaudeHook(resolvedPath);
65
52
  }
53
+ } catch {}
66
54
 
67
- const failCount = results.filter(r => !r.passed).length;
68
- process.exit(failCount > 0 ? 1 : 0);
69
- return;
70
- }
71
-
72
- // Deep mode: full 2,500+ rule scan
73
- const result = await check(path, options);
74
-
75
- // Generate shareable score card
76
- if (options.share && result) {
77
- const { generateScoreCard } = await import('../src/share.js');
78
- const cardPath = generateScoreCard(path, result);
79
- const chalk = (await import('chalk')).default;
80
- console.log('');
81
- console.log(chalk.bold.green(' Score card saved!'));
82
- console.log(chalk.gray(` Open: ${cardPath}`));
83
- console.log(chalk.gray(' Share it on Twitter, Discord, or Slack.'));
84
- console.log('');
85
- }
86
-
87
- // After first scan: add CLAUDE.md, offer hooks, collect email
88
- if (!options.ci && !options.quiet && !options.json && !options.sarif && !options.html && !options.silent) {
89
- const { isFirstScan, installClaudeMd, installAgentsMd, installCursorRules, installClaudeHook } = await import('../src/hooks.js');
90
- const { loadAuth, saveAuth } = await import('../src/auth.js');
91
- const { resolve } = await import('path');
92
- const resolvedPath = resolve(path);
93
-
94
- if (isFirstScan(resolvedPath)) {
95
- const chalk = (await import('chalk')).default;
96
-
97
- // Add Doorman instructions to AI config files (only appends, never overwrites)
98
- const claudeMd = installClaudeMd(resolvedPath);
99
- const agentsMd = installAgentsMd(resolvedPath);
100
- const cursorRules = installCursorRules(resolvedPath);
101
- if (claudeMd) console.log(chalk.dim(' Added Doorman to CLAUDE.md'));
102
- if (agentsMd) console.log(chalk.dim(' Added Doorman to AGENTS.md'));
103
- if (cursorRules) console.log(chalk.dim(' Added Doorman to .cursorrules'));
104
-
105
- // Auto-run hook for Claude Code (non-blocking, just a scan)
106
- const hookInstalled = installClaudeHook(resolvedPath);
107
- if (hookInstalled) console.log(chalk.dim(' Auto-run enabled for Claude Code'));
108
-
109
- // NOTE: No pre-commit hook on first scan — too aggressive.
110
- // Users can opt in with: npx getdoorman init
111
-
112
- // Collect email
113
- const auth = loadAuth();
114
- if (!auth?.email) {
115
- const readline = await import('readline');
116
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
117
- console.log('');
118
- const email = await new Promise(r => {
119
- rl.question(chalk.green(' Enter your email for updates (optional): '), a => { rl.close(); r(a.trim()); });
120
- });
121
- if (email && email.includes('@')) {
122
- saveAuth(email);
123
- console.log(chalk.green(' ✓ Saved!'));
124
- }
125
- }
126
- console.log('');
127
- }
128
- }
129
-
130
- process.exit(result.exitCode || 0);
131
- } catch (err) {
132
- console.error('Error:', err.message);
133
- process.exit(2);
134
- }
135
- });
136
-
137
- program
138
- .command('fix')
139
- .description('Tell your AI to fix issues — works in Claude, Codex, Cursor')
140
- .argument('[path]', 'Path to scan', '.')
141
- .action(async (path) => {
142
- const chalk = (await import('chalk')).default;
143
- const { collectFiles } = await import('../src/scanner.js');
144
- const { runSimpleChecks } = await import('../src/simple-reporter.js');
145
-
146
- const files = await collectFiles(path, { silent: true });
147
- const results = runSimpleChecks(files);
148
- const failed = results.filter(r => !r.passed);
149
-
150
- if (failed.length === 0) {
151
- console.log('');
152
- console.log(chalk.green.bold(' All clear. Nothing to fix.'));
153
- console.log('');
154
- return;
155
- }
156
-
157
- // Build the prompt
158
- const issues = [];
159
- for (const r of failed) {
160
- for (const f of r.findings.slice(0, 5)) {
161
- const loc = f.file ? ` in ${f.file}${f.line ? ':' + f.line : ''}` : '';
162
- issues.push(`- ${r.name}: ${f.message}${loc}`);
163
- }
164
- }
165
- const prompt = `Fix these issues in my code:\n\n${issues.join('\n')}`;
166
-
167
- // Detect environment: AI tool or manual terminal
168
- const isAI = process.env.CLAUDE_CODE || process.env.CURSOR_SESSION || process.env.CODEX_SANDBOX || process.env.TERM_PROGRAM === 'claude';
169
- const isCI = process.env.CI || process.env.GITHUB_ACTIONS;
170
-
171
- if (isAI) {
172
- // Inside an AI tool — just output the prompt directly, the AI will act on it
173
- console.log(prompt);
174
- } else if (isCI) {
175
- // CI — just list issues
176
- console.log(prompt);
177
- } else {
178
- // Manual terminal — show formatted + copy to clipboard
179
- console.log('');
180
- console.log(chalk.bold(' Paste this into Claude, Codex, or Cursor:'));
181
- console.log('');
182
- console.log(chalk.cyan(' ┌───────────────────────────────────────────────'));
183
- for (const line of prompt.split('\n')) {
184
- console.log(chalk.cyan(' │ ') + line);
185
- }
186
- console.log(chalk.cyan(' └───────────────────────────────────────────────'));
187
- console.log('');
188
-
189
- // Copy to clipboard
190
- try {
191
- const { execSync } = await import('child_process');
192
- const cmd = process.platform === 'darwin' ? 'pbcopy' : process.platform === 'win32' ? 'clip' : 'xclip -selection clipboard';
193
- execSync(cmd, { input: prompt, stdio: ['pipe', 'ignore', 'ignore'] });
194
- console.log(chalk.green(' ✓ Copied to clipboard! Just paste it.'));
195
- } catch {
196
- console.log(chalk.dim(' Tip: copy the text above and paste into your AI tool'));
197
- }
198
- console.log('');
199
- }
200
- });
201
-
202
- program
203
- .command('review')
204
- .description('Review only changed files (for git hooks and PRs)')
205
- .argument('[path]', 'Path to scan', '.')
206
- .option('--json', 'Output as JSON')
207
- .option('--min-score <number>', 'Minimum score', '70')
208
- .action(async (path, options) => {
209
- try {
210
- const result = await check(path, { ...options, incremental: true });
211
- process.exit(result.exitCode || 0);
55
+ const failCount = results.filter(r => !r.passed).length;
56
+ process.exit(options.ci && failCount > 0 ? 1 : 0);
212
57
  } catch (err) {
213
58
  console.error('Error:', err.message);
214
59
  process.exit(2);
215
60
  }
216
61
  });
217
62
 
218
- program
219
- .command('ignore')
220
- .description('Ignore a rule (mark as false positive)')
221
- .argument('<ruleId>', 'Rule ID to ignore (e.g., SEC-INJ-001)')
222
- .option('--file <path>', 'Only ignore in this file')
223
- .option('--reason <reason>', 'Reason for ignoring')
224
- .action(async (ruleId, options) => {
225
- const { addIgnore } = await import('../src/config.js');
226
- addIgnore('.', ruleId, options.file || null, options.reason || 'User dismissed');
227
- console.log(`Ignored ${ruleId}${options.file ? ' in ' + options.file : ''}`);
228
- });
229
-
230
- program
231
- .command('init')
232
- .description('Initialize Doorman: create config, hooks, and auto-run')
233
- .option('--no-hook', 'Skip pre-commit hook installation')
234
- .option('--claude', 'Install Claude Code auto-run hook')
235
- .action(async (options) => {
236
- const { writeFileSync, existsSync, mkdirSync } = await import('fs');
237
- const { installClaudeHook, installGitHook, installClaudeMd } = await import('../src/hooks.js');
238
- const { resolve } = await import('path');
239
-
240
- // 1. Config file — create .doormanrc with recommended preset
241
- if (existsSync('.doormanrc') || existsSync('.doormanrc.json')) {
242
- const which = existsSync('.doormanrc') ? '.doormanrc' : '.doormanrc.json';
243
- console.log(` ✓ Config already exists at ${which}`);
244
- } else {
245
- const config = {
246
- extends: 'recommended',
247
- rules: {},
248
- categories: [],
249
- severity: 'medium',
250
- ignore: ['test/**', 'vendor/**', '*.min.js'],
251
- };
252
- writeFileSync('.doormanrc', JSON.stringify(config, null, 2) + '\n');
253
- console.log(' ✓ Created .doormanrc (recommended preset)');
254
- }
255
-
256
- // 2. Pre-commit hook
257
- if (options.hook !== false) {
258
- const installed = installGitHook(resolve('.'));
259
- if (installed) {
260
- console.log(' ✓ Installed pre-commit hook — critical issues will block commits');
261
- } else if (!existsSync('.git')) {
262
- console.log(' ⚠ Not a git repository — skipping pre-commit hook');
263
- } else {
264
- console.log(' ✓ Pre-commit hook already installed');
265
- }
266
- }
267
-
268
- // 3. Claude Code hook
269
- if (options.claude) {
270
- const installed = installClaudeHook(resolve('.'));
271
- if (installed) {
272
- console.log(' ✓ Claude Code hook installed — Doorman runs after every file edit');
273
- } else {
274
- console.log(' ✓ Claude Code hook already installed');
275
- }
276
- }
277
-
278
- // 4. CLAUDE.md — teach Claude to use Doorman
279
- const mdInstalled = installClaudeMd(resolve('.'));
280
- if (mdInstalled) {
281
- console.log(' ✓ Added Doorman to CLAUDE.md — Claude will suggest it automatically');
282
- } else {
283
- console.log(' ✓ CLAUDE.md already mentions Doorman');
284
- }
285
-
286
- console.log('');
287
- console.log('Doorman is ready. Run `npx getdoorman check` to scan your codebase.');
288
- if (!options.claude) {
289
- console.log('Tip: Run `npx getdoorman init --claude` to auto-scan when Claude writes code.');
290
- }
291
- });
292
-
293
- program
294
- .command('dashboard')
295
- .description('Open your project dashboard in the browser')
296
- .argument('[path]', 'Project path', '.')
297
- .action(async (path) => {
298
- const chalk = (await import('chalk')).default;
299
- const { openDashboard } = await import('../src/dashboard.js');
300
- const result = openDashboard(path);
301
- if (result.error) {
302
- console.log('');
303
- console.log(chalk.yellow(` ${result.error}`));
304
- console.log('');
305
- } else {
306
- console.log('');
307
- console.log(chalk.green(` ✓ Dashboard opened: ${result.path}`));
308
- console.log('');
309
- }
310
- });
311
-
312
- program
313
- .command('benchmark')
314
- .description('Run the real-world vulnerability detection benchmark')
315
- .action(async () => {
316
- console.log('Running Doorman benchmark...');
317
- const { execSync } = await import('child_process');
318
- try {
319
- execSync('node --test test/benchmark-real-world.test.js', { stdio: 'inherit' });
320
- } catch (e) {
321
- // Test runner reports results
322
- }
323
- });
324
-
325
- program
326
- .command('hook')
327
- .description('Install Doorman as a pre-commit git hook')
328
- .option('--remove', 'Remove the git hook')
329
- .action(async (options) => {
330
- const { writeFileSync, unlinkSync, existsSync, mkdirSync } = await import('fs');
331
- const hookPath = '.git/hooks/pre-commit';
332
-
333
- if (options.remove) {
334
- if (existsSync(hookPath)) {
335
- unlinkSync(hookPath);
336
- console.log('Removed Doorman pre-commit hook');
337
- }
338
- return;
339
- }
340
-
341
- if (!existsSync('.git/hooks')) mkdirSync('.git/hooks', { recursive: true });
342
-
343
- const hookScript = `#!/bin/sh
344
- # Doorman pre-commit hook
345
- npx getdoorman review --min-score 70
346
- `;
347
- writeFileSync(hookPath, hookScript, { mode: 0o755 });
348
- console.log('Installed Doorman pre-commit hook');
349
- console.log('Every commit will be checked. Remove with: doorman hook --remove');
350
- });
351
-
352
- program
353
- .command('credits')
354
- .description('Check your credit balance or buy more')
355
- .option('--buy', 'Open the credit purchase page')
356
- .action(async (options) => {
357
- const chalk = (await import('chalk')).default;
358
- const { loadAuth, checkCredits, getCreditPacks } = await import('../src/auth.js');
359
-
360
- const auth = loadAuth();
361
-
362
- if (!auth || !auth.email) {
363
- console.log('');
364
- console.log(chalk.dim(' No account yet. Run `npx getdoorman fix` to get 3 free credits.'));
365
- console.log('');
366
- return;
367
- }
368
-
369
- const credits = await checkCredits(auth.email);
370
- const packs = getCreditPacks(auth.email);
371
-
372
- console.log('');
373
- console.log(chalk.bold(' Doorman Account'));
374
- console.log(` Email: ${auth.email}`);
375
- console.log(` Credits: ${credits ? credits.credits : 'unknown (offline)'}`);
376
- console.log('');
377
-
378
- if (options.buy || (credits && credits.credits <= 0)) {
379
- console.log(chalk.bold(' Buy credits:'));
380
- console.log('');
381
- for (const pack of packs) {
382
- const bonus = pack.bonus ? chalk.green(` (${pack.bonus})`) : '';
383
- console.log(` ${String(pack.credits).padStart(3)} credits — ${pack.price}${bonus} ${chalk.cyan(pack.buyUrl)}`);
384
- }
385
- console.log('');
386
- // Try to open browser
387
- try {
388
- const { execSync } = await import('child_process');
389
- const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
390
- execSync(`${cmd} ${packs[0].buyUrl}`, { stdio: 'ignore' });
391
- console.log(chalk.dim(' Opened in your browser.'));
392
- } catch { /* ignore */ }
393
- }
394
- console.log('');
395
- });
396
-
397
- // ai-fix is now merged into `fix` — kept as hidden alias for backward compat
398
- program
399
- .command('ai-fix')
400
- .description(false) // hidden
401
- .argument('[path]', 'Path to scan', '.')
402
- .action(async () => {
403
- console.log('ai-fix has been merged into `fix`. Just run: npx getdoorman fix');
404
- });
405
-
406
- program
407
- .command('report')
408
- .description('Generate a compliance report (SOC2, GDPR, HIPAA, PCI-DSS)')
409
- .argument('[path]', 'Path to scan', '.')
410
- .option('--framework <name>', 'Framework: SOC2, GDPR, HIPAA, PCI, or all', 'all')
411
- .option('--output <path>', 'Output file path', 'doorman-report.html')
412
- .option('--project <name>', 'Project name for the report')
413
- .action(async (path, options) => {
414
- const chalk = (await import('chalk')).default;
415
- const { basename, resolve } = await import('path');
416
-
417
- const framework = options.framework.toLowerCase();
418
- const validFrameworks = ['soc2', 'gdpr', 'hipaa', 'pci', 'all'];
419
- if (!validFrameworks.includes(framework)) {
420
- console.error(`Error: Unknown framework "${options.framework}". Choose from: SOC2, GDPR, HIPAA, PCI, or all`);
421
- process.exit(1);
422
- }
423
-
424
- const projectName = options.project || basename(resolve(path));
425
-
426
- console.log('');
427
- console.log(chalk.bold.cyan(' Doorman — Compliance Report Generator'));
428
- console.log('');
429
-
430
- // Step 1: Run a full scan
431
- console.log(chalk.gray(' Running full scan...'));
432
- let result;
433
- try {
434
- result = await check(path, { silent: true, full: true });
435
- } catch (err) {
436
- console.error(chalk.red(` Scan failed: ${err.message}`));
437
- process.exit(1);
438
- }
439
-
440
- console.log(chalk.gray(` Found ${result.findings.length} finding(s), score: ${result.score}/100`));
441
- console.log('');
442
-
443
- // Step 2: Generate the compliance report
444
- const { summary } = generateReport(
445
- { findings: result.findings, score: result.score, stack: result.stack },
446
- { framework, projectName, outputPath: options.output }
447
- );
448
-
449
- // Step 3: Print summary
450
- const frameworkEntries = Object.entries(summary.frameworks);
451
- for (const [, fw] of frameworkEntries) {
452
- const statusColor = fw.status === 'compliant' ? chalk.green : fw.status === 'partial' ? chalk.yellow : chalk.red;
453
- console.log(` ${fw.name.padEnd(22)} ${statusColor(fw.status.toUpperCase().padEnd(15))} ${fw.score}%`);
454
- }
455
-
456
- console.log('');
457
- console.log(chalk.bold.green(` Report saved to ${options.output}`));
458
- console.log(chalk.gray(' Open in a browser to view the full compliance report.'));
459
- console.log('');
460
- });
461
-
462
63
  program.parse();
package/bin/getdoorman.js CHANGED
@@ -1,11 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { Command } from 'commander';
4
- import { check } from '../src/index.js';
5
- import { generateReport } from '../src/compliance.js';
6
3
  import { readFileSync } from 'fs';
7
4
  import { fileURLToPath } from 'url';
8
- import { dirname, join } from 'path';
5
+ import { dirname, join, resolve } from 'path';
6
+ import { Command } from 'commander';
9
7
 
10
8
  const __dirname = dirname(fileURLToPath(import.meta.url));
11
9
  const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
@@ -14,449 +12,52 @@ const program = new Command();
14
12
 
15
13
  program
16
14
  .name('getdoorman')
17
- .description('Find security issues. Tell your AI to fix them.')
15
+ .description('10 checks. Zero false positives. Ship with confidence.')
18
16
  .version(pkg.version);
19
17
 
20
18
  program
21
- .command('check')
22
- .alias('scan')
23
- .description('Scan your code for issues (always free)')
24
- .argument('[path]', 'Path to scan', '.')
25
- .option('--ci', 'CI mode — exit with code 1 if score below threshold')
26
- .option('--min-score <number>', 'Minimum score to pass in CI mode', '70')
19
+ .command('check', { isDefault: true })
20
+ .description('Check your code before shipping')
21
+ .argument('[path]', 'Path to check', '.')
27
22
  .option('--json', 'Output results as JSON')
28
- .option('--sarif [path]', 'Output SARIF 2.1.0 report (for GitHub Code Scanning, VS Code)')
29
- .option('--html [path]', 'Output standalone HTML report')
30
- .option('--category <categories>', 'Only run specific categories (comma-separated)')
31
- .option('--severity <level>', 'Minimum severity to report (critical,high,medium,low,info)', 'low')
32
- .option('--full', 'Force full scan (skip incremental)')
33
- .option('--no-cache', 'Disable file hash caching (force re-scan all files)')
34
- .option('--timeout <seconds>', 'Custom scan timeout in seconds', '60')
35
- .option('--verbose', 'Show all findings including suggestions')
36
- .option('--detail', 'Show all findings individually (disables smart summary)')
37
- .option('--strict', 'Show all severity levels including medium/low')
38
- .option('-q, --quiet', 'Only show one-line summary (no findings detail)')
39
- .option('--config <path>', 'Path to a custom config file')
40
- .option('--baseline [path]', 'Only show NEW findings (diff against baseline file)')
41
- .option('--save-baseline [path]', 'Save current findings as baseline for future diffs')
42
- .option('--profile', 'Show performance profile of slowest rules')
43
- .option('--share', 'Generate a shareable score card')
44
- .option('--deep', 'Run full 2,500+ rule scan (advanced)')
23
+ .option('--ci', 'Exit with code 1 if issues found')
45
24
  .action(async (path, options) => {
46
25
  try {
47
- // Default: simple 10-check mode. --deep for full scan.
48
- if (!options.deep && !options.json && !options.sarif && !options.html && !options.ci && !options.detail && !options.strict) {
49
- const { collectFiles } = await import('../src/scanner.js');
50
- const { runSimpleChecks, printSimpleReport } = await import('../src/simple-reporter.js');
51
- const files = await collectFiles(path, { silent: true });
52
- const results = runSimpleChecks(files);
26
+ const { collectFiles } = await import('../src/scanner.js');
27
+ const { runSimpleChecks, printSimpleReport } = await import('../src/simple-reporter.js');
28
+
29
+ const files = await collectFiles(resolve(path), { silent: true });
30
+ const results = runSimpleChecks(files);
31
+
32
+ if (options.json) {
33
+ const output = results.map(r => ({
34
+ check: r.name,
35
+ passed: r.passed,
36
+ issues: r.findings.map(f => ({ message: f.message, file: f.file, line: f.line })),
37
+ }));
38
+ console.log(JSON.stringify(output, null, 2));
39
+ } else {
53
40
  printSimpleReport(results);
41
+ }
54
42
 
55
- // Viral hooks on first scan
43
+ // On first scan: add doorman to AI tool configs
44
+ try {
56
45
  const { isFirstScan, installClaudeMd, installAgentsMd, installCursorRules, installClaudeHook } = await import('../src/hooks.js');
57
- const { resolve } = await import('path');
58
46
  const resolvedPath = resolve(path);
59
47
  if (isFirstScan(resolvedPath)) {
60
- const chalk = (await import('chalk')).default;
61
48
  installClaudeMd(resolvedPath);
62
49
  installAgentsMd(resolvedPath);
63
50
  installCursorRules(resolvedPath);
64
51
  installClaudeHook(resolvedPath);
65
52
  }
53
+ } catch {}
66
54
 
67
- const failCount = results.filter(r => !r.passed).length;
68
- process.exit(failCount > 0 ? 1 : 0);
69
- return;
70
- }
71
-
72
- // Deep mode: full 2,500+ rule scan
73
- const result = await check(path, options);
74
-
75
- // Generate shareable score card
76
- if (options.share && result) {
77
- const { generateScoreCard } = await import('../src/share.js');
78
- const cardPath = generateScoreCard(path, result);
79
- const chalk = (await import('chalk')).default;
80
- console.log('');
81
- console.log(chalk.bold.green(' Score card saved!'));
82
- console.log(chalk.gray(` Open: ${cardPath}`));
83
- console.log(chalk.gray(' Share it on Twitter, Discord, or Slack.'));
84
- console.log('');
85
- }
86
-
87
- // After first scan: add CLAUDE.md, offer hooks, collect email
88
- if (!options.ci && !options.quiet && !options.json && !options.sarif && !options.html && !options.silent) {
89
- const { isFirstScan, installClaudeMd, installAgentsMd, installCursorRules, installClaudeHook } = await import('../src/hooks.js');
90
- const { loadAuth, saveAuth } = await import('../src/auth.js');
91
- const { resolve } = await import('path');
92
- const resolvedPath = resolve(path);
93
-
94
- if (isFirstScan(resolvedPath)) {
95
- const chalk = (await import('chalk')).default;
96
-
97
- // Add Doorman instructions to AI config files (only appends, never overwrites)
98
- const claudeMd = installClaudeMd(resolvedPath);
99
- const agentsMd = installAgentsMd(resolvedPath);
100
- const cursorRules = installCursorRules(resolvedPath);
101
- if (claudeMd) console.log(chalk.dim(' Added Doorman to CLAUDE.md'));
102
- if (agentsMd) console.log(chalk.dim(' Added Doorman to AGENTS.md'));
103
- if (cursorRules) console.log(chalk.dim(' Added Doorman to .cursorrules'));
104
-
105
- // Auto-run hook for Claude Code (non-blocking, just a scan)
106
- const hookInstalled = installClaudeHook(resolvedPath);
107
- if (hookInstalled) console.log(chalk.dim(' Auto-run enabled for Claude Code'));
108
-
109
- // NOTE: No pre-commit hook on first scan — too aggressive.
110
- // Users can opt in with: npx getdoorman init
111
-
112
- // Collect email
113
- const auth = loadAuth();
114
- if (!auth?.email) {
115
- const readline = await import('readline');
116
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
117
- console.log('');
118
- const email = await new Promise(r => {
119
- rl.question(chalk.green(' Enter your email for updates (optional): '), a => { rl.close(); r(a.trim()); });
120
- });
121
- if (email && email.includes('@')) {
122
- saveAuth(email);
123
- console.log(chalk.green(' ✓ Saved!'));
124
- }
125
- }
126
- console.log('');
127
- }
128
- }
129
-
130
- process.exit(result.exitCode || 0);
131
- } catch (err) {
132
- console.error('Error:', err.message);
133
- process.exit(2);
134
- }
135
- });
136
-
137
- program
138
- .command('fix')
139
- .description('Tell your AI to fix issues — works in Claude, Codex, Cursor')
140
- .argument('[path]', 'Path to scan', '.')
141
- .action(async (path) => {
142
- const chalk = (await import('chalk')).default;
143
- const { collectFiles } = await import('../src/scanner.js');
144
- const { runSimpleChecks } = await import('../src/simple-reporter.js');
145
-
146
- const files = await collectFiles(path, { silent: true });
147
- const results = runSimpleChecks(files);
148
- const failed = results.filter(r => !r.passed);
149
-
150
- if (failed.length === 0) {
151
- console.log('');
152
- console.log(chalk.green.bold(' All clear. Nothing to fix.'));
153
- console.log('');
154
- return;
155
- }
156
-
157
- // Build the prompt
158
- const issues = [];
159
- for (const r of failed) {
160
- for (const f of r.findings.slice(0, 5)) {
161
- const loc = f.file ? ` in ${f.file}${f.line ? ':' + f.line : ''}` : '';
162
- issues.push(`- ${r.name}: ${f.message}${loc}`);
163
- }
164
- }
165
- const prompt = `Fix these issues in my code:\n\n${issues.join('\n')}`;
166
-
167
- // Detect environment: AI tool or manual terminal
168
- const isAI = process.env.CLAUDE_CODE || process.env.CURSOR_SESSION || process.env.CODEX_SANDBOX || process.env.TERM_PROGRAM === 'claude';
169
- const isCI = process.env.CI || process.env.GITHUB_ACTIONS;
170
-
171
- if (isAI) {
172
- // Inside an AI tool — just output the prompt directly, the AI will act on it
173
- console.log(prompt);
174
- } else if (isCI) {
175
- // CI — just list issues
176
- console.log(prompt);
177
- } else {
178
- // Manual terminal — show formatted + copy to clipboard
179
- console.log('');
180
- console.log(chalk.bold(' Paste this into Claude, Codex, or Cursor:'));
181
- console.log('');
182
- console.log(chalk.cyan(' ┌───────────────────────────────────────────────'));
183
- for (const line of prompt.split('\n')) {
184
- console.log(chalk.cyan(' │ ') + line);
185
- }
186
- console.log(chalk.cyan(' └───────────────────────────────────────────────'));
187
- console.log('');
188
-
189
- // Copy to clipboard
190
- try {
191
- const { execSync } = await import('child_process');
192
- const cmd = process.platform === 'darwin' ? 'pbcopy' : process.platform === 'win32' ? 'clip' : 'xclip -selection clipboard';
193
- execSync(cmd, { input: prompt, stdio: ['pipe', 'ignore', 'ignore'] });
194
- console.log(chalk.green(' ✓ Copied to clipboard! Just paste it.'));
195
- } catch {
196
- console.log(chalk.dim(' Tip: copy the text above and paste into your AI tool'));
197
- }
198
- console.log('');
199
- }
200
- });
201
-
202
- program
203
- .command('review')
204
- .description('Review only changed files (for git hooks and PRs)')
205
- .argument('[path]', 'Path to scan', '.')
206
- .option('--json', 'Output as JSON')
207
- .option('--min-score <number>', 'Minimum score', '70')
208
- .action(async (path, options) => {
209
- try {
210
- const result = await check(path, { ...options, incremental: true });
211
- process.exit(result.exitCode || 0);
55
+ const failCount = results.filter(r => !r.passed).length;
56
+ process.exit(options.ci && failCount > 0 ? 1 : 0);
212
57
  } catch (err) {
213
58
  console.error('Error:', err.message);
214
59
  process.exit(2);
215
60
  }
216
61
  });
217
62
 
218
- program
219
- .command('ignore')
220
- .description('Ignore a rule (mark as false positive)')
221
- .argument('<ruleId>', 'Rule ID to ignore (e.g., SEC-INJ-001)')
222
- .option('--file <path>', 'Only ignore in this file')
223
- .option('--reason <reason>', 'Reason for ignoring')
224
- .action(async (ruleId, options) => {
225
- const { addIgnore } = await import('../src/config.js');
226
- addIgnore('.', ruleId, options.file || null, options.reason || 'User dismissed');
227
- console.log(`Ignored ${ruleId}${options.file ? ' in ' + options.file : ''}`);
228
- });
229
-
230
- program
231
- .command('init')
232
- .description('Initialize Doorman: create config, hooks, and auto-run')
233
- .option('--no-hook', 'Skip pre-commit hook installation')
234
- .option('--claude', 'Install Claude Code auto-run hook')
235
- .action(async (options) => {
236
- const { writeFileSync, existsSync, mkdirSync } = await import('fs');
237
- const { installClaudeHook, installGitHook, installClaudeMd } = await import('../src/hooks.js');
238
- const { resolve } = await import('path');
239
-
240
- // 1. Config file — create .doormanrc with recommended preset
241
- if (existsSync('.doormanrc') || existsSync('.doormanrc.json')) {
242
- const which = existsSync('.doormanrc') ? '.doormanrc' : '.doormanrc.json';
243
- console.log(` ✓ Config already exists at ${which}`);
244
- } else {
245
- const config = {
246
- extends: 'recommended',
247
- rules: {},
248
- categories: [],
249
- severity: 'medium',
250
- ignore: ['test/**', 'vendor/**', '*.min.js'],
251
- };
252
- writeFileSync('.doormanrc', JSON.stringify(config, null, 2) + '\n');
253
- console.log(' ✓ Created .doormanrc (recommended preset)');
254
- }
255
-
256
- // 2. Pre-commit hook
257
- if (options.hook !== false) {
258
- const installed = installGitHook(resolve('.'));
259
- if (installed) {
260
- console.log(' ✓ Installed pre-commit hook — critical issues will block commits');
261
- } else if (!existsSync('.git')) {
262
- console.log(' ⚠ Not a git repository — skipping pre-commit hook');
263
- } else {
264
- console.log(' ✓ Pre-commit hook already installed');
265
- }
266
- }
267
-
268
- // 3. Claude Code hook
269
- if (options.claude) {
270
- const installed = installClaudeHook(resolve('.'));
271
- if (installed) {
272
- console.log(' ✓ Claude Code hook installed — Doorman runs after every file edit');
273
- } else {
274
- console.log(' ✓ Claude Code hook already installed');
275
- }
276
- }
277
-
278
- // 4. CLAUDE.md — teach Claude to use Doorman
279
- const mdInstalled = installClaudeMd(resolve('.'));
280
- if (mdInstalled) {
281
- console.log(' ✓ Added Doorman to CLAUDE.md — Claude will suggest it automatically');
282
- } else {
283
- console.log(' ✓ CLAUDE.md already mentions Doorman');
284
- }
285
-
286
- console.log('');
287
- console.log('Doorman is ready. Run `npx getdoorman check` to scan your codebase.');
288
- if (!options.claude) {
289
- console.log('Tip: Run `npx getdoorman init --claude` to auto-scan when Claude writes code.');
290
- }
291
- });
292
-
293
- program
294
- .command('dashboard')
295
- .description('Open your project dashboard in the browser')
296
- .argument('[path]', 'Project path', '.')
297
- .action(async (path) => {
298
- const chalk = (await import('chalk')).default;
299
- const { openDashboard } = await import('../src/dashboard.js');
300
- const result = openDashboard(path);
301
- if (result.error) {
302
- console.log('');
303
- console.log(chalk.yellow(` ${result.error}`));
304
- console.log('');
305
- } else {
306
- console.log('');
307
- console.log(chalk.green(` ✓ Dashboard opened: ${result.path}`));
308
- console.log('');
309
- }
310
- });
311
-
312
- program
313
- .command('benchmark')
314
- .description('Run the real-world vulnerability detection benchmark')
315
- .action(async () => {
316
- console.log('Running Doorman benchmark...');
317
- const { execSync } = await import('child_process');
318
- try {
319
- execSync('node --test test/benchmark-real-world.test.js', { stdio: 'inherit' });
320
- } catch (e) {
321
- // Test runner reports results
322
- }
323
- });
324
-
325
- program
326
- .command('hook')
327
- .description('Install Doorman as a pre-commit git hook')
328
- .option('--remove', 'Remove the git hook')
329
- .action(async (options) => {
330
- const { writeFileSync, unlinkSync, existsSync, mkdirSync } = await import('fs');
331
- const hookPath = '.git/hooks/pre-commit';
332
-
333
- if (options.remove) {
334
- if (existsSync(hookPath)) {
335
- unlinkSync(hookPath);
336
- console.log('Removed Doorman pre-commit hook');
337
- }
338
- return;
339
- }
340
-
341
- if (!existsSync('.git/hooks')) mkdirSync('.git/hooks', { recursive: true });
342
-
343
- const hookScript = `#!/bin/sh
344
- # Doorman pre-commit hook
345
- npx getdoorman review --min-score 70
346
- `;
347
- writeFileSync(hookPath, hookScript, { mode: 0o755 });
348
- console.log('Installed Doorman pre-commit hook');
349
- console.log('Every commit will be checked. Remove with: doorman hook --remove');
350
- });
351
-
352
- program
353
- .command('credits')
354
- .description('Check your credit balance or buy more')
355
- .option('--buy', 'Open the credit purchase page')
356
- .action(async (options) => {
357
- const chalk = (await import('chalk')).default;
358
- const { loadAuth, checkCredits, getCreditPacks } = await import('../src/auth.js');
359
-
360
- const auth = loadAuth();
361
-
362
- if (!auth || !auth.email) {
363
- console.log('');
364
- console.log(chalk.dim(' No account yet. Run `npx getdoorman fix` to get 3 free credits.'));
365
- console.log('');
366
- return;
367
- }
368
-
369
- const credits = await checkCredits(auth.email);
370
- const packs = getCreditPacks(auth.email);
371
-
372
- console.log('');
373
- console.log(chalk.bold(' Doorman Account'));
374
- console.log(` Email: ${auth.email}`);
375
- console.log(` Credits: ${credits ? credits.credits : 'unknown (offline)'}`);
376
- console.log('');
377
-
378
- if (options.buy || (credits && credits.credits <= 0)) {
379
- console.log(chalk.bold(' Buy credits:'));
380
- console.log('');
381
- for (const pack of packs) {
382
- const bonus = pack.bonus ? chalk.green(` (${pack.bonus})`) : '';
383
- console.log(` ${String(pack.credits).padStart(3)} credits — ${pack.price}${bonus} ${chalk.cyan(pack.buyUrl)}`);
384
- }
385
- console.log('');
386
- // Try to open browser
387
- try {
388
- const { execSync } = await import('child_process');
389
- const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
390
- execSync(`${cmd} ${packs[0].buyUrl}`, { stdio: 'ignore' });
391
- console.log(chalk.dim(' Opened in your browser.'));
392
- } catch { /* ignore */ }
393
- }
394
- console.log('');
395
- });
396
-
397
- // ai-fix is now merged into `fix` — kept as hidden alias for backward compat
398
- program
399
- .command('ai-fix')
400
- .description(false) // hidden
401
- .argument('[path]', 'Path to scan', '.')
402
- .action(async () => {
403
- console.log('ai-fix has been merged into `fix`. Just run: npx getdoorman fix');
404
- });
405
-
406
- program
407
- .command('report')
408
- .description('Generate a compliance report (SOC2, GDPR, HIPAA, PCI-DSS)')
409
- .argument('[path]', 'Path to scan', '.')
410
- .option('--framework <name>', 'Framework: SOC2, GDPR, HIPAA, PCI, or all', 'all')
411
- .option('--output <path>', 'Output file path', 'doorman-report.html')
412
- .option('--project <name>', 'Project name for the report')
413
- .action(async (path, options) => {
414
- const chalk = (await import('chalk')).default;
415
- const { basename, resolve } = await import('path');
416
-
417
- const framework = options.framework.toLowerCase();
418
- const validFrameworks = ['soc2', 'gdpr', 'hipaa', 'pci', 'all'];
419
- if (!validFrameworks.includes(framework)) {
420
- console.error(`Error: Unknown framework "${options.framework}". Choose from: SOC2, GDPR, HIPAA, PCI, or all`);
421
- process.exit(1);
422
- }
423
-
424
- const projectName = options.project || basename(resolve(path));
425
-
426
- console.log('');
427
- console.log(chalk.bold.cyan(' Doorman — Compliance Report Generator'));
428
- console.log('');
429
-
430
- // Step 1: Run a full scan
431
- console.log(chalk.gray(' Running full scan...'));
432
- let result;
433
- try {
434
- result = await check(path, { silent: true, full: true });
435
- } catch (err) {
436
- console.error(chalk.red(` Scan failed: ${err.message}`));
437
- process.exit(1);
438
- }
439
-
440
- console.log(chalk.gray(` Found ${result.findings.length} finding(s), score: ${result.score}/100`));
441
- console.log('');
442
-
443
- // Step 2: Generate the compliance report
444
- const { summary } = generateReport(
445
- { findings: result.findings, score: result.score, stack: result.stack },
446
- { framework, projectName, outputPath: options.output }
447
- );
448
-
449
- // Step 3: Print summary
450
- const frameworkEntries = Object.entries(summary.frameworks);
451
- for (const [, fw] of frameworkEntries) {
452
- const statusColor = fw.status === 'compliant' ? chalk.green : fw.status === 'partial' ? chalk.yellow : chalk.red;
453
- console.log(` ${fw.name.padEnd(22)} ${statusColor(fw.status.toUpperCase().padEnd(15))} ${fw.score}%`);
454
- }
455
-
456
- console.log('');
457
- console.log(chalk.bold.green(` Report saved to ${options.output}`));
458
- console.log(chalk.gray(' Open in a browser to view the full compliance report.'));
459
- console.log('');
460
- });
461
-
462
63
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "getdoorman",
3
- "version": "1.1.1",
3
+ "version": "1.2.1",
4
4
  "description": "Zero-config security scanner for AI-assisted development. 2000+ rules, 11 languages, 4 detection engines.",
5
5
  "main": "src/index.js",
6
6
  "exports": {
@@ -12,14 +12,52 @@ const CHECKS = [
12
12
  fail: 'API key hardcoded in source code',
13
13
  check(files) {
14
14
  const patterns = [
15
+ // Cloud providers
15
16
  { 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/ },
17
+ { name: 'AWS Secret Key', regex: /(?:aws_secret|secret_access_key)\s*[:=]\s*['"][A-Za-z0-9/+=]{40}['"]/ },
18
+ { name: 'Google API Key', regex: /AIza[0-9A-Za-z_-]{35}/ },
19
+ { name: 'Google OAuth Secret', regex: /GOCSPX-[a-zA-Z0-9_-]{28}/ },
20
+ { name: 'Vercel Token', regex: /vercel_[a-zA-Z0-9]{24,}/ },
21
+ { name: 'Netlify Token', regex: /nfp_[a-zA-Z0-9]{40,}/ },
22
+ // AI providers
23
+ { name: 'OpenAI API Key', regex: /sk-(?:proj-)?[a-zA-Z0-9]{32,}/ },
18
24
  { name: 'Anthropic API Key', regex: /sk-ant-[a-zA-Z0-9-]{20,}/ },
25
+ { name: 'Groq API Key', regex: /gsk_[a-zA-Z0-9]{48,}/ },
26
+ { name: 'Replicate API Token', regex: /r8_[a-zA-Z0-9]{38}/ },
27
+ { name: 'Hugging Face Token', regex: /hf_[a-zA-Z0-9]{34}/ },
28
+ { name: 'Together AI Key', regex: /tog_[a-zA-Z0-9]{40,}/ },
29
+ { name: 'Pinecone API Key', regex: /pcsk_[a-zA-Z0-9]{50,}/ },
30
+ // Payment
31
+ { name: 'Stripe Secret Key', regex: /sk_live_[0-9a-zA-Z]{24,}/ },
32
+ { name: 'Stripe Publishable (live)', regex: /pk_live_[0-9a-zA-Z]{24,}/ },
33
+ // Auth
34
+ { name: 'Clerk Secret Key', regex: /sk_live_[a-zA-Z0-9]{27,}/ },
35
+ { name: 'Clerk Publishable Key', regex: /pk_live_[a-zA-Z0-9]{27,}/ },
36
+ { name: 'Auth0 Client Secret', regex: /(?:auth0|AUTH0).*secret.*['"][a-zA-Z0-9_-]{32,}['"]/ },
37
+ // Dev tools
19
38
  { name: 'GitHub Token', regex: /ghp_[0-9a-zA-Z]{36}/ },
39
+ { name: 'GitHub OAuth Secret', regex: /gho_[0-9a-zA-Z]{36}/ },
40
+ { name: 'GitLab Token', regex: /glpat-[0-9a-zA-Z_-]{20,}/ },
41
+ { name: 'Slack Bot Token', regex: /xoxb-[0-9]{11,}-[0-9a-zA-Z]{24,}/ },
42
+ { name: 'Slack Webhook', regex: /hooks\.slack\.com\/services\/T[A-Z0-9]+\/B[A-Z0-9]+\/[a-zA-Z0-9]+/ },
43
+ { name: 'Discord Bot Token', regex: /[MN][A-Za-z0-9]{23,}\.[A-Za-z0-9_-]{6}\.[A-Za-z0-9_-]{27}/ },
44
+ { name: 'Twilio Auth Token', regex: /(?:twilio|TWILIO).*[0-9a-f]{32}/ },
45
+ { name: 'SendGrid API Key', regex: /SG\.[a-zA-Z0-9_-]{22}\.[a-zA-Z0-9_-]{43}/ },
46
+ { name: 'Resend API Key', regex: /re_[a-zA-Z0-9]{30,}/ },
47
+ { name: 'Mailgun API Key', regex: /key-[0-9a-zA-Z]{32}/ },
48
+ { name: 'Postmark Token', regex: /(?:postmark|POSTMARK).*['"][0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}['"]/ },
49
+ // Database
20
50
  { 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,}/ },
51
+ { name: 'Firebase Private Key', regex: /-----BEGIN RSA PRIVATE KEY-----/ },
52
+ { name: 'MongoDB Connection String', regex: /mongodb\+srv:\/\/[^:]+:[^@]+@/ },
53
+ { name: 'Postgres Connection String', regex: /postgres(?:ql)?:\/\/[^:]+:[^@]+@/ },
54
+ { name: 'PlanetScale Connection', regex: /mysql:\/\/[^:]+:[^@]+@aws\.connect\.psdb\.cloud/ },
55
+ { name: 'Neon Postgres', regex: /postgres(?:ql)?:\/\/[^:]+:[^@]+@[^/]*neon\.tech/ },
56
+ { name: 'Turso Database Token', regex: /eyJhbGciOiJFZERTQS[a-zA-Z0-9_-]{50,}/ },
57
+ { name: 'Upstash Redis Token', regex: /AX[a-zA-Z0-9]{34,}/ },
58
+ { name: 'Redis Connection String', regex: /redis:\/\/[^:]+:[^@]+@/ },
59
+ // SSH & certificates
60
+ { name: 'SSH Private Key', regex: /-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/ },
23
61
  ];
24
62
  const findings = [];
25
63
  for (const [fp, content] of files) {
@@ -64,15 +102,25 @@ const CHECKS = [
64
102
  fail: 'SQL query built with user input',
65
103
  check(files) {
66
104
  const findings = [];
67
- const sqlPattern = /(?:query|execute|raw)\s*\(\s*`[^`]*(?:SELECT|INSERT|UPDATE|DELETE|FROM|WHERE)\b[^`]*\$\{/i;
105
+ const patterns = [
106
+ // JS/TS template literals with SQL
107
+ /(?:query|execute|raw|prepare)\s*\(\s*`[^`]*(?:SELECT|INSERT|UPDATE|DELETE)\s.*(?:FROM|INTO|SET)\b[^`]*\$\{/i,
108
+ // String concatenation with SQL keywords
109
+ /(?:query|execute)\s*\(\s*['"](?:SELECT|INSERT|UPDATE|DELETE)\s.*['"]\s*\+\s*(?:req\.|params\.|input|user)/i,
110
+ // Python f-string/format SQL
111
+ /(?:execute|cursor\.execute)\s*\(\s*f['"].*(?:SELECT|INSERT|UPDATE|DELETE)\b/i,
112
+ ];
68
113
  for (const [fp, content] of files) {
69
114
  if (/test|spec|mock/i.test(fp)) continue;
70
115
  if (!fp.match(/\.(js|ts|jsx|tsx|py|rb|go|java|php)$/)) continue;
71
116
  const lines = content.split('\n');
72
117
  for (let i = 0; i < lines.length; i++) {
73
118
  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 });
119
+ for (const p of patterns) {
120
+ if (p.test(block)) {
121
+ findings.push({ message: 'SQL query built with user input', file: fp, line: i + 1 });
122
+ break;
123
+ }
76
124
  }
77
125
  }
78
126
  }
@@ -90,14 +138,23 @@ const CHECKS = [
90
138
  for (const [fp, content] of files) {
91
139
  if (/test|spec|mock/i.test(fp)) continue;
92
140
  if (!fp.match(/\.(js|ts|jsx|tsx)$/)) continue;
93
- if (!fp.match(/api|route|handler|controller/i)) continue;
141
+ if (!fp.match(/api|route|handler|controller|middleware/i)) continue;
94
142
  const lines = content.split('\n');
95
143
  for (let i = 0; i < lines.length; i++) {
144
+ // Next.js route handlers
96
145
  if (/export\s+(?:async\s+)?function\s+(?:GET|POST|PUT|DELETE|PATCH)\b/.test(lines[i])) {
97
146
  const body = lines.slice(i, Math.min(lines.length, i + 30)).join('\n');
98
- if (!/try\s*\{|\.catch\s*\(/.test(body)) {
147
+ if (!/try\s*\{|\.catch\s*\(|errorHandler|withErrorHandling/.test(body)) {
99
148
  findings.push({ message: `${fp.split('/').pop()} has no error handling`, file: fp, line: i + 1 });
100
- break; // one per file
149
+ break;
150
+ }
151
+ }
152
+ // Express route handlers
153
+ if (/(?:app|router)\.\s*(?:get|post|put|delete|patch)\s*\(/.test(lines[i])) {
154
+ const body = lines.slice(i, Math.min(lines.length, i + 20)).join('\n');
155
+ if (/async/.test(body) && !/try\s*\{|\.catch\s*\(|asyncHandler|expressAsyncErrors/.test(body)) {
156
+ findings.push({ message: `Async route without try/catch`, file: fp, line: i + 1 });
157
+ break;
101
158
  }
102
159
  }
103
160
  }
@@ -113,14 +170,20 @@ const CHECKS = [
113
170
  fail: 'Password or secret hardcoded in source',
114
171
  check(files) {
115
172
  const findings = [];
116
- const pattern = /(?:password|passwd|pwd|secret|secret_key|jwt_secret)\s*[:=]\s*['"][^'"]{8,}['"]/i;
173
+ const patterns = [
174
+ /(?:password|passwd|pwd)\s*[:=]\s*['"][^'"]{8,}['"]/i,
175
+ /(?:secret|secret_key|jwt_secret|app_secret|client_secret)\s*[:=]\s*['"][^'"]{8,}['"]/i,
176
+ /(?:auth_token|access_token|bearer)\s*[:=]\s*['"][^'"]{20,}['"]/i,
177
+ /(?:database_url|db_url|database_password|db_password)\s*[:=]\s*['"][^'"]{8,}['"]/i,
178
+ ];
179
+ const combined = new RegExp(patterns.map(p => p.source).join('|'), 'i');
117
180
  for (const [fp, content] of files) {
118
- if (/test|spec|mock|example|sample|\.env/i.test(fp)) continue;
181
+ if (/test|spec|mock|example|sample|\.env|fixture/i.test(fp)) continue;
119
182
  if (!fp.match(/\.(js|ts|jsx|tsx|py|rb|go|java|php|yml|yaml|json)$/)) continue;
120
183
  const lines = content.split('\n');
121
184
  for (let i = 0; i < lines.length; i++) {
122
185
  if (/^\s*(\/\/|#|\*|\/\*)/.test(lines[i])) continue;
123
- if (pattern.test(lines[i]) && !/process\.env|os\.environ|ENV\[|config\.|getenv/i.test(lines[i])) {
186
+ if (combined.test(lines[i]) && !/process\.env|os\.environ|ENV\[|config\.|getenv|placeholder|example|changeme|TODO/i.test(lines[i])) {
124
187
  findings.push({ message: 'Hardcoded secret', file: fp, line: i + 1 });
125
188
  }
126
189
  }
@@ -152,20 +215,21 @@ const CHECKS = [
152
215
  },
153
216
  },
154
217
  {
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',
218
+ id: 'sensitive-logs',
219
+ name: 'Sensitive Data in Logs',
220
+ icon: '📋',
221
+ pass: 'No passwords or tokens in logs',
222
+ fail: 'Sensitive data may be logged',
160
223
  check(files) {
161
224
  const findings = [];
162
225
  for (const [fp, content] of files) {
163
226
  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 });
227
+ if (!fp.match(/\.(js|ts|jsx|tsx|py|rb|go|java|php)$/)) continue;
228
+ const lines = content.split('\n');
229
+ for (let i = 0; i < lines.length; i++) {
230
+ if (/console\.log\(.*(?:password|secret|token|apiKey|api_key|authorization|credential)/i.test(lines[i]) ||
231
+ /logger?\.\w+\(.*(?:password|secret|token|apiKey|credential)/i.test(lines[i])) {
232
+ findings.push({ message: 'Password or token may be logged', file: fp, line: i + 1 });
169
233
  }
170
234
  }
171
235
  }
@@ -52,7 +52,7 @@ export function printSimpleReport(results) {
52
52
  console.log(chalk.green.bold(' All clear. Ship it.'));
53
53
  } else {
54
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.'));
55
+ console.log(chalk.gray(' Tell Claude or Codex: "fix the issues Doorman found"'));
56
56
  }
57
57
  console.log('');
58
58