@id-wispera/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.
Files changed (70) hide show
  1. package/README.md +250 -0
  2. package/dist/commands/audit.d.ts +6 -0
  3. package/dist/commands/audit.d.ts.map +1 -0
  4. package/dist/commands/audit.js +82 -0
  5. package/dist/commands/audit.js.map +1 -0
  6. package/dist/commands/auth.d.ts +7 -0
  7. package/dist/commands/auth.d.ts.map +1 -0
  8. package/dist/commands/auth.js +310 -0
  9. package/dist/commands/auth.js.map +1 -0
  10. package/dist/commands/create.d.ts +6 -0
  11. package/dist/commands/create.d.ts.map +1 -0
  12. package/dist/commands/create.js +88 -0
  13. package/dist/commands/create.js.map +1 -0
  14. package/dist/commands/exec.d.ts +8 -0
  15. package/dist/commands/exec.d.ts.map +1 -0
  16. package/dist/commands/exec.js +163 -0
  17. package/dist/commands/exec.js.map +1 -0
  18. package/dist/commands/import.d.ts +7 -0
  19. package/dist/commands/import.d.ts.map +1 -0
  20. package/dist/commands/import.js +1166 -0
  21. package/dist/commands/import.js.map +1 -0
  22. package/dist/commands/init.d.ts +6 -0
  23. package/dist/commands/init.d.ts.map +1 -0
  24. package/dist/commands/init.js +50 -0
  25. package/dist/commands/init.js.map +1 -0
  26. package/dist/commands/list.d.ts +6 -0
  27. package/dist/commands/list.d.ts.map +1 -0
  28. package/dist/commands/list.js +91 -0
  29. package/dist/commands/list.js.map +1 -0
  30. package/dist/commands/migrate.d.ts +7 -0
  31. package/dist/commands/migrate.d.ts.map +1 -0
  32. package/dist/commands/migrate.js +105 -0
  33. package/dist/commands/migrate.js.map +1 -0
  34. package/dist/commands/provision.d.ts +7 -0
  35. package/dist/commands/provision.d.ts.map +1 -0
  36. package/dist/commands/provision.js +303 -0
  37. package/dist/commands/provision.js.map +1 -0
  38. package/dist/commands/revoke.d.ts +6 -0
  39. package/dist/commands/revoke.d.ts.map +1 -0
  40. package/dist/commands/revoke.js +70 -0
  41. package/dist/commands/revoke.js.map +1 -0
  42. package/dist/commands/scan.d.ts +16 -0
  43. package/dist/commands/scan.d.ts.map +1 -0
  44. package/dist/commands/scan.js +700 -0
  45. package/dist/commands/scan.js.map +1 -0
  46. package/dist/commands/share.d.ts +6 -0
  47. package/dist/commands/share.d.ts.map +1 -0
  48. package/dist/commands/share.js +144 -0
  49. package/dist/commands/share.js.map +1 -0
  50. package/dist/commands/show.d.ts +6 -0
  51. package/dist/commands/show.d.ts.map +1 -0
  52. package/dist/commands/show.js +64 -0
  53. package/dist/commands/show.js.map +1 -0
  54. package/dist/index.d.ts +7 -0
  55. package/dist/index.d.ts.map +1 -0
  56. package/dist/index.js +76 -0
  57. package/dist/index.js.map +1 -0
  58. package/dist/utils/display.d.ts +78 -0
  59. package/dist/utils/display.d.ts.map +1 -0
  60. package/dist/utils/display.js +290 -0
  61. package/dist/utils/display.js.map +1 -0
  62. package/dist/utils/prompts.d.ts +67 -0
  63. package/dist/utils/prompts.d.ts.map +1 -0
  64. package/dist/utils/prompts.js +353 -0
  65. package/dist/utils/prompts.js.map +1 -0
  66. package/dist/utils/vault-helpers.d.ts +21 -0
  67. package/dist/utils/vault-helpers.d.ts.map +1 -0
  68. package/dist/utils/vault-helpers.js +45 -0
  69. package/dist/utils/vault-helpers.js.map +1 -0
  70. package/package.json +71 -0
