@iota-big3/sdk-security
Version:
Advanced security features including zero trust, quantum-safe crypto, and ML threat detection
804 lines • 28.6 kB
JavaScript
"use strict";
/**
* HashiCorp Vault Provider
* Enterprise-grade secrets management with dynamic secrets and encryption
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.VaultProvider = void 0;
const tslib_1 = require("tslib");
const crypto = tslib_1.__importStar(require("crypto"));
const events_1 = require("events");
const types_1 = require("../types");
class VaultProvider extends events_1.EventEmitter {
constructor(config) {
super();
this.provider = types_1.SecretsProvider.HASHICORP_VAULT;
this.cache = new Map();
this.leases = new Map();
this.isInitialized = false;
this.config = config;
this.validateConfig();
}
/**
* Initialize Vault connection
*/
async initialize() {
if (this.isInitialized)
return;
try {
await this.authenticate();
this.isInitialized = true;
this.emit('initialized', { provider: this.provider });
// Start token renewal if needed
if (this.config.authentication.renewable) {
this.startTokenRenewal();
}
}
catch (error) {
this.emit('error', { error, operation: 'initialize' });
throw error;
}
}
/**
* Authenticate with Vault
*/
async authenticate() {
const { method, credentials } = this.config.authentication;
switch (method) {
case 'TOKEN':
this.token = credentials?.token;
break;
case 'APP_ROLE':
this.token = await this.authenticateAppRole(credentials?.roleId, credentials?.secretId);
break;
case 'AWS_IAM':
this.token = await this.authenticateAWS(credentials?.awsRole);
break;
case 'KUBERNETES':
this.token = await this.authenticateKubernetes();
break;
default:
throw new Error(`Unsupported auth method: ${method}`);
}
// Set token expiry
if (this.config.authentication.ttl) {
this.tokenExpiry = new Date(Date.now() + this.config.authentication.ttl * 1000);
}
}
/**
* Get secret from Vault
*/
async getSecret(name, version) {
await this.ensureInitialized();
// Check cache
if (this.config.caching?.enabled) {
const cached = this.getFromCache(name);
if (cached)
return cached;
}
try {
// Simulate Vault API call
const path = this.buildPath(name, version);
const response = await this.vaultRequest('GET', path);
const secret = {
name,
value: response.data,
type: this.inferSecretType(name, response.data),
createdAt: new Date(response.metadata.created_time),
updatedAt: new Date(response.metadata.updated_time),
version: response.metadata.version,
tags: response.metadata.custom_metadata
};
// Cache if enabled
if (this.config.caching?.enabled) {
this.addToCache(name, secret);
}
// Audit
this.auditLog({
timestamp: new Date(),
action: types_1.SecretAction.READ,
secretName: name,
actor: { id: 'system', type: 'SYSTEM' },
result: 'SUCCESS'
});
return secret;
}
catch (error) {
this.auditLog({
timestamp: new Date(),
action: types_1.SecretAction.READ,
secretName: name,
actor: { id: 'system', type: 'SYSTEM' },
result: 'FAILURE',
error: error.message
});
throw error;
}
}
/**
* Set secret in Vault
*/
async setSecret(name, value, metadata) {
await this.ensureInitialized();
try {
const path = this.buildPath(name);
const data = {
data: typeof value === 'object' ? value : { value },
metadata: {
custom_metadata: metadata?.tags,
max_versions: 10,
cas_required: false,
delete_version_after: metadata?.compliance?.retentionPeriod
? `${metadata.compliance.retentionPeriod}d`
: '0s'
}
};
await this.vaultRequest('POST', path, data);
// Set rotation if specified
if (metadata?.rotationPolicy) {
await this.scheduleRotation(name, metadata.rotationPolicy);
}
// Clear cache
this.cache.delete(name);
// Audit
this.auditLog({
timestamp: new Date(),
action: types_1.SecretAction.CREATE,
secretName: name,
actor: { id: 'system', type: 'SYSTEM' },
result: 'SUCCESS',
metadata: { type: metadata?.type }
});
this.emit('secret:created', { name, metadata });
}
catch (error) {
this.auditLog({
timestamp: new Date(),
action: types_1.SecretAction.CREATE,
secretName: name,
actor: { id: 'system', type: 'SYSTEM' },
result: 'FAILURE',
error: error.message
});
throw error;
}
}
/**
* Delete secret from Vault
*/
async deleteSecret(name, permanent) {
await this.ensureInitialized();
try {
const path = this.buildPath(name);
if (permanent) {
// Permanent deletion
await this.vaultRequest('DELETE', `${path}/metadata`);
}
else {
// Soft delete (latest version)
await this.vaultRequest('DELETE', path);
}
// Clear cache
this.cache.delete(name);
// Audit
this.auditLog({
timestamp: new Date(),
action: types_1.SecretAction.DELETE,
secretName: name,
actor: { id: 'system', type: 'SYSTEM' },
result: 'SUCCESS',
metadata: { permanent }
});
this.emit('secret:deleted', { name, permanent });
}
catch (error) {
this.auditLog({
timestamp: new Date(),
action: types_1.SecretAction.DELETE,
secretName: name,
actor: { id: 'system', type: 'SYSTEM' },
result: 'FAILURE',
error: error.message
});
throw error;
}
}
/**
* List secrets with optional filter
*/
async listSecrets(filter) {
await this.ensureInitialized();
try {
// List from KV engine
const response = await this.vaultRequest('LIST', this.config.namespace || 'secret');
const keys = response.data.keys || [];
// Get metadata for each key
const secrets = [];
for (const key of keys) {
if (filter?.namePattern && !key.match(new RegExp(filter.namePattern))) {
continue;
}
try {
const metadata = await this.getSecretMetadata(key);
// Apply filters
if (filter) {
if (filter.type && metadata.type !== filter.type)
continue;
if (filter.environment && metadata.environment !== filter.environment)
continue;
if (filter.owner && metadata.owner !== filter.owner)
continue;
if (filter.tags && !this.matchTags(metadata.tags, filter.tags))
continue;
if (filter.modifiedSince && metadata.updatedAt < filter.modifiedSince)
continue;
if (filter.expiringWithin && metadata.expiresAt) {
const expiryDays = (metadata.expiresAt.getTime() - Date.now()) / (1000 * 60 * 60 * 24);
if (expiryDays > filter.expiringWithin)
continue;
}
}
secrets.push(metadata);
}
catch (error) {
// Skip secrets we can't read
continue;
}
}
return secrets;
}
catch (error) {
throw new Error(`Failed to list secrets: ${error.message}`);
}
}
/**
* Rotate a secret
*/
async rotateSecret(name) {
await this.ensureInitialized();
try {
// Get current secret
const current = await this.getSecret(name);
// Generate new value based on type
const newValue = await this.generateNewSecretValue(current);
// Store new version
await this.setSecret(name, newValue, {
...current,
rotationPolicy: {
...current.rotationPolicy,
lastRotated: new Date()
}
});
// Audit
this.auditLog({
timestamp: new Date(),
action: types_1.SecretAction.ROTATE,
secretName: name,
actor: { id: 'system', type: 'SYSTEM' },
result: 'SUCCESS'
});
this.emit('secret:rotated', { name, oldVersion: current.version });
}
catch (error) {
this.auditLog({
timestamp: new Date(),
action: types_1.SecretAction.ROTATE,
secretName: name,
actor: { id: 'system', type: 'SYSTEM' },
result: 'FAILURE',
error: error.message
});
throw error;
}
}
/**
* Schedule secret rotation
*/
async scheduleRotation(name, policy) {
// In production, would integrate with Vault's rotation schedules
// or external schedulers like AWS EventBridge
if (policy.autoRotate && policy.interval) {
const nextRotation = new Date();
nextRotation.setDate(nextRotation.getDate() + policy.interval);
// Store rotation schedule
this.emit('rotation:scheduled', {
name,
nextRotation,
interval: policy.interval
});
}
}
/**
* Get secret versions
*/
async getSecretVersions(name) {
await this.ensureInitialized();
const path = `${this.buildPath(name)}/metadata`;
const response = await this.vaultRequest('GET', path);
return Object.keys(response.data.versions || {});
}
/**
* Rollback to previous version
*/
async rollbackSecret(name, version) {
await this.ensureInitialized();
try {
// Get the specified version
const secret = await this.getSecret(name, version);
// Set it as the current version
await this.setSecret(name, secret.value, secret);
// Audit
this.auditLog({
timestamp: new Date(),
action: types_1.SecretAction.ROLLBACK,
secretName: name,
actor: { id: 'system', type: 'SYSTEM' },
result: 'SUCCESS',
metadata: { targetVersion: version }
});
this.emit('secret:rollback', { name, version });
}
catch (error) {
this.auditLog({
timestamp: new Date(),
action: types_1.SecretAction.ROLLBACK,
secretName: name,
actor: { id: 'system', type: 'SYSTEM' },
result: 'FAILURE',
error: error.message
});
throw error;
}
}
/**
* Get multiple secrets
*/
async getBulkSecrets(names) {
const secrets = await Promise.all(names.map(name => this.getSecret(name).catch(() => null)));
return secrets.filter(Boolean);
}
/**
* Import secrets
*/
async importSecrets(secrets) {
for (const secret of secrets) {
await this.setSecret(secret.name, secret.value, secret);
}
this.emit('secrets:imported', { count: secrets.length });
}
/**
* Export secrets
*/
async exportSecrets(filter) {
const metadata = await this.listSecrets(filter);
const secrets = [];
for (const meta of metadata) {
try {
const secret = await this.getSecret(meta.name);
secrets.push(secret);
}
catch (error) {
// Skip secrets we can't export
continue;
}
}
this.auditLog({
timestamp: new Date(),
action: types_1.SecretAction.EXPORT,
secretName: '*',
actor: { id: 'system', type: 'SYSTEM' },
result: 'SUCCESS',
metadata: { count: secrets.length }
});
return secrets;
}
/**
* Health check
*/
async healthCheck() {
const startTime = Date.now();
try {
// Check token validity
if (this.tokenExpiry && this.tokenExpiry < new Date()) {
await this.authenticate();
}
// Try to access sys/health
await this.vaultRequest('GET', 'sys/health');
return {
healthy: true,
provider: this.provider,
connectivity: true,
authentication: true,
lastCheck: new Date(),
latency: Date.now() - startTime
};
}
catch (error) {
return {
healthy: false,
provider: this.provider,
connectivity: false,
authentication: false,
lastCheck: new Date(),
latency: Date.now() - startTime,
errors: [error.message]
};
}
}
/**
* Create dynamic database credentials
*/
async createDynamicSecret(config) {
await this.ensureInitialized();
const path = `${config.backend}/creds/${config.role}`;
const response = await this.vaultRequest('GET', path);
const lease = {
leaseId: response.lease_id,
secretName: `${config.backend}/${config.role}`,
credentials: response.data,
leaseDuration: response.lease_duration,
renewable: response.renewable,
createdAt: new Date(),
expiresAt: new Date(Date.now() + response.lease_duration * 1000)
};
// Track lease
this.leases.set(lease.leaseId, lease);
// Schedule renewal if needed
if (lease.renewable) {
this.scheduleLeaseRenewal(lease);
}
this.emit('dynamic-secret:created', lease);
return lease;
}
/**
* Renew dynamic secret lease
*/
async renewLease(leaseId) {
await this.ensureInitialized();
const lease = this.leases.get(leaseId);
if (!lease) {
throw new Error(`Lease not found: ${leaseId}`);
}
await this.vaultRequest('PUT', 'sys/leases/renew', { lease_id: leaseId });
// Update expiry
lease.expiresAt = new Date(Date.now() + lease.leaseDuration * 1000);
this.emit('lease:renewed', { leaseId });
}
/**
* Revoke dynamic secret lease
*/
async revokeLease(leaseId) {
await this.ensureInitialized();
await this.vaultRequest('PUT', 'sys/leases/revoke', { lease_id: leaseId });
this.leases.delete(leaseId);
this.emit('lease:revoked', { leaseId });
}
/**
* Encryption service: Encrypt data
*/
async encrypt(plaintext, context) {
await this.ensureInitialized();
const data = {
plaintext: Buffer.isBuffer(plaintext)
? plaintext.toString('base64')
: Buffer.from(plaintext).toString('base64'),
context: context ? Buffer.from(JSON.stringify(context)).toString('base64') : undefined
};
const response = await this.vaultRequest('POST', 'transit/encrypt/app-key', data);
return response.data.ciphertext;
}
/**
* Encryption service: Decrypt data
*/
async decrypt(ciphertext, context) {
await this.ensureInitialized();
const data = {
ciphertext,
context: context ? Buffer.from(JSON.stringify(context)).toString('base64') : undefined
};
const response = await this.vaultRequest('POST', 'transit/decrypt/app-key', data);
const plaintext = Buffer.from(response.data.plaintext, 'base64');
return plaintext.toString('utf8');
}
/**
* Generate data encryption key
*/
async generateDataKey(bits = 256) {
await this.ensureInitialized();
const response = await this.vaultRequest('POST', 'transit/datakey/plaintext/app-key', {
bits: bits / 8
});
return {
plaintext: response.data.plaintext,
ciphertext: response.data.ciphertext
};
}
/**
* Rewrap ciphertext with new key version
*/
async rewrapKey(ciphertext, newKeyVersion) {
await this.ensureInitialized();
const response = await this.vaultRequest('POST', 'transit/rewrap/app-key', {
ciphertext,
key_version: parseInt(newKeyVersion)
});
return response.data.ciphertext;
}
/**
* PKI: Generate certificate
*/
async generateCertificate(config) {
await this.ensureInitialized();
const response = await this.vaultRequest('POST', 'pki/issue/app-cert', {
common_name: config.commonName,
alt_names: config.altNames?.join(','),
ip_sans: config.ipSans?.join(','),
ttl: config.ttl ? `${config.ttl}s` : '8760h', // 1 year default
key_type: config.keyType || 'rsa',
key_bits: config.keyBits || 2048,
exclude_cn_from_sans: config.excludeCnFromSans || false
});
return {
certificate: response.data.certificate,
privateKey: response.data.private_key,
certificateChain: response.data.ca_chain?.join('\n'),
serialNumber: response.data.serial_number,
notBefore: new Date(response.data.not_before),
notAfter: new Date(response.data.not_after),
subject: this.parseCertificateSubject(response.data.certificate)
};
}
/**
* PKI: Sign CSR
*/
async signCSR(csr, config) {
await this.ensureInitialized();
const response = await this.vaultRequest('POST', 'pki/sign/app-cert', {
csr,
ttl: config.ttl ? `${config.ttl}s` : '8760h',
format: config.format || 'pem'
});
return {
certificate: response.data.certificate,
certificateChain: response.data.ca_chain?.join('\n'),
serialNumber: response.data.serial_number,
notBefore: new Date(response.data.not_before),
notAfter: new Date(response.data.not_after),
subject: this.parseCertificateSubject(response.data.certificate)
};
}
/**
* PKI: Revoke certificate
*/
async revokeCertificate(serial, reason) {
await this.ensureInitialized();
await this.vaultRequest('POST', 'pki/revoke', {
serial_number: serial,
revocation_reason: reason
});
this.emit('certificate:revoked', { serial, reason });
}
/**
* PKI: Get CRL
*/
async getCRL() {
await this.ensureInitialized();
const response = await this.vaultRequest('GET', 'pki/crl');
return response.data.crl;
}
/**
* PKI: Get certificate
*/
async getCertificate(serial) {
await this.ensureInitialized();
const response = await this.vaultRequest('GET', `pki/cert/${serial}`);
return {
certificate: response.data.certificate,
serialNumber: serial,
notBefore: new Date(response.data.not_before),
notAfter: new Date(response.data.not_after),
subject: this.parseCertificateSubject(response.data.certificate)
};
}
/**
* Helper methods
*/
validateConfig() {
if (!this.config.endpoint) {
throw new Error('Vault endpoint is required');
}
if (!this.config.authentication) {
throw new Error('Authentication configuration is required');
}
}
async ensureInitialized() {
if (!this.isInitialized) {
await this.initialize();
}
}
buildPath(name, version) {
const base = `${this.config.namespace || 'secret'}/data/${name}`;
return version ? `${base}?version=${version}` : base;
}
getFromCache(name) {
if (!this.config.caching?.enabled)
return null;
const cached = this.cache.get(name);
if (!cached)
return null;
const age = Date.now() - cached.cachedAt.getTime();
if (age > (this.config.caching.ttl * 1000)) {
this.cache.delete(name);
return null;
}
return cached.secret;
}
addToCache(name, secret) {
if (!this.config.caching?.enabled)
return;
// Check cache size
if (this.cache.size >= (this.config.caching.maxSize || 100)) {
// Remove oldest entry
const oldest = Array.from(this.cache.entries())
.sort((a, b) => a[1].cachedAt.getTime() - b[1].cachedAt.getTime())[0];
this.cache.delete(oldest[0]);
}
this.cache.set(name, { secret, cachedAt: new Date() });
}
inferSecretType(name, value) {
if (name.includes('api') || name.includes('key'))
return types_1.SecretType.API_KEY;
if (name.includes('db') || name.includes('database'))
return types_1.SecretType.DATABASE_CREDENTIAL;
if (name.includes('cert'))
return types_1.SecretType.CERTIFICATE;
if (name.includes('ssh'))
return types_1.SecretType.SSH_KEY;
if (name.includes('token'))
return types_1.SecretType.TOKEN;
return types_1.SecretType.CUSTOM;
}
matchTags(secretTags, filterTags) {
if (!filterTags)
return true;
if (!secretTags)
return false;
return Object.entries(filterTags).every(([key, value]) => secretTags[key] === value);
}
async getSecretMetadata(name) {
const path = `${this.buildPath(name)}/metadata`;
const response = await this.vaultRequest('GET', path);
return {
name,
type: this.inferSecretType(name, {}),
createdAt: new Date(response.data.created_time),
updatedAt: new Date(response.data.updated_time),
version: response.data.current_version,
tags: response.data.custom_metadata
};
}
async generateNewSecretValue(secret) {
switch (secret.type) {
case types_1.SecretType.API_KEY:
case types_1.SecretType.TOKEN:
return crypto.randomBytes(32).toString('hex');
case types_1.SecretType.PASSWORD:
return this.generatePassword();
case types_1.SecretType.DATABASE_CREDENTIAL:
return {
username: secret.value.username,
password: this.generatePassword()
};
default:
throw new Error(`Cannot auto-rotate secret type: ${secret.type}`);
}
}
generatePassword() {
const length = 24;
const charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*';
let password = '';
for (let i = 0; i < length; i++) {
password += charset[Math.floor(Math.random() * charset.length)];
}
return password;
}
async vaultRequest(method, path, data) {
// In production, would use node-vault or axios to make actual API calls
// For now, simulate responses
await new Promise(resolve => setTimeout(resolve, 50)); // Simulate network delay
// Mock responses based on path
if (path === 'sys/health') {
return { initialized: true, sealed: false };
}
if (path.includes('/data/')) {
return {
data: data?.data || { value: 'mock-secret-value' },
metadata: {
created_time: new Date().toISOString(),
updated_time: new Date().toISOString(),
version: '1',
custom_metadata: {}
}
};
}
if (path.includes('transit/encrypt')) {
return {
data: {
ciphertext: `vault:v1:${Buffer.from(JSON.stringify(data)).toString('base64')}`
}
};
}
if (path.includes('transit/decrypt')) {
const encoded = data.ciphertext.replace('vault:v1:', '');
return {
data: {
plaintext: encoded
}
};
}
if (path.includes('pki/issue')) {
return {
data: {
certificate: '-----BEGIN CERTIFICATE-----\nMOCK CERTIFICATE\n-----END CERTIFICATE-----',
private_key: '-----BEGIN RSA PRIVATE KEY-----\nMOCK KEY\n-----END RSA PRIVATE KEY-----',
ca_chain: ['-----BEGIN CERTIFICATE-----\nMOCK CA\n-----END CERTIFICATE-----'],
serial_number: crypto.randomBytes(8).toString('hex'),
not_before: new Date().toISOString(),
not_after: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toISOString()
}
};
}
return { data: {}, metadata: {} };
}
async authenticateAppRole(roleId, secretId) {
// Mock AppRole authentication
return `mock-token-${roleId}-${secretId}`;
}
async authenticateAWS(role) {
// Mock AWS authentication
return `mock-aws-token-${role || 'default'}`;
}
async authenticateKubernetes() {
// Mock Kubernetes authentication
return 'mock-k8s-token';
}
startTokenRenewal() {
// In production, would implement token renewal logic
setInterval(() => {
if (this.tokenExpiry && this.tokenExpiry < new Date(Date.now() + 60000)) {
this.authenticate().catch(err => {
this.emit('error', { error: err, operation: 'token-renewal' });
});
}
}, 30000); // Check every 30 seconds
}
scheduleLeaseRenewal(lease) {
// Renew at 80% of lease duration
const renewAt = lease.leaseDuration * 0.8 * 1000;
setTimeout(() => {
this.renewLease(lease.leaseId).catch(err => {
this.emit('error', { error: err, operation: 'lease-renewal', leaseId: lease.leaseId });
});
}, renewAt);
}
parseCertificateSubject(cert) {
// In production, would parse actual certificate
return {
CN: 'app.example.com',
O: 'Example Corp',
OU: 'Engineering',
C: 'US',
ST: 'CA',
L: 'San Francisco'
};
}
auditLog(event) {
this.emit('audit', event);
// In production, would send to audit log
if (this.config.namespace?.includes('audit')) {
// Store audit event
}
}
}
exports.VaultProvider = VaultProvider;
//# sourceMappingURL=vault-provider.js.map