trojanhorse-js
Version:
A comprehensive JavaScript library for fetching, managing, and analyzing global threat intelligence from multiple open-source feeds and security news sources. Unlike its mythological namesake, this Trojan protects your digital fortress.
1,307 lines (1,300 loc) • 141 kB
JavaScript
'use strict';
var CryptoJS = require('crypto-js');
var events = require('events');
var axios = require('axios');
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
class TrojanHorseError extends Error {
code;
statusCode;
details;
constructor(message, code, statusCode, details) {
super(message);
this.name = 'TrojanHorseError';
this.code = code;
this.statusCode = statusCode || 500;
this.details = details || {};
Error.captureStackTrace(this, TrojanHorseError);
}
}
class SecurityError extends TrojanHorseError {
constructor(message, details) {
super(message, 'SECURITY_ERROR', 403, details);
this.name = 'SecurityError';
}
}
class AuthenticationError extends TrojanHorseError {
constructor(message, details) {
super(message, 'AUTH_ERROR', 401, details);
this.name = 'AuthenticationError';
}
}
class RateLimitError extends TrojanHorseError {
retryAfter;
constructor(message, retryAfter, details) {
super(message, 'RATE_LIMIT_ERROR', 429, details);
this.name = 'RateLimitError';
this.retryAfter = retryAfter || 60;
}
}
let argon2 = null;
let argon2Available = false;
async function loadArgon2() {
if (argon2Available) {
return argon2;
}
try {
if (typeof process !== 'undefined' && process.versions?.node) {
try {
if (typeof require !== 'undefined') {
argon2 = require('argon2');
argon2Available = true;
return argon2;
}
const { createRequire } = await import('module');
const moduleRequire = createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('trojanhorse.js', document.baseURI).href)) || new URL('file://' + __filename).href);
argon2 = moduleRequire('argon2');
argon2Available = true;
return argon2;
}
catch (importError) {
try {
const argon2Module = await import('argon2');
argon2 = argon2Module.default || argon2Module;
argon2Available = true;
return argon2;
}
catch (dynamicImportError) {
console.warn('Argon2 not available, falling back to PBKDF2');
argon2Available = false;
return null;
}
}
}
else {
console.warn('Argon2 not available in browser environment, falling back to PBKDF2');
return null;
}
}
catch (error) {
console.warn('Argon2 unavailable, falling back to PBKDF2:', String(error));
return null;
}
}
class CryptoEngine {
static ALGORITHM = 'AES-256-GCM';
static KEY_SIZE = 32;
static IV_SIZE_GCM = 12;
static SALT_SIZE = 32;
static ARGON2_MEMORY = 64 * 1024;
static ARGON2_TIME = 3;
static ARGON2_PARALLELISM = 4;
static PBKDF2_ITERATIONS = 100000;
argon2Instance = null;
constructor() {
if (typeof crypto === 'undefined' && typeof window?.crypto === 'undefined') {
throw new SecurityError('No cryptographic API available');
}
this.initializeArgon2();
}
async initializeArgon2() {
try {
this.argon2Instance = await loadArgon2();
}
catch (error) {
console.warn('Failed to initialize Argon2, using PBKDF2 fallback');
}
}
generateSecureRandom(length) {
if (length <= 0 || length > 1024) {
throw new SecurityError('Invalid random length: must be 1-1024 bytes');
}
try {
if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
const array = new Uint8Array(length);
crypto.getRandomValues(array);
return array;
}
else if (typeof window !== 'undefined' && window.crypto?.getRandomValues) {
const array = new Uint8Array(length);
window.crypto.getRandomValues(array);
return array;
}
else {
const wordArray = CryptoJS.lib.WordArray.random(length);
return new Uint8Array(this.wordArrayToUint8Array(wordArray));
}
}
catch (error) {
throw new SecurityError('Failed to generate secure random bytes', { originalError: error });
}
}
generateSalt(length = CryptoEngine.SALT_SIZE) {
const saltBytes = this.generateSecureRandom(length);
return Buffer.from(saltBytes).toString('base64');
}
async deriveKey(password, salt, options = {}) {
if (!password || password.length < 8) {
throw new SecurityError('Password must be at least 8 characters long');
}
if (!salt || salt.length < 16) {
throw new SecurityError('Salt must be at least 16 characters long');
}
if (!this.argon2Instance) {
await this.initializeArgon2();
}
const saltBuffer = Buffer.from(salt, 'base64');
if (this.argon2Instance) {
try {
const hash = await this.argon2Instance.hash(password, {
type: this.argon2Instance.argon2id,
memoryCost: options.memoryCost || CryptoEngine.ARGON2_MEMORY,
timeCost: options.timeCost || CryptoEngine.ARGON2_TIME,
parallelism: options.parallelism || CryptoEngine.ARGON2_PARALLELISM,
hashLength: CryptoEngine.KEY_SIZE,
salt: saltBuffer,
raw: true
});
return {
key: hash,
method: 'Argon2id'
};
}
catch (error) {
console.warn('Argon2 failed, falling back to PBKDF2:', String(error));
}
}
try {
const key = CryptoJS.PBKDF2(password, salt, {
keySize: CryptoEngine.KEY_SIZE / 4,
iterations: CryptoEngine.PBKDF2_ITERATIONS,
hasher: CryptoJS.algo.SHA256
});
const keyBytes = [];
for (let i = 0; i < key.words.length; i++) {
const word = key.words[i];
if (word !== undefined) {
keyBytes.push((word >> 24) & 0xff);
keyBytes.push((word >> 16) & 0xff);
keyBytes.push((word >> 8) & 0xff);
keyBytes.push(word & 0xff);
}
}
return {
key: Buffer.from(keyBytes.slice(0, CryptoEngine.KEY_SIZE)),
method: 'PBKDF2-Fallback'
};
}
catch (error) {
throw new SecurityError('Key derivation failed', { originalError: error });
}
}
async encrypt(data, password, options = {}) {
try {
if (!data) {
throw new SecurityError('Data cannot be empty');
}
if (!password || password.length < 8) {
throw new SecurityError('Password must be at least 8 characters long');
}
const salt = this.generateSalt(CryptoEngine.SALT_SIZE);
const iv = this.generateSecureRandom(CryptoEngine.IV_SIZE_GCM);
const ivBase64 = Buffer.from(iv).toString('base64');
const keyResult = await this.deriveKey(password, salt, {
memoryCost: options.iterations || CryptoEngine.ARGON2_MEMORY,
timeCost: CryptoEngine.ARGON2_TIME,
parallelism: CryptoEngine.ARGON2_PARALLELISM
});
const serializedData = JSON.stringify(data);
if (typeof require !== 'undefined') {
try {
const nodeCrypto = require('crypto');
let cipher;
let authTag;
try {
cipher = nodeCrypto.createCipher('aes-256-gcm', keyResult.key);
cipher.setAutoPadding(false);
let encrypted = cipher.update(serializedData, 'utf8', 'base64');
encrypted += cipher.final('base64');
authTag = cipher.getAuthTag ? cipher.getAuthTag().toString('base64') : '';
this.secureErase(keyResult.key);
password = '';
return {
encrypted,
authTag,
salt,
iv: ivBase64,
algorithm: CryptoEngine.ALGORITHM,
iterations: options.iterations || CryptoEngine.ARGON2_MEMORY,
timestamp: Date.now(),
memoryKb: CryptoEngine.ARGON2_MEMORY,
parallelism: CryptoEngine.ARGON2_PARALLELISM,
keyDerivation: keyResult.method
};
}
catch (gcmError) {
throw new Error(`GCM not supported: ${gcmError instanceof Error ? gcmError.message : String(gcmError)}`);
}
}
catch (nodeError) {
console.warn('Node.js crypto failed, using CryptoJS fallback:', String(nodeError));
}
}
const keyWordArray = CryptoJS.enc.Hex.parse(keyResult.key.toString('hex'));
const ivWordArray = CryptoJS.enc.Base64.parse(ivBase64);
const encrypted = CryptoJS.AES.encrypt(serializedData, keyWordArray, {
iv: ivWordArray,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});
const encryptedBase64 = encrypted.ciphertext.toString(CryptoJS.enc.Base64);
const authTag = CryptoJS.HmacSHA256(encryptedBase64 + ivBase64 + salt, keyWordArray).toString();
this.secureErase(keyResult.key);
password = '';
return {
encrypted: encryptedBase64,
authTag,
salt,
iv: ivBase64,
algorithm: CryptoEngine.ALGORITHM,
iterations: options.iterations || CryptoEngine.ARGON2_MEMORY,
timestamp: Date.now(),
memoryKb: CryptoEngine.ARGON2_MEMORY,
parallelism: CryptoEngine.ARGON2_PARALLELISM,
keyDerivation: keyResult.method
};
}
catch (error) {
if (error instanceof SecurityError) {
throw error;
}
throw new SecurityError('Encryption failed', { originalError: error });
}
}
async decrypt(vault, password) {
try {
if (!vault?.encrypted || !vault.salt || !vault.iv || !vault.authTag) {
throw new SecurityError('Invalid vault structure');
}
if (!password || password.length < 8) {
throw new SecurityError('Invalid password');
}
const keyResult = await this.deriveKey(password, vault.salt, {
memoryCost: vault.memoryKb || CryptoEngine.ARGON2_MEMORY,
timeCost: CryptoEngine.ARGON2_TIME,
parallelism: vault.parallelism || CryptoEngine.ARGON2_PARALLELISM
});
if (typeof require !== 'undefined') {
try {
const nodeCrypto = require('crypto');
try {
const decipher = nodeCrypto.createDecipher('aes-256-gcm', keyResult.key);
if (decipher.setAuthTag && vault.authTag) {
decipher.setAuthTag(Buffer.from(vault.authTag, 'base64'));
}
let decryptedString = decipher.update(vault.encrypted, 'base64', 'utf8');
decryptedString += decipher.final('utf8');
this.secureErase(keyResult.key);
password = '';
return JSON.parse(decryptedString);
}
catch (gcmError) {
throw new Error(`GCM decrypt not supported: ${gcmError instanceof Error ? gcmError.message : String(gcmError)}`);
}
}
catch (nodeError) {
console.warn('Node.js crypto GCM failed, using CryptoJS fallback:', String(nodeError));
}
}
const keyWordArray = CryptoJS.enc.Hex.parse(keyResult.key.toString('hex'));
const ivWordArray = CryptoJS.enc.Base64.parse(vault.iv);
const expectedAuthTag = CryptoJS.HmacSHA256(vault.encrypted + vault.iv + vault.salt, keyWordArray).toString();
if (expectedAuthTag !== vault.authTag) {
throw new SecurityError('Authentication verification failed - data may be corrupted or tampered');
}
try {
const decrypted = CryptoJS.AES.decrypt(vault.encrypted, keyWordArray, {
iv: ivWordArray,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});
const plaintextString = decrypted.toString(CryptoJS.enc.Utf8);
if (!plaintextString) {
throw new SecurityError('Decryption failed - malformed data or invalid key');
}
return JSON.parse(plaintextString);
}
catch (decryptError) {
const errorMessage = decryptError instanceof Error ? decryptError.message : String(decryptError);
console.error('🔍 AES Decryption Error:', errorMessage);
throw new SecurityError(`Decryption failed - ${errorMessage}`);
}
}
catch (error) {
if (error instanceof SecurityError) {
throw error;
}
throw new SecurityError('Decryption failed', { originalError: error });
}
}
hash(data) {
if (!data) {
throw new SecurityError('Data to hash cannot be empty');
}
try {
return CryptoJS.SHA256(data).toString(CryptoJS.enc.Hex);
}
catch (error) {
throw new SecurityError('Hashing failed', { originalError: error });
}
}
hmac(data, key) {
if (!data || !key) {
throw new SecurityError('Data and key cannot be empty');
}
try {
return CryptoJS.HmacSHA256(data, key).toString(CryptoJS.enc.Hex);
}
catch (error) {
throw new SecurityError('HMAC generation failed', { originalError: error });
}
}
secureErase(data) {
try {
if (Buffer.isBuffer(data)) {
const randomData = this.generateSecureRandom(data.length);
for (let i = 0; i < data.length; i++) {
const value = randomData[i];
if (value !== undefined) {
data[i] = value;
}
}
data.fill(0);
}
else if (data && typeof data === 'object' && data.words) {
for (let i = 0; i < data.words.length; i++) {
data.words[i] = 0;
}
}
else if (typeof data === 'string') {
data = '';
}
else if (data instanceof Uint8Array) {
const randomData = this.generateSecureRandom(data.length);
for (let i = 0; i < data.length; i++) {
const value = randomData[i];
if (value !== undefined) {
data[i] = value;
}
}
data.fill(0);
}
}
catch (error) {
console.warn('Secure erase failed:', error);
}
}
wordArrayToUint8Array(wordArray) {
const words = wordArray.words;
const sigBytes = wordArray.sigBytes;
const bytes = [];
for (let i = 0; i < sigBytes; i++) {
bytes.push((words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff);
}
return bytes;
}
validateEncryptionParams(vault) {
try {
return !!(vault &&
typeof vault.encrypted === 'string' &&
typeof vault.authTag === 'string' &&
typeof vault.salt === 'string' &&
typeof vault.iv === 'string' &&
vault.algorithm === CryptoEngine.ALGORITHM &&
typeof vault.timestamp === 'number' &&
vault.timestamp > 0);
}
catch (error) {
return false;
}
}
getCryptoInfo() {
const hasNodeCrypto = typeof require !== 'undefined';
return {
implementation: hasNodeCrypto ? 'Node.js Crypto' : 'CryptoJS Fallback',
algorithm: CryptoEngine.ALGORITHM,
keyDerivation: 'Argon2id',
secure: true
};
}
isSecureContext() {
if (typeof window === 'undefined') {
return true;
}
return window.isSecureContext || location.protocol === 'https:';
}
}
class KeyVault {
cryptoEngine;
encryptedVault = null;
decryptedKeys = null;
isLocked = true;
options;
autoLockTimer = null;
lastAccessTime = null;
failedAttempts = 0;
maxFailedAttempts = 5;
lockoutDuration = 300000;
constructor(options = {}) {
this.cryptoEngine = new CryptoEngine();
this.options = {
algorithm: 'AES-256-GCM',
keyDerivation: 'Argon2id',
iterations: 65536,
saltBytes: 32,
autoLock: true,
lockTimeout: 300000,
requireMFA: false,
...options
};
}
extractApiKey(keyData) {
if (!keyData) {
return undefined;
}
if (typeof keyData === 'string') {
return keyData;
}
if (typeof keyData === 'object') {
return keyData.key || keyData.secret || keyData.token;
}
return undefined;
}
normalizeApiKeys(apiKeys) {
const normalized = {};
for (const [provider, keyData] of Object.entries(apiKeys)) {
const stringKey = this.extractApiKey(keyData);
if (stringKey) {
normalized[provider] = stringKey;
}
}
return normalized;
}
async createVault(password, apiKeys) {
this.validatePassword(password);
this.validateApiKeys(apiKeys);
try {
const vault = await this.cryptoEngine.encrypt(apiKeys, password, this.options);
this.encryptedVault = vault;
this.decryptedKeys = this.normalizeApiKeys(apiKeys);
this.isLocked = false;
this.updateAccessTime();
this.setupAutoLock();
return vault;
}
catch (error) {
throw new SecurityError('Failed to create vault', { originalError: error });
}
}
loadVault(vault) {
if (!this.cryptoEngine.validateEncryptionParams(vault)) {
throw new SecurityError('Invalid vault structure');
}
this.encryptedVault = vault;
this.isLocked = true;
this.decryptedKeys = null;
}
async unlock(password) {
if (!this.encryptedVault) {
throw new SecurityError('No vault loaded');
}
if (this.failedAttempts >= this.maxFailedAttempts) {
throw new AuthenticationError('Vault is locked due to too many failed attempts');
}
this.validatePassword(password);
try {
const decryptedData = await this.cryptoEngine.decrypt(this.encryptedVault, password);
this.validateApiKeys(decryptedData);
this.decryptedKeys = this.normalizeApiKeys(decryptedData);
this.isLocked = false;
this.failedAttempts = 0;
this.updateAccessTime();
this.setupAutoLock();
}
catch (error) {
this.failedAttempts++;
if (this.failedAttempts >= this.maxFailedAttempts) {
this.lockVault();
setTimeout(() => {
this.failedAttempts = 0;
}, this.lockoutDuration);
}
throw new AuthenticationError('Failed to unlock vault - invalid password');
}
}
lock() {
this.lockVault();
}
getApiKey(provider) {
if (this.isLocked || !this.decryptedKeys) {
throw new SecurityError('Vault is locked - please unlock first');
}
this.updateAccessTime();
const key = this.decryptedKeys[provider];
if (!key) {
throw new SecurityError(`API key for provider '${provider}' not found`);
}
return key;
}
async setApiKey(provider, apiKey, password) {
if (this.isLocked || !this.decryptedKeys) {
throw new SecurityError('Vault is locked - please unlock first');
}
if (!provider || !apiKey) {
throw new SecurityError('Provider and API key cannot be empty');
}
this.decryptedKeys[provider] = apiKey;
const newVault = await this.cryptoEngine.encrypt(this.decryptedKeys, password, this.options);
this.encryptedVault = newVault;
this.updateAccessTime();
}
async removeApiKey(provider, password) {
if (this.isLocked || !this.decryptedKeys) {
throw new SecurityError('Vault is locked - please unlock first');
}
if (!this.decryptedKeys[provider]) {
throw new SecurityError(`API key for provider '${provider}' not found`);
}
delete this.decryptedKeys[provider];
const newVault = await this.cryptoEngine.encrypt(this.decryptedKeys, password, this.options);
this.encryptedVault = newVault;
this.updateAccessTime();
}
async rotateKey(provider, newKey, options = {}) {
if (this.isLocked || !this.decryptedKeys) {
throw new SecurityError('Vault is locked - please unlock first');
}
if (!this.decryptedKeys[provider]) {
throw new SecurityError(`API key for provider '${provider}' not found`);
}
const { gracePeriod = 0, password, notifyRotation = true } = options;
const oldKey = this.decryptedKeys[provider];
try {
this.decryptedKeys[provider] = newKey;
this.updateAccessTime();
if (password && this.encryptedVault) {
const updatedVault = await this.cryptoEngine.encrypt(this.decryptedKeys, password, this.options);
this.encryptedVault = updatedVault;
}
if (gracePeriod > 0) {
setTimeout(() => {
if (typeof oldKey === 'string') {
const keyArray = oldKey.split('');
for (let i = 0; i < keyArray.length; i++) {
keyArray[i] = Math.random().toString(36).charAt(0);
}
}
}, gracePeriod);
}
if (notifyRotation) {
console.info(`🔄 API key rotated for provider: ${provider}`);
}
this.auditLog('info', `API key rotated for provider: ${provider}`);
}
catch (error) {
if (this.decryptedKeys && oldKey) {
this.decryptedKeys[provider] = oldKey;
}
throw new SecurityError(`Key rotation failed for ${provider}: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
async rotateMultipleKeys(keyUpdates, options = {}) {
const results = { success: [], failed: {} };
const { continueOnError = true } = options;
for (const [provider, newKey] of Object.entries(keyUpdates)) {
try {
await this.rotateKey(provider, newKey, {
...options,
notifyRotation: false
});
results.success.push(provider);
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
results.failed[provider] = errorMessage;
console.error(`❌ Failed to rotate key for ${provider}: ${errorMessage}`);
if (!continueOnError) {
throw error;
}
}
}
console.info(`🔄 Batch key rotation completed: ${results.success.length} successful, ${Object.keys(results.failed).length} failed`);
return results;
}
setupKeyRotation(config) {
const { providers, rotationInterval, keyGenerator, password } = config;
return setInterval(async () => {
console.info('🔄 Starting scheduled key rotation...');
const keyUpdates = {};
for (const provider of providers) {
try {
keyUpdates[provider] = await keyGenerator(provider);
}
catch (error) {
console.error(`Failed to generate new key for ${provider}:`, error);
}
}
try {
const results = await this.rotateMultipleKeys(keyUpdates, {
password,
gracePeriod: 5 * 60 * 1000,
continueOnError: true
});
console.info(`✅ Scheduled rotation completed: ${results.success.length} keys rotated`);
}
catch (error) {
console.error('❌ Scheduled key rotation failed:', error);
}
}, rotationInterval);
}
getStatus() {
return {
isLocked: this.isLocked,
hasVault: !!this.encryptedVault,
keyCount: this.decryptedKeys ? Object.keys(this.decryptedKeys).length : 0,
lastAccess: this.lastAccessTime,
autoLockEnabled: this.options.autoLock || false,
failedAttempts: this.failedAttempts
};
}
getProviders() {
if (this.isLocked || !this.decryptedKeys) {
throw new SecurityError('Vault is locked - please unlock first');
}
this.updateAccessTime();
return Object.keys(this.decryptedKeys);
}
async testApiKey(provider) {
const key = this.getApiKey(provider);
if (!key || key.length < 8) {
return false;
}
return true;
}
exportVault() {
if (!this.encryptedVault) {
throw new SecurityError('No vault to export');
}
return { ...this.encryptedVault };
}
lockVault() {
this.isLocked = true;
this.decryptedKeys = null;
this.clearAutoLockTimer();
if (this.decryptedKeys) {
this.cryptoEngine.secureErase(this.decryptedKeys);
}
}
updateAccessTime() {
this.lastAccessTime = new Date();
if (this.options.autoLock) {
this.setupAutoLock();
}
}
setupAutoLock() {
this.clearAutoLockTimer();
if (this.options.autoLock && this.options.lockTimeout) {
this.autoLockTimer = setTimeout(() => {
this.lockVault();
}, this.options.lockTimeout);
}
}
clearAutoLockTimer() {
if (this.autoLockTimer) {
clearTimeout(this.autoLockTimer);
this.autoLockTimer = null;
}
}
validatePassword(password) {
if (!password || typeof password !== 'string') {
throw new SecurityError('Password is required');
}
if (password.length < 12) {
throw new SecurityError('Password must be at least 12 characters long');
}
const entropy = this.calculatePasswordEntropy(password);
if (entropy < 50) {
throw new SecurityError(`Password is too weak (entropy: ${entropy.toFixed(1)} bits). Minimum required: 50 bits`);
}
if (this.hasWeakPatterns(password)) {
throw new SecurityError('Password contains weak patterns');
}
}
calculatePasswordEntropy(password) {
const charSets = {
lowercase: /[a-z]/.test(password) ? 26 : 0,
uppercase: /[A-Z]/.test(password) ? 26 : 0,
digits: /[0-9]/.test(password) ? 10 : 0,
symbols: /[^a-zA-Z0-9]/.test(password) ? 32 : 0
};
const charsetSize = Object.values(charSets).reduce((sum, size) => sum + size, 0);
if (charsetSize === 0) {
return 0;
}
const entropy = password.length * Math.log2(charsetSize);
const uniqueChars = new Set(password).size;
const repetitionPenalty = uniqueChars / password.length;
return entropy * repetitionPenalty;
}
hasWeakPatterns(password) {
const weakPatterns = [
/(.)\1{2,}/,
/012|123|234|345|456|567|678|789|890/,
/abc|bcd|cde|def|efg|fgh|ghi|hij|ijk|jkl|klm|lmn|mno|nop|opq|pqr|qrs|rst|stu|tuv|uvw|vwx|wxy|xyz/i,
/^(password|admin|user|login|qwerty|123456|letmein)$/i,
/^.{1,3}$/
];
return weakPatterns.some(pattern => pattern.test(password));
}
validateApiKeys(apiKeys) {
if (!apiKeys || typeof apiKeys !== 'object') {
throw new SecurityError('API keys must be an object');
}
const keys = Object.keys(apiKeys);
if (keys.length === 0) {
throw new SecurityError('At least one API key is required');
}
keys.forEach(provider => {
const key = apiKeys[provider];
if (!key || typeof key !== 'string' || key.length < 8) {
throw new SecurityError(`Invalid API key for provider: ${provider}`);
}
});
}
sanitizeProvider(provider) {
return provider.replace(/[^a-zA-Z0-9_-]/g, '');
}
auditLog(level, message, details) {
const timestamp = new Date().toISOString();
console[level](`[KeyVault Audit] ${timestamp} - ${message}`, details || '');
}
}
class ThreatCorrelationEngine {
config;
constructor(config = {}) {
if (config.minimumSources !== undefined && config.minimumSources < 1) {
throw new Error('Invalid configuration: minimumSources must be at least 1');
}
if (config.consensusThreshold !== undefined && (config.consensusThreshold < 0 || config.consensusThreshold > 1)) {
throw new Error('Invalid configuration: consensusThreshold must be between 0 and 1');
}
this.config = {
minimumSources: 2,
consensusThreshold: 0.5,
temporalWindowDays: 7,
confidenceWeighting: {},
enableGeolocationAnalysis: false,
enablePatternDetection: true,
riskScoreWeights: {
consensus: 0.4,
recency: 0.3,
severity: 0.3,
sourceReliability: 0.2
},
...config
};
}
async correlate(indicators) {
if (indicators.length === 0) {
return {
relatedIndicators: [],
crossReferences: [],
enrichmentData: { reputation: { overall: 0, categories: [] } },
riskFactors: [],
patterns: [],
correlationScore: 0,
consensusLevel: 'weak',
riskScore: 0,
sources: [],
indicators: []
};
}
const sources = [...new Set(indicators.map(i => i.source))];
const correlationScore = Math.min(indicators.length / this.config.minimumSources, 1);
const consensusLevel = correlationScore > 0.7 ? 'strong' : correlationScore > 0.4 ? 'moderate' : 'weak';
const riskScore = indicators.reduce((sum, i) => sum + i.confidence, 0) / indicators.length;
return {
relatedIndicators: indicators,
crossReferences: [],
enrichmentData: { reputation: { overall: riskScore * 100, categories: [] } },
riskFactors: [],
patterns: [],
correlationScore,
consensusLevel,
riskScore,
sources,
indicators
};
}
exportResult(result, format = 'json') {
switch (format) {
case 'json':
return JSON.stringify({ ...result, timestamp: new Date().toISOString() }, null, 2);
case 'stix': {
const stixObject = {
type: 'bundle',
id: `bundle--${Date.now()}`,
spec_version: '2.1',
objects: [{
type: 'indicator',
id: `indicator--${Date.now()}`,
created: new Date().toISOString(),
modified: new Date().toISOString(),
pattern: result.relatedIndicators?.[0]?.value || 'unknown',
labels: ['malicious-activity'],
confidence: Math.round((result.correlationScore || 0) * 100)
}]
};
return JSON.stringify(stixObject, null, 2);
}
case 'csv': {
const headers = 'type,value,confidence,sources,risk_score\n';
const rows = (result.relatedIndicators || []).map((indicator) => `${indicator.type},${indicator.value},${indicator.confidence},"${(result.sources || []).join(';')}",${result.riskScore || 0}`).join('\n');
return headers + rows;
}
default:
throw new Error(`Unsupported export format: ${format}`);
}
}
addIntegration(_name, _integration) {
}
async shareResult(_result, _platform) {
}
}
class CircuitBreaker extends events.EventEmitter {
config;
state = 'CLOSED';
failureCount = 0;
successCount = 0;
lastFailureTime = null;
lastSuccessTime = null;
stateChangeTime = Date.now();
requestHistory = [];
responseTimes = [];
constructor(config = {}) {
super();
this.config = {
failureThreshold: 5,
successThreshold: 3,
timeout: 60000,
monitoringWindow: 60000,
volumeThreshold: 10,
errorFilter: () => true,
...config
};
setInterval(() => this.cleanupOldRecords(), this.config.monitoringWindow);
}
async execute(fn) {
if (this.state === 'OPEN') {
if (this.shouldAttemptReset()) {
this.setState('HALF_OPEN');
}
else {
const error = new Error('Circuit breaker is OPEN');
this.recordRequest(false, 0, error.message);
throw error;
}
}
const startTime = Date.now();
try {
const result = await fn();
const responseTime = Date.now() - startTime;
this.onSuccess(responseTime);
return result;
}
catch (error) {
const responseTime = Date.now() - startTime;
this.onFailure(error, responseTime);
throw error;
}
}
async executeWithRetry(fn, maxRetries = 3, retryDelay = 1000) {
let lastError;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await this.execute(fn);
}
catch (error) {
lastError = error;
if (this.state === 'OPEN') {
throw error;
}
if (attempt === maxRetries) {
throw error;
}
const delay = retryDelay * Math.pow(2, attempt);
await this.sleep(delay);
}
}
throw lastError;
}
async executeBatch(functions, options = {}) {
const { maxConcurrency = 5, failFast = false, continueOnFailure = true } = options;
const results = [];
const executing = [];
for (let i = 0; i < functions.length; i++) {
const fn = functions[i];
const executePromise = fn ? this.execute(fn) : Promise.reject(new Error('No function provided'))
.then(result => {
results[i] = { success: true, result };
})
.catch(error => {
results[i] = { success: false, error };
if (failFast && !continueOnFailure) {
throw error;
}
});
executing.push(executePromise);
if (executing.length >= maxConcurrency) {
await Promise.race(executing);
const completed = executing.filter(p => p === Promise.resolve());
completed.forEach(p => {
const index = executing.indexOf(p);
if (index > -1) {
executing.splice(index, 1);
}
});
}
}
await Promise.allSettled(executing);
return results;
}
onSuccess(responseTime) {
this.recordRequest(true, responseTime);
this.lastSuccessTime = Date.now();
if (this.state === 'HALF_OPEN') {
this.successCount++;
if (this.successCount >= this.config.successThreshold) {
this.setState('CLOSED');
this.reset();
}
}
else if (this.state === 'CLOSED') {
this.failureCount = Math.max(0, this.failureCount - 1);
}
this.emit('success', { responseTime, state: this.state });
}
onFailure(error, responseTime) {
if (this.config.errorFilter && !this.config.errorFilter(error)) {
this.recordRequest(false, responseTime, 'filtered-error');
return;
}
this.recordRequest(false, responseTime, error.message);
this.lastFailureTime = Date.now();
this.failureCount++;
if (this.state === 'HALF_OPEN' || this.shouldOpen()) {
this.setState('OPEN');
}
this.emit('failure', {
error: error.message,
responseTime,
state: this.state,
failureCount: this.failureCount
});
}
shouldOpen() {
if (this.state === 'OPEN') {
return false;
}
const recentRequests = this.getRecentRequests();
if (recentRequests.length < this.config.volumeThreshold) {
return false;
}
return this.failureCount >= this.config.failureThreshold;
}
shouldAttemptReset() {
return this.lastFailureTime !== null &&
(Date.now() - this.lastFailureTime) >= this.config.timeout;
}
setState(newState) {
if (this.state !== newState) {
const oldState = this.state;
this.state = newState;
this.stateChangeTime = Date.now();
if (this.config.onStateChange) {
this.config.onStateChange(newState);
}
this.emit('stateChange', {
from: oldState,
to: newState,
timestamp: this.stateChangeTime
});
console.log(`Circuit breaker state changed: ${oldState} -> ${newState}`);
}
}
reset() {
this.failureCount = 0;
this.successCount = 0;
this.lastFailureTime = null;
}
recordRequest(success, responseTime, error) {
const record = {
timestamp: Date.now(),
success,
responseTime,
error: error || ''
};
this.requestHistory.push(record);
this.responseTimes.push(responseTime);
if (this.responseTimes.length > 1000) {
this.responseTimes = this.responseTimes.slice(-1e3);
}
this.emit('request', record);
}
getRecentRequests() {
const cutoff = Date.now() - this.config.monitoringWindow;
return this.requestHistory.filter(record => record.timestamp >= cutoff);
}
cleanupOldRecords() {
const cutoff = Date.now() - this.config.monitoringWindow;
this.requestHistory = this.requestHistory.filter(record => record.timestamp >= cutoff);
}
calculatePercentile(values, percentile) {
if (values.length === 0) {
return 0;
}
const sorted = [...values].sort((a, b) => a - b);
const index = Math.ceil((percentile / 100) * sorted.length) - 1;
const validIndex = Math.max(0, Math.min(index, sorted.length - 1));
return sorted[validIndex] ?? 0;
}
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
getStats() {
const recentRequests = this.getRecentRequests();
const recentFailures = recentRequests.filter(r => !r.success);
const recentSuccesses = recentRequests.filter(r => r.success);
const responseTimeStats = this.responseTimes.length > 0 ? {
average: this.responseTimes.reduce((a, b) => a + b, 0) / this.responseTimes.length,
min: Math.min(...this.responseTimes),
max: Math.max(...this.responseTimes),
p95: this.calculatePercentile(this.responseTimes, 95),
p99: this.calculatePercentile(this.responseTimes, 99)
} : {
average: 0, min: 0, max: 0, p95: 0, p99: 0
};
return {
state: this.state,
failureCount: this.failureCount,
successCount: this.successCount,
totalRequests: this.requestHistory.length,
lastFailureTime: this.lastFailureTime,
lastSuccessTime: this.lastSuccessTime,
stateChangeTime: this.stateChangeTime,
requestStats: {
total: recentRequests.length,
failures: recentFailures.length,
successes: recentSuccesses.length,
timeouts: recentFailures.filter(r => r.error?.includes('timeout')).length,
circuitOpen: recentRequests.filter(r => r.error === 'Circuit breaker is OPEN').length
},
responseTimeStats
};
}
getState() {
return this.state;
}
getConfig() {
return { ...this.config };
}
updateConfig(newConfig) {
this.config = { ...this.config, ...newConfig };
this.emit('configUpdate', this.config);
}
open() {
this.setState('OPEN');
this.lastFailureTime = Date.now();
}
close() {
this.setState('CLOSED');
this.reset();
}
halfOpen() {
this.setState('HALF_OPEN');
this.successCount = 0;
}
isHealthy() {
if (this.state === 'OPEN') {
return false;
}
const recentRequests = this.getRecentRequests();
if (recentRequests.length < this.config.volumeThreshold) {
return true;
}
const failureRate = recentRequests.filter(r => !r.success).length / recentRequests.length;
return failureRate < (this.config.failureThreshold / this.config.volumeThreshold);
}
getHealthScore() {
if (this.state === 'OPEN') {
return 0;
}
const recentRequests = this.getRecentRequests();
if (recentRequests.length === 0) {
return 100;
}
const successRate = recentRequests.filter(r => r.success).length / recentRequests.length;
const baseScore = successRate * 100;
const avgResponseTime = this.responseTimes.reduce((a, b) => a + b, 0) / this.responseTimes.length;
const responseTimePenalty = Math.min(avgResponseTime / 5000, 1) * 20;
return Math.max(0, baseScore - responseTimePenalty);
}
exportMetrics() {
const stats = this.getStats();
return {
'circuit_breaker_state': this.state === 'CLOSED' ? 0 : this.state === 'HALF_OPEN' ? 1 : 2,
'circuit_breaker_failure_count': stats.failureCount,
'circuit_breaker_success_count': stats.successCount,
'circuit_breaker_total_requests': stats.totalRequests,
'circuit_breaker_recent_failures': stats.requestStats.failures,
'circuit_breaker_recent_successes': stats.requestStats.successes,
'circuit_breaker_response_time_avg': stats.responseTimeStats.average,
'circuit_breaker_response_time_p95': stats.responseTimeStats.p95,
'circuit_breaker_response_time_p99': stats.responseTimeStats.p99,
'circuit_breaker_health_score': this.getHealthScore()
};
}
}
class URLhausFeed {
axiosInstance;
config;
lastFetchTime = 0;
MIN_FETCH_INTERVAL = 300000;
stats;
cache = new Map();
promiseCache = new Map();
DEFAULT_CACHE_TTL = 600000;
constructor() {
this.config = {
name: 'URLhaus',
type: 'csv',
endpoint: 'https://urlhaus.abuse.ch/downloads/csv_recent/',
authentication: {
type: 'none',
required: false
},
rateLimit: {
requestsPerHour: 12,
burstLimit: 1
},
enabled: true,
priority: 'high',
sslPinning: true,
timeout: 30000,
retries: 3,
cacheTTL: this.DEFAULT_CACHE_TTL
};
this.stats = {
lastFetch: null,
nextAllowedFetch: new Date(Date.now() + this.MIN_FETCH_INTERVAL),
rateLimit: this.config.rateLimit,
successCount: 0,
errorCount: 0,
requestsProcessed: 0
};
this.axiosInstance = axios.create({
timeout: this.config.timeout || 30000,
headers: {
'User-Agent': 'TrojanHorse.js/1.0.1 (Threat Intelligence Library)',
'Accept': 'text/csv',
'Cache-Control': 'no-cache'
},
httpsAgent: undefined,
validateStatus: (status) => status >= 200 && status < 300
});
this.setupInterceptors();
}
updateConfig(newConfig) {
this.config = { ...this.config, ...newConfig };
if (newConfig.timeout) {
this.axiosInstance.defaults.timeout = newConfig.timeout;
}
}
async fetchThreatData() {
const cacheKey = 'recent_urls';
const cached = this.getCachedData(cacheKey);
if (cached) {
return cached;
}
if (this.promiseCache.has(cacheKey)) {
return await this.promiseCache.get(cacheKey);
}
const requestPromise = this.performFetch(cacheKey);
this.promiseCache.set(cacheKey, requestPromise);
try {
const result = await requestPromise;
return result;
}
finally {
this.promiseCache.delete(cacheKey);
}
}
getConfig() {
return { ...this.config };
}
async checkAvailability() {
try {
const response = await this.axiosInstance.head(this.config.endpoint);
return response.status === 200;
}
catch (error) {
return false;
}
}
getStats() {
return {
lastFetch: this.lastFetchTime ? new Date(this.lastFetchTime) : null,
nextAllowedFetch: new Date(this.lastFetchTime + this.MIN_FETCH_INTERVAL),
rateLimit: this.config.rateLimit,
successCount: this.stats.successCount,
errorCount: this.stats.errorCount,
requestsProcessed: this.stats.requestsProcessed
};
}
getCachedData(key) {
const entry = this.cache.get(key);
if (!entry) {
return null;
}
const now = Date.now();
const ttl = this.config.cacheTTL || this.DEFAULT_CACHE_TTL;
if (now - entry.timestamp > ttl) {
this.cache.delete(key);
return null;
}
return entry.data;
}
setCachedData(key, data) {
this.cache.set(key, {
data,
timestamp: Date.now()
});
}
setupInterceptors() {
this.axiosInstance.interceptors.request.use((config) => {
if (process.env.NODE_ENV !== 'test') ;
return config;
}, (error) => Promise.reject(error));
this.axiosInstance.interceptors.response.use((response) => {
const maxSize = 50 * 1024 * 1024;
const contentLength = response.headers['content-length'];
if (contentLength && parseInt(contentLength) > maxSize) {
throw new TrojanHorseError('Response too large', 'RESPONSE_TOO_LARGE', response.status);
}
return response;
}, (error) => Promise.reject(error));
}
checkRateLimit() {
const now = Date.now();
const timeSinceLastFetch = now - this.lastFetchTime;
if (timeSinceLastFetch < this.MIN_FETCH_INTERVAL) {
const waitTime = this.MIN_FETCH_INTERVAL - time