@@ -0,0 +1,700 @@
1
+ /**
2
+ * Scan command: idw scan [path]
3
+ * Supports system-wide credential scanning and provider-specific scanning
4
+ */
5
+ import { Command } from 'commander';
6
+ import ora from 'ora';
7
+ import chalk from 'chalk';
8
+ import { readdir, readFile, stat, access } from 'fs/promises';
9
+ import { join, extname, relative, resolve } from 'path';
10
+ import { homedir } from 'os';
11
+ import { detectCredentials, mightContainCredentials, getDetectionStats } from '@id-wispera/core';
12
+ import { error, success, warning, displayDetectionResults, info, title } from '../utils/display.js';
13
+ // Files/directories to skip
14
+ const SKIP_DIRS = new Set([
15
+ 'node_modules',
16
+ '.git',
17
+ '.svn',
18
+ '.hg',
19
+ 'dist',
20
+ 'build',
21
+ 'coverage',
22
+ '.next',
23
+ '__pycache__',
24
+ 'vendor',
25
+ '.venv',
26
+ 'venv',
27
+ '.turbo',
28
+ '.cache',
29
+ 'Library',
30
+ 'Applications',
31
+ '.Trash',
32
+ ]);
33
+ const SKIP_EXTENSIONS = new Set([
34
+ '.png',
35
+ '.jpg',
36
+ '.jpeg',
37
+ '.gif',
38
+ '.ico',
39
+ '.svg',
40
+ '.woff',
41
+ '.woff2',
42
+ '.ttf',
43
+ '.eot',
44
+ '.mp3',
45
+ '.mp4',
46
+ '.avi',
47
+ '.zip',
48
+ '.tar',
49
+ '.gz',
50
+ '.pdf',
51
+ '.exe',
52
+ '.dll',
53
+ '.so',
54
+ '.dylib',
55
+ '.lock',
56
+ '.sqlite',
57
+ '.db',
58
+ ]);
59
+ // Known credential locations for system scan
60
+ const KNOWN_LOCATIONS = [
61
+ { path: '.openclaw', name: 'OpenClaw' },
62
+ { path: '.aws', name: 'AWS' },
63
+ { path: '.ssh', name: 'SSH' },
64
+ { path: '.docker', name: 'Docker' },
65
+ { path: '.kube', name: 'Kubernetes' },
66
+ { path: '.config/gcloud', name: 'Google Cloud' },
67
+ { path: '.azure', name: 'Azure' },
68
+ { path: '.npmrc', name: 'npm', isFile: true },
69
+ { path: '.pypirc', name: 'PyPI', isFile: true },
70
+ { path: '.netrc', name: 'Netrc', isFile: true },
71
+ { path: '.gitconfig', name: 'Git', isFile: true },
72
+ ];
73
+ /**
74
+ * Check file permissions and return warning if insecure
75
+ */
76
+ async function checkFilePermissions(filePath) {
77
+ try {
78
+ const stats = await stat(filePath);
79
+ const mode = stats.mode;
80
+ const worldReadable = (mode & 0o004) !== 0;
81
+ const groupReadable = (mode & 0o040) !== 0;
82
+ if (worldReadable) {
83
+ return `World-readable (mode: ${(mode & 0o777).toString(8)}): ${filePath}`;
84
+ }
85
+ if (groupReadable) {
86
+ return `Group-readable (mode: ${(mode & 0o777).toString(8)}): ${filePath}`;
87
+ }
88
+ return undefined;
89
+ }
90
+ catch {
91
+ return undefined;
92
+ }
93
+ }
94
+ /**
95
+ * Scan OpenClaw credentials specifically
96
+ */
97
+ async function scanOpenClawCredentials() {
98
+ const result = {
99
+ installed: false,
100
+ credentials: [],
101
+ permissionWarnings: [],
102
+ unreadablePaths: [],
103
+ };
104
+ const openclawPath = join(homedir(), '.openclaw');
105
+ try {
106
+ await access(openclawPath);
107
+ result.installed = true;
108
+ }
109
+ catch {
110
+ return result;
111
+ }
112
+ // Scan WhatsApp credentials
113
+ const whatsappPath = join(openclawPath, 'credentials', 'whatsapp');
114
+ try {
115
+ const accounts = await readdir(whatsappPath);
116
+ for (const accountId of accounts) {
117
+ const credsPath = join(whatsappPath, accountId, 'creds.json');
118
+ try {
119
+ const content = await readFile(credsPath, 'utf-8');
120
+ const data = JSON.parse(content);
121
+ const permWarning = await checkFilePermissions(credsPath);
122
+ if (permWarning)
123
+ result.permissionWarnings.push(permWarning);
124
+ result.credentials.push({
125
+ name: `WhatsApp Session (accountId: ${accountId})`,
126
+ type: 'Session Keys',
127
+ visaType: 'Access Visa',
128
+ riskLevel: 'HIGH',
129
+ filePath: credsPath,
130
+ rawValue: content,
131
+ metadata: { accountId, botName: data.me?.name },
132
+ });
133
+ }
134
+ catch (err) {
135
+ if (err.code === 'EACCES') {
136
+ result.unreadablePaths.push(credsPath);
137
+ }
138
+ }
139
+ }
140
+ }
141
+ catch {
142
+ // WhatsApp dir doesn't exist or not readable
143
+ }
144
+ // Scan auth profiles (LLM API keys)
145
+ const agentsPath = join(openclawPath, 'agents');
146
+ try {
147
+ const agents = await readdir(agentsPath);
148
+ for (const agentId of agents) {
149
+ const authPath = join(agentsPath, agentId, 'agent', 'auth-profiles.json');
150
+ try {
151
+ const content = await readFile(authPath, 'utf-8');
152
+ const data = JSON.parse(content);
153
+ const permWarning = await checkFilePermissions(authPath);
154
+ if (permWarning)
155
+ result.permissionWarnings.push(permWarning);
156
+ for (const [provider, profile] of Object.entries(data)) {
157
+ if (profile.type === 'api-key' && profile.key) {
158
+ const keyPreview = profile.key.substring(0, 10) + '...' + profile.key.substring(profile.key.length - 4);
159
+ result.credentials.push({
160
+ name: `${capitalizeFirst(provider)} API Key (${keyPreview})`,
161
+ type: 'API Key',
162
+ visaType: 'Privilege',
163
+ riskLevel: 'CRITICAL',
164
+ filePath: authPath,
165
+ rawValue: profile.key,
166
+ metadata: { provider, model: profile.model, agentId },
167
+ });
168
+ }
169
+ }
170
+ }
171
+ catch (err) {
172
+ if (err.code === 'EACCES') {
173
+ result.unreadablePaths.push(authPath);
174
+ }
175
+ }
176
+ }
177
+ }
178
+ catch {
179
+ // Agents dir doesn't exist
180
+ }
181
+ // Scan OAuth tokens
182
+ const oauthPath = join(openclawPath, 'credentials', 'oauth.json');
183
+ try {
184
+ const content = await readFile(oauthPath, 'utf-8');
185
+ const data = JSON.parse(content);
186
+ const permWarning = await checkFilePermissions(oauthPath);
187
+ if (permWarning)
188
+ result.permissionWarnings.push(permWarning);
189
+ for (const [provider, tokens] of Object.entries(data)) {
190
+ if (tokens.access_token) {
191
+ const isExpired = tokens.expires_at && tokens.expires_at * 1000 < Date.now();
192
+ result.credentials.push({
193
+ name: `${capitalizeFirst(provider)} OAuth (${tokens.access_token.substring(0, 10)}...${isExpired ? ' EXPIRED' : ''})`,
194
+ type: 'OAuth Token',
195
+ visaType: 'Access Visa',
196
+ riskLevel: 'MEDIUM',
197
+ filePath: oauthPath,
198
+ rawValue: tokens.access_token,
199
+ metadata: { provider, hasRefreshToken: !!tokens.refresh_token, isExpired },
200
+ });
201
+ }
202
+ }
203
+ }
204
+ catch {
205
+ // OAuth file doesn't exist
206
+ }
207
+ // Scan openclaw.json for channel tokens
208
+ const configPath = join(openclawPath, 'openclaw.json');
209
+ try {
210
+ const content = await readFile(configPath, 'utf-8');
211
+ const data = JSON.parse(content);
212
+ const permWarning = await checkFilePermissions(configPath);
213
+ if (permWarning)
214
+ result.permissionWarnings.push(permWarning);
215
+ // Telegram
216
+ if (data.channels?.telegram?.token) {
217
+ const token = data.channels.telegram.token;
218
+ const preview = token.substring(0, 7) + '...' + token.substring(token.length - 4);
219
+ result.credentials.push({
220
+ name: `Telegram Bot Token (${preview})`,
221
+ type: 'Bot Token',
222
+ visaType: 'Access Visa',
223
+ riskLevel: 'HIGH',
224
+ filePath: configPath,
225
+ rawValue: token,
226
+ metadata: { channel: 'telegram' },
227
+ });
228
+ }
229
+ // Slack
230
+ if (data.channels?.slack?.botToken) {
231
+ const token = data.channels.slack.botToken;
232
+ const preview = token.substring(0, 10) + '...' + token.substring(token.length - 4);
233
+ result.credentials.push({
234
+ name: `Slack Bot Token (${preview})`,
235
+ type: 'Bot Token',
236
+ visaType: 'Access Visa',
237
+ riskLevel: 'HIGH',
238
+ filePath: configPath,
239
+ rawValue: token,
240
+ metadata: { channel: 'slack' },
241
+ });
242
+ }
243
+ if (data.channels?.slack?.appToken) {
244
+ const token = data.channels.slack.appToken;
245
+ const preview = token.substring(0, 10) + '...' + token.substring(token.length - 4);
246
+ result.credentials.push({
247
+ name: `Slack App Token (${preview})`,
248
+ type: 'Bot Token',
249
+ visaType: 'Access Visa',
250
+ riskLevel: 'HIGH',
251
+ filePath: configPath,
252
+ rawValue: token,
253
+ metadata: { channel: 'slack' },
254
+ });
255
+ }
256
+ // Discord
257
+ if (data.channels?.discord?.token) {
258
+ const token = data.channels.discord.token;
259
+ const preview = token.substring(0, 10) + '...' + token.substring(token.length - 4);
260
+ result.credentials.push({
261
+ name: `Discord Bot Token (${preview})`,
262
+ type: 'Bot Token',
263
+ visaType: 'Access Visa',
264
+ riskLevel: 'HIGH',
265
+ filePath: configPath,
266
+ rawValue: token,
267
+ metadata: { channel: 'discord' },
268
+ });
269
+ }
270
+ // Gateway token
271
+ if (data.gateway?.token) {
272
+ const token = data.gateway.token;
273
+ const preview = token.substring(0, 6) + '...' + token.substring(token.length - 4);
274
+ result.credentials.push({
275
+ name: `Gateway Token (${preview})`,
276
+ type: 'Auth Token',
277
+ visaType: 'Privilege',
278
+ riskLevel: 'HIGH',
279
+ filePath: configPath,
280
+ rawValue: token,
281
+ metadata: { gatewayPort: data.gateway.port },
282
+ });
283
+ }
284
+ }
285
+ catch {
286
+ // Config file doesn't exist
287
+ }
288
+ // Scan allowlists
289
+ const credsPath = join(openclawPath, 'credentials');
290
+ try {
291
+ const files = await readdir(credsPath);
292
+ for (const file of files) {
293
+ if (file.endsWith('-allowFrom.json')) {
294
+ const filePath = join(credsPath, file);
295
+ try {
296
+ const content = await readFile(filePath, 'utf-8');
297
+ const data = JSON.parse(content);
298
+ const channel = file.replace('-allowFrom.json', '');
299
+ const pairedCount = Object.keys(data).length;
300
+ result.credentials.push({
301
+ name: `Pairing: ${channel} (${pairedCount} paired users)`,
302
+ type: 'Allowlist',
303
+ visaType: 'Access Visa',
304
+ riskLevel: 'LOW',
305
+ filePath,
306
+ rawValue: content,
307
+ metadata: { channel, pairedCount },
308
+ });
309
+ }
310
+ catch {
311
+ // Can't read file
312
+ }
313
+ }
314
+ }
315
+ }
316
+ catch {
317
+ // Credentials dir doesn't exist
318
+ }
319
+ return result;
320
+ }
321
+ function capitalizeFirst(str) {
322
+ return str.charAt(0).toUpperCase() + str.slice(1);
323
+ }
324
+ /**
325
+ * Display OpenClaw scan results in table format
326
+ */
327
+ function displayOpenClawResults(result) {
328
+ console.log();
329
+ console.log(chalk.cyan.bold(`🔍 OpenClaw Installation Detected at ~/.openclaw/`));
330
+ console.log();
331
+ if (result.credentials.length === 0) {
332
+ success('No credentials found in OpenClaw installation.');
333
+ return;
334
+ }
335
+ // Sort by risk level
336
+ const riskOrder = { CRITICAL: 0, HIGH: 1, MEDIUM: 2, LOW: 3 };
337
+ result.credentials.sort((a, b) => riskOrder[a.riskLevel] - riskOrder[b.riskLevel]);
338
+ // Calculate column widths
339
+ const maxCredLen = Math.max(35, ...result.credentials.map(c => c.name.length));
340
+ const maxTypeLen = Math.max(13, ...result.credentials.map(c => c.type.length));
341
+ const maxVisaLen = Math.max(12, ...result.credentials.map(c => c.visaType.length));
342
+ // Header
343
+ const headerLine = '┌' + '─'.repeat(maxCredLen + 2) + '┬' + '─'.repeat(maxTypeLen + 2) + '┬' + '─'.repeat(maxVisaLen + 2) + '┬' + '─'.repeat(13) + '┐';
344
+ const headerRow = '│ ' + 'Credential'.padEnd(maxCredLen) + ' │ ' + 'Type'.padEnd(maxTypeLen) + ' │ ' + 'Visa Type'.padEnd(maxVisaLen) + ' │ ' + 'Risk Level'.padEnd(11) + ' │';
345
+ const separatorLine = '├' + '─'.repeat(maxCredLen + 2) + '┼' + '─'.repeat(maxTypeLen + 2) + '┼' + '─'.repeat(maxVisaLen + 2) + '┼' + '─'.repeat(13) + '┤';
346
+ const footerLine = '└' + '─'.repeat(maxCredLen + 2) + '┴' + '─'.repeat(maxTypeLen + 2) + '┴' + '─'.repeat(maxVisaLen + 2) + '┴' + '─'.repeat(13) + '┘';
347
+ console.log(headerLine);
348
+ console.log(headerRow);
349
+ console.log(separatorLine);
350
+ // Rows
351
+ for (const cred of result.credentials) {
352
+ const riskEmoji = cred.riskLevel === 'CRITICAL' ? '🔴' :
353
+ cred.riskLevel === 'HIGH' ? '⚠️ ' :
354
+ cred.riskLevel === 'MEDIUM' ? '⚠️ ' : 'ℹ️ ';
355
+ const riskColor = cred.riskLevel === 'CRITICAL' ? chalk.red :
356
+ cred.riskLevel === 'HIGH' ? chalk.yellow :
357
+ cred.riskLevel === 'MEDIUM' ? chalk.yellow : chalk.blue;
358
+ const row = '│ ' +
359
+ cred.name.padEnd(maxCredLen) + ' │ ' +
360
+ cred.type.padEnd(maxTypeLen) + ' │ ' +
361
+ cred.visaType.padEnd(maxVisaLen) + ' │ ' +
362
+ riskEmoji + riskColor(cred.riskLevel.padEnd(8)) + ' │';
363
+ console.log(row);
364
+ }
365
+ // Summary row
366
+ const criticalCount = result.credentials.filter(c => c.riskLevel === 'CRITICAL').length;
367
+ const governed = 0; // Will be updated when we check against vault
368
+ console.log(separatorLine);
369
+ const summaryRow = '│ ' +
370
+ `${result.credentials.length} credentials found`.padEnd(maxCredLen) + ' │ ' +
371
+ `${governed} governed`.padEnd(maxTypeLen) + ' │ ' +
372
+ `0 passports`.padEnd(maxVisaLen) + ' │ ' +
373
+ chalk.red(`${criticalCount} CRITICAL`.padEnd(11)) + ' │';
374
+ console.log(summaryRow);
375
+ console.log(footerLine);
376
+ // Permission warnings
377
+ if (result.permissionWarnings.length > 0) {
378
+ console.log();
379
+ warning('Permission Issues Detected:');
380
+ for (const warn of result.permissionWarnings) {
381
+ console.log(chalk.yellow(` ⚠ ${warn}`));
382
+ }
383
+ }
384
+ // Unreadable paths
385
+ if (result.unreadablePaths.length > 0) {
386
+ console.log();
387
+ warning('Unreadable Paths (permission denied):');
388
+ for (const path of result.unreadablePaths) {
389
+ console.log(chalk.red(` ✗ ${path}`));
390
+ }
391
+ }
392
+ console.log();
393
+ console.log(chalk.cyan(`⚡ Import all into ID Wispera? Run: ${chalk.bold('idw import --format openclaw')}`));
394
+ }
395
+ /**
396
+ * Recursively scan a directory for files
397
+ */
398
+ export async function* walkDirectory(dir, onUnreadable) {
399
+ try {
400
+ const entries = await readdir(dir, { withFileTypes: true });
401
+ for (const entry of entries) {
402
+ const fullPath = join(dir, entry.name);
403
+ if (entry.isDirectory()) {
404
+ if (!SKIP_DIRS.has(entry.name) && !entry.name.startsWith('.')) {
405
+ yield* walkDirectory(fullPath, onUnreadable);
406
+ }
407
+ }
408
+ else if (entry.isFile()) {
409
+ const ext = extname(entry.name).toLowerCase();
410
+ if (!SKIP_EXTENSIONS.has(ext)) {
411
+ yield fullPath;
412
+ }
413
+ }
414
+ }
415
+ }
416
+ catch (err) {
417
+ if (err.code === 'EACCES' && onUnreadable) {
418
+ onUnreadable(dir, 'Permission denied');
419
+ }
420
+ }
421
+ }
422
+ /**
423
+ * Scan a file for credentials
424
+ */
425
+ export async function scanFile(filePath) {
426
+ try {
427
+ const stats = await stat(filePath);
428
+ // Skip large files (> 1MB)
429
+ if (stats.size > 1024 * 1024) {
430
+ return [];
431
+ }
432
+ const content = await readFile(filePath, 'utf-8');
433
+ // Quick check before full scan
434
+ if (!mightContainCredentials(content)) {
435
+ return [];
436
+ }
437
+ return detectCredentials(content);
438
+ }
439
+ catch {
440
+ // Ignore files that can't be read
441
+ return [];
442
+ }
443
+ }
444
+ /**
445
+ * Scan known credential locations (system scan)
446
+ */
447
+ async function scanKnownLocations(providers, onUnreadable) {
448
+ const home = homedir();
449
+ const allResults = [];
450
+ for (const loc of KNOWN_LOCATIONS) {
451
+ // Filter by provider if specified
452
+ if (providers && !providers.some(p => loc.name.toLowerCase().includes(p.toLowerCase()))) {
453
+ continue;
454
+ }
455
+ const fullPath = join(home, loc.path);
456
+ try {
457
+ await access(fullPath);
458
+ if (loc.isFile) {
459
+ const results = await scanFile(fullPath);
460
+ if (results.length > 0) {
461
+ allResults.push({ file: fullPath, results });
462
+ }
463
+ }
464
+ else {
465
+ for await (const filePath of walkDirectory(fullPath, onUnreadable)) {
466
+ const results = await scanFile(filePath);
467
+ if (results.length > 0) {
468
+ allResults.push({ file: filePath, results });
469
+ }
470
+ }
471
+ }
472
+ }
473
+ catch (err) {
474
+ if (err.code === 'EACCES') {
475
+ onUnreadable(fullPath, 'Permission denied');
476
+ }
477
+ // ENOENT is fine - location doesn't exist
478
+ }
479
+ }
480
+ return allResults;
481
+ }
482
+ export function createScanCommand() {
483
+ const command = new Command('scan')
484
+ .description('Scan for exposed credentials')
485
+ .argument('[path]', 'Path to scan (default: current directory)')
486
+ .option('-s, --system', 'Scan all known credential locations (fast)')
487
+ .option('-f, --full', 'Scan entire home directory (thorough but slower)')
488
+ .option('-p, --providers <list>', 'Scan specific providers (comma-separated: openclaw,aws,ssh)')
489
+ .option('-v, --verbose', 'Show all files scanned')
490
+ .option('--output <file>', 'Save results to JSON file')
491
+ .option('--min-confidence <level>', 'Minimum confidence threshold (0-1)', '0.5')
492
+ .option('--include-low', 'Include low confidence results')
493
+ .action(async (scanPath, options) => {
494
+ const minConfidence = parseFloat(options.minConfidence);
495
+ const unreadablePaths = [];
496
+ const onUnreadable = (path, error) => {
497
+ unreadablePaths.push({ path, error });
498
+ };
499
+ console.log();
500
+ // Determine scan mode
501
+ const providerList = options.providers ? options.providers.split(',').map((p) => p.trim()) : null;
502
+ // OpenClaw-specific scan
503
+ if (providerList?.includes('openclaw') || options.system) {
504
+ const openclawResult = await scanOpenClawCredentials();
505
+ if (openclawResult.installed) {
506
+ displayOpenClawResults(openclawResult);
507
+ // If only scanning OpenClaw, we're done
508
+ if (providerList?.length === 1 && providerList[0] === 'openclaw') {
509
+ return;
510
+ }
511
+ }
512
+ else if (providerList?.includes('openclaw')) {
513
+ info('OpenClaw is not installed at ~/.openclaw/');
514
+ }
515
+ }
516
+ // System-wide scan of known locations
517
+ if (options.system && !scanPath) {
518
+ title('System Credential Scan');
519
+ info('Scanning known credential locations...');
520
+ console.log();
521
+ const spinner = ora('Scanning known locations...').start();
522
+ const allResults = await scanKnownLocations(providerList, onUnreadable);
523
+ spinner.stop();
524
+ if (allResults.length === 0) {
525
+ success('No additional credentials detected in known locations.');
526
+ }
527
+ else {
528
+ displayGenericResults(allResults, unreadablePaths, minConfidence, options);
529
+ }
530
+ // Show unreadable paths
531
+ if (unreadablePaths.length > 0) {
532
+ console.log();
533
+ warning('Unreadable Paths:');
534
+ for (const { path, error } of unreadablePaths) {
535
+ console.log(chalk.red(` ✗ ${path}: ${error}`));
536
+ }
537
+ }
538
+ return;
539
+ }
540
+ // Full home directory scan
541
+ if (options.full) {
542
+ const home = homedir();
543
+ title('Full Home Directory Scan');
544
+ console.log(chalk.yellow('⚠ This will scan your entire home directory and may take a while.'));
545
+ info(`Path: ${home}`);
546
+ console.log();
547
+ const spinner = ora('Scanning files...').start();
548
+ const allResults = [];
549
+ let filesScanned = 0;
550
+ for await (const filePath of walkDirectory(home, onUnreadable)) {
551
+ filesScanned++;
552
+ if (filesScanned % 500 === 0) {
553
+ spinner.text = `Scanned ${filesScanned} files...`;
554
+ }
555
+ const results = await scanFile(filePath);
556
+ const filteredResults = options.includeLow
557
+ ? results
558
+ : results.filter(r => r.confidence >= minConfidence);
559
+ if (filteredResults.length > 0) {
560
+ allResults.push({ file: filePath, results: filteredResults });
561
+ }
562
+ }
563
+ spinner.stop();
564
+ console.log(chalk.dim(`Scanned ${filesScanned} files`));
565
+ displayGenericResults(allResults, unreadablePaths, minConfidence, options);
566
+ // Show unreadable paths
567
+ if (unreadablePaths.length > 0) {
568
+ console.log();
569
+ warning(`${unreadablePaths.length} paths were unreadable (permission denied)`);
570
+ if (options.verbose) {
571
+ for (const { path } of unreadablePaths) {
572
+ console.log(chalk.red(` ✗ ${path}`));
573
+ }
574
+ }
575
+ }
576
+ return;
577
+ }
578
+ // Default: scan specified path or current directory
579
+ // Use resolve() to handle both relative and absolute paths correctly
580
+ const absolutePath = scanPath ? resolve(scanPath) : process.cwd();
581
+ title('Scanning for Credentials');
582
+ info(`Path: ${absolutePath}`);
583
+ console.log();
584
+ const spinner = ora('Scanning files...').start();
585
+ const allResults = [];
586
+ let filesScanned = 0;
587
+ let filesWithCredentials = 0;
588
+ try {
589
+ const pathStats = await stat(absolutePath);
590
+ if (pathStats.isFile()) {
591
+ const results = await scanFile(absolutePath);
592
+ if (results.length > 0) {
593
+ allResults.push({ file: absolutePath, results });
594
+ filesWithCredentials++;
595
+ }
596
+ filesScanned = 1;
597
+ }
598
+ else {
599
+ for await (const filePath of walkDirectory(absolutePath, onUnreadable)) {
600
+ filesScanned++;
601
+ if (options.verbose) {
602
+ spinner.text = `Scanning: ${relative(absolutePath, filePath)}`;
603
+ }
604
+ else if (filesScanned % 100 === 0) {
605
+ spinner.text = `Scanned ${filesScanned} files...`;
606
+ }
607
+ const results = await scanFile(filePath);
608
+ const filteredResults = options.includeLow
609
+ ? results
610
+ : results.filter(r => r.confidence >= minConfidence);
611
+ if (filteredResults.length > 0) {
612
+ allResults.push({ file: filePath, results: filteredResults });
613
+ filesWithCredentials++;
614
+ }
615
+ }
616
+ }
617
+ spinner.stop();
618
+ console.log(chalk.dim(`Scanned ${filesScanned} files`));
619
+ console.log();
620
+ if (allResults.length === 0) {
621
+ success('No credentials detected!');
622
+ }
623
+ else {
624
+ displayGenericResults(allResults, unreadablePaths, minConfidence, options, absolutePath);
625
+ }
626
+ // Show unreadable paths
627
+ if (unreadablePaths.length > 0) {
628
+ console.log();
629
+ warning('Unreadable Paths:');
630
+ for (const { path, error } of unreadablePaths) {
631
+ console.log(chalk.red(` ✗ ${path}: ${error}`));
632
+ }
633
+ }
634
+ // Export if requested
635
+ if (options.output && allResults.length > 0) {
636
+ const exportData = {
637
+ scannedAt: new Date().toISOString(),
638
+ path: absolutePath,
639
+ filesScanned,
640
+ results: allResults.map(({ file, results }) => ({
641
+ file: relative(absolutePath, file),
642
+ detections: results.map(r => ({
643
+ type: r.type,
644
+ line: r.line,
645
+ column: r.column,
646
+ confidence: r.confidence,
647
+ pattern: r.pattern,
648
+ })),
649
+ })),
650
+ };
651
+ const fs = await import('fs/promises');
652
+ await fs.writeFile(options.output, JSON.stringify(exportData, null, 2), 'utf-8');
653
+ console.log();
654
+ info(`Results saved to ${options.output}`);
655
+ }
656
+ console.log();
657
+ info('Run `idw import <file>` to import detected credentials as passports.');
658
+ }
659
+ catch (err) {
660
+ spinner.fail('Scan failed');
661
+ error(err instanceof Error ? err.message : 'Unknown error');
662
+ process.exit(1);
663
+ }
664
+ });
665
+ return command;
666
+ }
667
+ /**
668
+ * Display generic scan results
669
+ */
670
+ function displayGenericResults(allResults, _unreadablePaths, _minConfidence, _options, basePath) {
671
+ if (allResults.length === 0) {
672
+ success('No credentials detected!');
673
+ return;
674
+ }
675
+ warning(`Found potential credentials in ${allResults.length} file(s):`);
676
+ console.log();
677
+ let totalCredentials = 0;
678
+ for (const { file, results } of allResults) {
679
+ const displayPath = basePath ? relative(basePath, file) || file : file;
680
+ console.log(chalk.bold(chalk.cyan(displayPath)));
681
+ console.log(displayDetectionResults(results, displayPath));
682
+ totalCredentials += results.length;
683
+ }
684
+ // Summary
685
+ console.log();
686
+ title('Summary');
687
+ const allDetections = allResults.flatMap(r => r.results);
688
+ const stats = getDetectionStats(allDetections);
689
+ console.log(` Total detections: ${chalk.yellow(stats.total)}`);
690
+ console.log(` High confidence: ${chalk.red(stats.highConfidence)}`);
691
+ console.log(` Files affected: ${chalk.yellow(allResults.length)}`);
692
+ console.log();
693
+ console.log(' By type:');
694
+ for (const [type, count] of Object.entries(stats.byType)) {
695
+ if (count > 0) {
696
+ console.log(` ${type}: ${count}`);
697
+ }
698
+ }
699
+ }
700
+ //# sourceMappingURL=scan.js.map