@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.
- package/README.md +250 -0
- package/dist/commands/audit.d.ts +6 -0
- package/dist/commands/audit.d.ts.map +1 -0
- package/dist/commands/audit.js +82 -0
- package/dist/commands/audit.js.map +1 -0
- package/dist/commands/auth.d.ts +7 -0
- package/dist/commands/auth.d.ts.map +1 -0
- package/dist/commands/auth.js +310 -0
- package/dist/commands/auth.js.map +1 -0
- package/dist/commands/create.d.ts +6 -0
- package/dist/commands/create.d.ts.map +1 -0
- package/dist/commands/create.js +88 -0
- package/dist/commands/create.js.map +1 -0
- package/dist/commands/exec.d.ts +8 -0
- package/dist/commands/exec.d.ts.map +1 -0
- package/dist/commands/exec.js +163 -0
- package/dist/commands/exec.js.map +1 -0
- package/dist/commands/import.d.ts +7 -0
- package/dist/commands/import.d.ts.map +1 -0
- package/dist/commands/import.js +1166 -0
- package/dist/commands/import.js.map +1 -0
- package/dist/commands/init.d.ts +6 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +50 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/list.d.ts +6 -0
- package/dist/commands/list.d.ts.map +1 -0
- package/dist/commands/list.js +91 -0
- package/dist/commands/list.js.map +1 -0
- package/dist/commands/migrate.d.ts +7 -0
- package/dist/commands/migrate.d.ts.map +1 -0
- package/dist/commands/migrate.js +105 -0
- package/dist/commands/migrate.js.map +1 -0
- package/dist/commands/provision.d.ts +7 -0
- package/dist/commands/provision.d.ts.map +1 -0
- package/dist/commands/provision.js +303 -0
- package/dist/commands/provision.js.map +1 -0
- package/dist/commands/revoke.d.ts +6 -0
- package/dist/commands/revoke.d.ts.map +1 -0
- package/dist/commands/revoke.js +70 -0
- package/dist/commands/revoke.js.map +1 -0
- package/dist/commands/scan.d.ts +16 -0
- package/dist/commands/scan.d.ts.map +1 -0
- package/dist/commands/scan.js +700 -0
- package/dist/commands/scan.js.map +1 -0
- package/dist/commands/share.d.ts +6 -0
- package/dist/commands/share.d.ts.map +1 -0
- package/dist/commands/share.js +144 -0
- package/dist/commands/share.js.map +1 -0
- package/dist/commands/show.d.ts +6 -0
- package/dist/commands/show.d.ts.map +1 -0
- package/dist/commands/show.js +64 -0
- package/dist/commands/show.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +76 -0
- package/dist/index.js.map +1 -0
- package/dist/utils/display.d.ts +78 -0
- package/dist/utils/display.d.ts.map +1 -0
- package/dist/utils/display.js +290 -0
- package/dist/utils/display.js.map +1 -0
- package/dist/utils/prompts.d.ts +67 -0
- package/dist/utils/prompts.d.ts.map +1 -0
- package/dist/utils/prompts.js +353 -0
- package/dist/utils/prompts.js.map +1 -0
- package/dist/utils/vault-helpers.d.ts +21 -0
- package/dist/utils/vault-helpers.d.ts.map +1 -0
- package/dist/utils/vault-helpers.js +45 -0
- package/dist/utils/vault-helpers.js.map +1 -0
- 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
|