pal-explorer-cli 0.4.12 → 0.4.14

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 (100) hide show
  1. package/README.md +149 -149
  2. package/bin/pal.js +77 -4
  3. package/extensions/@palexplorer/analytics/extension.json +20 -1
  4. package/extensions/@palexplorer/analytics/index.js +19 -9
  5. package/extensions/@palexplorer/audit/extension.json +14 -0
  6. package/extensions/@palexplorer/auth-email/extension.json +15 -0
  7. package/extensions/@palexplorer/auth-oauth/extension.json +15 -0
  8. package/extensions/@palexplorer/chat/extension.json +14 -0
  9. package/extensions/@palexplorer/discovery/extension.json +17 -0
  10. package/extensions/@palexplorer/discovery/index.js +1 -1
  11. package/extensions/@palexplorer/email-notifications/extension.json +23 -0
  12. package/extensions/@palexplorer/groups/extension.json +15 -0
  13. package/extensions/@palexplorer/share-links/extension.json +15 -0
  14. package/extensions/@palexplorer/sync/extension.json +16 -0
  15. package/extensions/@palexplorer/user-mgmt/extension.json +15 -0
  16. package/lib/capabilities.js +24 -24
  17. package/lib/commands/analytics.js +175 -175
  18. package/lib/commands/api-keys.js +131 -131
  19. package/lib/commands/audit.js +235 -235
  20. package/lib/commands/auth.js +137 -137
  21. package/lib/commands/backup.js +76 -76
  22. package/lib/commands/billing.js +148 -148
  23. package/lib/commands/chat.js +217 -217
  24. package/lib/commands/cloud-backup.js +231 -231
  25. package/lib/commands/comment.js +99 -99
  26. package/lib/commands/completion.js +265 -203
  27. package/lib/commands/compliance.js +218 -218
  28. package/lib/commands/config.js +136 -136
  29. package/lib/commands/connect.js +44 -44
  30. package/lib/commands/dept.js +294 -294
  31. package/lib/commands/device.js +146 -146
  32. package/lib/commands/download.js +240 -226
  33. package/lib/commands/explorer.js +178 -178
  34. package/lib/commands/extension.js +1060 -970
  35. package/lib/commands/favorite.js +90 -90
  36. package/lib/commands/federation.js +270 -270
  37. package/lib/commands/file.js +533 -533
  38. package/lib/commands/group.js +271 -271
  39. package/lib/commands/gui-share.js +29 -29
  40. package/lib/commands/init.js +61 -61
  41. package/lib/commands/invite.js +59 -59
  42. package/lib/commands/list.js +58 -58
  43. package/lib/commands/log.js +116 -116
  44. package/lib/commands/nearby.js +108 -108
  45. package/lib/commands/network.js +251 -251
  46. package/lib/commands/notify.js +198 -198
  47. package/lib/commands/org.js +273 -273
  48. package/lib/commands/pal.js +403 -180
  49. package/lib/commands/permissions.js +216 -216
  50. package/lib/commands/pin.js +97 -97
  51. package/lib/commands/protocol.js +357 -357
  52. package/lib/commands/rbac.js +147 -147
  53. package/lib/commands/recover.js +36 -36
  54. package/lib/commands/register.js +171 -171
  55. package/lib/commands/relay.js +131 -131
  56. package/lib/commands/remote.js +368 -368
  57. package/lib/commands/revoke.js +50 -50
  58. package/lib/commands/scanner.js +280 -280
  59. package/lib/commands/schedule.js +344 -344
  60. package/lib/commands/scim.js +203 -203
  61. package/lib/commands/search.js +181 -181
  62. package/lib/commands/serve.js +438 -438
  63. package/lib/commands/server.js +350 -350
  64. package/lib/commands/share-link.js +199 -199
  65. package/lib/commands/share.js +336 -323
  66. package/lib/commands/sso.js +200 -200
  67. package/lib/commands/status.js +145 -145
  68. package/lib/commands/stream.js +562 -562
  69. package/lib/commands/su.js +187 -187
  70. package/lib/commands/sync.js +979 -979
  71. package/lib/commands/transfers.js +152 -152
  72. package/lib/commands/uninstall.js +188 -188
  73. package/lib/commands/update.js +204 -204
  74. package/lib/commands/user.js +276 -276
  75. package/lib/commands/vfs.js +84 -84
  76. package/lib/commands/web-login.js +79 -79
  77. package/lib/commands/web.js +52 -52
  78. package/lib/commands/webhook.js +180 -180
  79. package/lib/commands/whoami.js +59 -59
  80. package/lib/commands/workspace.js +121 -121
  81. package/lib/core/billing.js +16 -5
  82. package/lib/core/dhtDiscovery.js +9 -2
  83. package/lib/core/discoveryClient.js +13 -7
  84. package/lib/core/extensions.js +142 -1
  85. package/lib/core/identity.js +33 -2
  86. package/lib/core/imageProcessor.js +109 -0
  87. package/lib/core/imageTorrent.js +167 -0
  88. package/lib/core/permissions.js +1 -1
  89. package/lib/core/pro.js +11 -4
  90. package/lib/core/serverList.js +4 -1
  91. package/lib/core/shares.js +12 -1
  92. package/lib/core/signalingServer.js +14 -2
  93. package/lib/core/su.js +1 -1
  94. package/lib/core/users.js +1 -1
  95. package/lib/protocol/messages.js +12 -3
  96. package/lib/utils/explorer.js +1 -1
  97. package/lib/utils/fuzzy.js +47 -0
  98. package/lib/utils/help.js +357 -357
  99. package/lib/utils/torrent.js +1 -0
  100. package/package.json +4 -3
