import * as plugins from '../plugins.js'; import * as paths from '../paths.js'; import { logger } from '../logger.js'; import { SecurityLogger, SecurityLogLevel, SecurityEventType } from './classes.securitylogger.js'; import { RustSecurityBridge } from './classes.rustsecuritybridge.js'; import { LRUCache } from 'lru-cache'; /** * Reputation check result information */ export interface IReputationResult { score: number; // 0 (worst) to 100 (best) isSpam: boolean; // true if the IP is known for spam isProxy: boolean; // true if the IP is a known proxy isTor: boolean; // true if the IP is a known Tor exit node isVPN: boolean; // true if the IP is a known VPN country?: string; // Country code (if available) asn?: string; // Autonomous System Number (if available) org?: string; // Organization name (if available) blacklists?: string[]; // Names of blacklists that include this IP timestamp: number; // When this result was created/retrieved error?: string; // Error message if check failed } /** * Reputation threshold scores */ export enum ReputationThreshold { HIGH_RISK = 20, // Score below this is considered high risk MEDIUM_RISK = 50, // Score below this is considered medium risk LOW_RISK = 80 // Score below this is considered low risk (but not trusted) } /** * IP type classifications */ export enum IPType { RESIDENTIAL = 'residential', DATACENTER = 'datacenter', PROXY = 'proxy', TOR = 'tor', VPN = 'vpn', UNKNOWN = 'unknown' } /** * Options for the IP Reputation Checker */ export interface IIPReputationOptions { maxCacheSize?: number; // Maximum number of IPs to cache (default: 10000) cacheTTL?: number; // TTL for cache entries in ms (default: 24 hours) dnsblServers?: string[]; // List of DNSBL servers to check highRiskThreshold?: number; // Score below this is high risk mediumRiskThreshold?: number; // Score below this is medium risk lowRiskThreshold?: number; // Score below this is low risk enableLocalCache?: boolean; // Whether to persist cache to disk (default: true) enableDNSBL?: boolean; // Whether to use DNSBL checks (default: true) enableIPInfo?: boolean; // Whether to use IP info service (default: true) } /** * IP reputation checker — delegates DNSBL lookups to the Rust security bridge. * Retains LRU caching and disk persistence in TypeScript. */ export class IPReputationChecker { private static instance: IPReputationChecker; private reputationCache: LRUCache; private options: Required; private storageManager?: any; private static readonly DEFAULT_OPTIONS: Required = { maxCacheSize: 10000, cacheTTL: 24 * 60 * 60 * 1000, dnsblServers: [], highRiskThreshold: ReputationThreshold.HIGH_RISK, mediumRiskThreshold: ReputationThreshold.MEDIUM_RISK, lowRiskThreshold: ReputationThreshold.LOW_RISK, enableLocalCache: true, enableDNSBL: true, enableIPInfo: true }; constructor(options: IIPReputationOptions = {}, storageManager?: any) { this.options = { ...IPReputationChecker.DEFAULT_OPTIONS, ...options }; this.storageManager = storageManager; this.reputationCache = new LRUCache({ max: this.options.maxCacheSize, ttl: this.options.cacheTTL, }); if (this.options.enableLocalCache) { this.loadCache().catch(error => { logger.log('error', `Failed to load IP reputation cache during initialization: ${error.message}`); }); } } public static getInstance(options: IIPReputationOptions = {}, storageManager?: any): IPReputationChecker { if (!IPReputationChecker.instance) { IPReputationChecker.instance = new IPReputationChecker(options, storageManager); } return IPReputationChecker.instance; } /** * Check an IP address's reputation via the Rust bridge */ public async checkReputation(ip: string): Promise { try { if (!this.isValidIPAddress(ip)) { logger.log('warn', `Invalid IP address format: ${ip}`); return this.createErrorResult(ip, 'Invalid IP address format'); } // Check cache first const cachedResult = this.reputationCache.get(ip); if (cachedResult) { logger.log('info', `Using cached reputation data for IP ${ip}`, { score: cachedResult.score, isSpam: cachedResult.isSpam }); return cachedResult; } // Delegate to Rust bridge const bridge = RustSecurityBridge.getInstance(); const rustResult = await bridge.checkIpReputation(ip); const result: IReputationResult = { score: rustResult.score, isSpam: rustResult.listed_count > 0, isProxy: rustResult.ip_type === 'proxy', isTor: rustResult.ip_type === 'tor', isVPN: rustResult.ip_type === 'vpn', blacklists: rustResult.dnsbl_results .filter(d => d.listed) .map(d => d.server), timestamp: Date.now(), }; this.reputationCache.set(ip, result); if (this.options.enableLocalCache) { this.saveCache().catch(error => { logger.log('error', `Failed to save IP reputation cache: ${error.message}`); }); } this.logReputationCheck(ip, result); return result; } catch (error) { logger.log('error', `Error checking IP reputation for ${ip}: ${error.message}`, { ip, stack: error.stack }); const errorResult = this.createErrorResult(ip, error.message); // Cache error results to avoid repeated failing lookups this.reputationCache.set(ip, errorResult); return errorResult; } } private createErrorResult(ip: string, errorMessage: string): IReputationResult { return { score: 50, isSpam: false, isProxy: false, isTor: false, isVPN: false, timestamp: Date.now(), error: errorMessage }; } private isValidIPAddress(ip: string): boolean { const ipv4Pattern = /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; return ipv4Pattern.test(ip); } private logReputationCheck(ip: string, result: IReputationResult): void { let logLevel = SecurityLogLevel.INFO; if (result.score < this.options.highRiskThreshold) { logLevel = SecurityLogLevel.WARN; } SecurityLogger.getInstance().logEvent({ level: logLevel, type: SecurityEventType.IP_REPUTATION, message: `IP reputation check ${result.isSpam ? 'flagged spam' : 'completed'} for ${ip}`, ipAddress: ip, details: { score: result.score, isSpam: result.isSpam, isProxy: result.isProxy, isTor: result.isTor, isVPN: result.isVPN, country: result.country, blacklists: result.blacklists }, success: !result.isSpam }); } private async saveCache(): Promise { try { const entries = Array.from(this.reputationCache.entries()).map(([ip, data]) => ({ ip, data })); if (entries.length === 0) { return; } const cacheData = JSON.stringify(entries); if (this.storageManager) { await this.storageManager.set('/security/ip-reputation-cache.json', cacheData); logger.log('info', `Saved ${entries.length} IP reputation cache entries to StorageManager`); } else { const cacheDir = plugins.path.join(paths.dataDir, 'security'); await plugins.smartfs.directory(cacheDir).recursive().create(); const cacheFile = plugins.path.join(cacheDir, 'ip_reputation_cache.json'); await plugins.smartfs.file(cacheFile).write(cacheData); logger.log('info', `Saved ${entries.length} IP reputation cache entries to disk`); } } catch (error) { logger.log('error', `Failed to save IP reputation cache: ${error.message}`); } } private async loadCache(): Promise { try { let cacheData: string | null = null; let fromFilesystem = false; if (this.storageManager) { try { cacheData = await this.storageManager.get('/security/ip-reputation-cache.json'); if (!cacheData) { const cacheFile = plugins.path.join(paths.dataDir, 'security', 'ip_reputation_cache.json'); if (plugins.fs.existsSync(cacheFile)) { logger.log('info', 'Migrating IP reputation cache from filesystem to StorageManager'); cacheData = plugins.fs.readFileSync(cacheFile, 'utf8'); fromFilesystem = true; await this.storageManager.set('/security/ip-reputation-cache.json', cacheData); logger.log('info', 'IP reputation cache migrated to StorageManager successfully'); try { plugins.fs.unlinkSync(cacheFile); logger.log('info', 'Old cache file removed after migration'); } catch (deleteError) { logger.log('warn', `Could not delete old cache file: ${deleteError.message}`); } } } } catch (error) { logger.log('error', `Error loading from StorageManager: ${error.message}`); } } else { const cacheFile = plugins.path.join(paths.dataDir, 'security', 'ip_reputation_cache.json'); if (plugins.fs.existsSync(cacheFile)) { cacheData = plugins.fs.readFileSync(cacheFile, 'utf8'); fromFilesystem = true; } } if (cacheData) { const entries = JSON.parse(cacheData); const now = Date.now(); const validEntries = entries.filter(entry => { const age = now - entry.data.timestamp; return age < this.options.cacheTTL; }); for (const entry of validEntries) { this.reputationCache.set(entry.ip, entry.data); } const source = fromFilesystem ? 'disk' : 'StorageManager'; logger.log('info', `Loaded ${validEntries.length} IP reputation cache entries from ${source}`); } } catch (error) { logger.log('error', `Failed to load IP reputation cache: ${error.message}`); } } public static getRiskLevel(score: number): 'high' | 'medium' | 'low' | 'trusted' { if (score < ReputationThreshold.HIGH_RISK) { return 'high'; } else if (score < ReputationThreshold.MEDIUM_RISK) { return 'medium'; } else if (score < ReputationThreshold.LOW_RISK) { return 'low'; } else { return 'trusted'; } } public updateStorageManager(storageManager: any): void { this.storageManager = storageManager; logger.log('info', 'IPReputationChecker storage manager updated'); if (this.options.enableLocalCache && this.reputationCache.size > 0) { this.saveCache().catch(error => { logger.log('error', `Failed to save cache to new storage manager: ${error.message}`); }); } } }