getdoorman 1.0.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.
Files changed (123) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +181 -0
  3. package/bin/doorman.js +444 -0
  4. package/package.json +74 -0
  5. package/src/ai-fixer.js +559 -0
  6. package/src/ast-scanner.js +434 -0
  7. package/src/auth.js +149 -0
  8. package/src/baseline.js +48 -0
  9. package/src/compliance.js +539 -0
  10. package/src/config.js +466 -0
  11. package/src/custom-rules.js +32 -0
  12. package/src/dashboard.js +202 -0
  13. package/src/detector.js +142 -0
  14. package/src/fix-engine.js +48 -0
  15. package/src/fix-registry-extra.js +95 -0
  16. package/src/fix-registry-go-rust.js +77 -0
  17. package/src/fix-registry-java-csharp.js +77 -0
  18. package/src/fix-registry-js.js +99 -0
  19. package/src/fix-registry-mcp-ai.js +57 -0
  20. package/src/fix-registry-python.js +87 -0
  21. package/src/fixer-ruby-php.js +608 -0
  22. package/src/fixer.js +2113 -0
  23. package/src/hooks.js +115 -0
  24. package/src/ignore.js +176 -0
  25. package/src/index.js +384 -0
  26. package/src/metrics.js +126 -0
  27. package/src/monorepo.js +65 -0
  28. package/src/presets.js +54 -0
  29. package/src/reporter.js +975 -0
  30. package/src/rule-worker.js +36 -0
  31. package/src/rules/ast-rules.js +756 -0
  32. package/src/rules/bugs/accessibility.js +235 -0
  33. package/src/rules/bugs/ai-codegen-fixable.js +172 -0
  34. package/src/rules/bugs/ai-codegen.js +365 -0
  35. package/src/rules/bugs/code-smell-bugs.js +247 -0
  36. package/src/rules/bugs/crypto-bugs.js +195 -0
  37. package/src/rules/bugs/docker-bugs.js +158 -0
  38. package/src/rules/bugs/general.js +361 -0
  39. package/src/rules/bugs/go-bugs.js +279 -0
  40. package/src/rules/bugs/index.js +73 -0
  41. package/src/rules/bugs/js-api.js +257 -0
  42. package/src/rules/bugs/js-array-object.js +210 -0
  43. package/src/rules/bugs/js-async-fixable.js +223 -0
  44. package/src/rules/bugs/js-async.js +211 -0
  45. package/src/rules/bugs/js-closure-scope.js +182 -0
  46. package/src/rules/bugs/js-database.js +203 -0
  47. package/src/rules/bugs/js-error-handling.js +148 -0
  48. package/src/rules/bugs/js-logic.js +261 -0
  49. package/src/rules/bugs/js-memory.js +214 -0
  50. package/src/rules/bugs/js-node.js +361 -0
  51. package/src/rules/bugs/js-react.js +373 -0
  52. package/src/rules/bugs/js-regex.js +200 -0
  53. package/src/rules/bugs/js-state.js +272 -0
  54. package/src/rules/bugs/js-type-coercion.js +318 -0
  55. package/src/rules/bugs/nextjs-bugs.js +242 -0
  56. package/src/rules/bugs/nextjs-fixable.js +120 -0
  57. package/src/rules/bugs/node-fixable.js +178 -0
  58. package/src/rules/bugs/python-advanced.js +245 -0
  59. package/src/rules/bugs/python-fixable.js +98 -0
  60. package/src/rules/bugs/python.js +284 -0
  61. package/src/rules/bugs/react-fixable.js +207 -0
  62. package/src/rules/bugs/ruby-bugs.js +182 -0
  63. package/src/rules/bugs/shell-bugs.js +181 -0
  64. package/src/rules/bugs/silent-failures.js +261 -0
  65. package/src/rules/bugs/ts-bugs.js +235 -0
  66. package/src/rules/bugs/unused-vars.js +65 -0
  67. package/src/rules/compliance/accessibility-ext.js +468 -0
  68. package/src/rules/compliance/education.js +322 -0
  69. package/src/rules/compliance/financial.js +421 -0
  70. package/src/rules/compliance/frameworks.js +507 -0
  71. package/src/rules/compliance/healthcare.js +520 -0
  72. package/src/rules/compliance/index.js +2714 -0
  73. package/src/rules/compliance/regional-eu.js +480 -0
  74. package/src/rules/compliance/regional-international.js +903 -0
  75. package/src/rules/cost/index.js +1993 -0
  76. package/src/rules/data/index.js +2503 -0
  77. package/src/rules/dependencies/index.js +1684 -0
  78. package/src/rules/deployment/index.js +2050 -0
  79. package/src/rules/index.js +71 -0
  80. package/src/rules/infrastructure/index.js +3048 -0
  81. package/src/rules/performance/index.js +3455 -0
  82. package/src/rules/quality/index.js +3175 -0
  83. package/src/rules/reliability/index.js +3040 -0
  84. package/src/rules/scope-rules.js +815 -0
  85. package/src/rules/security/ai-api.js +1177 -0
  86. package/src/rules/security/auth.js +1328 -0
  87. package/src/rules/security/cors.js +127 -0
  88. package/src/rules/security/crypto.js +527 -0
  89. package/src/rules/security/csharp.js +862 -0
  90. package/src/rules/security/csrf.js +193 -0
  91. package/src/rules/security/dart.js +835 -0
  92. package/src/rules/security/deserialization.js +291 -0
  93. package/src/rules/security/file-upload.js +187 -0
  94. package/src/rules/security/go.js +850 -0
  95. package/src/rules/security/headers.js +235 -0
  96. package/src/rules/security/index.js +65 -0
  97. package/src/rules/security/injection.js +1639 -0
  98. package/src/rules/security/mcp-server.js +71 -0
  99. package/src/rules/security/misconfiguration.js +660 -0
  100. package/src/rules/security/oauth-jwt.js +329 -0
  101. package/src/rules/security/path-traversal.js +295 -0
  102. package/src/rules/security/php.js +1054 -0
  103. package/src/rules/security/prototype-pollution.js +283 -0
  104. package/src/rules/security/rate-limiting.js +208 -0
  105. package/src/rules/security/ruby.js +1061 -0
  106. package/src/rules/security/rust.js +693 -0
  107. package/src/rules/security/secrets.js +747 -0
  108. package/src/rules/security/shell.js +647 -0
  109. package/src/rules/security/ssrf.js +298 -0
  110. package/src/rules/security/supply-chain-advanced.js +393 -0
  111. package/src/rules/security/supply-chain.js +734 -0
  112. package/src/rules/security/swift.js +835 -0
  113. package/src/rules/security/taint.js +27 -0
  114. package/src/rules/security/xss.js +520 -0
  115. package/src/scan-cache.js +71 -0
  116. package/src/scanner.js +710 -0
  117. package/src/scope-analyzer.js +685 -0
  118. package/src/share.js +88 -0
  119. package/src/taint.js +300 -0
  120. package/src/telemetry.js +183 -0
  121. package/src/tracer.js +190 -0
  122. package/src/upload.js +35 -0
  123. package/src/worker.js +31 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 SafeLaunch Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,181 @@
