getdoorman 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/doorman.js +31 -426
- package/bin/getdoorman.js +31 -426
- package/package.json +1 -1
- package/src/rules/bugs/general.js +3 -2
- package/src/rules/compliance/healthcare.js +1 -1
- package/src/rules/compliance/index.js +24 -24
- package/src/rules/data/index.js +26 -26
- package/src/rules/dependencies/index.js +5 -5
- package/src/rules/deployment/index.js +7 -7
- package/src/rules/infrastructure/index.js +28 -28
- package/src/rules/performance/index.js +1 -1
- package/src/rules/reliability/index.js +1 -1
- package/src/rules/security/ai-api.js +1 -0
- package/src/rules/security/auth.js +2 -2
- package/src/rules/security/cors.js +2 -2
- package/src/rules/security/crypto.js +2 -2
- package/src/rules/security/csharp.js +3 -3
- package/src/rules/security/csrf.js +1 -1
- package/src/rules/security/file-upload.js +1 -1
- package/src/rules/security/go.js +3 -3
- package/src/rules/security/misconfiguration.js +4 -4
- package/src/rules/security/oauth-jwt.js +5 -2
- package/src/rules/security/prototype-pollution.js +2 -2
- package/src/rules/security/rate-limiting.js +1 -1
- package/src/rules/security/rust.js +3 -3
- package/src/rules/security/ssrf.js +8 -4
- package/src/rules/security/supply-chain-advanced.js +2 -2
- package/src/rules/security/xss.js +2 -2
- package/src/simple-checks.js +307 -0
- package/src/simple-reporter.js +60 -0
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,445 +12,52 @@ const program = new Command();
|
|
|
14
12
|
|
|
15
13
|
program
|
|
16
14
|
.name('getdoorman')
|
|
17
|
-
.description('
|
|
15
|
+
.description('10 checks. Zero false positives. Ship with confidence.')
|
|
18
16
|
.version(pkg.version);
|
|
19
17
|
|
|
20
18
|
program
|
|
21
|
-
.command('check')
|
|
22
|
-
.
|
|
23
|
-
.
|
|
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('--
|
|
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')
|
|
23
|
+
.option('--ci', 'Exit with code 1 if issues found')
|
|
44
24
|
.action(async (path, options) => {
|
|
45
25
|
try {
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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 {
|
|
40
|
+
printSimpleReport(results);
|
|
58
41
|
}
|
|
59
42
|
|
|
60
|
-
//
|
|
61
|
-
|
|
43
|
+
// On first scan: add doorman to AI tool configs
|
|
44
|
+
try {
|
|
62
45
|
const { isFirstScan, installClaudeMd, installAgentsMd, installCursorRules, installClaudeHook } = await import('../src/hooks.js');
|
|
63
|
-
const { loadAuth, saveAuth } = await import('../src/auth.js');
|
|
64
|
-
const { resolve } = await import('path');
|
|
65
46
|
const resolvedPath = resolve(path);
|
|
66
|
-
|
|
67
47
|
if (isFirstScan(resolvedPath)) {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const agentsMd = installAgentsMd(resolvedPath);
|
|
73
|
-
const cursorRules = installCursorRules(resolvedPath);
|
|
74
|
-
if (claudeMd) console.log(chalk.dim(' Added Doorman to CLAUDE.md'));
|
|
75
|
-
if (agentsMd) console.log(chalk.dim(' Added Doorman to AGENTS.md'));
|
|
76
|
-
if (cursorRules) console.log(chalk.dim(' Added Doorman to .cursorrules'));
|
|
77
|
-
|
|
78
|
-
// Auto-run hook for Claude Code (non-blocking, just a scan)
|
|
79
|
-
const hookInstalled = installClaudeHook(resolvedPath);
|
|
80
|
-
if (hookInstalled) console.log(chalk.dim(' Auto-run enabled for Claude Code'));
|
|
81
|
-
|
|
82
|
-
// NOTE: No pre-commit hook on first scan — too aggressive.
|
|
83
|
-
// Users can opt in with: npx getdoorman init
|
|
84
|
-
|
|
85
|
-
// Collect email
|
|
86
|
-
const auth = loadAuth();
|
|
87
|
-
if (!auth?.email) {
|
|
88
|
-
const readline = await import('readline');
|
|
89
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
90
|
-
console.log('');
|
|
91
|
-
const email = await new Promise(r => {
|
|
92
|
-
rl.question(chalk.green(' Enter your email for updates (optional): '), a => { rl.close(); r(a.trim()); });
|
|
93
|
-
});
|
|
94
|
-
if (email && email.includes('@')) {
|
|
95
|
-
saveAuth(email);
|
|
96
|
-
console.log(chalk.green(' ✓ Saved!'));
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
console.log('');
|
|
48
|
+
installClaudeMd(resolvedPath);
|
|
49
|
+
installAgentsMd(resolvedPath);
|
|
50
|
+
installCursorRules(resolvedPath);
|
|
51
|
+
installClaudeHook(resolvedPath);
|
|
100
52
|
}
|
|
101
|
-
}
|
|
53
|
+
} catch {}
|
|
102
54
|
|
|
103
|
-
|
|
55
|
+
const failCount = results.filter(r => !r.passed).length;
|
|
56
|
+
process.exit(options.ci && failCount > 0 ? 1 : 0);
|
|
104
57
|
} catch (err) {
|
|
105
58
|
console.error('Error:', err.message);
|
|
106
59
|
process.exit(2);
|
|
107
60
|
}
|
|
108
61
|
});
|
|
109
62
|
|
|
110
|
-
program
|
|
111
|
-
.command('fix')
|
|
112
|
-
.description('Tell your AI to fix issues — works in Claude, Codex, Cursor')
|
|
113
|
-
.argument('[severity]', 'Filter: critical, high, medium, or all', 'critical')
|
|
114
|
-
.argument('[path]', 'Path to scan', '.')
|
|
115
|
-
.action(async (severity, path, options) => {
|
|
116
|
-
const chalk = (await import('chalk')).default;
|
|
117
|
-
|
|
118
|
-
// Load cached scan or run a quick scan
|
|
119
|
-
let findings = [];
|
|
120
|
-
try {
|
|
121
|
-
const { loadScanCache } = await import('../src/scan-cache.js');
|
|
122
|
-
const { resolve } = await import('path');
|
|
123
|
-
const cache = loadScanCache(resolve(path));
|
|
124
|
-
if (cache && cache.findings) findings = cache.findings;
|
|
125
|
-
} catch {}
|
|
126
|
-
|
|
127
|
-
if (findings.length === 0) {
|
|
128
|
-
console.log('Scanning...');
|
|
129
|
-
const result = await check(path, { silent: true, full: true });
|
|
130
|
-
findings = result.findings;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Filter by severity
|
|
134
|
-
const sevFilter = severity.toLowerCase();
|
|
135
|
-
let filtered;
|
|
136
|
-
if (sevFilter === 'all') {
|
|
137
|
-
filtered = findings;
|
|
138
|
-
} else if (sevFilter === 'critical') {
|
|
139
|
-
filtered = findings.filter(f => f.severity === 'critical');
|
|
140
|
-
} else if (sevFilter === 'high') {
|
|
141
|
-
filtered = findings.filter(f => f.severity === 'critical' || f.severity === 'high');
|
|
142
|
-
} else if (sevFilter === 'medium') {
|
|
143
|
-
filtered = findings.filter(f => f.severity === 'critical' || f.severity === 'high' || f.severity === 'medium');
|
|
144
|
-
} else {
|
|
145
|
-
filtered = findings.filter(f => f.severity === sevFilter);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const top = filtered.slice(0, 20);
|
|
149
|
-
|
|
150
|
-
if (top.length === 0) {
|
|
151
|
-
console.log(`No ${sevFilter} issues found.`);
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// Build the prompt
|
|
156
|
-
const issueList = top.map(f => {
|
|
157
|
-
const loc = f.file ? ` in ${f.file}${f.line ? ':' + f.line : ''}` : '';
|
|
158
|
-
return `- ${f.severity.toUpperCase()}: ${f.title}${loc}`;
|
|
159
|
-
}).join('\n');
|
|
160
|
-
|
|
161
|
-
const prompt = `Fix these ${sevFilter === 'all' ? '' : sevFilter + ' '}issues in my code:\n\n${issueList}`;
|
|
162
|
-
|
|
163
|
-
// Detect environment: AI tool or manual terminal
|
|
164
|
-
const isAI = process.env.CLAUDE_CODE || process.env.CURSOR_SESSION || process.env.CODEX_SANDBOX || process.env.TERM_PROGRAM === 'claude';
|
|
165
|
-
const isCI = process.env.CI || process.env.GITHUB_ACTIONS;
|
|
166
|
-
|
|
167
|
-
if (isAI) {
|
|
168
|
-
// Inside an AI tool — just output the prompt directly, the AI will act on it
|
|
169
|
-
console.log(prompt);
|
|
170
|
-
} else if (isCI) {
|
|
171
|
-
// CI — just list issues
|
|
172
|
-
console.log(prompt);
|
|
173
|
-
} else {
|
|
174
|
-
// Manual terminal — show formatted + copy to clipboard
|
|
175
|
-
console.log('');
|
|
176
|
-
console.log(chalk.bold(' Paste this into Claude, Codex, or Cursor:'));
|
|
177
|
-
console.log('');
|
|
178
|
-
console.log(chalk.cyan(' ┌───────────────────────────────────────────────'));
|
|
179
|
-
for (const line of prompt.split('\n')) {
|
|
180
|
-
console.log(chalk.cyan(' │ ') + line);
|
|
181
|
-
}
|
|
182
|
-
console.log(chalk.cyan(' └───────────────────────────────────────────────'));
|
|
183
|
-
console.log('');
|
|
184
|
-
|
|
185
|
-
// Copy to clipboard
|
|
186
|
-
try {
|
|
187
|
-
const { execSync } = await import('child_process');
|
|
188
|
-
const cmd = process.platform === 'darwin' ? 'pbcopy' : process.platform === 'win32' ? 'clip' : 'xclip -selection clipboard';
|
|
189
|
-
execSync(cmd, { input: prompt, stdio: ['pipe', 'ignore', 'ignore'] });
|
|
190
|
-
console.log(chalk.green(' ✓ Copied to clipboard! Just paste it.'));
|
|
191
|
-
} catch {
|
|
192
|
-
console.log(chalk.dim(' Tip: copy the text above and paste into your AI tool'));
|
|
193
|
-
}
|
|
194
|
-
console.log('');
|
|
195
|
-
}
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
program
|
|
199
|
-
.command('review')
|
|
200
|
-
.description('Review only changed files (for git hooks and PRs)')
|
|
201
|
-
.argument('[path]', 'Path to scan', '.')
|
|
202
|
-
.option('--json', 'Output as JSON')
|
|
203
|
-
.option('--min-score <number>', 'Minimum score', '70')
|
|
204
|
-
.action(async (path, options) => {
|
|
205
|
-
try {
|
|
206
|
-
const result = await check(path, { ...options, incremental: true });
|
|
207
|
-
process.exit(result.exitCode || 0);
|
|
208
|
-
} catch (err) {
|
|
209
|
-
console.error('Error:', err.message);
|
|
210
|
-
process.exit(2);
|
|
211
|
-
}
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
program
|
|
215
|
-
.command('ignore')
|
|
216
|
-
.description('Ignore a rule (mark as false positive)')
|
|
217
|
-
.argument('<ruleId>', 'Rule ID to ignore (e.g., SEC-INJ-001)')
|
|
218
|
-
.option('--file <path>', 'Only ignore in this file')
|
|
219
|
-
.option('--reason <reason>', 'Reason for ignoring')
|
|
220
|
-
.action(async (ruleId, options) => {
|
|
221
|
-
const { addIgnore } = await import('../src/config.js');
|
|
222
|
-
addIgnore('.', ruleId, options.file || null, options.reason || 'User dismissed');
|
|
223
|
-
console.log(`Ignored ${ruleId}${options.file ? ' in ' + options.file : ''}`);
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
program
|
|
227
|
-
.command('init')
|
|
228
|
-
.description('Initialize Doorman: create config, hooks, and auto-run')
|
|
229
|
-
.option('--no-hook', 'Skip pre-commit hook installation')
|
|
230
|
-
.option('--claude', 'Install Claude Code auto-run hook')
|
|
231
|
-
.action(async (options) => {
|
|
232
|
-
const { writeFileSync, existsSync, mkdirSync } = await import('fs');
|
|
233
|
-
const { installClaudeHook, installGitHook, installClaudeMd } = await import('../src/hooks.js');
|
|
234
|
-
const { resolve } = await import('path');
|
|
235
|
-
|
|
236
|
-
// 1. Config file — create .doormanrc with recommended preset
|
|
237
|
-
if (existsSync('.doormanrc') || existsSync('.doormanrc.json')) {
|
|
238
|
-
const which = existsSync('.doormanrc') ? '.doormanrc' : '.doormanrc.json';
|
|
239
|
-
console.log(` ✓ Config already exists at ${which}`);
|
|
240
|
-
} else {
|
|
241
|
-
const config = {
|
|
242
|
-
extends: 'recommended',
|
|
243
|
-
rules: {},
|
|
244
|
-
categories: [],
|
|
245
|
-
severity: 'medium',
|
|
246
|
-
ignore: ['test/**', 'vendor/**', '*.min.js'],
|
|
247
|
-
};
|
|
248
|
-
writeFileSync('.doormanrc', JSON.stringify(config, null, 2) + '\n');
|
|
249
|
-
console.log(' ✓ Created .doormanrc (recommended preset)');
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// 2. Pre-commit hook
|
|
253
|
-
if (options.hook !== false) {
|
|
254
|
-
const installed = installGitHook(resolve('.'));
|
|
255
|
-
if (installed) {
|
|
256
|
-
console.log(' ✓ Installed pre-commit hook — critical issues will block commits');
|
|
257
|
-
} else if (!existsSync('.git')) {
|
|
258
|
-
console.log(' ⚠ Not a git repository — skipping pre-commit hook');
|
|
259
|
-
} else {
|
|
260
|
-
console.log(' ✓ Pre-commit hook already installed');
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// 3. Claude Code hook
|
|
265
|
-
if (options.claude) {
|
|
266
|
-
const installed = installClaudeHook(resolve('.'));
|
|
267
|
-
if (installed) {
|
|
268
|
-
console.log(' ✓ Claude Code hook installed — Doorman runs after every file edit');
|
|
269
|
-
} else {
|
|
270
|
-
console.log(' ✓ Claude Code hook already installed');
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
// 4. CLAUDE.md — teach Claude to use Doorman
|
|
275
|
-
const mdInstalled = installClaudeMd(resolve('.'));
|
|
276
|
-
if (mdInstalled) {
|
|
277
|
-
console.log(' ✓ Added Doorman to CLAUDE.md — Claude will suggest it automatically');
|
|
278
|
-
} else {
|
|
279
|
-
console.log(' ✓ CLAUDE.md already mentions Doorman');
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
console.log('');
|
|
283
|
-
console.log('Doorman is ready. Run `npx getdoorman check` to scan your codebase.');
|
|
284
|
-
if (!options.claude) {
|
|
285
|
-
console.log('Tip: Run `npx getdoorman init --claude` to auto-scan when Claude writes code.');
|
|
286
|
-
}
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
program
|
|
290
|
-
.command('dashboard')
|
|
291
|
-
.description('Open your project dashboard in the browser')
|
|
292
|
-
.argument('[path]', 'Project path', '.')
|
|
293
|
-
.action(async (path) => {
|
|
294
|
-
const chalk = (await import('chalk')).default;
|
|
295
|
-
const { openDashboard } = await import('../src/dashboard.js');
|
|
296
|
-
const result = openDashboard(path);
|
|
297
|
-
if (result.error) {
|
|
298
|
-
console.log('');
|
|
299
|
-
console.log(chalk.yellow(` ${result.error}`));
|
|
300
|
-
console.log('');
|
|
301
|
-
} else {
|
|
302
|
-
console.log('');
|
|
303
|
-
console.log(chalk.green(` ✓ Dashboard opened: ${result.path}`));
|
|
304
|
-
console.log('');
|
|
305
|
-
}
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
program
|
|
309
|
-
.command('benchmark')
|
|
310
|
-
.description('Run the real-world vulnerability detection benchmark')
|
|
311
|
-
.action(async () => {
|
|
312
|
-
console.log('Running Doorman benchmark...');
|
|
313
|
-
const { execSync } = await import('child_process');
|
|
314
|
-
try {
|
|
315
|
-
execSync('node --test test/benchmark-real-world.test.js', { stdio: 'inherit' });
|
|
316
|
-
} catch (e) {
|
|
317
|
-
// Test runner reports results
|
|
318
|
-
}
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
program
|
|
322
|
-
.command('hook')
|
|
323
|
-
.description('Install Doorman as a pre-commit git hook')
|
|
324
|
-
.option('--remove', 'Remove the git hook')
|
|
325
|
-
.action(async (options) => {
|
|
326
|
-
const { writeFileSync, unlinkSync, existsSync, mkdirSync } = await import('fs');
|
|
327
|
-
const hookPath = '.git/hooks/pre-commit';
|
|
328
|
-
|
|
329
|
-
if (options.remove) {
|
|
330
|
-
if (existsSync(hookPath)) {
|
|
331
|
-
unlinkSync(hookPath);
|
|
332
|
-
console.log('Removed Doorman pre-commit hook');
|
|
333
|
-
}
|
|
334
|
-
return;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
if (!existsSync('.git/hooks')) mkdirSync('.git/hooks', { recursive: true });
|
|
338
|
-
|
|
339
|
-
const hookScript = `#!/bin/sh
|
|
340
|
-
# Doorman pre-commit hook
|
|
341
|
-
npx getdoorman review --min-score 70
|
|
342
|
-
`;
|
|
343
|
-
writeFileSync(hookPath, hookScript, { mode: 0o755 });
|
|
344
|
-
console.log('Installed Doorman pre-commit hook');
|
|
345
|
-
console.log('Every commit will be checked. Remove with: doorman hook --remove');
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
program
|
|
349
|
-
.command('credits')
|
|
350
|
-
.description('Check your credit balance or buy more')
|
|
351
|
-
.option('--buy', 'Open the credit purchase page')
|
|
352
|
-
.action(async (options) => {
|
|
353
|
-
const chalk = (await import('chalk')).default;
|
|
354
|
-
const { loadAuth, checkCredits, getCreditPacks } = await import('../src/auth.js');
|
|
355
|
-
|
|
356
|
-
const auth = loadAuth();
|
|
357
|
-
|
|
358
|
-
if (!auth || !auth.email) {
|
|
359
|
-
console.log('');
|
|
360
|
-
console.log(chalk.dim(' No account yet. Run `npx getdoorman fix` to get 3 free credits.'));
|
|
361
|
-
console.log('');
|
|
362
|
-
return;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
const credits = await checkCredits(auth.email);
|
|
366
|
-
const packs = getCreditPacks(auth.email);
|
|
367
|
-
|
|
368
|
-
console.log('');
|
|
369
|
-
console.log(chalk.bold(' Doorman Account'));
|
|
370
|
-
console.log(` Email: ${auth.email}`);
|
|
371
|
-
console.log(` Credits: ${credits ? credits.credits : 'unknown (offline)'}`);
|
|
372
|
-
console.log('');
|
|
373
|
-
|
|
374
|
-
if (options.buy || (credits && credits.credits <= 0)) {
|
|
375
|
-
console.log(chalk.bold(' Buy credits:'));
|
|
376
|
-
console.log('');
|
|
377
|
-
for (const pack of packs) {
|
|
378
|
-
const bonus = pack.bonus ? chalk.green(` (${pack.bonus})`) : '';
|
|
379
|
-
console.log(` ${String(pack.credits).padStart(3)} credits — ${pack.price}${bonus} ${chalk.cyan(pack.buyUrl)}`);
|
|
380
|
-
}
|
|
381
|
-
console.log('');
|
|
382
|
-
// Try to open browser
|
|
383
|
-
try {
|
|
384
|
-
const { execSync } = await import('child_process');
|
|
385
|
-
const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
386
|
-
execSync(`${cmd} ${packs[0].buyUrl}`, { stdio: 'ignore' });
|
|
387
|
-
console.log(chalk.dim(' Opened in your browser.'));
|
|
388
|
-
} catch { /* ignore */ }
|
|
389
|
-
}
|
|
390
|
-
console.log('');
|
|
391
|
-
});
|
|
392
|
-
|
|
393
|
-
// ai-fix is now merged into `fix` — kept as hidden alias for backward compat
|
|
394
|
-
program
|
|
395
|
-
.command('ai-fix')
|
|
396
|
-
.description(false) // hidden
|
|
397
|
-
.argument('[path]', 'Path to scan', '.')
|
|
398
|
-
.action(async () => {
|
|
399
|
-
console.log('ai-fix has been merged into `fix`. Just run: npx getdoorman fix');
|
|
400
|
-
});
|
|
401
|
-
|
|
402
|
-
program
|
|
403
|
-
.command('report')
|
|
404
|
-
.description('Generate a compliance report (SOC2, GDPR, HIPAA, PCI-DSS)')
|
|
405
|
-
.argument('[path]', 'Path to scan', '.')
|
|
406
|
-
.option('--framework <name>', 'Framework: SOC2, GDPR, HIPAA, PCI, or all', 'all')
|
|
407
|
-
.option('--output <path>', 'Output file path', 'doorman-report.html')
|
|
408
|
-
.option('--project <name>', 'Project name for the report')
|
|
409
|
-
.action(async (path, options) => {
|
|
410
|
-
const chalk = (await import('chalk')).default;
|
|
411
|
-
const { basename, resolve } = await import('path');
|
|
412
|
-
|
|
413
|
-
const framework = options.framework.toLowerCase();
|
|
414
|
-
const validFrameworks = ['soc2', 'gdpr', 'hipaa', 'pci', 'all'];
|
|
415
|
-
if (!validFrameworks.includes(framework)) {
|
|
416
|
-
console.error(`Error: Unknown framework "${options.framework}". Choose from: SOC2, GDPR, HIPAA, PCI, or all`);
|
|
417
|
-
process.exit(1);
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
const projectName = options.project || basename(resolve(path));
|
|
421
|
-
|
|
422
|
-
console.log('');
|
|
423
|
-
console.log(chalk.bold.cyan(' Doorman — Compliance Report Generator'));
|
|
424
|
-
console.log('');
|
|
425
|
-
|
|
426
|
-
// Step 1: Run a full scan
|
|
427
|
-
console.log(chalk.gray(' Running full scan...'));
|
|
428
|
-
let result;
|
|
429
|
-
try {
|
|
430
|
-
result = await check(path, { silent: true, full: true });
|
|
431
|
-
} catch (err) {
|
|
432
|
-
console.error(chalk.red(` Scan failed: ${err.message}`));
|
|
433
|
-
process.exit(1);
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
console.log(chalk.gray(` Found ${result.findings.length} finding(s), score: ${result.score}/100`));
|
|
437
|
-
console.log('');
|
|
438
|
-
|
|
439
|
-
// Step 2: Generate the compliance report
|
|
440
|
-
const { summary } = generateReport(
|
|
441
|
-
{ findings: result.findings, score: result.score, stack: result.stack },
|
|
442
|
-
{ framework, projectName, outputPath: options.output }
|
|
443
|
-
);
|
|
444
|
-
|
|
445
|
-
// Step 3: Print summary
|
|
446
|
-
const frameworkEntries = Object.entries(summary.frameworks);
|
|
447
|
-
for (const [, fw] of frameworkEntries) {
|
|
448
|
-
const statusColor = fw.status === 'compliant' ? chalk.green : fw.status === 'partial' ? chalk.yellow : chalk.red;
|
|
449
|
-
console.log(` ${fw.name.padEnd(22)} ${statusColor(fw.status.toUpperCase().padEnd(15))} ${fw.score}%`);
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
console.log('');
|
|
453
|
-
console.log(chalk.bold.green(` Report saved to ${options.output}`));
|
|
454
|
-
console.log(chalk.gray(' Open in a browser to view the full compliance report.'));
|
|
455
|
-
console.log('');
|
|
456
|
-
});
|
|
457
|
-
|
|
458
63
|
program.parse();
|