filepilot-enterprise-vault 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/db.cjs ADDED
@@ -0,0 +1,999 @@
1
+ const { Sequelize, DataTypes } = require('sequelize');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+
5
+ const DATA_DIR = process.env.VAULT_DATA_DIR || process.cwd();
6
+
7
+ // Load environment variables from .env if present in DATA_DIR
8
+ const envPath = path.join(DATA_DIR, '.env');
9
+ console.log(`[DB] DATA_DIR resolved to: ${DATA_DIR}`);
10
+ console.log(`[DB] Checking for .env file at: ${envPath} (Exists: ${fs.existsSync(envPath)})`);
11
+ if (fs.existsSync(envPath)) {
12
+ try {
13
+ const content = fs.readFileSync(envPath, 'utf8');
14
+ content.split(/\r?\n/).forEach(line => {
15
+ const parts = line.split('=');
16
+ if (parts.length >= 2) {
17
+ const key = parts[0].trim();
18
+ const val = parts.slice(1).join('=').trim().replace(/^['"]|['"]$/g, '');
19
+ if (key && !key.startsWith('#')) {
20
+ console.log(`[DB] Setting process.env.${key} from .env file`);
21
+ if (!process.env[key]) {
22
+ process.env[key] = val;
23
+ }
24
+ }
25
+ }
26
+ });
27
+ } catch (err) {
28
+ console.error("Failed to load .env file:", err);
29
+ }
30
+ }
31
+
32
+ const configPath = process.env.VAULT_CONFIG_PATH || path.join(DATA_DIR, 'config.json');
33
+
34
+ // Default database configuration (embedded SQLite fallback on first boot)
35
+ let dbConfig = {
36
+ dialect: 'sqlite',
37
+ storage: path.join(DATA_DIR, 'vault.db'),
38
+ logging: false
39
+ };
40
+
41
+ if (process.env.DB_DIALECT) {
42
+ const dialect = process.env.DB_DIALECT;
43
+ if (dialect === 'postgres') {
44
+ dbConfig = {
45
+ dialect: 'postgres',
46
+ host: process.env.DB_HOST || 'localhost',
47
+ port: parseInt(process.env.DB_PORT) || 5432,
48
+ username: process.env.DB_USERNAME,
49
+ password: process.env.DB_PASSWORD,
50
+ database: process.env.DB_NAME,
51
+ logging: false,
52
+ dialectOptions: (process.env.DB_SSL === 'true') ? {
53
+ ssl: {
54
+ require: true,
55
+ rejectUnauthorized: false
56
+ }
57
+ } : {}
58
+ };
59
+ } else if (dialect === 'mysql') {
60
+ dbConfig = {
61
+ dialect: 'mysql',
62
+ host: process.env.DB_HOST || 'localhost',
63
+ port: parseInt(process.env.DB_PORT) || 3306,
64
+ username: process.env.DB_USERNAME,
65
+ password: process.env.DB_PASSWORD,
66
+ database: process.env.DB_NAME,
67
+ logging: false
68
+ };
69
+ } else {
70
+ dbConfig = {
71
+ dialect: 'sqlite',
72
+ storage: process.env.DB_STORAGE || path.join(DATA_DIR, 'vault.db'),
73
+ logging: false
74
+ };
75
+ }
76
+ } else if (fs.existsSync(configPath)) {
77
+ try {
78
+ const raw = fs.readFileSync(configPath, 'utf8');
79
+ const parsed = JSON.parse(raw);
80
+ if (parsed.database) {
81
+ if (parsed.database.dialect === 'postgres') {
82
+ dbConfig = {
83
+ dialect: 'postgres',
84
+ host: parsed.database.host || 'localhost',
85
+ port: parseInt(parsed.database.port) || 5432,
86
+ username: parsed.database.username,
87
+ password: parsed.database.password,
88
+ database: parsed.database.database,
89
+ logging: false,
90
+ dialectOptions: parsed.database.ssl ? {
91
+ ssl: {
92
+ require: true,
93
+ rejectUnauthorized: false
94
+ }
95
+ } : {}
96
+ };
97
+ } else if (parsed.database.dialect === 'mysql') {
98
+ dbConfig = {
99
+ dialect: 'mysql',
100
+ host: parsed.database.host || 'localhost',
101
+ port: parseInt(parsed.database.port) || 3306,
102
+ username: parsed.database.username,
103
+ password: parsed.database.password,
104
+ database: parsed.database.database,
105
+ logging: false
106
+ };
107
+ } else {
108
+ dbConfig = {
109
+ dialect: 'sqlite',
110
+ storage: parsed.database.storage || path.join(DATA_DIR, 'vault.db'),
111
+ logging: false
112
+ };
113
+ }
114
+ }
115
+ } catch (err) {
116
+ console.error('Error loading database configuration:', err);
117
+ }
118
+ }
119
+
120
+ const sequelize = new Sequelize(dbConfig);
121
+
122
+ // Define Models:
123
+ // 1. User
124
+ const User = sequelize.define('User', {
125
+ id: {
126
+ type: DataTypes.UUID,
127
+ defaultValue: DataTypes.UUIDV4,
128
+ primaryKey: true
129
+ },
130
+ username: {
131
+ type: DataTypes.STRING,
132
+ allowNull: false,
133
+ unique: true
134
+ },
135
+ passwordHash: {
136
+ type: DataTypes.STRING,
137
+ allowNull: false
138
+ },
139
+ role: {
140
+ type: DataTypes.STRING, // 'admin' | 'manager' | 'operator' | 'auditor'
141
+ allowNull: false,
142
+ defaultValue: 'operator'
143
+ },
144
+ mfa_enabled: {
145
+ type: DataTypes.BOOLEAN,
146
+ allowNull: false,
147
+ defaultValue: false
148
+ },
149
+ mfa_secret_encrypted: {
150
+ type: DataTypes.TEXT,
151
+ allowNull: true
152
+ },
153
+ mfa_backup_codes_hash: {
154
+ type: DataTypes.TEXT,
155
+ allowNull: true
156
+ }
157
+ });
158
+
159
+ // 2. VaultGroup
160
+ const VaultGroup = sequelize.define('VaultGroup', {
161
+ id: {
162
+ type: DataTypes.STRING,
163
+ primaryKey: true
164
+ },
165
+ name: {
166
+ type: DataTypes.STRING,
167
+ allowNull: false
168
+ },
169
+ ipAllowlist: {
170
+ type: DataTypes.TEXT,
171
+ allowNull: true,
172
+ defaultValue: ''
173
+ },
174
+ wrapped_dek: {
175
+ type: DataTypes.TEXT,
176
+ allowNull: true
177
+ },
178
+ dek_version: {
179
+ type: DataTypes.INTEGER,
180
+ defaultValue: 1
181
+ },
182
+ migrated: {
183
+ type: DataTypes.BOOLEAN,
184
+ defaultValue: false
185
+ },
186
+ kms_provider: {
187
+ type: DataTypes.STRING,
188
+ allowNull: false,
189
+ defaultValue: 'local'
190
+ },
191
+ kms_config: {
192
+ type: DataTypes.TEXT,
193
+ allowNull: true
194
+ },
195
+ kms_credentials_encrypted: {
196
+ type: DataTypes.TEXT,
197
+ allowNull: true
198
+ },
199
+ legal_hold_active: {
200
+ type: DataTypes.BOOLEAN,
201
+ defaultValue: false
202
+ },
203
+ legal_hold_reason: {
204
+ type: DataTypes.TEXT,
205
+ allowNull: true
206
+ },
207
+ legal_hold_placed_by: {
208
+ type: DataTypes.STRING,
209
+ allowNull: true
210
+ },
211
+ legal_hold_placed_at: {
212
+ type: DataTypes.DATE,
213
+ allowNull: true
214
+ },
215
+ legal_hold_freeze_writes: {
216
+ type: DataTypes.BOOLEAN,
217
+ defaultValue: false
218
+ }
219
+ });
220
+
221
+ // 3. ConnectionProfile
222
+ const ConnectionProfile = sequelize.define('ConnectionProfile', {
223
+ id: {
224
+ type: DataTypes.STRING,
225
+ primaryKey: true
226
+ },
227
+ groupId: {
228
+ type: DataTypes.STRING,
229
+ allowNull: false
230
+ },
231
+ name: {
232
+ type: DataTypes.STRING,
233
+ allowNull: false
234
+ },
235
+ protocol: {
236
+ type: DataTypes.STRING,
237
+ allowNull: false
238
+ },
239
+ host: {
240
+ type: DataTypes.STRING,
241
+ allowNull: false
242
+ },
243
+ port: {
244
+ type: DataTypes.INTEGER,
245
+ allowNull: false
246
+ },
247
+ username: {
248
+ type: DataTypes.STRING,
249
+ allowNull: false
250
+ },
251
+ passwordEncrypted: {
252
+ type: DataTypes.TEXT,
253
+ allowNull: true
254
+ },
255
+ authMode: {
256
+ type: DataTypes.STRING,
257
+ allowNull: false,
258
+ defaultValue: 'password'
259
+ },
260
+ clientProfileId: {
261
+ type: DataTypes.STRING,
262
+ allowNull: true
263
+ },
264
+ jumpHost: {
265
+ type: DataTypes.STRING,
266
+ allowNull: true
267
+ },
268
+ jumpPort: {
269
+ type: DataTypes.INTEGER,
270
+ allowNull: true
271
+ },
272
+ jumpUsername: {
273
+ type: DataTypes.STRING,
274
+ allowNull: true
275
+ },
276
+ jumpPasswordEncrypted: {
277
+ type: DataTypes.TEXT,
278
+ allowNull: true
279
+ },
280
+ jumpAuthMode: {
281
+ type: DataTypes.STRING,
282
+ allowNull: true,
283
+ defaultValue: 'password'
284
+ }
285
+ });
286
+
287
+ // 4. AccessKey
288
+ const AccessKey = sequelize.define('AccessKey', {
289
+ token_hash: {
290
+ type: DataTypes.STRING,
291
+ primaryKey: true
292
+ },
293
+ userId: {
294
+ type: DataTypes.UUID,
295
+ allowNull: true
296
+ },
297
+ groupId: {
298
+ type: DataTypes.STRING,
299
+ allowNull: false
300
+ },
301
+ status: {
302
+ type: DataTypes.STRING,
303
+ allowNull: false,
304
+ defaultValue: 'active'
305
+ },
306
+ expiresAt: {
307
+ type: DataTypes.STRING,
308
+ allowNull: true
309
+ }
310
+ });
311
+
312
+ // Associations
313
+ User.hasMany(AccessKey, { foreignKey: 'userId', onDelete: 'SET NULL' });
314
+ AccessKey.belongsTo(User, { foreignKey: 'userId' });
315
+
316
+ VaultGroup.hasMany(ConnectionProfile, { foreignKey: 'groupId', onDelete: 'CASCADE' });
317
+ ConnectionProfile.belongsTo(VaultGroup, { foreignKey: 'groupId' });
318
+
319
+ VaultGroup.hasMany(AccessKey, { foreignKey: 'groupId', onDelete: 'CASCADE' });
320
+ AccessKey.belongsTo(VaultGroup, { foreignKey: 'groupId' });
321
+
322
+ // 5. TransferLog
323
+ const TransferLog = sequelize.define('TransferLog', {
324
+ id: {
325
+ type: DataTypes.INTEGER,
326
+ primaryKey: true,
327
+ autoIncrement: true
328
+ },
329
+ token: {
330
+ type: DataTypes.STRING,
331
+ allowNull: true
332
+ },
333
+ username: {
334
+ type: DataTypes.STRING,
335
+ allowNull: true
336
+ },
337
+ connectionId: {
338
+ type: DataTypes.STRING,
339
+ allowNull: false
340
+ },
341
+ filePath: {
342
+ type: DataTypes.TEXT,
343
+ allowNull: false
344
+ },
345
+ fileSize: {
346
+ type: DataTypes.BIGINT,
347
+ allowNull: true,
348
+ defaultValue: 0
349
+ },
350
+ action: {
351
+ type: DataTypes.STRING, // 'upload' | 'download' | 'delete'
352
+ allowNull: false
353
+ },
354
+ status: {
355
+ type: DataTypes.STRING, // 'success' | 'failed'
356
+ allowNull: false,
357
+ defaultValue: 'success'
358
+ },
359
+ errorMessage: {
360
+ type: DataTypes.TEXT,
361
+ allowNull: true
362
+ },
363
+ archived: {
364
+ type: DataTypes.BOOLEAN,
365
+ allowNull: false,
366
+ defaultValue: false
367
+ },
368
+ entry_hash: {
369
+ type: DataTypes.STRING(64),
370
+ allowNull: true
371
+ },
372
+ prev_hash: {
373
+ type: DataTypes.STRING(64),
374
+ allowNull: true
375
+ },
376
+ metadata: {
377
+ type: DataTypes.TEXT,
378
+ allowNull: true
379
+ },
380
+ hash_version: {
381
+ type: DataTypes.INTEGER,
382
+ allowNull: true
383
+ }
384
+ }, {
385
+ hooks: {
386
+ beforeCreate: async (log, options) => {
387
+ const crypto = require('crypto');
388
+ const prevLog = await TransferLog.findOne({
389
+ order: [['id', 'DESC']]
390
+ });
391
+ const prevHash = prevLog ? (prevLog.entry_hash || '0') : '0';
392
+ log.prev_hash = prevHash;
393
+ log.hash_version = 2; // New entries are version 2
394
+
395
+ const logFields = {
396
+ token: log.token || null,
397
+ username: log.username || null,
398
+ connectionId: log.connectionId,
399
+ filePath: log.filePath,
400
+ fileSize: log.fileSize ? parseInt(log.fileSize) : 0,
401
+ action: log.action,
402
+ status: log.status || 'success',
403
+ errorMessage: log.errorMessage || null,
404
+ metadata: log.metadata || null,
405
+ prev_hash: prevHash
406
+ };
407
+
408
+ const hashInput = JSON.stringify(logFields);
409
+ log.entry_hash = crypto.createHash('sha256').update(hashInput).digest('hex');
410
+ }
411
+ }
412
+ });
413
+
414
+ // 6. FileVersion
415
+ const FileVersion = sequelize.define('FileVersion', {
416
+ id: {
417
+ type: DataTypes.INTEGER,
418
+ primaryKey: true,
419
+ autoIncrement: true
420
+ },
421
+ connectionId: {
422
+ type: DataTypes.STRING,
423
+ allowNull: false
424
+ },
425
+ filePath: {
426
+ type: DataTypes.TEXT,
427
+ allowNull: false
428
+ },
429
+ version: {
430
+ type: DataTypes.INTEGER,
431
+ allowNull: false
432
+ },
433
+ size: {
434
+ type: DataTypes.BIGINT,
435
+ allowNull: false
436
+ },
437
+ hash: {
438
+ type: DataTypes.STRING,
439
+ allowNull: true
440
+ },
441
+ backupPath: {
442
+ type: DataTypes.TEXT,
443
+ allowNull: true
444
+ },
445
+ modifiedBy: {
446
+ type: DataTypes.STRING, // username or user_id
447
+ allowNull: true
448
+ }
449
+ });
450
+
451
+ // 7. SystemConfig
452
+ const SystemConfig = sequelize.define('SystemConfig', {
453
+ key: {
454
+ type: DataTypes.STRING,
455
+ primaryKey: true
456
+ },
457
+ value: {
458
+ type: DataTypes.TEXT,
459
+ allowNull: true
460
+ }
461
+ });
462
+
463
+ // 8. PendingReversion
464
+ const PendingReversion = sequelize.define('PendingReversion', {
465
+ id: {
466
+ type: DataTypes.INTEGER,
467
+ autoIncrement: true,
468
+ primaryKey: true
469
+ },
470
+ groupId: {
471
+ type: DataTypes.STRING,
472
+ allowNull: false
473
+ },
474
+ connectionId: {
475
+ type: DataTypes.STRING,
476
+ allowNull: false
477
+ },
478
+ filePath: {
479
+ type: DataTypes.STRING,
480
+ allowNull: false
481
+ },
482
+ content: {
483
+ type: DataTypes.TEXT,
484
+ allowNull: false
485
+ },
486
+ status: {
487
+ type: DataTypes.STRING,
488
+ defaultValue: 'pending'
489
+ },
490
+ tokenHash: {
491
+ type: DataTypes.STRING,
492
+ allowNull: true
493
+ }
494
+ });
495
+
496
+ // 9. AdminSession
497
+ const AdminSession = sequelize.define('AdminSession', {
498
+ id: {
499
+ type: DataTypes.UUID,
500
+ defaultValue: DataTypes.UUIDV4,
501
+ primaryKey: true
502
+ },
503
+ session_hash: {
504
+ type: DataTypes.STRING(64),
505
+ unique: true,
506
+ allowNull: false
507
+ },
508
+ userId: {
509
+ type: DataTypes.UUID,
510
+ allowNull: false,
511
+ references: {
512
+ model: 'Users',
513
+ key: 'id'
514
+ },
515
+ onDelete: 'CASCADE'
516
+ },
517
+ ipAddress: {
518
+ type: DataTypes.STRING(45),
519
+ allowNull: false
520
+ },
521
+ userAgent: {
522
+ type: DataTypes.TEXT,
523
+ allowNull: true
524
+ },
525
+ csrfToken: {
526
+ type: DataTypes.STRING(32),
527
+ allowNull: false
528
+ },
529
+ expiresAt: {
530
+ type: DataTypes.DATE,
531
+ allowNull: false
532
+ },
533
+ lastActivityAt: {
534
+ type: DataTypes.DATE,
535
+ allowNull: false,
536
+ defaultValue: DataTypes.NOW
537
+ }
538
+ });
539
+
540
+ // 9.5. MfaChallenge
541
+ const MfaChallenge = sequelize.define('MfaChallenge', {
542
+ id: {
543
+ type: DataTypes.STRING,
544
+ primaryKey: true
545
+ },
546
+ userId: {
547
+ type: DataTypes.UUID,
548
+ allowNull: false,
549
+ references: {
550
+ model: 'Users',
551
+ key: 'id'
552
+ },
553
+ onDelete: 'CASCADE'
554
+ },
555
+ createdAt: {
556
+ type: DataTypes.DATE,
557
+ allowNull: false,
558
+ defaultValue: DataTypes.NOW
559
+ },
560
+ expiresAt: {
561
+ type: DataTypes.DATE,
562
+ allowNull: false
563
+ },
564
+ consumed: {
565
+ type: DataTypes.BOOLEAN,
566
+ allowNull: false,
567
+ defaultValue: false
568
+ }
569
+ });
570
+
571
+ // Associations
572
+ User.hasMany(AdminSession, { foreignKey: 'userId', onDelete: 'CASCADE' });
573
+ AdminSession.belongsTo(User, { foreignKey: 'userId' });
574
+
575
+ User.hasMany(MfaChallenge, { foreignKey: 'userId', onDelete: 'CASCADE' });
576
+ MfaChallenge.belongsTo(User, { foreignKey: 'userId' });
577
+
578
+ // 10. KeyVersion
579
+ const KeyVersion = sequelize.define('KeyVersion', {
580
+ version: {
581
+ type: DataTypes.INTEGER,
582
+ primaryKey: true
583
+ },
584
+ createdAt: {
585
+ type: DataTypes.DATE,
586
+ allowNull: false,
587
+ defaultValue: DataTypes.NOW
588
+ },
589
+ retiredAt: {
590
+ type: DataTypes.DATE,
591
+ allowNull: true
592
+ }
593
+ });
594
+
595
+ // Initialize database (sync schema)
596
+ // Initialize database (sync schema)
597
+ async function initDb() {
598
+ const crypto = require('crypto');
599
+
600
+ let existingKeys = [];
601
+ try {
602
+ const [results] = await sequelize.query("SELECT * FROM `AccessKeys` LIMIT 1");
603
+ if (results.length > 0 && 'token' in results[0]) {
604
+ const [allKeys] = await sequelize.query("SELECT * FROM `AccessKeys`");
605
+ existingKeys = allKeys;
606
+ console.log(`[Migration] Found ${existingKeys.length} existing plaintext tokens to migrate.`);
607
+ await sequelize.query("DROP TABLE `AccessKeys`");
608
+ console.log("[Migration] Dropped old AccessKeys table.");
609
+ }
610
+ } catch (err) {
611
+ // Table or column doesn't exist yet, which is fine
612
+ }
613
+
614
+ if (sequelize.getDialect() === 'sqlite') {
615
+ await sequelize.query("PRAGMA foreign_keys = OFF;");
616
+ }
617
+ await sequelize.sync({ alter: true });
618
+ if (sequelize.getDialect() === 'sqlite') {
619
+ await sequelize.query("PRAGMA foreign_keys = ON;");
620
+ }
621
+
622
+ // Enforce DB-level delete prevention triggers on TransferLogs
623
+ try {
624
+ const dialect = (sequelize.connectionManager && sequelize.connectionManager.dialectName) || sequelize.getDialect();
625
+ if (dialect === 'sqlite') {
626
+ await sequelize.query(`
627
+ CREATE TRIGGER IF NOT EXISTS prevent_delete_transferlog
628
+ BEFORE DELETE ON \`TransferLogs\`
629
+ BEGIN
630
+ SELECT RAISE(ABORT, 'Deletes are not allowed on TransferLogs table');
631
+ END;
632
+ `);
633
+ console.log("[DB Trigger] SQLite prevent_delete_transferlog trigger configured.");
634
+ } else if (dialect === 'mysql') {
635
+ await sequelize.query(`DROP TRIGGER IF EXISTS prevent_delete_transferlog;`);
636
+ await sequelize.query(`
637
+ CREATE TRIGGER prevent_delete_transferlog BEFORE DELETE ON \`TransferLogs\`
638
+ FOR EACH ROW
639
+ BEGIN
640
+ SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Deletes are not allowed on TransferLogs table';
641
+ END;
642
+ `);
643
+ console.log("[DB Trigger] MySQL prevent_delete_transferlog trigger configured.");
644
+
645
+ // Defensively attempt to revoke DELETE, DROP privileges on TransferLogs for the current user
646
+ try {
647
+ await sequelize.query(`REVOKE DELETE, DROP ON \`TransferLogs\` FROM CURRENT_USER;`);
648
+ console.log("[DB Privilege] Revoked DELETE, DROP on TransferLogs from CURRENT_USER.");
649
+ } catch (privErr) {
650
+ console.warn(`[DB Privilege Warning] Could not revoke privileges (current connection user may have global grants or lack grant privileges): ${privErr.message}`);
651
+ }
652
+ } else if (dialect === 'postgres') {
653
+ await sequelize.query(`
654
+ CREATE OR REPLACE FUNCTION prevent_delete_transferlog_func()
655
+ RETURNS TRIGGER AS $$
656
+ BEGIN
657
+ RAISE EXCEPTION 'Deletes are not allowed on TransferLogs table';
658
+ END;
659
+ $$ LANGUAGE plpgsql;
660
+ `);
661
+ await sequelize.query(`DROP TRIGGER IF EXISTS prevent_delete_transferlog ON "TransferLogs";`);
662
+ await sequelize.query(`
663
+ CREATE TRIGGER prevent_delete_transferlog
664
+ BEFORE DELETE ON "TransferLogs"
665
+ FOR EACH ROW
666
+ EXECUTE FUNCTION prevent_delete_transferlog_func();
667
+ `);
668
+ console.log("[DB Trigger] PostgreSQL prevent_delete_transferlog trigger configured.");
669
+
670
+ try {
671
+ await sequelize.query(`REVOKE DELETE, TRUNCATE ON "TransferLogs" FROM CURRENT_USER;`);
672
+ console.log("[DB Privilege] Revoked DELETE, TRUNCATE on TransferLogs from CURRENT_USER.");
673
+ } catch (privErr) {
674
+ console.warn(`[DB Privilege Warning] Could not revoke privileges: ${privErr.message}`);
675
+ }
676
+ }
677
+ } catch (triggerErr) {
678
+ console.error("Failed to configure database-level triggers or privileges:", triggerErr);
679
+ }
680
+
681
+ if (existingKeys.length > 0) {
682
+ console.log(`[Migration] Migrating ${existingKeys.length} tokens to SHA-256...`);
683
+ for (const key of existingKeys) {
684
+ const rawToken = key.token;
685
+ if (rawToken) {
686
+ const hashed = crypto.createHash('sha256').update(rawToken).digest('hex');
687
+ await AccessKey.create({
688
+ token_hash: hashed,
689
+ userId: key.userId,
690
+ groupId: key.groupId,
691
+ status: key.status,
692
+ expiresAt: key.expiresAt
693
+ });
694
+ }
695
+ }
696
+ console.log("[Migration] Successfully migrated all existing tokens in-place.");
697
+ }
698
+
699
+ // Seed default system configs if not present
700
+ await SystemConfig.findOrCreate({
701
+ where: { key: 'siem_webhook_url' },
702
+ defaults: { value: '' }
703
+ });
704
+
705
+ await SystemConfig.findOrCreate({
706
+ where: { key: 'siem_webhook_secret' },
707
+ defaults: { value: crypto.randomBytes(32).toString('hex') }
708
+ });
709
+
710
+ await SystemConfig.findOrCreate({
711
+ where: { key: 'audit_logging_enabled' },
712
+ defaults: { value: 'true' }
713
+ });
714
+
715
+ // Default admin user auto-seeding removed for security. First admin account must be created via setup wizard.
716
+
717
+ // One-time migration from db.json if tables are empty
718
+ const groupCount = await VaultGroup.count();
719
+ const dbJsonPath = path.join(DATA_DIR, 'db.json');
720
+ if (fs.existsSync(dbJsonPath)) {
721
+ try {
722
+ const rawJson = fs.readFileSync(dbJsonPath, 'utf8');
723
+ const dbJson = JSON.parse(rawJson);
724
+
725
+ if (groupCount === 0) {
726
+ console.log('Migrating data from db.json into database...');
727
+ // 1. Migrate Groups & Profiles
728
+ if (dbJson.groups) {
729
+ for (const g of dbJson.groups) {
730
+ await VaultGroup.create({
731
+ id: g.id,
732
+ name: g.name,
733
+ ipAllowlist: g.ip_allowlist || ''
734
+ });
735
+
736
+ if (g.profiles) {
737
+ for (const p of g.profiles) {
738
+ let keyVal = p.password || '';
739
+ if (p.options && p.options.private_key) {
740
+ keyVal = p.options.private_key;
741
+ }
742
+ await ConnectionProfile.create({
743
+ id: p.id,
744
+ groupId: g.id,
745
+ name: p.name,
746
+ protocol: p.protocol,
747
+ host: p.host,
748
+ port: parseInt(p.port) || 22,
749
+ username: p.username || '',
750
+ passwordEncrypted: keyVal,
751
+ authMode: p.options && p.options.private_key ? 'keypair' : 'password'
752
+ });
753
+ }
754
+ }
755
+ }
756
+ }
757
+ }
758
+
759
+ // 2. Migrate Access Tokens if table is empty AND SEED_DEV_TOKENS=true is set
760
+ const tokenCount = await AccessKey.count();
761
+ if (tokenCount === 0 && dbJson.tokens && process.env.SEED_DEV_TOKENS === 'true') {
762
+ console.log('Seeding dev tokens from db.json into database...');
763
+ for (const t of dbJson.tokens) {
764
+ const username = t.user || 'Unknown User';
765
+ const [usr] = await User.findOrCreate({
766
+ where: { username },
767
+ defaults: {
768
+ passwordHash: crypto.createHash('sha256').update('temporary-user-password').digest('hex'),
769
+ role: 'operator'
770
+ }
771
+ });
772
+
773
+ const hashed = crypto.createHash('sha256').update(t.token).digest('hex');
774
+ await AccessKey.create({
775
+ token_hash: hashed,
776
+ userId: usr.id,
777
+ groupId: t.groupId,
778
+ status: t.status || 'active',
779
+ expiresAt: t.expires_at || ''
780
+ });
781
+ }
782
+ }
783
+
784
+ // 3. Backfill TransferLog hashes if missing
785
+ const unhashedLogs = await TransferLog.findAll({
786
+ where: { entry_hash: null },
787
+ order: [['id', 'ASC']]
788
+ });
789
+ if (unhashedLogs.length > 0) {
790
+ console.log(`[Migration] Backfilling hash chain for ${unhashedLogs.length} audit logs...`);
791
+ for (const log of unhashedLogs) {
792
+ const prevLog = await TransferLog.findOne({
793
+ where: {
794
+ id: {
795
+ [Sequelize.Op.lt]: log.id
796
+ }
797
+ },
798
+ order: [['id', 'DESC']]
799
+ });
800
+ const prevHash = prevLog ? (prevLog.entry_hash || '0') : '0';
801
+ log.prev_hash = prevHash;
802
+
803
+ const logFields = {
804
+ token: log.token || null,
805
+ username: log.username || null,
806
+ connectionId: log.connectionId,
807
+ filePath: log.filePath,
808
+ fileSize: log.fileSize ? parseInt(log.fileSize) : 0,
809
+ action: log.action,
810
+ status: log.status || 'success',
811
+ errorMessage: log.errorMessage || null,
812
+ prev_hash: prevHash
813
+ };
814
+ const hashInput = JSON.stringify(logFields);
815
+ log.entry_hash = crypto.createHash('sha256').update(hashInput).digest('hex');
816
+ await log.save();
817
+ }
818
+ console.log('[Migration] Finished backfilling audit log hashes.');
819
+ }
820
+
821
+ // 4. Envelope Encryption Migration for VaultGroups
822
+ await KeyVersion.findOrCreate({
823
+ where: { version: 1 },
824
+ defaults: { createdAt: new Date() }
825
+ });
826
+
827
+ const loadKekLocal = () => {
828
+ const envKey = process.env.VAULT_MASTER_KEY;
829
+ if (envKey) {
830
+ if (envKey.length === 64 && /^[0-9a-fA-F]+$/.test(envKey)) {
831
+ return Buffer.from(envKey, 'hex');
832
+ } else if (envKey.length === 44 && /^[0-9a-zA-Z+/=]+$/.test(envKey)) {
833
+ return Buffer.from(envKey, 'base64');
834
+ } else {
835
+ return Buffer.from(envKey, 'utf8');
836
+ }
837
+ }
838
+ const keyPath = path.join(DATA_DIR, '.vault_key');
839
+ if (fs.existsSync(keyPath)) {
840
+ const fileKey = fs.readFileSync(keyPath).toString('utf8').trim();
841
+ if (fileKey.length === 64 && /^[0-9a-fA-F]+$/.test(fileKey)) {
842
+ return Buffer.from(fileKey, 'hex');
843
+ } else if (fileKey.length === 44 && /^[0-9a-zA-Z+/=]+$/.test(fileKey)) {
844
+ return Buffer.from(fileKey, 'base64');
845
+ } else {
846
+ return fs.readFileSync(keyPath);
847
+ }
848
+ }
849
+ return crypto.scryptSync("fallback-vault-salt", "salt", 32);
850
+ };
851
+
852
+ const localKek = loadKekLocal();
853
+
854
+ const decryptWithKeyLocal = (ciphertext, key) => {
855
+ if (!ciphertext || !ciphertext.startsWith('enc:')) return ciphertext;
856
+ try {
857
+ const parts = ciphertext.split(':');
858
+ if (parts.length !== 4) return ciphertext;
859
+ const iv = Buffer.from(parts[1], 'hex');
860
+ const tag = Buffer.from(parts[2], 'hex');
861
+ const encryptedText = Buffer.from(parts[3], 'hex');
862
+ const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
863
+ decipher.setAuthTag(tag);
864
+ let decrypted = decipher.update(encryptedText, 'hex', 'utf8');
865
+ decrypted += decipher.final('utf8');
866
+ return decrypted;
867
+ } catch (err) {
868
+ console.error("Migration decryption failed:", err);
869
+ return null;
870
+ }
871
+ };
872
+
873
+ const encryptWithKeyLocal = (plaintext, key) => {
874
+ if (!plaintext) return plaintext;
875
+ try {
876
+ const iv = crypto.randomBytes(12);
877
+ const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
878
+ let encrypted = cipher.update(plaintext, 'utf8', 'hex');
879
+ encrypted += cipher.final('hex');
880
+ const tag = cipher.getAuthTag().toString('hex');
881
+ return `enc:${iv.toString('hex')}:${tag}:${encrypted}`;
882
+ } catch (err) {
883
+ console.error("Migration encryption failed:", err);
884
+ throw err;
885
+ }
886
+ };
887
+
888
+ const wrapDekLocal = (dek, kek) => {
889
+ const iv = crypto.randomBytes(12);
890
+ const cipher = crypto.createCipheriv('aes-256-gcm', kek, iv);
891
+ let encrypted = cipher.update(dek);
892
+ encrypted = Buffer.concat([encrypted, cipher.final()]);
893
+ const tag = cipher.getAuthTag();
894
+ const combined = Buffer.concat([iv, tag, encrypted]);
895
+ return combined.toString('base64');
896
+ };
897
+
898
+ const unmigratedGroups = await VaultGroup.findAll({ where: { migrated: false } });
899
+ if (unmigratedGroups.length > 0) {
900
+ console.log(`[Migration] Migrating ${unmigratedGroups.length} VaultGroups to Envelope Encryption...`);
901
+ for (const group of unmigratedGroups) {
902
+ const dek = crypto.randomBytes(32);
903
+ const profiles = await ConnectionProfile.findAll({ where: { groupId: group.id } });
904
+
905
+ for (const p of profiles) {
906
+ const decPass = decryptWithKeyLocal(p.passwordEncrypted, localKek);
907
+ const decJumpPass = decryptWithKeyLocal(p.jumpPasswordEncrypted, localKek);
908
+
909
+ p.passwordEncrypted = encryptWithKeyLocal(decPass, dek);
910
+ p.jumpPasswordEncrypted = encryptWithKeyLocal(decJumpPass, dek);
911
+ await p.save();
912
+ }
913
+
914
+ group.wrapped_dek = wrapDekLocal(dek, localKek);
915
+ group.dek_version = 1;
916
+ group.migrated = true;
917
+ await group.save();
918
+ }
919
+ console.log('[Migration] Finished migrating groups to Envelope Encryption.');
920
+ }
921
+
922
+ } catch (err) {
923
+ console.error('Failed to migrate/seed data from db.json:', err);
924
+ }
925
+ }
926
+
927
+ // 3. Migrate legacy audit logs (move attribution JSON from errorMessage to metadata)
928
+ try {
929
+ const logs = await TransferLog.findAll();
930
+ const unmigratedLogs = logs.filter(l => l.hash_version !== 1 && l.hash_version !== 2);
931
+ if (unmigratedLogs.length > 0) {
932
+ console.log(`[Migration] Migrating ${unmigratedLogs.length} legacy TransferLog entries...`);
933
+ for (const log of unmigratedLogs) {
934
+ if (log.errorMessage && log.errorMessage.trim().startsWith('{') && log.errorMessage.trim().endsWith('}')) {
935
+ try {
936
+ JSON.parse(log.errorMessage);
937
+ log.metadata = log.errorMessage;
938
+ log.errorMessage = null;
939
+ } catch (e) {
940
+ // Not valid JSON, keep as plain text errorMessage
941
+ }
942
+ }
943
+ log.hash_version = 1;
944
+ await log.save();
945
+ }
946
+ console.log("[Migration] Legacy TransferLog entries migration completed.");
947
+ }
948
+ } catch (migErr) {
949
+ console.error("Failed to migrate legacy TransferLogs:", migErr);
950
+ }
951
+ }
952
+
953
+ function updateDbConnection(config) {
954
+ sequelize.config.database = config.database;
955
+ sequelize.config.username = config.username;
956
+ sequelize.config.password = config.password;
957
+ sequelize.config.host = config.host || 'localhost';
958
+ sequelize.config.port = config.port || 3306;
959
+
960
+ sequelize.options.dialect = config.dialect || 'mysql';
961
+ if (config.storage) {
962
+ sequelize.options.storage = config.storage;
963
+ }
964
+
965
+ sequelize.options.dialectOptions = config.ssl ? {
966
+ ssl: {
967
+ require: true,
968
+ rejectUnauthorized: false
969
+ }
970
+ } : {};
971
+
972
+ if (sequelize.connectionManager) {
973
+ if (sequelize.connectionManager.pool) {
974
+ try {
975
+ sequelize.connectionManager.pool.destroyAllNow();
976
+ } catch (_) {}
977
+ }
978
+ if (sequelize.connectionManager.connections) {
979
+ sequelize.connectionManager.connections = {};
980
+ }
981
+ }
982
+ }
983
+
984
+ module.exports = {
985
+ sequelize,
986
+ User,
987
+ VaultGroup,
988
+ ConnectionProfile,
989
+ AccessKey,
990
+ TransferLog,
991
+ FileVersion,
992
+ SystemConfig,
993
+ PendingReversion,
994
+ AdminSession,
995
+ MfaChallenge,
996
+ KeyVersion,
997
+ initDb,
998
+ updateDbConnection
999
+ };