@@ -1,50 +1,50 @@
1
- import chalk from 'chalk';
2
- import { removeShare, rotateShareKey, removeRecipientFromShare, listShares } from '../core/shares.js';
3
-
4
- export default function revokeCommand(program) {
5
- program
6
- .command('revoke <idOrPath>')
7
- .description('revoke access to a shared resource or remove a specific recipient')
8
- .option('--pal <palId>', 'Remove a specific pal from the share (triggers key rotation)')
9
- .addHelpText('after', `
10
- Examples:
11
- $ pe revoke abc123 Revoke share by ID
12
- $ pe revoke ./photos Revoke share by path
13
- $ pe revoke abc123 --pal alice Remove one recipient (rotates key)
14
- `)
15
- .action(async (idOrPath, opts) => {
16
- try {
17
- if (opts.pal) {
18
- // Remove a specific recipient and rotate keys
19
- const shares = listShares();
20
- const share = shares.find(s => s.id === idOrPath || s.path === idOrPath);
21
- if (!share) {
22
- console.error(chalk.red('Share not found.'));
23
- process.exitCode = 1;
24
- return;
25
- }
26
-
27
- removeRecipientFromShare(share.id, opts.pal);
28
- console.log(chalk.yellow(`Removed ${opts.pal} from share ${share.id}`));
29
-
30
- if (share.visibility === 'private' && (share.recipients || []).length > 0) {
31
- console.log(chalk.blue('Rotating share key and re-encrypting...'));
32
- const { newEncryptedShareKeys } = await rotateShareKey(share.id);
33
- const remaining = Object.keys(newEncryptedShareKeys).length;
34
- console.log(chalk.green(`Key rotated. ${remaining} recipient(s) will need new magnet on next serve.`));
35
- }
36
- return;
37
- }
38
-
39
- // Full revoke
40
- const removed = removeShare(idOrPath);
41
- if (removed) {
42
- console.log(chalk.green(`Successfully revoked share: ${idOrPath}`));
43
- } else {
44
- console.error(chalk.red('Share not found.'));
45
- }
46
- } catch (err) {
47
- console.error(chalk.red(`Error: ${err.message}`));
48
- }
49
- });
50
- }
1
+ import chalk from 'chalk';
2
+ import { removeShare, rotateShareKey, removeRecipientFromShare, listShares } from '../core/shares.js';
3
+
4
+ export default function revokeCommand(program) {
5
+ program
6
+ .command('revoke <idOrPath>')
7
+ .description('revoke access to a shared resource or remove a specific recipient')
8
+ .option('--pal <palId>', 'Remove a specific pal from the share (triggers key rotation)')
9
+ .addHelpText('after', `
10
+ Examples:
11
+ $ pal revoke abc123 Revoke share by ID
12
+ $ pal revoke ./photos Revoke share by path
13
+ $ pal revoke abc123 --pal alice Remove one recipient (rotates key)
14
+ `)
15
+ .action(async (idOrPath, opts) => {
16
+ try {
17
+ if (opts.pal) {
18
+ // Remove a specific recipient and rotate keys
19
+ const shares = listShares();
20
+ const share = shares.find(s => s.id === idOrPath || s.path === idOrPath);
21
+ if (!share) {
22
+ console.error(chalk.red('Share not found.'));
23
+ process.exitCode = 1;
24
+ return;
25
+ }
26
+
27
+ removeRecipientFromShare(share.id, opts.pal);
28
+ console.log(chalk.yellow(`Removed ${opts.pal} from share ${share.id}`));
29
+
30
+ if (share.visibility === 'private' && (share.recipients || []).length > 0) {
31
+ console.log(chalk.blue('Rotating share key and re-encrypting...'));
32
+ const { newEncryptedShareKeys } = await rotateShareKey(share.id);
33
+ const remaining = Object.keys(newEncryptedShareKeys).length;
34
+ console.log(chalk.green(`Key rotated. ${remaining} recipient(s) will need new magnet on next serve.`));
35
+ }
36
+ return;
37
+ }
38
+
39
+ // Full revoke
40
+ const removed = removeShare(idOrPath);
41
+ if (removed) {
42
+ console.log(chalk.green(`Successfully revoked share: ${idOrPath}`));
43
+ } else {
44
+ console.error(chalk.red('Share not found.'));
45
+ }
46
+ } catch (err) {
47
+ console.error(chalk.red(`Error: ${err.message}`));
48
+ }
49
+ });
50
+ }
@@ -1,280 +1,280 @@
1
- import chalk from 'chalk';
2
- import fs from 'fs';
3
- import path from 'path';
4
-
5
- const BUILTIN_RULES = [
6
- { name: 'SSN', pattern: '\\d{3}-\\d{2}-\\d{4}', severity: 'critical', description: 'Social Security Number' },
7
- { name: 'CreditCard', pattern: '4\\d{3}[\\s-]?\\d{4}[\\s-]?\\d{4}[\\s-]?\\d{4}', severity: 'critical', description: 'Credit card number (Visa)' },
8
- { name: 'CreditCardMC', pattern: '5[1-5]\\d{2}[\\s-]?\\d{4}[\\s-]?\\d{4}[\\s-]?\\d{4}', severity: 'critical', description: 'Credit card number (Mastercard)' },
9
- { name: 'APIKey', pattern: '(?:sk_|pk_|api_|key_)[a-zA-Z0-9]{16,}', severity: 'high', description: 'API key' },
10
- { name: 'PrivateKey', pattern: '-----BEGIN PRIVATE KEY-----', severity: 'critical', description: 'Private key block' },
11
- { name: 'AWSKey', pattern: 'AKIA[0-9A-Z]{16}', severity: 'critical', description: 'AWS access key' },
12
- { name: 'Email', pattern: '\\b[\\w.-]+@[\\w.-]+\\.\\w{2,}\\b', severity: 'low', description: 'Email address' },
13
- ];
14
-
15
- const SEVERITY_ORDER = { critical: 4, high: 3, medium: 2, low: 1 };
16
- const SEVERITY_COLORS = {
17
- critical: chalk.red,
18
- high: chalk.yellow,
19
- medium: chalk.cyan,
20
- low: chalk.gray,
21
- };
22
-
23
- function classifyFile(maxSeverity) {
24
- if (maxSeverity === 'critical') return 'Restricted';
25
- if (maxSeverity === 'high') return 'Confidential';
26
- if (maxSeverity === 'medium') return 'Internal';
27
- return 'Public';
28
- }
29
-
30
- function scanContent(content, rules) {
31
- const findings = [];
32
- const lines = content.split('\n');
33
- for (const rule of rules) {
34
- const re = new RegExp(rule.pattern, 'g');
35
- for (let i = 0; i < lines.length; i++) {
36
- let match;
37
- while ((match = re.exec(lines[i])) !== null) {
38
- findings.push({
39
- rule: rule.name,
40
- severity: rule.severity,
41
- line: i + 1,
42
- match: match[0].length > 40 ? match[0].slice(0, 37) + '...' : match[0],
43
- });
44
- }
45
- }
46
- }
47
- return findings;
48
- }
49
-
50
- export default function scannerCommand(program) {
51
- const cmd = program
52
- .command('scanner')
53
- .description('content scanning and DLP (Enterprise)')
54
- .addHelpText('after', `
55
- Examples:
56
- $ pe scanner scan ./documents Scan directory for sensitive data
57
- $ pe scanner scan ./secrets.txt Scan a single file
58
- $ pe scanner rules List active DLP rules
59
- $ pe scanner rules --add MyRule "\\d{6}" high Add a custom rule
60
- $ pe scanner rules --remove MyRule Remove a custom rule
61
- $ pe scanner quarantine List quarantined files
62
- $ pe scanner release <id> Release file from quarantine
63
- `)
64
- .action(() => { cmd.outputHelp(); });
65
-
66
- cmd
67
- .command('scan <path>')
68
- .description('scan a file or directory for sensitive data')
69
- .action(async (targetPath) => {
70
- try {
71
- const extConfig = (await import('../utils/config.js')).default;
72
- const extSettings = extConfig.get('ext.content-scanner') || {};
73
- const customRules = extSettings.rules || [];
74
- const allRules = [...BUILTIN_RULES, ...customRules];
75
-
76
- const resolved = path.resolve(targetPath);
77
- if (!fs.existsSync(resolved)) {
78
- console.log(chalk.red(`Path not found: ${resolved}`));
79
- process.exitCode = 1;
80
- return;
81
- }
82
-
83
- const stat = fs.statSync(resolved);
84
- const files = [];
85
- if (stat.isDirectory()) {
86
- const entries = fs.readdirSync(resolved, { recursive: true });
87
- for (const entry of entries) {
88
- const full = path.join(resolved, entry);
89
- try {
90
- if (fs.statSync(full).isFile()) files.push(full);
91
- } catch { /* skip unreadable */ }
92
- }
93
- } else {
94
- files.push(resolved);
95
- }
96
-
97
- if (files.length === 0) {
98
- console.log(chalk.gray('No files to scan.'));
99
- return;
100
- }
101
-
102
- console.log(chalk.cyan(`Scanning ${files.length} file(s)...\n`));
103
- let totalFindings = 0;
104
- let maxSeverity = 'low';
105
-
106
- for (const file of files) {
107
- let content;
108
- try {
109
- content = fs.readFileSync(file, 'utf8');
110
- } catch {
111
- continue;
112
- }
113
-
114
- const findings = scanContent(content, allRules);
115
- if (findings.length === 0) continue;
116
-
117
- totalFindings += findings.length;
118
- const rel = path.relative(process.cwd(), file);
119
- console.log(` ${chalk.white(rel)}`);
120
-
121
- for (const f of findings) {
122
- const colorFn = SEVERITY_COLORS[f.severity] || chalk.white;
123
- console.log(` Line ${chalk.gray(f.line)}: ${colorFn(`[${f.severity.toUpperCase()}]`)} ${chalk.white(f.rule)} — ${chalk.dim(f.match)}`);
124
- if (SEVERITY_ORDER[f.severity] > SEVERITY_ORDER[maxSeverity]) {
125
- maxSeverity = f.severity;
126
- }
127
- }
128
- console.log('');
129
- }
130
-
131
- if (totalFindings === 0) {
132
- console.log(chalk.green('✔ No sensitive data found.'));
133
- console.log(` Classification: ${chalk.green('Public')}`);
134
- } else {
135
- const classification = classifyFile(maxSeverity);
136
- console.log(chalk.gray('─'.repeat(50)));
137
- console.log(` Total findings: ${chalk.white(totalFindings)}`);
138
- console.log(` Classification: ${SEVERITY_COLORS[maxSeverity](classification)}`);
139
- }
140
- } catch (err) {
141
- console.log(chalk.red(`Scan failed: ${err.message}`));
142
- process.exitCode = 1;
143
- }
144
- });
145
-
146
- cmd
147
- .command('rules')
148
- .description('list active DLP rules')
149
- .option('--add <name>', 'add a custom rule (requires <pattern> <severity> as args)')
150
- .option('--remove <name>', 'remove a custom rule')
151
- .action(async (opts, command) => {
152
- try {
153
- const extConfig = (await import('../utils/config.js')).default;
154
- const extSettings = extConfig.get('ext.content-scanner') || {};
155
-
156
- if (opts.remove) {
157
- const customRules = extSettings.rules || [];
158
- const idx = customRules.findIndex(r => r.name === opts.remove);
159
- if (idx === -1) {
160
- console.log(chalk.yellow(`Rule not found: ${opts.remove}`));
161
- process.exitCode = 1;
162
- return;
163
- }
164
- customRules.splice(idx, 1);
165
- extSettings.rules = customRules;
166
- extConfig.set('ext.content-scanner', extSettings);
167
- console.log(chalk.green(`✔ Rule removed: ${opts.remove}`));
168
- return;
169
- }
170
-
171
- if (opts.add) {
172
- const args = command.args;
173
- if (args.length < 2) {
174
- console.log(chalk.red('Usage: pe scanner rules --add <name> <pattern> <severity>'));
175
- process.exitCode = 1;
176
- return;
177
- }
178
- const pattern = args[0];
179
- try {
180
- const testRe = new RegExp(pattern);
181
- const start = Date.now();
182
- 'a'.repeat(25).match(testRe);
183
- if (Date.now() - start > 100) throw new Error('Pattern too complex');
184
- } catch (e) {
185
- console.error(chalk.red(`Invalid or unsafe regex pattern: ${e.message}`));
186
- process.exitCode = 1;
187
- return;
188
- }
189
- const severity = args[1] || 'medium';
190
- if (!['critical', 'high', 'medium', 'low'].includes(severity)) {
191
- console.log(chalk.red('Severity must be: critical, high, medium, or low'));
192
- process.exitCode = 1;
193
- return;
194
- }
195
- const customRules = extSettings.rules || [];
196
- if (customRules.find(r => r.name === opts.add)) {
197
- console.log(chalk.yellow(`Rule already exists: ${opts.add}`));
198
- process.exitCode = 1;
199
- return;
200
- }
201
- customRules.push({ name: opts.add, pattern, severity, description: `Custom rule: ${opts.add}` });
202
- extSettings.rules = customRules;
203
- extConfig.set('ext.content-scanner', extSettings);
204
- console.log(chalk.green(`✔ Rule added: ${opts.add}`));
205
- console.log(` Pattern: ${chalk.white(pattern)}`);
206
- console.log(` Severity: ${SEVERITY_COLORS[severity](severity)}`);
207
- return;
208
- }
209
-
210
- // List all rules
211
- const customRules = extSettings.rules || [];
212
- const allRules = [...BUILTIN_RULES, ...customRules];
213
- console.log(chalk.bold(`DLP Rules (${allRules.length})\n`));
214
- console.log(chalk.cyan(' Built-in:'));
215
- for (const r of BUILTIN_RULES) {
216
- const colorFn = SEVERITY_COLORS[r.severity] || chalk.white;
217
- console.log(` ${chalk.white(r.name)} ${colorFn(`[${r.severity}]`)} — ${chalk.dim(r.description)}`);
218
- }
219
- if (customRules.length > 0) {
220
- console.log('');
221
- console.log(chalk.cyan(' Custom:'));
222
- for (const r of customRules) {
223
- const colorFn = SEVERITY_COLORS[r.severity] || chalk.white;
224
- console.log(` ${chalk.white(r.name)} ${colorFn(`[${r.severity}]`)} — ${chalk.dim(r.pattern)}`);
225
- }
226
- }
227
- } catch (err) {
228
- console.log(chalk.red(`Rules failed: ${err.message}`));
229
- process.exitCode = 1;
230
- }
231
- });
232
-
233
- cmd
234
- .command('quarantine')
235
- .description('list quarantined files')
236
- .action(async () => {
237
- try {
238
- const extConfig = (await import('../utils/config.js')).default;
239
- const store = extConfig.get('ext_store.content-scanner') || {};
240
- const quarantined = store.quarantined || [];
241
- if (quarantined.length === 0) {
242
- console.log(chalk.dim('No quarantined files.'));
243
- return;
244
- }
245
- console.log(chalk.bold(`Quarantined Files (${quarantined.length})\n`));
246
- for (const f of quarantined) {
247
- const colorFn = SEVERITY_COLORS[f.severity] || chalk.white;
248
- console.log(` ${chalk.white(f.id)} ${colorFn(`[${f.severity}]`)} ${chalk.cyan(f.path)}`);
249
- console.log(` Reason: ${chalk.dim(f.reason || 'DLP violation')} Quarantined: ${chalk.dim(f.quarantinedAt)}`);
250
- }
251
- } catch (err) {
252
- console.log(chalk.red(`Quarantine list failed: ${err.message}`));
253
- process.exitCode = 1;
254
- }
255
- });
256
-
257
- cmd
258
- .command('release <id>')
259
- .description('release a file from quarantine')
260
- .action(async (id) => {
261
- try {
262
- const extConfig = (await import('../utils/config.js')).default;
263
- const store = extConfig.get('ext_store.content-scanner') || {};
264
- const quarantined = store.quarantined || [];
265
- const idx = quarantined.findIndex(f => f.id === id);
266
- if (idx === -1) {
267
- console.log(chalk.yellow(`Quarantined file not found: ${id}`));
268
- process.exitCode = 1;
269
- return;
270
- }
271
- const released = quarantined.splice(idx, 1)[0];
272
- store.quarantined = quarantined;
273
- extConfig.set('ext_store.content-scanner', store);
274
- console.log(chalk.green(`✔ Released from quarantine: ${released.path}`));
275
- } catch (err) {
276
- console.log(chalk.red(`Release failed: ${err.message}`));
277
- process.exitCode = 1;
278
- }
279
- });
280
- }
1
+ import chalk from 'chalk';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+
5
+ const BUILTIN_RULES = [
6
+ { name: 'SSN', pattern: '\\d{3}-\\d{2}-\\d{4}', severity: 'critical', description: 'Social Security Number' },
7
+ { name: 'CreditCard', pattern: '4\\d{3}[\\s-]?\\d{4}[\\s-]?\\d{4}[\\s-]?\\d{4}', severity: 'critical', description: 'Credit card number (Visa)' },
8
+ { name: 'CreditCardMC', pattern: '5[1-5]\\d{2}[\\s-]?\\d{4}[\\s-]?\\d{4}[\\s-]?\\d{4}', severity: 'critical', description: 'Credit card number (Mastercard)' },
9
+ { name: 'APIKey', pattern: '(?:sk_|pk_|api_|key_)[a-zA-Z0-9]{16,}', severity: 'high', description: 'API key' },
10
+ { name: 'PrivateKey', pattern: '-----BEGIN PRIVATE KEY-----', severity: 'critical', description: 'Private key block' },
11
+ { name: 'AWSKey', pattern: 'AKIA[0-9A-Z]{16}', severity: 'critical', description: 'AWS access key' },
12
+ { name: 'Email', pattern: '\\b[\\w.-]+@[\\w.-]+\\.\\w{2,}\\b', severity: 'low', description: 'Email address' },
13
+ ];
14
+
15
+ const SEVERITY_ORDER = { critical: 4, high: 3, medium: 2, low: 1 };
16
+ const SEVERITY_COLORS = {
17
+ critical: chalk.red,
18
+ high: chalk.yellow,
19
+ medium: chalk.cyan,
20
+ low: chalk.gray,
21
+ };
22
+
23
+ function classifyFile(maxSeverity) {
24
+ if (maxSeverity === 'critical') return 'Restricted';
25
+ if (maxSeverity === 'high') return 'Confidential';
26
+ if (maxSeverity === 'medium') return 'Internal';
27
+ return 'Public';
28
+ }
29
+
30
+ function scanContent(content, rules) {
31
+ const findings = [];
32
+ const lines = content.split('\n');
33
+ for (const rule of rules) {
34
+ const re = new RegExp(rule.pattern, 'g');
35
+ for (let i = 0; i < lines.length; i++) {
36
+ let match;
37
+ while ((match = re.exec(lines[i])) !== null) {
38
+ findings.push({
39
+ rule: rule.name,
40
+ severity: rule.severity,
41
+ line: i + 1,
42
+ match: match[0].length > 40 ? match[0].slice(0, 37) + '...' : match[0],
43
+ });
44
+ }
45
+ }
46
+ }
47
+ return findings;
48
+ }
49
+
50
+ export default function scannerCommand(program) {
51
+ const cmd = program
52
+ .command('scanner')
53
+ .description('content scanning and DLP (Enterprise)')
54
+ .addHelpText('after', `
55
+ Examples:
56
+ $ pal scanner scan ./documents Scan directory for sensitive data
57
+ $ pal scanner scan ./secrets.txt Scan a single file
58
+ $ pal scanner rules List active DLP rules
59
+ $ pal scanner rules --add MyRule "\\d{6}" high Add a custom rule
60
+ $ pal scanner rules --remove MyRule Remove a custom rule
61
+ $ pal scanner quarantine List quarantined files
62
+ $ pal scanner release <id> Release file from quarantine
63
+ `)
64
+ .action(() => { cmd.outputHelp(); });
65
+
66
+ cmd
67
+ .command('scan <path>')
68
+ .description('scan a file or directory for sensitive data')
69
+ .action(async (targetPath) => {
70
+ try {
71
+ const extConfig = (await import('../utils/config.js')).default;
72
+ const extSettings = extConfig.get('ext.content-scanner') || {};
73
+ const customRules = extSettings.rules || [];
74
+ const allRules = [...BUILTIN_RULES, ...customRules];
75
+
76
+ const resolved = path.resolve(targetPath);
77
+ if (!fs.existsSync(resolved)) {
78
+ console.log(chalk.red(`Path not found: ${resolved}`));
79
+ process.exitCode = 1;
80
+ return;
81
+ }
82
+
83
+ const stat = fs.statSync(resolved);
84
+ const files = [];
85
+ if (stat.isDirectory()) {
86
+ const entries = fs.readdirSync(resolved, { recursive: true });
87
+ for (const entry of entries) {
88
+ const full = path.join(resolved, entry);
89
+ try {
90
+ if (fs.statSync(full).isFile()) files.push(full);
91
+ } catch { /* skip unreadable */ }
92
+ }
93
+ } else {
94
+ files.push(resolved);
95
+ }
96
+
97
+ if (files.length === 0) {
98
+ console.log(chalk.gray('No files to scan.'));
99
+ return;
100
+ }
101
+
102
+ console.log(chalk.cyan(`Scanning ${files.length} file(s)...\n`));
103
+ let totalFindings = 0;
104
+ let maxSeverity = 'low';
105
+
106
+ for (const file of files) {
107
+ let content;
108
+ try {
109
+ content = fs.readFileSync(file, 'utf8');
110
+ } catch {
111
+ continue;
112
+ }
113
+
114
+ const findings = scanContent(content, allRules);
115
+ if (findings.length === 0) continue;
116
+
117
+ totalFindings += findings.length;
118
+ const rel = path.relative(process.cwd(), file);
119
+ console.log(` ${chalk.white(rel)}`);
120
+
121
+ for (const f of findings) {
122
+ const colorFn = SEVERITY_COLORS[f.severity] || chalk.white;
123
+ console.log(` Line ${chalk.gray(f.line)}: ${colorFn(`[${f.severity.toUpperCase()}]`)} ${chalk.white(f.rule)} — ${chalk.dim(f.match)}`);
124
+ if (SEVERITY_ORDER[f.severity] > SEVERITY_ORDER[maxSeverity]) {
125
+ maxSeverity = f.severity;
126
+ }
127
+ }
128
+ console.log('');
129
+ }
130
+
131
+ if (totalFindings === 0) {
132
+ console.log(chalk.green('✔ No sensitive data found.'));
133
+ console.log(` Classification: ${chalk.green('Public')}`);
134
+ } else {
135
+ const classification = classifyFile(maxSeverity);
136
+ console.log(chalk.gray('─'.repeat(50)));
137
+ console.log(` Total findings: ${chalk.white(totalFindings)}`);
138
+ console.log(` Classification: ${SEVERITY_COLORS[maxSeverity](classification)}`);
139
+ }
140
+ } catch (err) {
141
+ console.log(chalk.red(`Scan failed: ${err.message}`));
142
+ process.exitCode = 1;
143
+ }
144
+ });
145
+
146
+ cmd
147
+ .command('rules')
148
+ .description('list active DLP rules')
149
+ .option('--add <name>', 'add a custom rule (requires <pattern> <severity> as args)')
150
+ .option('--remove <name>', 'remove a custom rule')
151
+ .action(async (opts, command) => {
152
+ try {
153
+ const extConfig = (await import('../utils/config.js')).default;
154
+ const extSettings = extConfig.get('ext.content-scanner') || {};
155
+
156
+ if (opts.remove) {
157
+ const customRules = extSettings.rules || [];
158
+ const idx = customRules.findIndex(r => r.name === opts.remove);
159
+ if (idx === -1) {
160
+ console.log(chalk.yellow(`Rule not found: ${opts.remove}`));
161
+ process.exitCode = 1;
162
+ return;
163
+ }
164
+ customRules.splice(idx, 1);
165
+ extSettings.rules = customRules;
166
+ extConfig.set('ext.content-scanner', extSettings);
167
+ console.log(chalk.green(`✔ Rule removed: ${opts.remove}`));
168
+ return;
169
+ }
170
+
171
+ if (opts.add) {
172
+ const args = command.args;
173
+ if (args.length < 2) {
174
+ console.log(chalk.red('Usage: pal scanner rules --add <name> <pattern> <severity>'));
175
+ process.exitCode = 1;
176
+ return;
177
+ }
178
+ const pattern = args[0];
179
+ try {
180
+ const testRe = new RegExp(pattern);
181
+ const start = Date.now();
182
+ 'a'.repeat(25).match(testRe);
183
+ if (Date.now() - start > 100) throw new Error('Pattern too complex');
184
+ } catch (e) {
185
+ console.error(chalk.red(`Invalid or unsafe regex pattern: ${e.message}`));
186
+ process.exitCode = 1;
187
+ return;
188
+ }
189
+ const severity = args[1] || 'medium';
190
+ if (!['critical', 'high', 'medium', 'low'].includes(severity)) {
191
+ console.log(chalk.red('Severity must be: critical, high, medium, or low'));
192
+ process.exitCode = 1;
193
+ return;
194
+ }
195
+ const customRules = extSettings.rules || [];
196
+ if (customRules.find(r => r.name === opts.add)) {
197
+ console.log(chalk.yellow(`Rule already exists: ${opts.add}`));
198
+ process.exitCode = 1;
199
+ return;
200
+ }
201
+ customRules.push({ name: opts.add, pattern, severity, description: `Custom rule: ${opts.add}` });
202
+ extSettings.rules = customRules;
203
+ extConfig.set('ext.content-scanner', extSettings);
204
+ console.log(chalk.green(`✔ Rule added: ${opts.add}`));
205
+ console.log(` Pattern: ${chalk.white(pattern)}`);
206
+ console.log(` Severity: ${SEVERITY_COLORS[severity](severity)}`);
207
+ return;
208
+ }
209
+
210
+ // List all rules
211
+ const customRules = extSettings.rules || [];
212
+ const allRules = [...BUILTIN_RULES, ...customRules];
213
+ console.log(chalk.bold(`DLP Rules (${allRules.length})\n`));
214
+ console.log(chalk.cyan(' Built-in:'));
215
+ for (const r of BUILTIN_RULES) {
216
+ const colorFn = SEVERITY_COLORS[r.severity] || chalk.white;
217
+ console.log(` ${chalk.white(r.name)} ${colorFn(`[${r.severity}]`)} — ${chalk.dim(r.description)}`);
218
+ }
219
+ if (customRules.length > 0) {
220
+ console.log('');
221
+ console.log(chalk.cyan(' Custom:'));
222
+ for (const r of customRules) {
223
+ const colorFn = SEVERITY_COLORS[r.severity] || chalk.white;
224
+ console.log(` ${chalk.white(r.name)} ${colorFn(`[${r.severity}]`)} — ${chalk.dim(r.pattern)}`);
225
+ }
226
+ }
227
+ } catch (err) {
228
+ console.log(chalk.red(`Rules failed: ${err.message}`));
229
+ process.exitCode = 1;
230
+ }
231
+ });
232
+
233
+ cmd
234
+ .command('quarantine')
235
+ .description('list quarantined files')
236
+ .action(async () => {
237
+ try {
238
+ const extConfig = (await import('../utils/config.js')).default;
239
+ const store = extConfig.get('ext_store.content-scanner') || {};
240
+ const quarantined = store.quarantined || [];
241
+ if (quarantined.length === 0) {
242
+ console.log(chalk.dim('No quarantined files.'));
243
+ return;
244
+ }
245
+ console.log(chalk.bold(`Quarantined Files (${quarantined.length})\n`));
246
+ for (const f of quarantined) {
247
+ const colorFn = SEVERITY_COLORS[f.severity] || chalk.white;
248
+ console.log(` ${chalk.white(f.id)} ${colorFn(`[${f.severity}]`)} ${chalk.cyan(f.path)}`);
249
+ console.log(` Reason: ${chalk.dim(f.reason || 'DLP violation')} Quarantined: ${chalk.dim(f.quarantinedAt)}`);
250
+ }
251
+ } catch (err) {
252
+ console.log(chalk.red(`Quarantine list failed: ${err.message}`));
253
+ process.exitCode = 1;
254
+ }
255
+ });
256
+
257
+ cmd
258
+ .command('release <id>')
259
+ .description('release a file from quarantine')
260
+ .action(async (id) => {
261
+ try {
262
+ const extConfig = (await import('../utils/config.js')).default;
263
+ const store = extConfig.get('ext_store.content-scanner') || {};
264
+ const quarantined = store.quarantined || [];
265
+ const idx = quarantined.findIndex(f => f.id === id);
266
+ if (idx === -1) {
267
+ console.log(chalk.yellow(`Quarantined file not found: ${id}`));
268
+ process.exitCode = 1;
269
+ return;
270
+ }
271
+ const released = quarantined.splice(idx, 1)[0];
272
+ store.quarantined = quarantined;
273
+ extConfig.set('ext_store.content-scanner', store);
274
+ console.log(chalk.green(`✔ Released from quarantine: ${released.path}`));
275
+ } catch (err) {
276
+ console.log(chalk.red(`Release failed: ${err.message}`));
277
+ process.exitCode = 1;
278
+ }
279
+ });
280
+ }