pal-explorer-cli 0.4.12 → 0.4.13
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 +149 -149
- package/bin/pal.js +63 -2
- package/extensions/@palexplorer/analytics/extension.json +20 -1
- package/extensions/@palexplorer/analytics/index.js +19 -9
- package/extensions/@palexplorer/audit/extension.json +14 -0
- package/extensions/@palexplorer/auth-email/extension.json +15 -0
- package/extensions/@palexplorer/auth-oauth/extension.json +15 -0
- package/extensions/@palexplorer/chat/extension.json +14 -0
- package/extensions/@palexplorer/discovery/extension.json +17 -0
- package/extensions/@palexplorer/discovery/index.js +1 -1
- package/extensions/@palexplorer/email-notifications/extension.json +23 -0
- package/extensions/@palexplorer/groups/extension.json +15 -0
- package/extensions/@palexplorer/share-links/extension.json +15 -0
- package/extensions/@palexplorer/sync/extension.json +16 -0
- package/extensions/@palexplorer/user-mgmt/extension.json +15 -0
- package/lib/capabilities.js +24 -24
- package/lib/commands/analytics.js +175 -175
- package/lib/commands/api-keys.js +131 -131
- package/lib/commands/audit.js +235 -235
- package/lib/commands/auth.js +137 -137
- package/lib/commands/backup.js +76 -76
- package/lib/commands/billing.js +148 -148
- package/lib/commands/chat.js +217 -217
- package/lib/commands/cloud-backup.js +231 -231
- package/lib/commands/comment.js +99 -99
- package/lib/commands/completion.js +203 -203
- package/lib/commands/compliance.js +218 -218
- package/lib/commands/config.js +136 -136
- package/lib/commands/connect.js +44 -44
- package/lib/commands/dept.js +294 -294
- package/lib/commands/device.js +146 -146
- package/lib/commands/download.js +240 -226
- package/lib/commands/explorer.js +178 -178
- package/lib/commands/extension.js +1060 -970
- package/lib/commands/favorite.js +90 -90
- package/lib/commands/federation.js +270 -270
- package/lib/commands/file.js +533 -533
- package/lib/commands/group.js +271 -271
- package/lib/commands/gui-share.js +29 -29
- package/lib/commands/init.js +61 -61
- package/lib/commands/invite.js +59 -59
- package/lib/commands/list.js +58 -58
- package/lib/commands/log.js +116 -116
- package/lib/commands/nearby.js +108 -108
- package/lib/commands/network.js +251 -251
- package/lib/commands/notify.js +198 -198
- package/lib/commands/org.js +273 -273
- package/lib/commands/pal.js +403 -180
- package/lib/commands/permissions.js +216 -216
- package/lib/commands/pin.js +97 -97
- package/lib/commands/protocol.js +357 -357
- package/lib/commands/rbac.js +147 -147
- package/lib/commands/recover.js +36 -36
- package/lib/commands/register.js +171 -171
- package/lib/commands/relay.js +131 -131
- package/lib/commands/remote.js +368 -368
- package/lib/commands/revoke.js +50 -50
- package/lib/commands/scanner.js +280 -280
- package/lib/commands/schedule.js +344 -344
- package/lib/commands/scim.js +203 -203
- package/lib/commands/search.js +181 -181
- package/lib/commands/serve.js +438 -438
- package/lib/commands/server.js +350 -350
- package/lib/commands/share-link.js +199 -199
- package/lib/commands/share.js +336 -323
- package/lib/commands/sso.js +200 -200
- package/lib/commands/status.js +145 -145
- package/lib/commands/stream.js +562 -562
- package/lib/commands/su.js +187 -187
- package/lib/commands/sync.js +979 -979
- package/lib/commands/transfers.js +152 -152
- package/lib/commands/uninstall.js +188 -188
- package/lib/commands/update.js +204 -204
- package/lib/commands/user.js +276 -276
- package/lib/commands/vfs.js +84 -84
- package/lib/commands/web-login.js +79 -79
- package/lib/commands/web.js +52 -52
- package/lib/commands/webhook.js +180 -180
- package/lib/commands/whoami.js +59 -59
- package/lib/commands/workspace.js +121 -121
- package/lib/core/billing.js +16 -5
- package/lib/core/dhtDiscovery.js +9 -2
- package/lib/core/discoveryClient.js +13 -7
- package/lib/core/extensions.js +142 -1
- package/lib/core/identity.js +33 -2
- package/lib/core/imageProcessor.js +109 -0
- package/lib/core/imageTorrent.js +167 -0
- package/lib/core/permissions.js +1 -1
- package/lib/core/pro.js +11 -4
- package/lib/core/serverList.js +4 -1
- package/lib/core/shares.js +12 -1
- package/lib/core/signalingServer.js +14 -2
- package/lib/core/su.js +1 -1
- package/lib/core/users.js +1 -1
- package/lib/protocol/messages.js +12 -3
- package/lib/utils/explorer.js +1 -1
- package/lib/utils/help.js +357 -357
- package/lib/utils/torrent.js +1 -0
- package/package.json +4 -3
package/lib/commands/revoke.js
CHANGED
|
@@ -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
|
-
$
|
|
12
|
-
$
|
|
13
|
-
$
|
|
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
|
+
}
|
package/lib/commands/scanner.js
CHANGED
|
@@ -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
|
-
$
|
|
57
|
-
$
|
|
58
|
-
$
|
|
59
|
-
$
|
|
60
|
-
$
|
|
61
|
-
$
|
|
62
|
-
$
|
|
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:
|
|
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
|
+
}
|