corpus-cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,93 @@
1
+ import { readPolicyFile, readEnvFile } from '../utils/config.js';
2
+ import { green, amber, red, dim, bold, cyan } from '../utils/colors.js';
3
+ import { progressBar } from '../utils/table.js';
4
+ export async function runReport() {
5
+ const args = process.argv.slice(3);
6
+ if (args.includes('--help') || args.includes('-h')) {
7
+ process.stdout.write(`
8
+ corpus report
9
+
10
+ Fetch and display trust-score statistics for your project from the
11
+ Corpus API (last 30 days). Requires a corpus.policy.yaml with an
12
+ agent name and an optional .env.corpus with CORPUS_API_ENDPOINT.
13
+
14
+ Options:
15
+ --help Show this help
16
+
17
+ Examples:
18
+ corpus report
19
+
20
+ `);
21
+ return;
22
+ }
23
+ const policy = readPolicyFile();
24
+ if (!policy) {
25
+ process.stdout.write(red(' No corpus.policy.yaml found. Run corpus init first.\n'));
26
+ process.exit(1);
27
+ }
28
+ const env = readEnvFile();
29
+ const slug = String(policy.agent ?? 'unknown');
30
+ const endpoint = env.CORPUS_API_ENDPOINT ?? 'https://corpus.run';
31
+ process.stdout.write('\n');
32
+ process.stdout.write(bold(` CORPUS REPORT ${cyan(slug)} last 30 days\n`));
33
+ process.stdout.write(' ' + '\u2550'.repeat(51) + '\n\n');
34
+ // Fetch stats
35
+ try {
36
+ const url = `${endpoint}/api/project/${slug}`;
37
+ const res = await fetch(url);
38
+ if (!res.ok) {
39
+ if (res.status === 404) {
40
+ process.stdout.write(` Project '${slug}' not found on ${endpoint}\n`);
41
+ process.stdout.write(` Start sending events with the SDK to see data here.\n\n`);
42
+ return;
43
+ }
44
+ process.stdout.write(red(` API returned ${res.status}\n`));
45
+ process.exit(1);
46
+ }
47
+ const data = await res.json();
48
+ const s = data.stats;
49
+ // Pass rate bar
50
+ const barColor = s.passRate >= 95 ? green : s.passRate >= 80 ? amber : red;
51
+ process.stdout.write(` Pass rate ${barColor(s.passRate.toFixed(1) + '%')} ${barColor(progressBar(s.passRate))}\n`);
52
+ process.stdout.write(` Total actions ${s.totalRuns.toLocaleString()}\n`);
53
+ process.stdout.write(` Confirmed ${s.confirmCount} ${dim('user saw and approved')}\n`);
54
+ process.stdout.write(` Blocked ${s.blockCount} ${dim('stopped by hard policy')}\n`);
55
+ process.stdout.write(` Cancelled ${s.cancelledCount} ${dim('user saw and declined')}\n`);
56
+ // Policy breakdown
57
+ if (data.policyBreakdown.length > 0) {
58
+ process.stdout.write('\n');
59
+ process.stdout.write(bold(' POLICY BREAKDOWN\n'));
60
+ for (const p of data.policyBreakdown) {
61
+ const pColor = p.passRate >= 95 ? green : p.passRate >= 80 ? amber : red;
62
+ const name = p.policyTriggered.padEnd(22);
63
+ const extra = [];
64
+ if (p.blockCount > 0)
65
+ extra.push(`${p.blockCount} blocked`);
66
+ if (p.confirmCount > 0)
67
+ extra.push(`${p.confirmCount} confirmed`);
68
+ const detail = extra.length > 0 ? `[${extra.join(', ')}]` : '';
69
+ process.stdout.write(` ${name} ${pColor(p.passRate + '%')} (${p.totalRuns} runs) ${dim(detail)}\n`);
70
+ }
71
+ }
72
+ // Recent activity
73
+ if (data.recentActivity.length > 0) {
74
+ process.stdout.write('\n');
75
+ process.stdout.write(bold(' RECENT ACTIVITY (last 10)\n'));
76
+ for (const a of data.recentActivity.slice(0, 10)) {
77
+ const vColor = a.verdict === 'PASS' ? green : a.verdict === 'BLOCK' ? red : amber;
78
+ const verdict = vColor(a.verdict.padEnd(8));
79
+ const action = a.actionType.padEnd(18).slice(0, 18);
80
+ const policy = a.policyTriggered.padEnd(16).slice(0, 16);
81
+ const dur = `${a.durationMs}ms`.padStart(6);
82
+ process.stdout.write(` ${verdict} ${action} ${policy} ${dur}\n`);
83
+ }
84
+ }
85
+ process.stdout.write('\n');
86
+ process.stdout.write(dim(` View full dashboard: ${endpoint}/${slug}\n`));
87
+ process.stdout.write('\n');
88
+ }
89
+ catch (e) {
90
+ process.stdout.write(red(` Failed to fetch stats: ${e instanceof Error ? e.message : String(e)}\n`));
91
+ process.exit(1);
92
+ }
93
+ }
@@ -0,0 +1 @@
1
+ export declare function runScan(): Promise<void>;
@@ -0,0 +1,481 @@
1
+ import { readFileSync, writeFileSync, readdirSync, statSync, existsSync } from 'fs';
2
+ import { execSync } from 'child_process';
3
+ import path from 'path';
4
+ import { green, amber, red, dim, bold } from '../utils/colors.js';
5
+ import { checkForCVEs } from '@corpus/core';
6
+ import { checkDependencies } from '@corpus/core';
7
+ // ── Secret detection patterns ────────────────────────────────────────────────
8
+ const SECRET_PATTERNS = [
9
+ { name: 'AWS Access Key', regex: /AKIA[0-9A-Z]{16}/g, severity: 'CRITICAL' },
10
+ { name: 'GitHub Token', regex: /gh[pousr]_[A-Za-z0-9_]{36,255}/g, severity: 'CRITICAL' },
11
+ { name: 'OpenAI Key', regex: /sk-[A-Za-z0-9]{20,}/g, severity: 'CRITICAL' },
12
+ { name: 'Anthropic Key', regex: /sk-ant-[A-Za-z0-9-]{20,}/g, severity: 'CRITICAL' },
13
+ { name: 'Stripe Key', regex: /[sr]k_live_[A-Za-z0-9]{20,}/g, severity: 'CRITICAL' },
14
+ { name: 'Database URL', regex: /(?:postgres|mysql|mongodb|redis):\/\/[^\s'"]+:[^\s'"]+@[^\s'"]+/g, severity: 'CRITICAL' },
15
+ { name: 'Private Key', regex: /-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/g, severity: 'CRITICAL' },
16
+ { name: 'Slack Token', regex: /xox[baprs]-[0-9a-zA-Z-]{10,}/g, severity: 'CRITICAL' },
17
+ { name: 'SendGrid Key', regex: /SG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43}/g, severity: 'CRITICAL' },
18
+ { name: 'Generic Secret', regex: /(?:api_key|apikey|api_secret|secret_key|auth_token|access_token)\s*[=:]\s*['"]([A-Za-z0-9_\-]{16,})['"]/gi, severity: 'WARNING' },
19
+ { name: 'Password', regex: /(?:password|passwd|pwd)\s*[=:]\s*['"]([^'"]{8,})['"]/gi, severity: 'WARNING' },
20
+ ];
21
+ const PLACEHOLDER_SKIP = [
22
+ /^sk-(?:test|fake|dummy|placeholder|example|xxx|your)/i,
23
+ /^(?:test|fake|dummy|placeholder|example|changeme|TODO|your_)/i,
24
+ /^(?:xxx|aaa|123|abc)/i,
25
+ /<[A-Z_]+>/,
26
+ /^pk_test_/,
27
+ ];
28
+ // AI-specific patterns
29
+ const AI_PATTERNS = [
30
+ { name: 'Inlined Env Var', regex: /(?:const|let|var)\s+\w+\s*=\s*['"](?:postgres|mysql|mongodb|redis|https?):\/\/[^'"]+['"]/g, severity: 'WARNING', message: 'URL hardcoded instead of read from environment variable' },
31
+ { name: 'Disabled SSL', regex: /rejectUnauthorized\s*:\s*false/g, severity: 'INFO', message: 'SSL verification disabled' },
32
+ { name: 'Wildcard CORS', regex: /(?:cors|origin)\s*[=:]\s*['"]\*['"]/gi, severity: 'WARNING', message: 'CORS set to wildcard (*)' },
33
+ { name: 'chmod 777', regex: /chmod\s+777/g, severity: 'WARNING', message: 'Overly permissive file permissions' },
34
+ { name: 'eval()', regex: /\beval\s*\(/g, severity: 'CRITICAL', message: 'eval() enables arbitrary code execution' },
35
+ { name: 'Debug Endpoint', regex: /(?:app|router)\.\s*(?:get|post|use)\s*\(\s*['"]\/(?:debug|test|admin)/g, severity: 'WARNING', message: 'Debug/admin endpoint may be accessible in production' },
36
+ ];
37
+ // PII patterns
38
+ const PII_PATTERNS = [
39
+ { name: 'Email', regex: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g },
40
+ { name: 'SSN', regex: /\b\d{3}-\d{2}-\d{4}\b/g },
41
+ { name: 'Credit Card', regex: /\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/g },
42
+ ];
43
+ // Injection patterns
44
+ const INJECTION_PATTERNS = [
45
+ 'ignore previous instructions', 'ignore all previous', 'you are now',
46
+ 'system prompt', 'developer mode', 'jailbreak', 'bypass safety',
47
+ 'override:', 'new instructions:', 'forget everything',
48
+ ];
49
+ // ── File scanning ────────────────────────────────────────────────────────────
50
+ function isPlaceholder(val) {
51
+ return PLACEHOLDER_SKIP.some((p) => p.test(val));
52
+ }
53
+ function isTestFile(f) {
54
+ const l = f.toLowerCase();
55
+ return l.includes('test') || l.includes('spec') || l.includes('mock') || l.includes('fixture') || l.includes('__tests__');
56
+ }
57
+ function isScannable(f) {
58
+ const ext = path.extname(f).toLowerCase();
59
+ const name = path.basename(f).toLowerCase();
60
+ const codeExts = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.py', '.go', '.rs', '.java', '.kt', '.swift', '.rb', '.php', '.c', '.cpp', '.h', '.cs', '.json', '.yaml', '.yml', '.toml', '.sh', '.bash', '.sql', '.tf', '.hcl', '.xml', '.vue', '.svelte', '.ini', '.cfg', '.conf', '.plist', '.md'];
61
+ return codeExts.includes(ext) || name.startsWith('.env') || name === 'dockerfile' || name === 'makefile';
62
+ }
63
+ function getFilesToScan(paths, staged) {
64
+ if (staged) {
65
+ try {
66
+ const output = execSync('git diff --cached --name-only --diff-filter=ACMR', { encoding: 'utf-8' });
67
+ return output.trim().split('\n').filter((f) => f && isScannable(f));
68
+ }
69
+ catch {
70
+ return [];
71
+ }
72
+ }
73
+ const files = [];
74
+ for (const p of paths) {
75
+ if (!existsSync(p))
76
+ continue;
77
+ const stat = statSync(p);
78
+ if (stat.isFile() && isScannable(p)) {
79
+ files.push(p);
80
+ }
81
+ else if (stat.isDirectory()) {
82
+ walkDir(p, files);
83
+ }
84
+ }
85
+ return files;
86
+ }
87
+ function walkDir(dir, files) {
88
+ const skip = ['node_modules', '.git', 'dist', '.next', '__pycache__', '.venv', 'venv'];
89
+ try {
90
+ for (const entry of readdirSync(dir)) {
91
+ if (skip.includes(entry))
92
+ continue;
93
+ const full = path.join(dir, entry);
94
+ try {
95
+ const stat = statSync(full);
96
+ if (stat.isDirectory())
97
+ walkDir(full, files);
98
+ else if (stat.isFile() && isScannable(full))
99
+ files.push(full);
100
+ }
101
+ catch { /* permission errors */ }
102
+ }
103
+ }
104
+ catch { /* permission errors */ }
105
+ }
106
+ function scanFile(filepath, opts) {
107
+ const findings = [];
108
+ let content;
109
+ try {
110
+ content = readFileSync(filepath, 'utf-8');
111
+ }
112
+ catch {
113
+ return findings;
114
+ }
115
+ if (isTestFile(filepath))
116
+ return findings;
117
+ // Secrets
118
+ if (opts.secrets) {
119
+ for (const pat of SECRET_PATTERNS) {
120
+ pat.regex.lastIndex = 0;
121
+ let m;
122
+ while ((m = pat.regex.exec(content)) !== null) {
123
+ const val = m[1] ?? m[0];
124
+ if (isPlaceholder(val))
125
+ continue;
126
+ const line = content.slice(0, m.index).split('\n').length;
127
+ findings.push({
128
+ severity: pat.severity,
129
+ type: pat.name,
130
+ file: filepath,
131
+ line,
132
+ message: `${pat.name} detected`,
133
+ value: val.length > 12 ? val.slice(0, 6) + '...' + val.slice(-4) : '***',
134
+ });
135
+ }
136
+ }
137
+ // AI patterns
138
+ for (const pat of AI_PATTERNS) {
139
+ pat.regex.lastIndex = 0;
140
+ let m;
141
+ while ((m = pat.regex.exec(content)) !== null) {
142
+ const line = content.slice(0, m.index).split('\n').length;
143
+ findings.push({
144
+ severity: pat.severity,
145
+ type: pat.name,
146
+ file: filepath,
147
+ line,
148
+ message: pat.message,
149
+ isAiPattern: true,
150
+ });
151
+ }
152
+ }
153
+ }
154
+ // PII
155
+ if (opts.pii) {
156
+ for (const pat of PII_PATTERNS) {
157
+ pat.regex.lastIndex = 0;
158
+ let m;
159
+ while ((m = pat.regex.exec(content)) !== null) {
160
+ const line = content.slice(0, m.index).split('\n').length;
161
+ findings.push({
162
+ severity: 'WARNING',
163
+ type: `PII: ${pat.name}`,
164
+ file: filepath,
165
+ line,
166
+ message: `${pat.name} found in source code`,
167
+ });
168
+ }
169
+ }
170
+ }
171
+ // Injection
172
+ if (opts.injection) {
173
+ const lower = content.toLowerCase();
174
+ for (const pattern of INJECTION_PATTERNS) {
175
+ if (lower.includes(pattern)) {
176
+ const idx = lower.indexOf(pattern);
177
+ const line = content.slice(0, idx).split('\n').length;
178
+ findings.push({
179
+ severity: 'CRITICAL',
180
+ type: 'Injection Pattern',
181
+ file: filepath,
182
+ line,
183
+ message: `Prompt injection pattern found: "${pattern}"`,
184
+ });
185
+ }
186
+ }
187
+ }
188
+ // CVE patterns
189
+ if (opts.safety) {
190
+ try {
191
+ const cveFindings = checkForCVEs(content, filepath);
192
+ for (const cve of cveFindings) {
193
+ findings.push({
194
+ severity: cve.severity === 'HIGH' ? 'CRITICAL' : cve.severity === 'MEDIUM' ? 'WARNING' : cve.severity,
195
+ type: `CVE: ${cve.cveId}`,
196
+ file: filepath,
197
+ line: cve.line,
198
+ message: `${cve.name}: ${cve.description}`,
199
+ suggestion: cve.fixExample,
200
+ });
201
+ }
202
+ }
203
+ catch { }
204
+ }
205
+ return findings;
206
+ }
207
+ // ── Output formatting ────────────────────────────────────────────────────────
208
+ function formatPretty(findings, fileCount, timeMs) {
209
+ const critical = findings.filter((f) => f.severity === 'CRITICAL');
210
+ const warning = findings.filter((f) => f.severity === 'WARNING');
211
+ const info = findings.filter((f) => f.severity === 'INFO');
212
+ process.stdout.write('\n');
213
+ process.stdout.write(bold(` CORPUS SCAN ${fileCount} files scanned in ${(timeMs / 1000).toFixed(1)}s\n`));
214
+ process.stdout.write(' ' + '\u2550'.repeat(46) + '\n\n');
215
+ if (findings.length === 0) {
216
+ process.stdout.write(green(' No issues found. Your code is clean.\n\n'));
217
+ return;
218
+ }
219
+ for (const f of findings) {
220
+ const sev = f.severity === 'CRITICAL' ? red('CRITICAL') :
221
+ f.severity === 'WARNING' ? amber('WARNING ') :
222
+ dim('INFO ');
223
+ const ai = f.isAiPattern ? amber(' [AI]') : '';
224
+ process.stdout.write(` ${sev} ${f.file}:${f.line}${ai}\n`);
225
+ process.stdout.write(` ${f.message}\n`);
226
+ if (f.value)
227
+ process.stdout.write(` Value: ${dim(f.value)}\n`);
228
+ if (f.suggestion)
229
+ process.stdout.write(` Fix: ${dim(f.suggestion)}\n`);
230
+ process.stdout.write('\n');
231
+ }
232
+ const summary = [];
233
+ if (critical.length > 0)
234
+ summary.push(red(`${critical.length} critical`));
235
+ if (warning.length > 0)
236
+ summary.push(amber(`${warning.length} warning`));
237
+ if (info.length > 0)
238
+ summary.push(dim(`${info.length} info`));
239
+ process.stdout.write(` ${summary.join(', ')}\n\n`);
240
+ }
241
+ function formatJson(findings, fileCount, timeMs) {
242
+ process.stdout.write(JSON.stringify({
243
+ scannedFiles: fileCount,
244
+ scanTimeMs: timeMs,
245
+ findings: findings.map((f) => ({
246
+ severity: f.severity,
247
+ type: f.type,
248
+ file: f.file,
249
+ line: f.line,
250
+ message: f.message,
251
+ isAiPattern: f.isAiPattern ?? false,
252
+ })),
253
+ summary: {
254
+ critical: findings.filter((f) => f.severity === 'CRITICAL').length,
255
+ warning: findings.filter((f) => f.severity === 'WARNING').length,
256
+ info: findings.filter((f) => f.severity === 'INFO').length,
257
+ },
258
+ }, null, 2) + '\n');
259
+ }
260
+ // ── Auto-fix ─────────────────────────────────────────────────────────────────
261
+ function autoFix(findings) {
262
+ // Types that have actual auto-fix logic implemented
263
+ const AUTO_FIXABLE_TYPES = new Set(['Disabled SSL', 'Wildcard CORS']);
264
+ // Types that look fixable but require manual intervention
265
+ const MANUAL_FIX_TYPES = new Set(['Generic Secret', 'Password', 'Inlined Env Var']);
266
+ const fixable = findings.filter((f) => AUTO_FIXABLE_TYPES.has(f.type));
267
+ const manualOnly = findings.filter((f) => MANUAL_FIX_TYPES.has(f.type));
268
+ if (fixable.length === 0 && manualOnly.length === 0) {
269
+ process.stdout.write(dim(' No auto-fixable issues found.\n'));
270
+ process.stdout.write(dim(' Auto-fix works on: disabled SSL, wildcard CORS.\n\n'));
271
+ return;
272
+ }
273
+ if (manualOnly.length > 0) {
274
+ process.stdout.write(dim(` ${manualOnly.length} issue(s) require manual fix (Generic Secret, Password, Inlined Env Var).\n`));
275
+ }
276
+ if (fixable.length === 0) {
277
+ process.stdout.write(dim(' No auto-fixable issues found.\n\n'));
278
+ return;
279
+ }
280
+ process.stdout.write(bold(` AUTO-FIX: ${fixable.length} fixable issue(s)\n\n`));
281
+ // Group by file
282
+ const byFile = new Map();
283
+ for (const f of fixable) {
284
+ if (!byFile.has(f.file))
285
+ byFile.set(f.file, []);
286
+ byFile.get(f.file).push(f);
287
+ }
288
+ let fixed = 0;
289
+ const envEntries = [];
290
+ for (const [filepath, fileFindings] of byFile) {
291
+ let content = readFileSync(filepath, 'utf-8');
292
+ const lines = content.split('\n');
293
+ for (const finding of fileFindings) {
294
+ if (finding.type === 'Disabled SSL') {
295
+ // Replace rejectUnauthorized: false with true
296
+ content = content.replace(/rejectUnauthorized\s*:\s*false/g, 'rejectUnauthorized: true');
297
+ process.stdout.write(` ${green('\u2714')} ${filepath}:${finding.line} - Enabled SSL verification\n`);
298
+ fixed++;
299
+ }
300
+ else if (finding.type === 'Wildcard CORS') {
301
+ // Replace wildcard with env var reference
302
+ content = content.replace(/((?:cors|origin)\s*[=:]\s*)['"]?\*['"]?/gi, '$1process.env.CORS_ORIGIN || "http://localhost:3000"');
303
+ envEntries.push('CORS_ORIGIN=http://localhost:3000');
304
+ process.stdout.write(` ${green('\u2714')} ${filepath}:${finding.line} - Replaced wildcard CORS with env var\n`);
305
+ fixed++;
306
+ }
307
+ }
308
+ writeFileSync(filepath, content);
309
+ }
310
+ // Write env entries if any
311
+ if (envEntries.length > 0) {
312
+ const envPath = '.env';
313
+ let envContent = '';
314
+ if (existsSync(envPath)) {
315
+ envContent = readFileSync(envPath, 'utf-8');
316
+ }
317
+ for (const entry of envEntries) {
318
+ const key = entry.split('=')[0];
319
+ if (!envContent.includes(key)) {
320
+ envContent += `\n# Added by corpus scan --fix\n${entry}\n`;
321
+ }
322
+ }
323
+ writeFileSync(envPath, envContent);
324
+ }
325
+ process.stdout.write(`\n ${green(`${fixed} issue(s) auto-fixed.`)}\n`);
326
+ if (fixed < fixable.length) {
327
+ process.stdout.write(dim(` ${fixable.length - fixed} issue(s) need manual review.\n`));
328
+ }
329
+ process.stdout.write('\n');
330
+ }
331
+ // ── Main ─────────────────────────────────────────────────────────────────────
332
+ export async function runScan() {
333
+ const args = process.argv.slice(3);
334
+ const opts = {
335
+ paths: ['.'],
336
+ format: 'pretty',
337
+ staged: false,
338
+ secrets: false,
339
+ pii: false,
340
+ injection: false,
341
+ safety: false,
342
+ fix: false,
343
+ cve: false,
344
+ deps: false,
345
+ };
346
+ // Parse args — individual flags are collected first; if none are set we
347
+ // default to scanning everything.
348
+ let anyFilterFlag = false;
349
+ const paths = [];
350
+ for (let i = 0; i < args.length; i++) {
351
+ switch (args[i]) {
352
+ case '--format':
353
+ opts.format = args[++i] ?? 'pretty';
354
+ break;
355
+ case '--staged':
356
+ opts.staged = true;
357
+ break;
358
+ case '--json':
359
+ opts.format = 'json';
360
+ break;
361
+ case '--fix':
362
+ opts.fix = true;
363
+ break;
364
+ case '--secrets':
365
+ opts.secrets = true;
366
+ anyFilterFlag = true;
367
+ break;
368
+ case '--pii':
369
+ opts.pii = true;
370
+ anyFilterFlag = true;
371
+ break;
372
+ case '--injection':
373
+ opts.injection = true;
374
+ anyFilterFlag = true;
375
+ break;
376
+ case '--cve':
377
+ opts.cve = true;
378
+ anyFilterFlag = true;
379
+ break;
380
+ case '--deps':
381
+ opts.deps = true;
382
+ anyFilterFlag = true;
383
+ break;
384
+ case '--help':
385
+ case '-h':
386
+ process.stdout.write(`
387
+ corpus scan [paths...] [options]
388
+
389
+ Scan files for secrets, PII, injection patterns, and unsafe code.
390
+
391
+ Options:
392
+ --staged Scan only git staged files (for pre-commit hooks)
393
+ --fix Auto-fix: replace hardcoded secrets with env var references
394
+ --json Output as JSON
395
+ --secrets Scan for secrets only
396
+ --pii Scan for PII only
397
+ --injection Scan for injection patterns only
398
+ --cve Scan for CVE-linked vulnerability patterns
399
+ --deps Check imports for hallucinated/typosquatted packages
400
+ --format <fmt> Output format: pretty (default) or json
401
+ --help Show this help
402
+
403
+ Examples:
404
+ corpus scan Scan current directory
405
+ corpus scan src/ config/ Scan specific directories
406
+ corpus scan --staged Scan staged git changes (pre-commit)
407
+ corpus scan --fix Scan and auto-fix secrets
408
+ corpus scan --json Machine-readable output for CI
409
+
410
+ `);
411
+ return;
412
+ default:
413
+ if (!args[i].startsWith('-'))
414
+ paths.push(args[i]);
415
+ }
416
+ }
417
+ // If no filter flags were provided, enable all scan categories (default).
418
+ if (!anyFilterFlag) {
419
+ opts.secrets = true;
420
+ opts.pii = true;
421
+ opts.injection = true;
422
+ opts.safety = true;
423
+ opts.cve = true;
424
+ opts.deps = true;
425
+ }
426
+ if (paths.length > 0)
427
+ opts.paths = paths;
428
+ const start = Date.now();
429
+ const files = getFilesToScan(opts.paths, opts.staged);
430
+ if (files.length === 0) {
431
+ if (opts.format === 'json') {
432
+ process.stdout.write(JSON.stringify({ scannedFiles: 0, findings: [], summary: { critical: 0, warning: 0, info: 0 } }) + '\n');
433
+ }
434
+ else {
435
+ process.stdout.write(dim('\n No scannable files found.\n\n'));
436
+ }
437
+ return;
438
+ }
439
+ const allFindings = [];
440
+ for (const filepath of files) {
441
+ allFindings.push(...scanFile(filepath, opts));
442
+ }
443
+ // Dependency checking (async - runs after file scan)
444
+ if (opts.deps) {
445
+ for (const filepath of files) {
446
+ try {
447
+ const content = readFileSync(filepath, 'utf-8');
448
+ const depFindings = await checkDependencies(content, filepath, { projectRoot: opts.paths[0] || '.' });
449
+ for (const d of depFindings) {
450
+ allFindings.push({
451
+ severity: d.severity,
452
+ type: `Hallucinated Dep: ${d.package}`,
453
+ file: filepath,
454
+ line: d.line,
455
+ message: `Package '${d.package}' ${d.reason === 'nonexistent' ? 'does not exist on npm' : d.reason === 'typosquat' ? 'may be a typosquat' : 'is suspiciously unpopular'}`,
456
+ suggestion: d.suggestion,
457
+ });
458
+ }
459
+ }
460
+ catch { }
461
+ }
462
+ }
463
+ const timeMs = Date.now() - start;
464
+ if (opts.format === 'json') {
465
+ formatJson(allFindings, files.length, timeMs);
466
+ }
467
+ else {
468
+ formatPretty(allFindings, files.length, timeMs);
469
+ }
470
+ // Auto-fix mode
471
+ if (opts.fix && allFindings.length > 0) {
472
+ autoFix(allFindings);
473
+ }
474
+ // Exit code: 2 for critical, 1 for warnings, 0 for clean
475
+ const hasCritical = allFindings.some((f) => f.severity === 'CRITICAL');
476
+ const hasWarning = allFindings.some((f) => f.severity === 'WARNING');
477
+ if (hasCritical)
478
+ process.exit(2);
479
+ if (hasWarning)
480
+ process.exit(1);
481
+ }
@@ -0,0 +1 @@
1
+ export declare function runVerify(): Promise<void>;