libastion 0.0.1

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.
@@ -0,0 +1,128 @@
1
+ const db = require('../db');
2
+ const logger = require('../logger');
3
+
4
+ const service = 'audit';
5
+
6
+ /**
7
+ * Create a new audit session
8
+ */
9
+ function createSession(userId, hostId, clientIp) {
10
+ const result = db.prepare(
11
+ 'INSERT INTO audit_sessions (user_id, host_id, client_ip) VALUES (?, ?, ?)'
12
+ ).run(userId, hostId, clientIp);
13
+
14
+ return {
15
+ id: result.lastInsertRowid,
16
+ userId,
17
+ hostId,
18
+ clientIp,
19
+ };
20
+ }
21
+
22
+ /**
23
+ * End an audit session
24
+ */
25
+ function endSession(sessionId, status = 'closed') {
26
+ db.prepare(
27
+ 'UPDATE audit_sessions SET end_time = datetime(\'now\'), status = ? WHERE id = ?'
28
+ ).run(status, sessionId);
29
+ }
30
+
31
+ /**
32
+ * Record a command
33
+ */
34
+ function recordCommand(sessionId, command, blocked = false) {
35
+ db.prepare(
36
+ 'INSERT INTO audit_commands (session_id, command, blocked) VALUES (?, ?, ?)'
37
+ ).run(sessionId, command, blocked ? 1 : 0);
38
+ }
39
+
40
+ /**
41
+ * Record a file transfer
42
+ */
43
+ function recordTransfer(sessionId, direction, filePath, size = 0) {
44
+ db.prepare(
45
+ 'INSERT INTO audit_transfers (session_id, direction, path, size) VALUES (?, ?, ?, ?)'
46
+ ).run(sessionId, direction, filePath, size);
47
+ }
48
+
49
+ /**
50
+ * Record a port forward
51
+ */
52
+ function recordForward(sessionId, fwdType, srcAddr, srcPort, dstAddr, dstPort) {
53
+ db.prepare(
54
+ 'INSERT INTO audit_forwards (session_id, fwd_type, src_addr, src_port, dst_addr, dst_port) VALUES (?, ?, ?, ?, ?, ?)'
55
+ ).run(sessionId, fwdType, srcAddr, srcPort, dstAddr, dstPort);
56
+ }
57
+
58
+ /**
59
+ * Query audit commands
60
+ */
61
+ function queryCommands(sessionId, options = {}) {
62
+ let sql = 'SELECT * FROM audit_commands WHERE session_id = ?';
63
+ const params = [sessionId];
64
+
65
+ if (options.since) {
66
+ sql += ' AND timestamp >= ?';
67
+ params.push(options.since);
68
+ }
69
+ if (options.until) {
70
+ sql += ' AND timestamp <= ?';
71
+ params.push(options.until);
72
+ }
73
+ if (options.command) {
74
+ sql += ' AND command LIKE ?';
75
+ params.push(`%${options.command}%`);
76
+ }
77
+ if (options.blocked !== undefined) {
78
+ sql += ' AND blocked = ?';
79
+ params.push(options.blocked ? 1 : 0);
80
+ }
81
+
82
+ sql += ' ORDER BY timestamp DESC';
83
+ if (options.limit) {
84
+ sql += ' LIMIT ?';
85
+ params.push(options.limit);
86
+ }
87
+
88
+ return db.prepare(sql).all(...params);
89
+ }
90
+
91
+ /**
92
+ * Query sessions
93
+ */
94
+ function querySessions(options = {}) {
95
+ let sql = 'SELECT s.*, u.username, h.name as host_name, h.host as host_addr FROM audit_sessions s JOIN users u ON u.id = s.user_id JOIN hosts h ON h.id = s.host_id WHERE 1=1';
96
+ const params = [];
97
+
98
+ if (options.userId) {
99
+ sql += ' AND s.user_id = ?';
100
+ params.push(options.userId);
101
+ }
102
+ if (options.hostId) {
103
+ sql += ' AND s.host_id = ?';
104
+ params.push(options.hostId);
105
+ }
106
+ if (options.since) {
107
+ sql += ' AND s.start_time >= ?';
108
+ params.push(options.since);
109
+ }
110
+ if (options.until) {
111
+ sql += ' AND s.start_time <= ?';
112
+ params.push(options.until);
113
+ }
114
+ if (options.status) {
115
+ sql += ' AND s.status = ?';
116
+ params.push(options.status);
117
+ }
118
+
119
+ sql += ' ORDER BY s.start_time DESC';
120
+ if (options.limit) {
121
+ sql += ' LIMIT ?';
122
+ params.push(options.limit);
123
+ }
124
+
125
+ return db.prepare(sql).all(...params);
126
+ }
127
+
128
+ module.exports = { createSession, endSession, recordCommand, recordTransfer, recordForward, queryCommands, querySessions };
@@ -0,0 +1,98 @@
1
+ const db = require('../db');
2
+ const bcrypt = require('bcryptjs');
3
+ const logger = require('../logger');
4
+ const config = require('../config');
5
+
6
+ const service = 'auth';
7
+
8
+ // Create initial admin user if not exists
9
+ function initDefaults() {
10
+ const user = db.prepare('SELECT id FROM users WHERE username = ?').get('admin');
11
+ if (!user) {
12
+ const hash = bcrypt.hashSync('admin123', 10);
13
+ db.prepare(
14
+ 'INSERT INTO users (username, password_hash, role) VALUES (?, ?, ?)'
15
+ ).run('admin', hash, 'admin');
16
+ logger.info(`${service}: Created default admin user`, { username: 'admin' });
17
+ }
18
+ }
19
+
20
+ // Authenticate user by password
21
+ function authenticate(username, password) {
22
+ const user = db.prepare('SELECT * FROM users WHERE username = ?').get(username);
23
+ if (!user) {
24
+ logger.warn(`${service}: User not found`, { username });
25
+ return null;
26
+ }
27
+
28
+ // Check if locked
29
+ if (user.status === 'locked' && user.locked_until) {
30
+ const now = Date.now();
31
+ if (now < user.locked_until) {
32
+ logger.warn(`${service}: User locked`, { username });
33
+ return null;
34
+ } else {
35
+ // Lock expired, reset
36
+ db.prepare('UPDATE users SET status = ?, failed_attempts = 0, locked_until = NULL WHERE id = ?').run('active', user.id);
37
+ }
38
+ }
39
+
40
+ // Verify password
41
+ const valid = bcrypt.compareSync(password, user.password_hash);
42
+ if (!valid) {
43
+ // Increment failed attempts
44
+ const newAttempts = user.failed_attempts + 1;
45
+ let status = user.status;
46
+ let lockedUntil = user.locked_until;
47
+
48
+ if (newAttempts >= config.auth.maxFailedAttempts) {
49
+ status = 'locked';
50
+ lockedUntil = Date.now() + config.auth.lockDurationSec * 1000;
51
+ }
52
+
53
+ db.prepare(
54
+ 'UPDATE users SET failed_attempts = ?, status = ?, locked_until = ? WHERE id = ?'
55
+ ).run(newAttempts, status, lockedUntil, user.id);
56
+
57
+ logger.warn(`${service}: Failed login attempt`, { username, attempts: newAttempts });
58
+ return null;
59
+ }
60
+
61
+ // Reset failed attempts on successful login
62
+ if (user.failed_attempts > 0) {
63
+ db.prepare('UPDATE users SET failed_attempts = 0, status = ?, locked_until = NULL WHERE id = ?').run('active', user.id);
64
+ }
65
+
66
+ return {
67
+ id: user.id,
68
+ username: user.username,
69
+ role: user.role,
70
+ };
71
+ }
72
+
73
+ // Get user's authorized hosts
74
+ function getUserHosts(userId) {
75
+ const rows = db.prepare(`
76
+ SELECT h.* FROM hosts h
77
+ JOIN user_host_perms uhp ON uhp.host_id = h.id
78
+ WHERE uhp.user_id = ?
79
+ `).all(userId);
80
+ return rows;
81
+ }
82
+
83
+ // Create user
84
+ function createUser(username, password, role = 'user') {
85
+ const hash = bcrypt.hashSync(password, 10);
86
+ const result = db.prepare(
87
+ 'INSERT INTO users (username, password_hash, role) VALUES (?, ?, ?)'
88
+ ).run(username, hash, role);
89
+ return { id: result.lastInsertRowid, username, role };
90
+ }
91
+
92
+ // Update user password
93
+ function updatePassword(userId, newPassword) {
94
+ const hash = bcrypt.hashSync(newPassword, 10);
95
+ db.prepare('UPDATE users SET password_hash = ?, updated_at = datetime(\'now\') WHERE id = ?').run(hash, userId);
96
+ }
97
+
98
+ module.exports = { initDefaults, authenticate, getUserHosts, createUser, updatePassword };
@@ -0,0 +1,179 @@
1
+ const { Command } = require('commander');
2
+ const { createUser, updatePassword } = require('../auth');
3
+ const { addHost, listHosts, grantAccess, revokeAccess } = require('../hosts');
4
+ const { querySessions, queryCommands } = require('../audit');
5
+ const logger = require('../logger');
6
+
7
+ const service = 'cli';
8
+
9
+ const program = new Command();
10
+
11
+ program
12
+ .name('bastion')
13
+ .description('CLI for bastion host management')
14
+ .version('0.1.0');
15
+
16
+ // User commands
17
+ program
18
+ .command('add-user')
19
+ .description('Add a new user')
20
+ .argument('<username>', 'Username')
21
+ .argument('<password>', 'Password')
22
+ .option('-r, --role <role>', 'User role (admin/auditor/user)', 'user')
23
+ .action(async (username, password, options) => {
24
+ try {
25
+ const user = createUser(username, password, options.role);
26
+ console.log(`✓ User created: ${user.username} (role: ${user.role}, id: ${user.id})`);
27
+ } catch (e) {
28
+ console.error(`✗ Error: ${e.message}`);
29
+ }
30
+ });
31
+
32
+ program
33
+ .command('update-password')
34
+ .description('Update user password')
35
+ .argument('<username>', 'Username')
36
+ .argument('<password>', 'New password')
37
+ .action(async (username, password) => {
38
+ const db = require('../db');
39
+ const user = db.prepare('SELECT id FROM users WHERE username = ?').get(username);
40
+ if (!user) {
41
+ console.error(`✗ User not found: ${username}`);
42
+ return;
43
+ }
44
+ updatePassword(user.id, password);
45
+ console.log(`✓ Password updated for: ${username}`);
46
+ });
47
+
48
+ // Host commands
49
+ program
50
+ .command('add-host')
51
+ .description('Add a target host')
52
+ .argument('<name>', 'Host name (display name)')
53
+ .argument('<host>', 'Host address')
54
+ .argument('<port>', 'SSH port')
55
+ .argument('<username>', 'SSH username')
56
+ .argument('<password>', 'SSH password')
57
+ .action(async (name, host, port, username, password) => {
58
+ try {
59
+ const hostInfo = addHost(name, host, parseInt(port, 10), username, password);
60
+ console.log(`✓ Host added: ${hostInfo.name} (${hostInfo.host}:${hostInfo.port})`);
61
+ } catch (e) {
62
+ console.error(`✗ Error: ${e.message}`);
63
+ }
64
+ });
65
+
66
+ program
67
+ .command('list-hosts')
68
+ .description('List all target hosts')
69
+ .action(async () => {
70
+ const hosts = listHosts();
71
+ if (hosts.length === 0) {
72
+ console.log('No hosts configured.');
73
+ return;
74
+ }
75
+ console.log('\nTarget Hosts:');
76
+ console.log('─'.repeat(80));
77
+ hosts.forEach(h => {
78
+ console.log(` ID: ${h.id}`);
79
+ console.log(` Name: ${h.name}`);
80
+ console.log(` Address: ${h.host}:${h.port}`);
81
+ console.log(` Username: ${h.username}`);
82
+ console.log('─'.repeat(80));
83
+ });
84
+ });
85
+
86
+ // Permission commands
87
+ program
88
+ .command('grant')
89
+ .description('Grant a user access to a host')
90
+ .argument('<username>', 'Username')
91
+ .argument('<host-name>', 'Host name (display name)')
92
+ .action(async (username, hostName) => {
93
+ const db = require('../db');
94
+ const user = db.prepare('SELECT id FROM users WHERE username = ?').get(username);
95
+ if (!user) {
96
+ console.error(`✗ User not found: ${username}`);
97
+ return;
98
+ }
99
+ const host = db.prepare('SELECT id FROM hosts WHERE name = ?').get(hostName);
100
+ if (!host) {
101
+ console.error(`✗ Host not found: ${hostName}`);
102
+ return;
103
+ }
104
+ grantAccess(user.id, host.id);
105
+ console.log(`✓ Granted: ${username} -> ${hostName}`);
106
+ });
107
+
108
+ // Audit commands
109
+ program
110
+ .command('list-sessions')
111
+ .description('List audit sessions')
112
+ .option('-u, --user <id>', 'Filter by user ID')
113
+ .option('-H, --host <id>', 'Filter by host ID')
114
+ .option('-s, --since <date>', 'Filter since date (YYYY-MM-DD)')
115
+ .option('-e, --until <date>', 'Filter until date (YYYY-MM-DD)')
116
+ .option('--active', 'Show only active sessions')
117
+ .option('-l, --limit <n>', 'Limit results', '20')
118
+ .action(async (options) => {
119
+ const sessions = querySessions({
120
+ userId: options.user ? parseInt(options.user) : undefined,
121
+ hostId: options.host ? parseInt(options.host) : undefined,
122
+ since: options.since,
123
+ until: options.until,
124
+ status: options.active ? 'active' : undefined,
125
+ limit: parseInt(options.limit, 10),
126
+ });
127
+
128
+ if (sessions.length === 0) {
129
+ console.log('No sessions found.');
130
+ return;
131
+ }
132
+
133
+ console.log('\nAudit Sessions:');
134
+ console.log('─'.repeat(120));
135
+ sessions.forEach(s => {
136
+ console.log(` ID: ${s.id}`);
137
+ console.log(` User: ${s.username}`);
138
+ console.log(` Host: ${s.host_name} (${s.host_addr}:${22})`);
139
+ console.log(` Client IP: ${s.client_ip}`);
140
+ console.log(` Status: ${s.status}`);
141
+ console.log(` Started: ${s.start_time}`);
142
+ if (s.end_time) console.log(` Ended: ${s.end_time}`);
143
+ console.log('─'.repeat(120));
144
+ });
145
+ });
146
+
147
+ program
148
+ .command('show-commands')
149
+ .description('Show audit commands for a session')
150
+ .argument('<session-id>', 'Session ID')
151
+ .option('-s, --since <date>', 'Filter since')
152
+ .option('-e, --until <date>', 'Filter until')
153
+ .option('-c, --command <pattern>', 'Filter by command pattern')
154
+ .option('--blocked', 'Show only blocked commands')
155
+ .option('-l, --limit <n>', 'Limit results', '50')
156
+ .action(async (sessionId, options) => {
157
+ const commands = queryCommands(parseInt(sessionId, 10), {
158
+ since: options.since,
159
+ until: options.until,
160
+ command: options.command,
161
+ blocked: options.blocked,
162
+ limit: parseInt(options.limit, 10),
163
+ });
164
+
165
+ if (commands.length === 0) {
166
+ console.log('No commands found for this session.');
167
+ return;
168
+ }
169
+
170
+ console.log(`\nCommands for Session #${sessionId}:`);
171
+ console.log('─'.repeat(100));
172
+ commands.forEach(c => {
173
+ const blockMarker = c.blocked ? ' [BLOCKED]' : '';
174
+ console.log(` ${c.timestamp} ${c.command}${blockMarker}`);
175
+ });
176
+ });
177
+
178
+ // Execute
179
+ program.parse();
@@ -0,0 +1,31 @@
1
+ require('dotenv').config();
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+
5
+ let config = {};
6
+
7
+ try {
8
+ config = JSON.parse(
9
+ fs.readFileSync(path.join(__dirname, '..', '..', 'config.json'), 'utf-8')
10
+ );
11
+ } catch (e) {
12
+ console.error('Failed to load config.json:', e.message);
13
+ }
14
+
15
+ module.exports = {
16
+ server: {
17
+ port: parseInt(process.env.BASTION_PORT || config.server?.port || 2222, 10),
18
+ maxSessions: config.server?.maxSessions || 50,
19
+ sessionTimeoutSec: config.server?.sessionTimeoutSec || 1800,
20
+ idleTimeoutSec: config.server?.idleTimeoutSec || 900,
21
+ },
22
+ auth: {
23
+ maxFailedAttempts: config.auth?.maxFailedAttempts || 5,
24
+ lockDurationSec: config.auth?.lockDurationSec || 900,
25
+ },
26
+ policy: {
27
+ blockedCommands: config.policy?.blockedCommands || [],
28
+ },
29
+ masterKey: process.env.BASTION_MASTER_KEY || '',
30
+ logLevel: process.env.BASTION_LOG_LEVEL || config.logLevel || 'info',
31
+ };
package/src/crypto.js ADDED
@@ -0,0 +1,52 @@
1
+ const crypto = require('crypto');
2
+ const config = require('../config');
3
+
4
+ const ALGORITHM = 'aes-256-gcm';
5
+ const IV_LENGTH = 16;
6
+ const SALT_LENGTH = 32;
7
+ const KEY_LENGTH = 32;
8
+ const TAG_LENGTH = 16;
9
+
10
+ function deriveKey(masterKey) {
11
+ // Simple key derivation: hash master key
12
+ return crypto.createHash('sha256').update(masterKey).digest();
13
+ }
14
+
15
+ function encrypt(text, masterKey) {
16
+ if (!masterKey) throw new Error('Master key is not set');
17
+
18
+ const salt = crypto.randomBytes(SALT_LENGTH);
19
+ const iv = crypto.randomBytes(IV_LENGTH);
20
+ const key = deriveKey(masterKey);
21
+
22
+ const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
23
+ let encrypted = cipher.update(text, 'utf-8', 'hex');
24
+ encrypted += cipher.final('hex');
25
+ const tag = cipher.getAuthTag().toString('hex');
26
+
27
+ // Format: salt:iv:tag:encrypted
28
+ return [salt.toString('hex'), iv.toString('hex'), tag, encrypted].join(':');
29
+ }
30
+
31
+ function decrypt(encryptedText, masterKey) {
32
+ if (!masterKey) throw new Error('Master key is not set');
33
+
34
+ const parts = encryptedText.split(':');
35
+ if (parts.length !== 4) throw new Error('Invalid encrypted format');
36
+
37
+ const salt = Buffer.from(parts[0], 'hex');
38
+ const iv = Buffer.from(parts[1], 'hex');
39
+ const tag = Buffer.from(parts[2], 'hex');
40
+ const encrypted = parts[3];
41
+
42
+ const key = deriveKey(masterKey);
43
+ const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
44
+ decipher.setAuthTag(tag);
45
+
46
+ let decrypted = decipher.update(encrypted, 'hex', 'utf-8');
47
+ decrypted += decipher.final('utf-8');
48
+
49
+ return decrypted;
50
+ }
51
+
52
+ module.exports = { encrypt, decrypt };
@@ -0,0 +1,116 @@
1
+ const { DatabaseSync } = require('node:sqlite');
2
+ const path = require('path');
3
+ const fs = require('fs');
4
+
5
+ const DB_DIR = path.join(__dirname, '..', '..', 'data');
6
+ const DB_PATH = path.join(DB_DIR, 'bastion.db');
7
+
8
+ // Ensure data directory exists
9
+ if (!fs.existsSync(DB_DIR)) {
10
+ fs.mkdirSync(DB_DIR, { recursive: true });
11
+ }
12
+
13
+ const db = new DatabaseSync(DB_PATH);
14
+
15
+ // Enable WAL mode and foreign keys
16
+ db.exec('PRAGMA journal_mode = WAL');
17
+ db.exec('PRAGMA foreign_keys = ON');
18
+
19
+ // SQL schema
20
+ const SCHEMA = `
21
+ CREATE TABLE IF NOT EXISTS users (
22
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
23
+ username TEXT UNIQUE NOT NULL,
24
+ password_hash TEXT NOT NULL,
25
+ role TEXT NOT NULL DEFAULT 'user' CHECK(role IN ('admin', 'auditor', 'user')),
26
+ status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'locked', 'disabled')),
27
+ failed_attempts INTEGER NOT NULL DEFAULT 0,
28
+ locked_until INTEGER,
29
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
30
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
31
+ );
32
+
33
+ CREATE TABLE IF NOT EXISTS hosts (
34
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
35
+ name TEXT NOT NULL,
36
+ host TEXT NOT NULL,
37
+ port INTEGER NOT NULL DEFAULT 22,
38
+ username TEXT NOT NULL,
39
+ auth_type TEXT NOT NULL DEFAULT 'password' CHECK(auth_type IN ('password', 'keyboard-interactive')),
40
+ credential_enc TEXT NOT NULL,
41
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
42
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
43
+ );
44
+
45
+ CREATE TABLE IF NOT EXISTS user_host_perms (
46
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
47
+ user_id INTEGER NOT NULL,
48
+ host_id INTEGER NOT NULL,
49
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
50
+ FOREIGN KEY (user_id) REFERENCES users(id),
51
+ FOREIGN KEY (host_id) REFERENCES hosts(id),
52
+ UNIQUE(user_id, host_id)
53
+ );
54
+
55
+ CREATE INDEX IF NOT EXISTS idx_user_host_perms_user ON user_host_perms(user_id);
56
+ CREATE INDEX IF NOT EXISTS idx_user_host_perms_host ON user_host_perms(host_id);
57
+
58
+ CREATE TABLE IF NOT EXISTS audit_sessions (
59
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
60
+ user_id INTEGER NOT NULL,
61
+ host_id INTEGER NOT NULL,
62
+ client_ip TEXT NOT NULL,
63
+ start_time TEXT NOT NULL DEFAULT (datetime('now')),
64
+ end_time TEXT,
65
+ status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'closed', 'error')),
66
+ FOREIGN KEY (user_id) REFERENCES users(id),
67
+ FOREIGN KEY (host_id) REFERENCES hosts(id)
68
+ );
69
+
70
+ CREATE INDEX IF NOT EXISTS idx_audit_sessions_user ON audit_sessions(user_id);
71
+ CREATE INDEX IF NOT EXISTS idx_audit_sessions_host ON audit_sessions(host_id);
72
+ CREATE INDEX IF NOT EXISTS idx_audit_sessions_start ON audit_sessions(start_time);
73
+
74
+ CREATE TABLE IF NOT EXISTS audit_commands (
75
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
76
+ session_id INTEGER NOT NULL,
77
+ command TEXT NOT NULL,
78
+ timestamp TEXT NOT NULL DEFAULT (datetime('now')),
79
+ blocked INTEGER NOT NULL DEFAULT 0,
80
+ FOREIGN KEY (session_id) REFERENCES audit_sessions(id)
81
+ );
82
+
83
+ CREATE INDEX IF NOT EXISTS idx_audit_commands_session ON audit_commands(session_id);
84
+ CREATE INDEX IF NOT EXISTS idx_audit_commands_timestamp ON audit_commands(timestamp);
85
+
86
+ CREATE TABLE IF NOT EXISTS audit_transfers (
87
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
88
+ session_id INTEGER NOT NULL,
89
+ direction TEXT NOT NULL CHECK(direction IN ('upload', 'download')),
90
+ path TEXT NOT NULL,
91
+ size INTEGER NOT NULL DEFAULT 0,
92
+ timestamp TEXT NOT NULL DEFAULT (datetime('now')),
93
+ FOREIGN KEY (session_id) REFERENCES audit_sessions(id)
94
+ );
95
+
96
+ CREATE INDEX IF NOT EXISTS idx_audit_transfers_session ON audit_transfers(session_id);
97
+
98
+ CREATE TABLE IF NOT EXISTS audit_forwards (
99
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
100
+ session_id INTEGER NOT NULL,
101
+ fwd_type TEXT NOT NULL CHECK(fwd_type IN ('direct-tcpip', 'forwarded-tcpip')),
102
+ src_addr TEXT NOT NULL,
103
+ src_port INTEGER NOT NULL,
104
+ dst_addr TEXT NOT NULL,
105
+ dst_port INTEGER NOT NULL,
106
+ timestamp TEXT NOT NULL DEFAULT (datetime('now')),
107
+ FOREIGN KEY (session_id) REFERENCES audit_sessions(id)
108
+ );
109
+
110
+ CREATE INDEX IF NOT EXISTS idx_audit_forwards_session ON audit_forwards(session_id);
111
+ `;
112
+
113
+ // Execute schema
114
+ db.exec(SCHEMA);
115
+
116
+ module.exports = db;
@@ -0,0 +1,63 @@
1
+ const db = require('../db');
2
+ const { encrypt, decrypt } = require('../crypto');
3
+ const logger = require('../logger');
4
+ const config = require('../config');
5
+
6
+ const service = 'hosts';
7
+
8
+ // Add host with credential encryption
9
+ function addHost(name, host, port, username, password) {
10
+ const credEnc = encrypt(password, config.masterKey);
11
+ const result = db.prepare(
12
+ 'INSERT INTO hosts (name, host, port, username, credential_enc) VALUES (?, ?, ?, ?, ?)'
13
+ ).run(name, host, port, username, credEnc);
14
+
15
+ logger.info(`${service}: Added host`, { hostId: result.lastInsertRowid, name, host, port });
16
+
17
+ return {
18
+ id: result.lastInsertRowid,
19
+ name,
20
+ host,
21
+ port,
22
+ username,
23
+ };
24
+ }
25
+
26
+ // Get host by ID with decrypted credential
27
+ function getHost(id) {
28
+ const row = db.prepare('SELECT * FROM hosts WHERE id = ?').get(id);
29
+ if (!row) return null;
30
+
31
+ try {
32
+ row.credential = decrypt(row.credential_enc, config.masterKey);
33
+ } catch (e) {
34
+ logger.error(`${service}: Failed to decrypt credential`, { id, error: e.message });
35
+ row.credential = null;
36
+ }
37
+
38
+ return row;
39
+ }
40
+
41
+ // List all hosts (credentials not decrypted)
42
+ function listHosts() {
43
+ return db.prepare('SELECT id, name, host, port, username, created_at FROM hosts').all();
44
+ }
45
+
46
+ // Grant user access to host
47
+ function grantAccess(userId, hostId) {
48
+ const result = db.prepare(
49
+ 'INSERT INTO user_host_perms (user_id, host_id) VALUES (?, ?)'
50
+ ).run(userId, hostId);
51
+
52
+ logger.info(`${service}: Granted access`, { userId, hostId });
53
+ return { id: result.lastInsertRowid };
54
+ }
55
+
56
+ // Remove grant
57
+ function revokeAccess(userId, hostId) {
58
+ db.prepare(
59
+ 'DELETE FROM user_host_perms WHERE user_id = ? AND host_id = ?'
60
+ ).run(userId, hostId);
61
+ }
62
+
63
+ module.exports = { addHost, getHost, listHosts, grantAccess, revokeAccess };
package/src/logger.js ADDED
@@ -0,0 +1,31 @@
1
+ const winston = require('winston');
2
+ const config = require('./config');
3
+
4
+ const logger = winston.createLogger({
5
+ level: config.logLevel,
6
+ format: winston.format.combine(
7
+ winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
8
+ winston.format.errors({ stack: true }),
9
+ winston.format.printf(({ timestamp, level, message, service, ...meta }) => {
10
+ const prefix = service ? `[${service}]` : '';
11
+ return `${timestamp} ${level} ${prefix} ${message}`;
12
+ })
13
+ ),
14
+ transports: [
15
+ new winston.transports.Console({
16
+ format: winston.format.combine(
17
+ winston.format.colorize(),
18
+ winston.format.simple()
19
+ ),
20
+ }),
21
+ new winston.transports.File({
22
+ filename: 'logs/error.log',
23
+ level: 'error',
24
+ }),
25
+ new winston.transports.File({
26
+ filename: 'logs/combined.log',
27
+ }),
28
+ ],
29
+ });
30
+
31
+ module.exports = logger;