hayai-db
Version:
โก Instantly create and manage local databases with one command
385 lines (384 loc) โข 14.5 kB
JavaScript
import crypto from 'crypto';
import { readFile, writeFile, mkdir } from 'fs/promises';
import path from 'path';
import chalk from 'chalk';
import { spawn } from 'child_process';
export class SecurityManager {
static instance;
encryptionKey;
credentialsPath;
auditLogPath;
securityPolicyPath;
operationCounts = new Map();
constructor() {
this.encryptionKey = this.getOrCreateEncryptionKey();
this.credentialsPath = path.join(process.cwd(), '.hayai', 'credentials.enc');
this.auditLogPath = path.join(process.cwd(), '.hayai', 'audit.log');
this.securityPolicyPath = path.join(process.cwd(), '.hayai', 'security.json');
}
static getInstance() {
if (!SecurityManager.instance) {
SecurityManager.instance = new SecurityManager();
}
return SecurityManager.instance;
}
/**
* Generates or retrieves unique encryption key per installation
*/
getOrCreateEncryptionKey() {
const keyPath = path.join(process.cwd(), '.hayai', '.key');
try {
return require('fs').readFileSync(keyPath, 'utf8');
}
catch {
const key = crypto.randomBytes(32).toString('hex');
try {
require('fs').mkdirSync(path.dirname(keyPath), { recursive: true });
require('fs').writeFileSync(keyPath, key);
require('fs').chmodSync(keyPath, 0o600); // Only owner can read
}
catch (error) {
console.warn(chalk.yellow('โ ๏ธ Could not save encryption key securely'));
}
return key;
}
}
/**
* Encrypts sensitive data
*/
encrypt(text) {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv('aes-256-cbc', Buffer.from(this.encryptionKey, 'hex'), iv);
let encrypted = cipher.update(text, 'utf8', 'hex');
encrypted += cipher.final('hex');
return iv.toString('hex') + ':' + encrypted;
}
/**
* Decrypts sensitive data
*/
decrypt(text) {
const [ivHex, encryptedHex] = text.split(':');
const iv = Buffer.from(ivHex, 'hex');
const decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(this.encryptionKey, 'hex'), iv);
let decrypted = decipher.update(encryptedHex, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
/**
* Generates secure random password
*/
generateSecurePassword(length = 16) {
const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*';
let password = '';
for (let i = 0; i < length; i++) {
const randomIndex = crypto.randomInt(0, charset.length);
password += charset[randomIndex];
}
return password;
}
/**
* Stores credentials securely
*/
async storeCredentials(instanceName, credentials) {
try {
await mkdir(path.dirname(this.credentialsPath), { recursive: true });
let existingCredentials = {};
try {
const encryptedData = await readFile(this.credentialsPath, 'utf8');
const decryptedData = this.decrypt(encryptedData);
existingCredentials = JSON.parse(decryptedData);
}
catch {
// File doesn't exist or is corrupted - start fresh
}
existingCredentials[instanceName] = {
...credentials,
password: this.encrypt(credentials.password),
encrypted: true,
createdAt: new Date().toISOString()
};
const encryptedCredentials = this.encrypt(JSON.stringify(existingCredentials));
await writeFile(this.credentialsPath, encryptedCredentials);
// Set restrictive permissions
await require('fs').promises.chmod(this.credentialsPath, 0o600);
}
catch (error) {
throw new Error(`Failed to store credentials securely: ${error}`);
}
}
/**
* Retrieves credentials securely
*/
async getCredentials(instanceName) {
try {
const encryptedData = await readFile(this.credentialsPath, 'utf8');
const decryptedData = this.decrypt(encryptedData);
const credentials = JSON.parse(decryptedData);
if (credentials[instanceName]) {
const creds = credentials[instanceName];
creds.password = this.decrypt(creds.password);
creds.lastUsed = new Date().toISOString();
// Update last used timestamp
await this.storeCredentials(instanceName, creds);
return creds;
}
return null;
}
catch {
return null;
}
}
/**
* Validates if operation is allowed
*/
async validateOperation(operation, sourceInstance, targetInstance, user = 'local') {
const policy = await this.getSecurityPolicy();
// Check if operation is allowed
if (!policy.allowedOperations.includes(operation)) {
return { allowed: false, reason: `Operation '${operation}' is not permitted by security policy` };
}
// Check rate limiting
const operationKey = `${user}:${operation}`;
const currentCount = this.operationCounts.get(operationKey) || 0;
if (currentCount >= policy.maxOperationsPerHour) {
return { allowed: false, reason: `Rate limit exceeded: ${policy.maxOperationsPerHour} operations per hour` };
}
// Check cross-engine operations
if (targetInstance && !policy.allowCrossEngineOperations) {
// This would need engine comparison logic
// For now, assume different engines if different names
}
// Increment operation count
this.operationCounts.set(operationKey, currentCount + 1);
// Reset counts every hour
setTimeout(() => {
this.operationCounts.delete(operationKey);
}, 60 * 60 * 1000);
return { allowed: true };
}
/**
* Creates network isolation for operation
*/
async createNetworkIsolation() {
const networkName = `hayai-op-${crypto.randomUUID().substring(0, 8)}`;
return new Promise((resolve, reject) => {
const createNetwork = spawn('docker', [
'network', 'create',
'--driver', 'bridge',
'--internal', // Isolates from external networks
'--opt', 'com.docker.network.bridge.enable_icc=true', // Enable inter-container communication
networkName
]);
createNetwork.on('close', (code) => {
if (code === 0) {
console.log(chalk.green(`๐ Created isolated network: ${networkName}`));
resolve(networkName);
}
else {
reject(new Error('Failed to create isolated network'));
}
});
createNetwork.on('error', reject);
});
}
/**
* Connects containers to isolated network
*/
async connectToNetwork(networkName, containerName) {
return new Promise((resolve, reject) => {
const connect = spawn('docker', [
'network', 'connect', networkName, containerName
]);
connect.on('close', (code) => {
code === 0 ? resolve() : reject(new Error(`Failed to connect ${containerName} to network`));
});
connect.on('error', reject);
});
}
/**
* Removes isolated network after operation
*/
async cleanupNetwork(networkName) {
return new Promise((resolve) => {
const cleanup = spawn('docker', [
'network', 'rm', networkName
]);
cleanup.on('close', () => {
console.log(chalk.green(`๐งน Cleaned up network: ${networkName}`));
resolve();
});
cleanup.on('error', () => resolve()); // Don't fail on cleanup errors
});
}
/**
* Records operation in audit log
*/
async auditLog(log) {
try {
await mkdir(path.dirname(this.auditLogPath), { recursive: true });
const logEntry = JSON.stringify(log) + '\n';
await writeFile(this.auditLogPath, logEntry, { flag: 'a' });
console.log(chalk.gray(`๐ Audit: ${log.operation} ${log.source}${log.target ? ` โ ${log.target}` : ''} ${log.success ? 'โ
' : 'โ'}`));
}
catch (error) {
console.warn(chalk.yellow(`โ ๏ธ Failed to write audit log: ${error}`));
}
}
/**
* Gets security policy
*/
async getSecurityPolicy() {
try {
const policyData = await readFile(this.securityPolicyPath, 'utf8');
return JSON.parse(policyData);
}
catch {
// Return default policy
const defaultPolicy = {
requireAuthentication: false, // Start permissive for local development
allowCrossEngineOperations: true,
enableNetworkIsolation: false,
auditOperations: true,
maxOperationsPerHour: 50,
allowedOperations: ['clone', 'merge', 'migrate', 'backup', 'restore']
};
await this.saveSecurityPolicy(defaultPolicy);
return defaultPolicy;
}
}
/**
* Saves security policy
*/
async saveSecurityPolicy(policy) {
try {
await mkdir(path.dirname(this.securityPolicyPath), { recursive: true });
await writeFile(this.securityPolicyPath, JSON.stringify(policy, null, 2));
}
catch (error) {
throw new Error(`Failed to save security policy: ${error}`);
}
}
/**
* Creates secure credentials for new instance
*/
async createSecureCredentials(instanceName, engine) {
const credentials = {
username: engine === 'redis' ? '' : 'admin',
password: this.generateSecurePassword(),
database: engine === 'postgresql' || engine === 'mariadb' ? 'database' : undefined,
encrypted: false,
createdAt: new Date().toISOString()
};
await this.storeCredentials(instanceName, credentials);
return credentials;
}
/**
* Executes secure command with credentials
*/
async executeSecureCommand(command, instanceName, operation) {
let success = false;
let error;
try {
// Validate operation
const validation = await this.validateOperation(operation, instanceName);
if (!validation.allowed) {
throw new Error(validation.reason);
}
// Get credentials
const credentials = await this.getCredentials(instanceName);
if (!credentials) {
throw new Error(`No credentials found for instance: ${instanceName}`);
}
// Execute command with credentials injected securely
const result = await this.runSecureCommand(command, credentials);
success = true;
return result;
}
catch (err) {
error = err instanceof Error ? err.message : String(err);
throw err;
}
finally {
// Audit log
await this.auditLog({
timestamp: new Date().toISOString(),
operation,
source: instanceName,
target: '',
user: 'local',
success,
error
});
}
}
/**
* Executes command with secure environment variables
*/
async runSecureCommand(command, credentials) {
return new Promise((resolve, reject) => {
const env = {
...process.env,
PGPASSWORD: credentials.password,
MYSQL_PWD: credentials.password,
REDIS_PASSWORD: credentials.password
};
const child = spawn(command[0], command.slice(1), {
env,
stdio: ['inherit', 'pipe', 'pipe']
});
let stdout = '';
let stderr = '';
child.stdout.on('data', (data) => {
stdout += data.toString();
});
child.stderr.on('data', (data) => {
stderr += data.toString();
});
child.on('close', (code) => {
if (code === 0) {
resolve(stdout);
}
else {
reject(new Error(`Command failed: ${stderr}`));
}
});
child.on('error', reject);
});
}
/**
* Validates data integrity after operation
*/
async validateDataIntegrity(instanceName, engine) {
try {
const credentials = await this.getCredentials(instanceName);
if (!credentials)
return false;
switch (engine) {
case 'postgresql':
// Check if database is accessible and has tables
await this.runSecureCommand([
'docker', 'exec', `${instanceName}-db`,
'psql', '-U', credentials.username, '-d', credentials.database || 'database',
'-c', 'SELECT COUNT(*) FROM information_schema.tables;'
], credentials);
break;
case 'redis':
// Check if Redis is responsive
await this.runSecureCommand([
'docker', 'exec', `${instanceName}-db`,
'redis-cli', '-a', credentials.password, 'ping'
], credentials);
break;
default:
console.log(chalk.yellow(`โ ๏ธ Data integrity check not implemented for ${engine}`));
}
return true;
}
catch {
return false;
}
}
}
export const getSecurityManager = () => {
return SecurityManager.getInstance();
};