1
+ # Doorman
2
+
3
+ **The broad-spectrum safety net for AI-assisted development.**
4
+
5
+ 2,508 rules across 10 categories. 510 auto-fixes. Runs locally in seconds. Zero account, zero config, zero data sent anywhere.
6
+
7
+ ```bash
8
+ npx getdoorman check
9
+ ```
10
+
11
+ ---
12
+
13
+ ## Why Doorman?
14
+
15
+ AI coding tools (Claude Code, Cursor, Copilot) let anyone build apps fast. But "it works" and "it's safe" are two different things. Doorman catches what code review misses:
16
+
17
+ - The SQL injection your AI assistant introduced
18
+ - The API key hardcoded in your frontend
19
+ - The `curl | bash` in your CI pipeline
20
+ - The `colors` package that trashed production (supply chain attack)
21
+ - The endpoint that crashes at 100 concurrent users
22
+ - The $3,000/month in unnecessary AI API calls
23
+ - The GDPR violation in every form
24
+
25
+ **You built it with AI. Doorman makes sure it's safe to ship.**
26
+
27
+ ---
28
+
29
+ ## Quick Start
30
+
31
+ ```bash
32
+ # Run once
33
+ npx getdoorman check
34
+
35
+ # Install as pre-commit hook — findings block bad commits automatically
36
+ npx getdoorman init
37
+
38
+ # Auto-fix common issues
39
+ npx getdoorman fix
40
+ ```
41
+
42
+ ---
43
+
44
+ ## What It Checks (2,508 rules, 510 auto-fixes)
45
+
46
+ | Category | Rules | Examples |
47
+ |----------|-------|---------|
48
+ | **Security** | 300+ | SQL injection, XSS, hardcoded secrets, missing auth, CSRF, rate limiting, supply chain attacks |
49
+ | **Performance** | 180+ | N+1 queries, no caching, no pagination, memory leaks, blocking event loop |
50
+ | **Reliability** | 150+ | No error handling, missing health checks, no timeouts, no retries, no monitoring |
51
+ | **Infrastructure** | 120+ | Docker misconfig, Terraform public S3, no resource limits, privileged mode |
52
+ | **Code Quality** | 200+ | Console.log in prod, empty catch blocks, prototype pollution, for-in on arrays |
53
+ | **Deployment** | 150+ | `write-all` CI permissions, unpinned Actions, secrets in CI logs, no rollback |
54
+ | **Compliance** | 180+ | GDPR gaps, missing privacy policy, PII in logs, accessibility issues |
55
+ | **Data Protection** | 150+ | Weak hashing, no validation, MD5 passwords, unencrypted PII |
56
+ | **Cost** | 100+ | Uncached AI API calls, wasteful queries, unnecessary compute |
57
+ | **Dependencies** | 120+ | Outdated packages with CVEs, license issues, lockfile missing, typosquatting |
58
+
59
+ ---
60
+
61
+ ## Supply Chain Protection
62
+
63
+ Doorman includes dedicated supply chain attack detection — one of the fastest-growing attack vectors:
64
+
65
+ ```bash
66
+ $ npx getdoorman check --category security
67
+
68
+ CRITICAL Compromised package "colors" in dependencies
69
+ (Deliberate sabotage in 2022 — printed infinite garbage output)
70
+
71
+ CRITICAL Third-party action "actions/checkout@v3" not pinned to commit SHA
72
+ (Tags are mutable — account takeover can inject malicious code)
73
+
74
+ CRITICAL Remote script execution without integrity check — ci.yml:12
75
+ (curl https://... | bash allows MITM to execute arbitrary code)
76
+
77
+ HIGH Hardcoded npm auth token in .npmrc
78
+ (Committed tokens grant publish access to your packages)
79
+ ```
80
+
81
+ 31 supply chain rules covering: package manager attacks, CI/CD pipeline injection, Trojan Source (Unicode bidirectional), build artifact signing, container image pinning, Git hook abuse, and registry security.
82
+
83
+ ---
84
+
85
+ ## Benchmark Results
86
+
87
+ Tested against OWASP NodeGoat, DVNA, and OWASP Juice Shop plus a synthetic benchmark with 46 planted vulnerability classes:
88
+
89
+ | Metric | Score |
90
+ |--------|-------|
91
+ | Detection rate | **98%** (45/46 vulnerability classes) |
92
+ | Hard false positive rate | **13%** (within industry norm of 15–30%) |
93
+ | Supply chain rules | **31 rules**, 6 attack categories |
94
+
95
+ ---
96
+
97
+ ## Privacy-First
98
+
99
+ Your code never leaves your machine. Doorman runs 100% locally — no cloud, no account, no GitHub App, no data sent anywhere. This matters for:
100
+
101
+ - Regulated industries (HIPAA, PCI-DSS, SOC 2)
102
+ - Pre-IPO startups with unreleased code
103
+ - Air-gapped or offline CI environments
104
+ - Teams on GitLab, Bitbucket, or self-hosted git
105
+
106
+ ---
107
+
108
+ ## Pre-Commit Hook
109
+
110
+ Stop bad code from ever being committed:
111
+
112
+ ```bash
113
+ npx getdoorman init
114
+ ```
115
+
116
+ This installs a git hook that runs `doorman check` on every commit and blocks if critical issues are found. Takes 5 seconds to set up.
117
+
118
+ ---
119
+
120
+ ## CI/CD Integration
121
+
122
+ ```yaml
123
+ # .github/workflows/doorman.yml
124
+ name: Doorman
125
+ on: [pull_request]
126
+ jobs:
127
+ safety:
128
+ runs-on: ubuntu-latest
129
+ steps:
130
+ - uses: actions/checkout@v4
131
+ - run: npx getdoorman check --ci --min-score 70
132
+ ```
133
+
134
+ ---
135
+
136
+ ## CLI Reference
137
+
138
+ ```bash
139
+ npx getdoorman check # Scan current directory
140
+ npx getdoorman check ./my-app # Scan specific path
141
+ npx getdoorman check --full # Scan all files (not just changed)
142
+ npx getdoorman check --category security,performance
143
+ npx getdoorman check --ci --min-score 70 # Fail below threshold
144
+ npx getdoorman check --json # Machine-readable output
145
+
146
+ npx getdoorman fix # Auto-fix common issues
147
+ npx getdoorman fix --dry-run # Preview fixes
148
+
149
+ npx getdoorman init # Install pre-commit hook
150
+ npx getdoorman ignore SEC-XSS-009 # Suppress a rule
151
+ ```
152
+
153
+ ---
154
+
155
+ ## How It Works
156
+
157
+ Doorman uses a 4-layer detection architecture: regex pattern matching (2,508 rules for fast broad coverage), taint tracking (source-to-sink data flow across variable assignments), scope analysis (context-aware detection for loops, routes, and test files), and optional AST analysis via tree-sitter (structural matching with near-zero false positives). Each layer refines the previous one — fast layers catch issues, precise layers eliminate noise. See [Detection Methodology](docs/detection-methodology.md) for details.
158
+
159
+ ---
160
+
161
+ ## vs Alternatives
162
+
163
+ Doorman is designed for instant, zero-config scans — run `npx getdoorman check` and get results in under 5 seconds with no accounts, servers, or setup. It covers 10 categories beyond just security (performance, cost, compliance, infrastructure). For deep inter-procedural analysis, consider CodeQL; for enterprise dashboards, SonarQube; for custom AST rules, Semgrep. See [full comparison](docs/comparison.md).
164
+
165
+ ---
166
+
167
+ ## Supported Stacks
168
+
169
+ - **JavaScript/TypeScript**: Next.js, Express, Fastify, Remix, SvelteKit, Astro, Hono
170
+ - **Python**: Django, Flask, FastAPI
171
+ - **Ruby**: Rails, Sinatra
172
+ - **Go**: Standard library, Gin, Echo
173
+ - **Databases**: PostgreSQL, MySQL, SQLite, MongoDB, Redis
174
+ - **ORMs**: Prisma, Drizzle, TypeORM, Sequelize, Mongoose
175
+ - **Infrastructure**: Docker, GitHub Actions, GitLab CI, Terraform
176
+
177
+ ---
178
+
179
+ ## License
180
+
181
+ MIT
package/bin/doorman.js ADDED
@@ -0,0 +1,444 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import { check } from '../src/index.js';
5
+ import { generateReport } from '../src/compliance.js';
6
+
7
+ const program = new Command();
8
+
9
+ program
10
+ .name('doorman')
11
+ .description('Find security issues. Tell your AI to fix them.')
12
+ .version('1.0.0');
13
+
14
+ program
15
+ .command('check')
16
+ .alias('scan')
17
+ .description('Scan your code for issues (always free)')
18
+ .argument('[path]', 'Path to scan', '.')
19
+ .option('--ci', 'CI mode — exit with code 1 if score below threshold')
20
+ .option('--min-score <number>', 'Minimum score to pass in CI mode', '70')
21
+ .option('--json', 'Output results as JSON')
22
+ .option('--sarif [path]', 'Output SARIF 2.1.0 report (for GitHub Code Scanning, VS Code)')
23
+ .option('--html [path]', 'Output standalone HTML report')
24
+ .option('--category <categories>', 'Only run specific categories (comma-separated)')
25
+ .option('--severity <level>', 'Minimum severity to report (critical,high,medium,low,info)', 'low')
26
+ .option('--full', 'Force full scan (skip incremental)')
27
+ .option('--no-cache', 'Disable file hash caching (force re-scan all files)')
28
+ .option('--timeout <seconds>', 'Custom scan timeout in seconds', '60')
29
+ .option('--verbose', 'Show all findings including suggestions')
30
+ .option('--detail', 'Show all findings individually (disables smart summary)')
31
+ .option('--strict', 'Show all severity levels including medium/low')
32
+ .option('-q, --quiet', 'Only show one-line summary (no findings detail)')
33
+ .option('--config <path>', 'Path to a custom config file')
34
+ .option('--baseline [path]', 'Only show NEW findings (diff against baseline file)')
35
+ .option('--save-baseline [path]', 'Save current findings as baseline for future diffs')
36
+ .option('--profile', 'Show performance profile of slowest rules')
37
+ .option('--share', 'Generate a shareable score card')
38
+ .action(async (path, options) => {
39
+ try {
40
+ const result = await check(path, options);
41
+
42
+ // Generate shareable score card
43
+ if (options.share && result) {
44
+ const { generateScoreCard } = await import('../src/share.js');
45
+ const cardPath = generateScoreCard(path, result);
46
+ const chalk = (await import('chalk')).default;
47
+ console.log('');
48
+ console.log(chalk.bold.green(' Score card saved!'));
49
+ console.log(chalk.gray(` Open: ${cardPath}`));
50
+ console.log(chalk.gray(' Share it on Twitter, Discord, or Slack.'));
51
+ console.log('');
52
+ }
53
+
54
+ // After first scan, collect email + offer auto-run
55
+ if (!options.ci && !options.quiet && !options.json && !options.sarif && !options.html && !options.silent) {
56
+ const { isFirstScan } = await import('../src/hooks.js');
57
+ const { loadAuth, saveAuth } = await import('../src/auth.js');
58
+ const { resolve } = await import('path');
59
+ const resolvedPath = resolve(path);
60
+ const auth = loadAuth();
61
+
62
+ if (isFirstScan(resolvedPath) && !auth?.email) {
63
+ const chalk = (await import('chalk')).default;
64
+ const readline = await import('readline');
65
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
66
+
67
+ console.log('');
68
+ const email = await new Promise(r => {
69
+ rl.question(chalk.green(' Enter your email for updates (optional): '), a => { rl.close(); r(a.trim()); });
70
+ });
71
+
72
+ if (email && email.includes('@')) {
73
+ saveAuth(email);
74
+ // Send to API
75
+ try {
76
+ await fetch('https://api.doorman.sh/api/waitlist', {
77
+ method: 'POST',
78
+ headers: { 'Content-Type': 'application/json' },
79
+ body: JSON.stringify({ email, plan: 'cli-signup', source: 'cli' }),
80
+ signal: AbortSignal.timeout(3000),
81
+ });
82
+ } catch {}
83
+ console.log(chalk.green(' ✓ Saved! We\'ll notify you about updates.'));
84
+ }
85
+ console.log('');
86
+ }
87
+ }
88
+
89
+ process.exit(result.exitCode || 0);
90
+ } catch (err) {
91
+ console.error('Error:', err.message);
92
+ process.exit(2);
93
+ }
94
+ });
95
+
96
+ program
97
+ .command('fix')
98
+ .description('Tell your AI to fix issues — works in Claude, Codex, Cursor')
99
+ .argument('[severity]', 'Filter: critical, high, medium, or all', 'critical')
100
+ .argument('[path]', 'Path to scan', '.')
101
+ .action(async (severity, path, options) => {
102
+ const chalk = (await import('chalk')).default;
103
+
104
+ // Load cached scan or run a quick scan
105
+ let findings = [];
106
+ try {
107
+ const { loadScanCache } = await import('../src/scan-cache.js');
108
+ const { resolve } = await import('path');
109
+ const cache = loadScanCache(resolve(path));
110
+ if (cache && cache.findings) findings = cache.findings;
111
+ } catch {}
112
+
113
+ if (findings.length === 0) {
114
+ console.log('Scanning...');
115
+ const result = await check(path, { silent: true, full: true });
116
+ findings = result.findings;
117
+ }
118
+
119
+ // Filter by severity
120
+ const sevFilter = severity.toLowerCase();
121
+ let filtered;
122
+ if (sevFilter === 'all') {
123
+ filtered = findings;
124
+ } else if (sevFilter === 'critical') {
125
+ filtered = findings.filter(f => f.severity === 'critical');
126
+ } else if (sevFilter === 'high') {
127
+ filtered = findings.filter(f => f.severity === 'critical' || f.severity === 'high');
128
+ } else if (sevFilter === 'medium') {
129
+ filtered = findings.filter(f => f.severity === 'critical' || f.severity === 'high' || f.severity === 'medium');
130
+ } else {
131
+ filtered = findings.filter(f => f.severity === sevFilter);
132
+ }
133
+
134
+ const top = filtered.slice(0, 20);
135
+
136
+ if (top.length === 0) {
137
+ console.log(`No ${sevFilter} issues found.`);
138
+ return;
139
+ }
140
+
141
+ // Build the prompt
142
+ const issueList = top.map(f => {
143
+ const loc = f.file ? ` in ${f.file}${f.line ? ':' + f.line : ''}` : '';
144
+ return `- ${f.severity.toUpperCase()}: ${f.title}${loc}`;
145
+ }).join('\n');
146
+
147
+ const prompt = `Fix these ${sevFilter === 'all' ? '' : sevFilter + ' '}issues in my code:\n\n${issueList}`;
148
+
149
+ // Detect environment: AI tool or manual terminal
150
+ const isAI = process.env.CLAUDE_CODE || process.env.CURSOR_SESSION || process.env.CODEX_SANDBOX || process.env.TERM_PROGRAM === 'claude';
151
+ const isCI = process.env.CI || process.env.GITHUB_ACTIONS;
152
+
153
+ if (isAI) {
154
+ // Inside an AI tool — just output the prompt directly, the AI will act on it
155
+ console.log(prompt);
156
+ } else if (isCI) {
157
+ // CI — just list issues
158
+ console.log(prompt);
159
+ } else {
160
+ // Manual terminal — show formatted + copy to clipboard
161
+ console.log('');
162
+ console.log(chalk.bold(' Paste this into Claude, Codex, or Cursor:'));
163
+ console.log('');
164
+ console.log(chalk.cyan(' ┌───────────────────────────────────────────────'));
165
+ for (const line of prompt.split('\n')) {
166
+ console.log(chalk.cyan(' │ ') + line);
167
+ }
168
+ console.log(chalk.cyan(' └───────────────────────────────────────────────'));
169
+ console.log('');
170
+
171
+ // Copy to clipboard
172
+ try {
173
+ const { execSync } = await import('child_process');
174
+ const cmd = process.platform === 'darwin' ? 'pbcopy' : process.platform === 'win32' ? 'clip' : 'xclip -selection clipboard';
175
+ execSync(cmd, { input: prompt, stdio: ['pipe', 'ignore', 'ignore'] });
176
+ console.log(chalk.green(' ✓ Copied to clipboard! Just paste it.'));
177
+ } catch {
178
+ console.log(chalk.dim(' Tip: copy the text above and paste into your AI tool'));
179
+ }
180
+ console.log('');
181
+ }
182
+ });
183
+
184
+ program
185
+ .command('review')
186
+ .description('Review only changed files (for git hooks and PRs)')
187
+ .argument('[path]', 'Path to scan', '.')
188
+ .option('--json', 'Output as JSON')
189
+ .option('--min-score <number>', 'Minimum score', '70')
190
+ .action(async (path, options) => {
191
+ try {
192
+ const result = await check(path, { ...options, incremental: true });
193
+ process.exit(result.exitCode || 0);
194
+ } catch (err) {
195
+ console.error('Error:', err.message);
196
+ process.exit(2);
197
+ }
198
+ });
199
+
200
+ program
201
+ .command('ignore')
202
+ .description('Ignore a rule (mark as false positive)')
203
+ .argument('<ruleId>', 'Rule ID to ignore (e.g., SEC-INJ-001)')
204
+ .option('--file <path>', 'Only ignore in this file')
205
+ .option('--reason <reason>', 'Reason for ignoring')
206
+ .action(async (ruleId, options) => {
207
+ const { addIgnore } = await import('../src/config.js');
208
+ addIgnore('.', ruleId, options.file || null, options.reason || 'User dismissed');
209
+ console.log(`Ignored ${ruleId}${options.file ? ' in ' + options.file : ''}`);
210
+ });
211
+
212
+ program
213
+ .command('init')
214
+ .description('Initialize Doorman: create config, hooks, and auto-run')
215
+ .option('--no-hook', 'Skip pre-commit hook installation')
216
+ .option('--claude', 'Install Claude Code auto-run hook')
217
+ .action(async (options) => {
218
+ const { writeFileSync, existsSync, mkdirSync } = await import('fs');
219
+ const { installClaudeHook, installGitHook, installClaudeMd } = await import('../src/hooks.js');
220
+ const { resolve } = await import('path');
221
+
222
+ // 1. Config file — create .doormanrc with recommended preset
223
+ if (existsSync('.doormanrc') || existsSync('.doormanrc.json')) {
224
+ const which = existsSync('.doormanrc') ? '.doormanrc' : '.doormanrc.json';
225
+ console.log(` ✓ Config already exists at ${which}`);
226
+ } else {
227
+ const config = {
228
+ extends: 'recommended',
229
+ rules: {},
230
+ categories: [],
231
+ severity: 'medium',
232
+ ignore: ['test/**', 'vendor/**', '*.min.js'],
233
+ };
234
+ writeFileSync('.doormanrc', JSON.stringify(config, null, 2) + '\n');
235
+ console.log(' ✓ Created .doormanrc (recommended preset)');
236
+ }
237
+
238
+ // 2. Pre-commit hook
239
+ if (options.hook !== false) {
240
+ const installed = installGitHook(resolve('.'));
241
+ if (installed) {
242
+ console.log(' ✓ Installed pre-commit hook — critical issues will block commits');
243
+ } else if (!existsSync('.git')) {
244
+ console.log(' ⚠ Not a git repository — skipping pre-commit hook');
245
+ } else {
246
+ console.log(' ✓ Pre-commit hook already installed');
247
+ }
248
+ }
249
+
250
+ // 3. Claude Code hook
251
+ if (options.claude) {
252
+ const installed = installClaudeHook(resolve('.'));
253
+ if (installed) {
254
+ console.log(' ✓ Claude Code hook installed — Doorman runs after every file edit');
255
+ } else {
256
+ console.log(' ✓ Claude Code hook already installed');
257
+ }
258
+ }
259
+
260
+ // 4. CLAUDE.md — teach Claude to use Doorman
261
+ const mdInstalled = installClaudeMd(resolve('.'));
262
+ if (mdInstalled) {
263
+ console.log(' ✓ Added Doorman to CLAUDE.md — Claude will suggest it automatically');
264
+ } else {
265
+ console.log(' ✓ CLAUDE.md already mentions Doorman');
266
+ }
267
+
268
+ console.log('');
269
+ console.log('Doorman is ready. Run `npx getdoorman check` to scan your codebase.');
270
+ if (!options.claude) {
271
+ console.log('Tip: Run `npx getdoorman init --claude` to auto-scan when Claude writes code.');
272
+ }
273
+ });
274
+
275
+ program
276
+ .command('dashboard')
277
+ .description('Open your project dashboard in the browser')
278
+ .argument('[path]', 'Project path', '.')
279
+ .action(async (path) => {
280
+ const chalk = (await import('chalk')).default;
281
+ const { openDashboard } = await import('../src/dashboard.js');
282
+ const result = openDashboard(path);
283
+ if (result.error) {
284
+ console.log('');
285
+ console.log(chalk.yellow(` ${result.error}`));
286
+ console.log('');
287
+ } else {
288
+ console.log('');
289
+ console.log(chalk.green(` ✓ Dashboard opened: ${result.path}`));
290
+ console.log('');
291
+ }
292
+ });
293
+
294
+ program
295
+ .command('benchmark')
296
+ .description('Run the real-world vulnerability detection benchmark')
297
+ .action(async () => {
298
+ console.log('Running Doorman benchmark...');
299
+ const { execSync } = await import('child_process');
300
+ try {
301
+ execSync('node --test test/benchmark-real-world.test.js', { stdio: 'inherit' });
302
+ } catch (e) {
303
+ // Test runner reports results
304
+ }
305
+ });
306
+
307
+ program
308
+ .command('hook')
309
+ .description('Install Doorman as a pre-commit git hook')
310
+ .option('--remove', 'Remove the git hook')
311
+ .action(async (options) => {
312
+ const { writeFileSync, unlinkSync, existsSync, mkdirSync } = await import('fs');
313
+ const hookPath = '.git/hooks/pre-commit';
314
+
315
+ if (options.remove) {
316
+ if (existsSync(hookPath)) {
317
+ unlinkSync(hookPath);
318
+ console.log('Removed Doorman pre-commit hook');
319
+ }
320
+ return;
321
+ }
322
+
323
+ if (!existsSync('.git/hooks')) mkdirSync('.git/hooks', { recursive: true });
324
+
325
+ const hookScript = `#!/bin/sh
326
+ # Doorman pre-commit hook
327
+ npx getdoorman review --min-score 70
328
+ `;
329
+ writeFileSync(hookPath, hookScript, { mode: 0o755 });
330
+ console.log('Installed Doorman pre-commit hook');
331
+ console.log('Every commit will be checked. Remove with: doorman hook --remove');
332
+ });
333
+
334
+ program
335
+ .command('credits')
336
+ .description('Check your credit balance or buy more')
337
+ .option('--buy', 'Open the credit purchase page')
338
+ .action(async (options) => {
339
+ const chalk = (await import('chalk')).default;
340
+ const { loadAuth, checkCredits, getCreditPacks } = await import('../src/auth.js');
341
+
342
+ const auth = loadAuth();
343
+
344
+ if (!auth || !auth.email) {
345
+ console.log('');
346
+ console.log(chalk.dim(' No account yet. Run `npx getdoorman fix` to get 3 free credits.'));
347
+ console.log('');
348
+ return;
349
+ }
350
+
351
+ const credits = await checkCredits(auth.email);
352
+ const packs = getCreditPacks(auth.email);
353
+
354
+ console.log('');
355
+ console.log(chalk.bold(' Doorman Account'));
356
+ console.log(` Email: ${auth.email}`);
357
+ console.log(` Credits: ${credits ? credits.credits : 'unknown (offline)'}`);
358
+ console.log('');
359
+
360
+ if (options.buy || (credits && credits.credits <= 0)) {
361
+ console.log(chalk.bold(' Buy credits:'));
362
+ console.log('');
363
+ for (const pack of packs) {
364
+ const bonus = pack.bonus ? chalk.green(` (${pack.bonus})`) : '';
365
+ console.log(` ${String(pack.credits).padStart(3)} credits — ${pack.price}${bonus} ${chalk.cyan(pack.buyUrl)}`);
366
+ }
367
+ console.log('');
368
+ // Try to open browser
369
+ try {
370
+ const { execSync } = await import('child_process');
371
+ const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
372
+ execSync(`${cmd} ${packs[0].buyUrl}`, { stdio: 'ignore' });
373
+ console.log(chalk.dim(' Opened in your browser.'));
374
+ } catch { /* ignore */ }
375
+ }
376
+ console.log('');
377
+ });
378
+
379
+ // ai-fix is now merged into `fix` — kept as hidden alias for backward compat
380
+ program
381
+ .command('ai-fix')
382
+ .description(false) // hidden
383
+ .argument('[path]', 'Path to scan', '.')
384
+ .action(async () => {
385
+ console.log('ai-fix has been merged into `fix`. Just run: npx getdoorman fix');
386
+ });
387
+
388
+ program
389
+ .command('report')
390
+ .description('Generate a compliance report (SOC2, GDPR, HIPAA, PCI-DSS)')
391
+ .argument('[path]', 'Path to scan', '.')
392
+ .option('--framework <name>', 'Framework: SOC2, GDPR, HIPAA, PCI, or all', 'all')
393
+ .option('--output <path>', 'Output file path', 'doorman-report.html')
394
+ .option('--project <name>', 'Project name for the report')
395
+ .action(async (path, options) => {
396
+ const chalk = (await import('chalk')).default;
397
+ const { basename, resolve } = await import('path');
398
+
399
+ const framework = options.framework.toLowerCase();
400
+ const validFrameworks = ['soc2', 'gdpr', 'hipaa', 'pci', 'all'];
401
+ if (!validFrameworks.includes(framework)) {
402
+ console.error(`Error: Unknown framework "${options.framework}". Choose from: SOC2, GDPR, HIPAA, PCI, or all`);
403
+ process.exit(1);
404
+ }
405
+
406
+ const projectName = options.project || basename(resolve(path));
407
+
408
+ console.log('');
409
+ console.log(chalk.bold.cyan(' Doorman — Compliance Report Generator'));
410
+ console.log('');
411
+
412
+ // Step 1: Run a full scan
413
+ console.log(chalk.gray(' Running full scan...'));
414
+ let result;
415
+ try {
416
+ result = await check(path, { silent: true, full: true });
417
+ } catch (err) {
418
+ console.error(chalk.red(` Scan failed: ${err.message}`));
419
+ process.exit(1);
420
+ }
421
+
422
+ console.log(chalk.gray(` Found ${result.findings.length} finding(s), score: ${result.score}/100`));
423
+ console.log('');
424
+
425
+ // Step 2: Generate the compliance report
426
+ const { summary } = generateReport(
427
+ { findings: result.findings, score: result.score, stack: result.stack },
428
+ { framework, projectName, outputPath: options.output }
429
+ );
430
+
431
+ // Step 3: Print summary
432
+ const frameworkEntries = Object.entries(summary.frameworks);
433
+ for (const [, fw] of frameworkEntries) {
434
+ const statusColor = fw.status === 'compliant' ? chalk.green : fw.status === 'partial' ? chalk.yellow : chalk.red;
435
+ console.log(` ${fw.name.padEnd(22)} ${statusColor(fw.status.toUpperCase().padEnd(15))} ${fw.score}%`);
436
+ }
437
+
438
+ console.log('');
439
+ console.log(chalk.bold.green(` Report saved to ${options.output}`));
440
+ console.log(chalk.gray(' Open in a browser to view the full compliance report.'));
441
+ console.log('');
442
+ });
443
+
444
+ program.parse();