UNPKG

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
'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