dbsafedump 0.0.1 → 1.0.3
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/LICENSE +63 -0
- package/README.md +596 -0
- package/bin/cli.js +719 -1
- package/config.example.yml +34 -0
- package/dist/cli.js +532 -0
- package/package.json +48 -15
package/bin/cli.js
CHANGED
|
@@ -1 +1,719 @@
|
|
|
1
|
-
|
|
1
|
+
const { Command } = require('commander');
|
|
2
|
+
const chalk = require('chalk');
|
|
3
|
+
const inquirer = require('inquirer');
|
|
4
|
+
const { loadConfig, saveConfig, getDefaultConfig, generateSalt, generateConfig, getDefaultConfigPath, updateConfigField, listProfiles, validateProfile } = require('../src/config');
|
|
5
|
+
const { runDump, runSchema, runImport, runScrub, runTestConnections } = require('../src/commands');
|
|
6
|
+
const { checkLicense, getTierInfo, getMaxRows, canScrub, canUseMultiProfile, canUseCI, canUseSmartDiscovery, shouldShowBadge } = require('../src/license');
|
|
7
|
+
const { activate, deactivate, isActivated, getLicenseInfo, getCachePath, removeAll, saveLicenseKey, loadCache, decrypt } = require('../src/license');
|
|
8
|
+
const { getFingerprintData } = require('../src/license');
|
|
9
|
+
const { discoverSensitiveColumns, applyDiscoveryToConfig } = require('../src/smartDiscovery');
|
|
10
|
+
const { createConnection, closeConnection, getTables } = require('../src/database');
|
|
11
|
+
|
|
12
|
+
let currentTier = 'free';
|
|
13
|
+
|
|
14
|
+
async function checkAndShowLicense() {
|
|
15
|
+
const result = await checkLicense();
|
|
16
|
+
currentTier = result.tier || 'free';
|
|
17
|
+
|
|
18
|
+
if (shouldShowBadge()) {
|
|
19
|
+
console.log(chalk.gray(`DBSafeDump FREE Edition - Row limit: ${getMaxRows()}/table`));
|
|
20
|
+
console.log(chalk.gray('Activate license: dbsafedump activate "LICENSE-KEY"'));
|
|
21
|
+
console.log(chalk.gray('Upgrade at: https://dbsafedump.com/pricing\n'));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return currentTier;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const program = new Command();
|
|
28
|
+
|
|
29
|
+
program
|
|
30
|
+
.name('dbsafedump')
|
|
31
|
+
.description('Safe database dump with anonymization')
|
|
32
|
+
.version('1.0.1');
|
|
33
|
+
|
|
34
|
+
program
|
|
35
|
+
.command('init')
|
|
36
|
+
.description('Initialize DBSafeDump configuration')
|
|
37
|
+
.option('-f, --force', 'Overwrite existing config')
|
|
38
|
+
.action(async (options) => {
|
|
39
|
+
const configPath = getDefaultConfigPath();
|
|
40
|
+
const fs = require('fs');
|
|
41
|
+
|
|
42
|
+
if (fs.existsSync(configPath) && !options.force) {
|
|
43
|
+
console.log(chalk.yellow(`Config already exists: ${configPath}`));
|
|
44
|
+
console.log(chalk.gray('Use --force to overwrite'));
|
|
45
|
+
console.log(chalk.gray('Running config wizard...\n'));
|
|
46
|
+
} else {
|
|
47
|
+
const config = generateConfig();
|
|
48
|
+
saveConfig(config);
|
|
49
|
+
console.log(chalk.green(`Configuration created: ${configPath}`));
|
|
50
|
+
console.log(chalk.gray('\nGenerated unique salt for deterministic masking.\n'));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const { runConfigWizard } = require('../src/commands/config');
|
|
54
|
+
await runConfigWizard();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
program
|
|
58
|
+
.command('activate')
|
|
59
|
+
.description('Activate license on this machine')
|
|
60
|
+
.argument('<key>', 'License key')
|
|
61
|
+
.action(async (key) => {
|
|
62
|
+
console.log(chalk.blue(`\nDBSafeDump - License Activation\n`));
|
|
63
|
+
|
|
64
|
+
if (isActivated()) {
|
|
65
|
+
console.log(chalk.yellow('License already activated on this machine.'));
|
|
66
|
+
console.log(chalk.gray('Use: dbsafedump deactivate first\n'));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
console.log(chalk.gray('Connecting to license server...\n'));
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const result = await activate(key);
|
|
74
|
+
|
|
75
|
+
if (result.data && result.checksum) {
|
|
76
|
+
try {
|
|
77
|
+
const data = decrypt(result.data, result.checksum);
|
|
78
|
+
|
|
79
|
+
if (data.valid) {
|
|
80
|
+
saveLicenseKey(key);
|
|
81
|
+
|
|
82
|
+
console.log(chalk.green('License activated successfully!'));
|
|
83
|
+
console.log(chalk.gray(` Tier: ${data.tier.toUpperCase()}`));
|
|
84
|
+
console.log(chalk.gray(` Expires: ${data.expiresAt || 'N/A'}`));
|
|
85
|
+
console.log('');
|
|
86
|
+
} else {
|
|
87
|
+
console.log(chalk.red('License activation failed.'));
|
|
88
|
+
if (data.error) {
|
|
89
|
+
console.log(chalk.red(` Error: ${data.error}`));
|
|
90
|
+
}
|
|
91
|
+
console.log('');
|
|
92
|
+
console.log(chalk.gray(`[LOG] Activation failed: ${JSON.stringify(data)}\n`));
|
|
93
|
+
}
|
|
94
|
+
} catch (decryptError) {
|
|
95
|
+
console.log(chalk.red('Failed to verify license response.'));
|
|
96
|
+
console.log(chalk.red(` ${decryptError.message}`));
|
|
97
|
+
console.log(chalk.gray('\n This may indicate a server configuration issue.\n'));
|
|
98
|
+
console.log(chalk.gray(`[LOG] Decrypt error: ${decryptError.message}\n`));
|
|
99
|
+
}
|
|
100
|
+
} else {
|
|
101
|
+
console.log(chalk.red('Invalid response from license server.'));
|
|
102
|
+
console.log(chalk.gray(' Received:', JSON.stringify(result)));
|
|
103
|
+
console.log('');
|
|
104
|
+
console.log(chalk.gray(`[LOG] Invalid API response: ${JSON.stringify(result)}\n`));
|
|
105
|
+
}
|
|
106
|
+
} catch (error) {
|
|
107
|
+
console.log(chalk.red('Activation failed:'));
|
|
108
|
+
console.log(chalk.red(` ${error.message}`));
|
|
109
|
+
console.log(chalk.gray('\nMake sure you have an internet connection.\n'));
|
|
110
|
+
console.log(chalk.gray(`[LOG] Activation error: ${error.message}\n`));
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
program
|
|
115
|
+
.command('deactivate')
|
|
116
|
+
.description('Deactivate license (releases for another machine)')
|
|
117
|
+
.action(async () => {
|
|
118
|
+
console.log(chalk.blue(`\nDBSafeDump - License Deactivation\n`));
|
|
119
|
+
|
|
120
|
+
if (!isActivated()) {
|
|
121
|
+
console.log(chalk.yellow('No license activated on this machine.\n'));
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const licenseInfo = getLicenseInfo();
|
|
126
|
+
console.log(chalk.gray(`Current license: ${licenseInfo.key}`));
|
|
127
|
+
console.log(chalk.gray(`Tier: ${(licenseInfo.cached?.tier || 'unknown').toUpperCase()}\n`));
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
const result = await deactivate(licenseInfo.key);
|
|
131
|
+
|
|
132
|
+
let reason = null;
|
|
133
|
+
|
|
134
|
+
if (result.data && result.checksum) {
|
|
135
|
+
try {
|
|
136
|
+
const data = decrypt(result.data, result.checksum);
|
|
137
|
+
reason = data.reason;
|
|
138
|
+
} catch (e) {
|
|
139
|
+
// Nie udało się odszyfrować
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (reason === 'license_deactivated') {
|
|
144
|
+
removeAll();
|
|
145
|
+
console.log(chalk.green('License deactivated successfully.'));
|
|
146
|
+
console.log(chalk.gray('You can now activate this license on another machine.\n'));
|
|
147
|
+
} else if (reason === 'fingerprint_mismatch') {
|
|
148
|
+
console.log(chalk.red('This license is registered to a different machine.'));
|
|
149
|
+
console.log(chalk.gray('Local license data removed.\n'));
|
|
150
|
+
removeAll();
|
|
151
|
+
} else if (reason === 'license_not_found') {
|
|
152
|
+
console.log(chalk.red('License not found on server.'));
|
|
153
|
+
console.log(chalk.gray('Local license data removed.\n'));
|
|
154
|
+
removeAll();
|
|
155
|
+
} else {
|
|
156
|
+
console.log(chalk.yellow('Warning: Could not deactivate on server.'));
|
|
157
|
+
console.log(chalk.gray('Removing local license data...\n'));
|
|
158
|
+
removeAll();
|
|
159
|
+
}
|
|
160
|
+
} catch (error) {
|
|
161
|
+
console.log(chalk.yellow('Warning: Could not connect to server.'));
|
|
162
|
+
console.log(chalk.gray('Removing local license data anyway...\n'));
|
|
163
|
+
removeAll();
|
|
164
|
+
console.log(chalk.green('Local license removed.'));
|
|
165
|
+
console.log(chalk.gray('Note: Server-side license may still be registered to this machine.\n'));
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
program
|
|
170
|
+
.command('license')
|
|
171
|
+
.description('Show current license status')
|
|
172
|
+
.action(async () => {
|
|
173
|
+
console.log(chalk.blue(`\nDBSafeDump - License Status\n`));
|
|
174
|
+
|
|
175
|
+
if (!isActivated()) {
|
|
176
|
+
console.log(chalk.yellow('No license activated.'));
|
|
177
|
+
console.log(chalk.gray('Usage: dbsafedump activate "LICENSE-KEY"\n'));
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const info = getLicenseInfo();
|
|
182
|
+
const cached = loadCache();
|
|
183
|
+
|
|
184
|
+
console.log(chalk.green('License is activated\n'));
|
|
185
|
+
console.log(chalk.gray(`Key: ${info.key}`));
|
|
186
|
+
console.log(chalk.gray(`Tier: ${(cached?.tier || 'unknown').toUpperCase()}`));
|
|
187
|
+
console.log(chalk.gray(`Valid: ${cached?.valid ? 'Yes' : 'Unknown'}`));
|
|
188
|
+
|
|
189
|
+
if (info.cacheAge) {
|
|
190
|
+
const ageMinutes = Math.floor(info.cacheAge / 60000);
|
|
191
|
+
console.log(chalk.gray(`Cache age: ${ageMinutes} minutes`));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
console.log(chalk.gray(`\nStored in: ${getCachePath()}\n`));
|
|
195
|
+
|
|
196
|
+
await checkAndShowLicense();
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
program
|
|
200
|
+
.command('profiles')
|
|
201
|
+
.description('List available profiles (STARTER/PRO)')
|
|
202
|
+
.option('-c, --config <file>', 'Config file', getDefaultConfigPath())
|
|
203
|
+
.action(async (options) => {
|
|
204
|
+
const config = loadConfig(options.config);
|
|
205
|
+
|
|
206
|
+
if (!config) {
|
|
207
|
+
console.log(chalk.yellow('Config file not found. Running configuration wizard...\n'));
|
|
208
|
+
const runConfig = require('../src/commands/config');
|
|
209
|
+
await runConfig();
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
await checkAndShowLicense();
|
|
214
|
+
|
|
215
|
+
if (!canUseMultiProfile()) {
|
|
216
|
+
console.log(chalk.red('\nMulti-profile requires STARTER or PRO license.'));
|
|
217
|
+
console.log(chalk.gray('Get your license at: https://dbsafedump.com/pricing\n'));
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const profiles = listProfiles(options.config);
|
|
222
|
+
|
|
223
|
+
if (profiles.length === 0) {
|
|
224
|
+
console.log(chalk.yellow('\nNo profiles defined in config.'));
|
|
225
|
+
console.log(chalk.gray('\nAdd profiles to config.yml:'));
|
|
226
|
+
console.log(chalk.gray(' profiles:'));
|
|
227
|
+
console.log(chalk.gray(' dev:'));
|
|
228
|
+
console.log(chalk.gray(' connections:'));
|
|
229
|
+
console.log(chalk.gray(' source: { host: localhost, ... }'));
|
|
230
|
+
console.log(chalk.gray('\nThen use: dbsafedump dump --profile dev\n'));
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
console.log(chalk.blue(`\nAvailable profiles:\n`));
|
|
235
|
+
for (const name of profiles) {
|
|
236
|
+
const validation = validateProfile(config, name);
|
|
237
|
+
if (validation.valid) {
|
|
238
|
+
console.log(chalk.green(` ${name}`));
|
|
239
|
+
} else {
|
|
240
|
+
console.log(chalk.red(` ${name} (invalid: ${validation.error})`));
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
console.log('');
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
program
|
|
247
|
+
.command('generate-salt')
|
|
248
|
+
.description('Generate new salt for config (forces re-masking)')
|
|
249
|
+
.option('-c, --config <file>', 'Config file', getDefaultConfigPath())
|
|
250
|
+
.option('-y, --yes', 'Skip confirmation')
|
|
251
|
+
.action(async (options) => {
|
|
252
|
+
const configPath = options.config || getDefaultConfigPath();
|
|
253
|
+
const fs = require('fs');
|
|
254
|
+
|
|
255
|
+
const config = loadConfig(configPath);
|
|
256
|
+
|
|
257
|
+
if (!config) {
|
|
258
|
+
console.log(chalk.red('Config file not found'));
|
|
259
|
+
console.log(chalk.gray('Run: dbsafedump init first'));
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const oldSalt = config.salt;
|
|
264
|
+
|
|
265
|
+
if (!options.yes) {
|
|
266
|
+
console.log(chalk.yellow('Warning: Generating new salt will change all masked values!'));
|
|
267
|
+
console.log(chalk.yellow(' Existing masked data will no longer match.'));
|
|
268
|
+
console.log(chalk.gray(` Old salt: ${oldSalt ? oldSalt.substring(0, 8) + '...' : 'none'}\n`));
|
|
269
|
+
|
|
270
|
+
const { confirm } = await inquirer.prompt([
|
|
271
|
+
{
|
|
272
|
+
type: 'confirm',
|
|
273
|
+
name: 'confirm',
|
|
274
|
+
message: 'Generate new salt?',
|
|
275
|
+
default: false
|
|
276
|
+
}
|
|
277
|
+
]);
|
|
278
|
+
|
|
279
|
+
if (!confirm) {
|
|
280
|
+
console.log(chalk.gray('Cancelled.\n'));
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const newSalt = generateSalt();
|
|
286
|
+
updateConfigField(configPath, 'salt', newSalt);
|
|
287
|
+
|
|
288
|
+
console.log(chalk.green('New salt generated!'));
|
|
289
|
+
console.log(chalk.gray(` Old salt: ${oldSalt ? oldSalt.substring(0, 16) + '...' : 'none'}`));
|
|
290
|
+
console.log(chalk.green(` New salt: ${newSalt.substring(0, 16)}...`));
|
|
291
|
+
console.log(chalk.yellow('\nWarning: Re-run dump/import to apply new masking!\n'));
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
async function runDumpWithSchema(config, options) {
|
|
295
|
+
const isCI = options.ci || options.yes;
|
|
296
|
+
|
|
297
|
+
if (!isCI) {
|
|
298
|
+
console.log(chalk.blue(`\nRunning schema check first...\n`));
|
|
299
|
+
await runSchema(config);
|
|
300
|
+
|
|
301
|
+
console.log(chalk.yellow(`\nProceed with dump?`));
|
|
302
|
+
const { confirm } = await inquirer.prompt([
|
|
303
|
+
{
|
|
304
|
+
type: 'confirm',
|
|
305
|
+
name: 'confirm',
|
|
306
|
+
message: 'Continue with dump?',
|
|
307
|
+
default: true
|
|
308
|
+
}
|
|
309
|
+
]);
|
|
310
|
+
|
|
311
|
+
if (!confirm) {
|
|
312
|
+
console.log(chalk.gray('\nDump cancelled.\n'));
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (isCI) {
|
|
318
|
+
console.log(chalk.gray(`\n[CI MODE] Running in automated mode...\n`));
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
await runDump(config, options);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
program
|
|
325
|
+
.command('dump')
|
|
326
|
+
.description('Dump database with anonymization')
|
|
327
|
+
.option('-o, --output <file>', 'Output file', 'dump.sql')
|
|
328
|
+
.option('-c, --config <file>', 'Config file', getDefaultConfigPath())
|
|
329
|
+
.option('-p, --profile <name>', 'Use named profile (STARTER/PRO)')
|
|
330
|
+
.option('-y, --yes', 'Skip schema check and confirmation (for CI/CD)')
|
|
331
|
+
.option('--ci', 'CI/CD mode - no prompts (PRO)')
|
|
332
|
+
.action(async (options) => {
|
|
333
|
+
try {
|
|
334
|
+
const config = loadConfig(options.config, options.profile);
|
|
335
|
+
|
|
336
|
+
if (!config) {
|
|
337
|
+
console.log(chalk.red('Config file not found'));
|
|
338
|
+
console.log(chalk.gray('Run: dbsafedump init'));
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
await checkAndShowLicense();
|
|
343
|
+
|
|
344
|
+
if (options.profile && !canUseMultiProfile()) {
|
|
345
|
+
console.log(chalk.red('\nProfiles require STARTER or PRO license.'));
|
|
346
|
+
console.log(chalk.gray('Get your license at: https://dbsafedump.com/pricing\n'));
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (options.ci && !canUseCI()) {
|
|
351
|
+
console.log(chalk.red('\nCI/CD mode requires PRO license.'));
|
|
352
|
+
console.log(chalk.gray('Get your license at: https://dbsafedump.com/pricing\n'));
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (!config.connections || !config.connections.source) {
|
|
357
|
+
console.log(chalk.red('Invalid config: missing source connection'));
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
await runDumpWithSchema(config, options);
|
|
362
|
+
} catch (error) {
|
|
363
|
+
console.error(chalk.red(`\nError: ${error.message}`));
|
|
364
|
+
process.exit(1);
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
async function runImportWithSchema(config, options) {
|
|
369
|
+
const validModes = ['truncate', 'append'];
|
|
370
|
+
const isCI = options.ci || options.yes;
|
|
371
|
+
|
|
372
|
+
if (!isCI) {
|
|
373
|
+
console.log(chalk.blue(`\nRunning schema check first...\n`));
|
|
374
|
+
await runSchema(config);
|
|
375
|
+
|
|
376
|
+
console.log(chalk.yellow(`\nProceed with import?`));
|
|
377
|
+
const { confirm } = await inquirer.prompt([
|
|
378
|
+
{
|
|
379
|
+
type: 'confirm',
|
|
380
|
+
name: 'confirm',
|
|
381
|
+
message: 'Continue with import?',
|
|
382
|
+
default: true
|
|
383
|
+
}
|
|
384
|
+
]);
|
|
385
|
+
|
|
386
|
+
if (!confirm) {
|
|
387
|
+
console.log(chalk.gray('\nImport cancelled.\n'));
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const { mode } = await inquirer.prompt([
|
|
392
|
+
{
|
|
393
|
+
type: 'list',
|
|
394
|
+
name: 'mode',
|
|
395
|
+
message: 'Select import mode:',
|
|
396
|
+
choices: [
|
|
397
|
+
{ name: 'Truncate & Replace', value: 'truncate' },
|
|
398
|
+
{ name: 'Append', value: 'append' }
|
|
399
|
+
],
|
|
400
|
+
default: 'truncate'
|
|
401
|
+
}
|
|
402
|
+
]);
|
|
403
|
+
|
|
404
|
+
options.mode = mode;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (isCI) {
|
|
408
|
+
console.log(chalk.gray(`\n[CI MODE] Running in automated mode...\n`));
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
await runImport(config, options);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
program
|
|
415
|
+
.command('import')
|
|
416
|
+
.description('Dump and import directly to target database (STARTER/PRO only)')
|
|
417
|
+
.option('-c, --config <file>', 'Config file', getDefaultConfigPath())
|
|
418
|
+
.option('-p, --profile <name>', 'Use named profile (STARTER/PRO)')
|
|
419
|
+
.option('-m, --mode <mode>', 'Import mode: truncate or append (skip prompt)', 'prompt')
|
|
420
|
+
.option('-y, --yes', 'Skip schema check and confirmation (for CI/CD)')
|
|
421
|
+
.option('--ci', 'CI/CD mode - no prompts (PRO)')
|
|
422
|
+
.action(async (options) => {
|
|
423
|
+
try {
|
|
424
|
+
const validModes = ['truncate', 'append', 'prompt'];
|
|
425
|
+
if (options.mode && !validModes.includes(options.mode)) {
|
|
426
|
+
console.log(chalk.red(`Invalid mode: ${options.mode}`));
|
|
427
|
+
console.log(chalk.gray('Valid modes: truncate, append'));
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (options.mode === 'prompt') {
|
|
432
|
+
options.mode = undefined;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const config = loadConfig(options.config, options.profile);
|
|
436
|
+
|
|
437
|
+
if (!config) {
|
|
438
|
+
console.log(chalk.red('Config file not found'));
|
|
439
|
+
console.log(chalk.gray('Run: dbsafedump init'));
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
await checkAndShowLicense();
|
|
444
|
+
|
|
445
|
+
if (options.profile && !canUseMultiProfile()) {
|
|
446
|
+
console.log(chalk.red('\nProfiles require STARTER or PRO license.'));
|
|
447
|
+
console.log(chalk.gray('Get your license at: https://dbsafedump.com/pricing\n'));
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (options.ci && !canUseCI()) {
|
|
452
|
+
console.log(chalk.red('\nCI/CD mode requires PRO license.'));
|
|
453
|
+
console.log(chalk.gray('Get your license at: https://dbsafedump.com/pricing\n'));
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (!canScrub()) {
|
|
458
|
+
console.log(chalk.red('\nImport/Scrub requires STARTER or PRO license.'));
|
|
459
|
+
console.log(chalk.gray('Get your license at: https://dbsafedump.com/pricing\n'));
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (!config.connections || !config.connections.source || !config.connections.target) {
|
|
464
|
+
console.log(chalk.red('Invalid config: missing source or target connection'));
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
await runImportWithSchema(config, options);
|
|
469
|
+
} catch (error) {
|
|
470
|
+
console.error(chalk.red(`\nError: ${error.message}`));
|
|
471
|
+
process.exit(1);
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
program
|
|
476
|
+
.command('schema')
|
|
477
|
+
.description('Show database schema (tables, keys, row counts)')
|
|
478
|
+
.option('-c, --config <file>', 'Config file', getDefaultConfigPath())
|
|
479
|
+
.action(async (options) => {
|
|
480
|
+
try {
|
|
481
|
+
const config = loadConfig(options.config);
|
|
482
|
+
|
|
483
|
+
if (!config) {
|
|
484
|
+
console.log(chalk.red('Config file not found'));
|
|
485
|
+
console.log(chalk.gray('Run: dbsafedump init'));
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
if (!config.connections || !config.connections.source) {
|
|
490
|
+
console.log(chalk.red('Invalid config: missing source connection'));
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
await checkAndShowLicense();
|
|
495
|
+
await runSchema(config);
|
|
496
|
+
} catch (error) {
|
|
497
|
+
console.error(chalk.red(`\nError: ${error.message}`));
|
|
498
|
+
process.exit(1);
|
|
499
|
+
}
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
program
|
|
503
|
+
.command('test-connection')
|
|
504
|
+
.description('Test database connections (source and target)')
|
|
505
|
+
.option('-c, --config <file>', 'Config file', getDefaultConfigPath())
|
|
506
|
+
.action(async (options) => {
|
|
507
|
+
try {
|
|
508
|
+
const config = loadConfig(options.config);
|
|
509
|
+
|
|
510
|
+
if (!config) {
|
|
511
|
+
console.log(chalk.red('Config file not found'));
|
|
512
|
+
console.log(chalk.gray('Run: dbsafedump init'));
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
if (!config.connections || !config.connections.source) {
|
|
517
|
+
console.log(chalk.red('Invalid config: missing source connection'));
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
await checkAndShowLicense();
|
|
522
|
+
await runTestConnections(config);
|
|
523
|
+
} catch (error) {
|
|
524
|
+
console.error(chalk.red(`\nError: ${error.message}`));
|
|
525
|
+
process.exit(1);
|
|
526
|
+
}
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
program
|
|
530
|
+
.command('config')
|
|
531
|
+
.description('Configure database connections interactively')
|
|
532
|
+
.action(async () => {
|
|
533
|
+
const runConfig = require('../src/commands/config');
|
|
534
|
+
await runConfig();
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
program
|
|
538
|
+
.command('discover')
|
|
539
|
+
.description('Analyze database and suggest masking rules (STARTER/PRO)')
|
|
540
|
+
.option('-c, --config <file>', 'Config file', getDefaultConfigPath())
|
|
541
|
+
.option('-p, --profile <name>', 'Use named profile (STARTER/PRO)')
|
|
542
|
+
.option('-o, --output <file>', 'Save suggestions to file')
|
|
543
|
+
.option('-a, --apply', 'Apply suggestions to config file')
|
|
544
|
+
.action(async (options) => {
|
|
545
|
+
try {
|
|
546
|
+
const config = loadConfig(options.config, options.profile);
|
|
547
|
+
|
|
548
|
+
if (!config) {
|
|
549
|
+
console.log(chalk.red('Config file not found'));
|
|
550
|
+
console.log(chalk.gray('Run: dbsafedump init'));
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
await checkAndShowLicense();
|
|
555
|
+
|
|
556
|
+
if (!canUseSmartDiscovery()) {
|
|
557
|
+
console.log(chalk.red('\nSmart Discovery requires STARTER or PRO license.'));
|
|
558
|
+
console.log(chalk.gray('Get your license at: https://dbsafedump.com/pricing\n'));
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
if (!config.connections || !config.connections.source) {
|
|
563
|
+
console.log(chalk.red('Invalid config: missing source connection'));
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
const tier = getTierInfo();
|
|
568
|
+
console.log(chalk.blue(`\nDBSafeDump - Smart Discovery`));
|
|
569
|
+
console.log(chalk.gray(`Running ${tier === 'pro' ? 'FULL' : 'BASIC'} analysis...\n`));
|
|
570
|
+
|
|
571
|
+
const sourceConfig = config.connections.source;
|
|
572
|
+
const driver = sourceConfig.driver || 'mysql';
|
|
573
|
+
|
|
574
|
+
const sourceTest = await require('../src/database').testConnection(sourceConfig);
|
|
575
|
+
if (!sourceTest.success) {
|
|
576
|
+
console.log(chalk.red(`Source connection failed!`));
|
|
577
|
+
console.log(chalk.red(` ${sourceTest.message}\n`));
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
console.log(chalk.green(`Connected to ${driver}://${sourceConfig.host}/${sourceConfig.database}\n`));
|
|
581
|
+
|
|
582
|
+
const conn = await createConnection(sourceConfig);
|
|
583
|
+
const allTables = await getTables(conn, driver);
|
|
584
|
+
const { shouldProcessTable } = require('../src/commands/utils');
|
|
585
|
+
const tables = allTables.filter(t => shouldProcessTable(t, config));
|
|
586
|
+
|
|
587
|
+
console.log(chalk.gray(`Analyzing ${tables.length} tables...\n`));
|
|
588
|
+
|
|
589
|
+
const discoveryResults = await discoverSensitiveColumns(conn, driver, tables, config);
|
|
590
|
+
|
|
591
|
+
await closeConnection(conn, driver);
|
|
592
|
+
|
|
593
|
+
if (Object.keys(discoveryResults).length === 0) {
|
|
594
|
+
console.log(chalk.yellow('\nNo sensitive columns detected.\n'));
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
console.log(chalk.green(`\nDiscovered ${Object.keys(discoveryResults).length} tables with sensitive data:\n`));
|
|
599
|
+
|
|
600
|
+
for (const [table, columns] of Object.entries(discoveryResults)) {
|
|
601
|
+
console.log(chalk.cyan(` ${table}:`));
|
|
602
|
+
for (const col of columns) {
|
|
603
|
+
const mask = col.suggestedMask || 'sensitive';
|
|
604
|
+
const confidence = col.confidence || 'unknown';
|
|
605
|
+
console.log(chalk.gray(` - ${col.column}: ${mask} (${confidence})`));
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
const suggestions = require('../src/smartDiscovery').generateConfigSuggestions(discoveryResults);
|
|
610
|
+
|
|
611
|
+
console.log(chalk.blue(`\nSuggested rules:\n`));
|
|
612
|
+
for (const [key, mask] of Object.entries(suggestions)) {
|
|
613
|
+
console.log(chalk.gray(` "${key}": ${mask}`));
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
if (!options.apply && !options.output) {
|
|
617
|
+
console.log(chalk.blue(`\nNext steps:\n`));
|
|
618
|
+
console.log(chalk.gray(` dbsafedump discover --apply # Apply rules to config.yml`));
|
|
619
|
+
console.log(chalk.gray(` dbsafedump discover --output rules.json # Save to file\n`));
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
if (options.apply) {
|
|
623
|
+
const { updateConfigField } = require('../src/config');
|
|
624
|
+
let applied = 0;
|
|
625
|
+
for (const [key, mask] of Object.entries(suggestions)) {
|
|
626
|
+
if (updateConfigField(options.config || getDefaultConfigPath(), key, mask)) {
|
|
627
|
+
applied++;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
console.log(chalk.green(`\nApplied ${applied} rules to config.\n`));
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
if (options.output) {
|
|
634
|
+
const fs = require('fs');
|
|
635
|
+
fs.writeFileSync(options.output, JSON.stringify(suggestions, null, 2));
|
|
636
|
+
console.log(chalk.green(`Saved suggestions to: ${options.output}\n`));
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
} catch (error) {
|
|
640
|
+
console.error(chalk.red(`\nError: ${error.message}`));
|
|
641
|
+
console.error(chalk.gray(error.stack));
|
|
642
|
+
process.exit(1);
|
|
643
|
+
}
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
program
|
|
647
|
+
.command('scrub')
|
|
648
|
+
.description('Anonymize data in-place in database (PRO)')
|
|
649
|
+
.option('-c, --config <file>', 'Config file', getDefaultConfigPath())
|
|
650
|
+
.option('-p, --profile <name>', 'Use named profile (STARTER/PRO)')
|
|
651
|
+
.option('-y, --yes', 'Skip confirmation')
|
|
652
|
+
.option('--ci', 'CI/CD mode (PRO)')
|
|
653
|
+
.option('--force', 'Skip confirmation (for CI/CD)')
|
|
654
|
+
.action(async (options) => {
|
|
655
|
+
try {
|
|
656
|
+
const config = loadConfig(options.config, options.profile);
|
|
657
|
+
|
|
658
|
+
if (!config) {
|
|
659
|
+
console.log(chalk.red('Config file not found'));
|
|
660
|
+
console.log(chalk.gray('Run: dbsafedump init'));
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
await checkAndShowLicense();
|
|
665
|
+
|
|
666
|
+
if (!canUseCI()) {
|
|
667
|
+
console.log(chalk.red('\nScrub requires PRO license.'));
|
|
668
|
+
console.log(chalk.gray('Get your license at: https://dbsafedump.com/pricing\n'));
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
if (!config.connections || !config.connections.source) {
|
|
673
|
+
console.log(chalk.red('Invalid config: missing source connection'));
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
const skipConfirm = options.ci || options.yes || options.force;
|
|
678
|
+
const dbConfig = config.connections.source;
|
|
679
|
+
const driver = dbConfig.driver || 'mysql';
|
|
680
|
+
|
|
681
|
+
if (!skipConfirm) {
|
|
682
|
+
console.log(chalk.yellow(`\nWARNING: This will MODIFY data in your database!`));
|
|
683
|
+
console.log(chalk.yellow(`This operation CANNOT be undone!\n`));
|
|
684
|
+
console.log(chalk.red(`Target database:`));
|
|
685
|
+
console.log(chalk.red(` ${driver}://${dbConfig.host}/${dbConfig.database}\n`));
|
|
686
|
+
|
|
687
|
+
const { confirm } = await inquirer.prompt([
|
|
688
|
+
{
|
|
689
|
+
type: 'confirm',
|
|
690
|
+
name: 'confirm',
|
|
691
|
+
message: 'Type "yes" to confirm the scrub operation:',
|
|
692
|
+
default: false
|
|
693
|
+
}
|
|
694
|
+
]);
|
|
695
|
+
|
|
696
|
+
if (!confirm) {
|
|
697
|
+
console.log(chalk.gray('\nScrub cancelled.\n'));
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
} else {
|
|
701
|
+
console.log(chalk.yellow(`\nWARNING: This will MODIFY data in your database!`));
|
|
702
|
+
console.log(chalk.yellow(`This operation CANNOT be undone!\n`));
|
|
703
|
+
console.log(chalk.red(`Target database:`));
|
|
704
|
+
console.log(chalk.red(` ${driver}://${dbConfig.host}/${dbConfig.database}\n`));
|
|
705
|
+
console.log(chalk.gray(`[${options.force ? 'FORCE' : 'CI MODE'}] Proceeding with scrub...\n`));
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
await runScrub(config, options);
|
|
709
|
+
} catch (error) {
|
|
710
|
+
console.error(chalk.red(`\nError: ${error.message}`));
|
|
711
|
+
process.exit(1);
|
|
712
|
+
}
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
program.parse(process.argv);
|
|
716
|
+
|
|
717
|
+
if (!process.argv.slice(2).length) {
|
|
718
|
+
program.outputHelp();
|
|
719
|
+
}
|