2026-02-10 15:31:31 +00:00
|
|
|
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';
|
2026-02-10 16:25:55 +00:00
|
|
|
import { RustSecurityBridge } from './classes.rustsecuritybridge.js';
|
2025-10-28 20:27:00 +00:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-02-10 20:30:43 +00:00
|
|
|
* IP reputation checker — delegates DNSBL lookups to the Rust security bridge.
|
|
|
|
|
* Retains LRU caching and disk persistence in TypeScript.
|
2025-10-28 20:27:00 +00:00
|
|
|
*/
|
|
|
|
|
export class IPReputationChecker {
|
|
|
|
|
private static instance: IPReputationChecker;
|
|
|
|
|
private reputationCache: LRUCache<string, IReputationResult>;
|
|
|
|
|
private options: Required<IIPReputationOptions>;
|
2026-02-10 20:30:43 +00:00
|
|
|
private storageManager?: any;
|
|
|
|
|
|
2025-10-28 20:27:00 +00:00
|
|
|
private static readonly DEFAULT_OPTIONS: Required<IIPReputationOptions> = {
|
|
|
|
|
maxCacheSize: 10000,
|
2026-02-10 20:30:43 +00:00
|
|
|
cacheTTL: 24 * 60 * 60 * 1000,
|
|
|
|
|
dnsblServers: [],
|
2025-10-28 20:27:00 +00:00
|
|
|
highRiskThreshold: ReputationThreshold.HIGH_RISK,
|
|
|
|
|
mediumRiskThreshold: ReputationThreshold.MEDIUM_RISK,
|
|
|
|
|
lowRiskThreshold: ReputationThreshold.LOW_RISK,
|
|
|
|
|
enableLocalCache: true,
|
|
|
|
|
enableDNSBL: true,
|
|
|
|
|
enableIPInfo: true
|
|
|
|
|
};
|
2026-02-10 20:30:43 +00:00
|
|
|
|
2025-10-28 20:27:00 +00:00
|
|
|
constructor(options: IIPReputationOptions = {}, storageManager?: any) {
|
|
|
|
|
this.options = {
|
|
|
|
|
...IPReputationChecker.DEFAULT_OPTIONS,
|
|
|
|
|
...options
|
|
|
|
|
};
|
2026-02-10 20:30:43 +00:00
|
|
|
|
2025-10-28 20:27:00 +00:00
|
|
|
this.storageManager = storageManager;
|
2026-02-10 20:30:43 +00:00
|
|
|
|
2025-10-28 20:27:00 +00:00
|
|
|
this.reputationCache = new LRUCache<string, IReputationResult>({
|
|
|
|
|
max: this.options.maxCacheSize,
|
2026-02-10 20:30:43 +00:00
|
|
|
ttl: this.options.cacheTTL,
|
2025-10-28 20:27:00 +00:00
|
|
|
});
|
2026-02-10 20:30:43 +00:00
|
|
|
|
2025-10-28 20:27:00 +00:00
|
|
|
if (this.options.enableLocalCache) {
|
|
|
|
|
this.loadCache().catch(error => {
|
|
|
|
|
logger.log('error', `Failed to load IP reputation cache during initialization: ${error.message}`);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-10 20:30:43 +00:00
|
|
|
|
2025-10-28 20:27:00 +00:00
|
|
|
public static getInstance(options: IIPReputationOptions = {}, storageManager?: any): IPReputationChecker {
|
|
|
|
|
if (!IPReputationChecker.instance) {
|
|
|
|
|
IPReputationChecker.instance = new IPReputationChecker(options, storageManager);
|
|
|
|
|
}
|
|
|
|
|
return IPReputationChecker.instance;
|
|
|
|
|
}
|
2026-02-10 20:30:43 +00:00
|
|
|
|
2025-10-28 20:27:00 +00:00
|
|
|
/**
|
2026-02-10 20:30:43 +00:00
|
|
|
* Check an IP address's reputation via the Rust bridge
|
2025-10-28 20:27:00 +00:00
|
|
|
*/
|
|
|
|
|
public async checkReputation(ip: string): Promise<IReputationResult> {
|
|
|
|
|
try {
|
|
|
|
|
if (!this.isValidIPAddress(ip)) {
|
|
|
|
|
logger.log('warn', `Invalid IP address format: ${ip}`);
|
|
|
|
|
return this.createErrorResult(ip, 'Invalid IP address format');
|
|
|
|
|
}
|
2026-02-10 16:25:55 +00:00
|
|
|
|
2025-10-28 20:27:00 +00:00
|
|
|
// 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;
|
|
|
|
|
}
|
2026-02-10 16:25:55 +00:00
|
|
|
|
2026-02-10 20:30:43 +00:00
|
|
|
// Delegate to Rust bridge
|
2026-02-10 16:25:55 +00:00
|
|
|
const bridge = RustSecurityBridge.getInstance();
|
2026-02-10 20:30:43 +00:00
|
|
|
const rustResult = await bridge.checkIpReputation(ip);
|
2026-02-10 16:25:55 +00:00
|
|
|
|
2025-10-28 20:27:00 +00:00
|
|
|
const result: IReputationResult = {
|
2026-02-10 20:30:43 +00:00
|
|
|
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(),
|
2025-10-28 20:27:00 +00:00
|
|
|
};
|
2026-02-10 16:25:55 +00:00
|
|
|
|
2025-10-28 20:27:00 +00:00
|
|
|
this.reputationCache.set(ip, result);
|
2026-02-10 16:25:55 +00:00
|
|
|
|
2025-10-28 20:27:00 +00:00
|
|
|
if (this.options.enableLocalCache) {
|
|
|
|
|
this.saveCache().catch(error => {
|
|
|
|
|
logger.log('error', `Failed to save IP reputation cache: ${error.message}`);
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-02-10 16:25:55 +00:00
|
|
|
|
2025-10-28 20:27:00 +00:00
|
|
|
this.logReputationCheck(ip, result);
|
|
|
|
|
return result;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.log('error', `Error checking IP reputation for ${ip}: ${error.message}`, {
|
|
|
|
|
ip,
|
|
|
|
|
stack: error.stack
|
|
|
|
|
});
|
2026-02-10 20:30:43 +00:00
|
|
|
const errorResult = this.createErrorResult(ip, error.message);
|
|
|
|
|
// Cache error results to avoid repeated failing lookups
|
|
|
|
|
this.reputationCache.set(ip, errorResult);
|
|
|
|
|
return errorResult;
|
2025-10-28 20:27:00 +00:00
|
|
|
}
|
|
|
|
|
}
|
2026-02-10 20:30:43 +00:00
|
|
|
|
2025-10-28 20:27:00 +00:00
|
|
|
private createErrorResult(ip: string, errorMessage: string): IReputationResult {
|
|
|
|
|
return {
|
2026-02-10 20:30:43 +00:00
|
|
|
score: 50,
|
2025-10-28 20:27:00 +00:00
|
|
|
isSpam: false,
|
|
|
|
|
isProxy: false,
|
|
|
|
|
isTor: false,
|
|
|
|
|
isVPN: false,
|
|
|
|
|
timestamp: Date.now(),
|
|
|
|
|
error: errorMessage
|
|
|
|
|
};
|
|
|
|
|
}
|
2026-02-10 20:30:43 +00:00
|
|
|
|
2025-10-28 20:27:00 +00:00
|
|
|
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);
|
|
|
|
|
}
|
2026-02-10 20:30:43 +00:00
|
|
|
|
2025-10-28 20:27:00 +00:00
|
|
|
private logReputationCheck(ip: string, result: IReputationResult): void {
|
|
|
|
|
let logLevel = SecurityLogLevel.INFO;
|
|
|
|
|
if (result.score < this.options.highRiskThreshold) {
|
|
|
|
|
logLevel = SecurityLogLevel.WARN;
|
|
|
|
|
}
|
2026-02-10 20:30:43 +00:00
|
|
|
|
2025-10-28 20:27:00 +00:00
|
|
|
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
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-02-10 20:30:43 +00:00
|
|
|
|
2025-10-28 20:27:00 +00:00
|
|
|
private async saveCache(): Promise<void> {
|
|
|
|
|
try {
|
|
|
|
|
const entries = Array.from(this.reputationCache.entries()).map(([ip, data]) => ({
|
|
|
|
|
ip,
|
|
|
|
|
data
|
|
|
|
|
}));
|
2026-02-10 20:30:43 +00:00
|
|
|
|
2025-10-28 20:27:00 +00:00
|
|
|
if (entries.length === 0) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-02-10 20:30:43 +00:00
|
|
|
|
2025-10-28 20:27:00 +00:00
|
|
|
const cacheData = JSON.stringify(entries);
|
2026-02-10 20:30:43 +00:00
|
|
|
|
2025-10-28 20:27:00 +00:00
|
|
|
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');
|
2026-02-10 15:31:31 +00:00
|
|
|
await plugins.smartfs.directory(cacheDir).recursive().create();
|
2025-10-28 20:27:00 +00:00
|
|
|
const cacheFile = plugins.path.join(cacheDir, 'ip_reputation_cache.json');
|
2026-02-10 15:31:31 +00:00
|
|
|
await plugins.smartfs.file(cacheFile).write(cacheData);
|
2025-10-28 20:27:00 +00:00
|
|
|
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}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-10 20:30:43 +00:00
|
|
|
|
2025-10-28 20:27:00 +00:00
|
|
|
private async loadCache(): Promise<void> {
|
|
|
|
|
try {
|
|
|
|
|
let cacheData: string | null = null;
|
|
|
|
|
let fromFilesystem = false;
|
2026-02-10 20:30:43 +00:00
|
|
|
|
2025-10-28 20:27:00 +00:00
|
|
|
if (this.storageManager) {
|
|
|
|
|
try {
|
|
|
|
|
cacheData = await this.storageManager.get('/security/ip-reputation-cache.json');
|
2026-02-10 20:30:43 +00:00
|
|
|
|
2025-10-28 20:27:00 +00:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-10 20:30:43 +00:00
|
|
|
|
2025-10-28 20:27:00 +00:00
|
|
|
if (cacheData) {
|
|
|
|
|
const entries = JSON.parse(cacheData);
|
|
|
|
|
const now = Date.now();
|
|
|
|
|
const validEntries = entries.filter(entry => {
|
|
|
|
|
const age = now - entry.data.timestamp;
|
2026-02-10 20:30:43 +00:00
|
|
|
return age < this.options.cacheTTL;
|
2025-10-28 20:27:00 +00:00
|
|
|
});
|
2026-02-10 20:30:43 +00:00
|
|
|
|
2025-10-28 20:27:00 +00:00
|
|
|
for (const entry of validEntries) {
|
|
|
|
|
this.reputationCache.set(entry.ip, entry.data);
|
|
|
|
|
}
|
2026-02-10 20:30:43 +00:00
|
|
|
|
2025-10-28 20:27:00 +00:00
|
|
|
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}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-10 20:30:43 +00:00
|
|
|
|
2025-10-28 20:27:00 +00:00
|
|
|
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';
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-10 20:30:43 +00:00
|
|
|
|
2025-10-28 20:27:00 +00:00
|
|
|
public updateStorageManager(storageManager: any): void {
|
|
|
|
|
this.storageManager = storageManager;
|
|
|
|
|
logger.log('info', 'IPReputationChecker storage manager updated');
|
2026-02-10 20:30:43 +00:00
|
|
|
|
2025-10-28 20:27:00 +00:00
|
|
|
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}`);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-10 20:30:43 +00:00
|
|
|
}
|