512 lines
38 KiB
JavaScript
512 lines
38 KiB
JavaScript
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 { LRUCache } from 'lru-cache';
|
|
/**
|
|
* Reputation threshold scores
|
|
*/
|
|
export var ReputationThreshold;
|
|
(function (ReputationThreshold) {
|
|
ReputationThreshold[ReputationThreshold["HIGH_RISK"] = 20] = "HIGH_RISK";
|
|
ReputationThreshold[ReputationThreshold["MEDIUM_RISK"] = 50] = "MEDIUM_RISK";
|
|
ReputationThreshold[ReputationThreshold["LOW_RISK"] = 80] = "LOW_RISK"; // Score below this is considered low risk (but not trusted)
|
|
})(ReputationThreshold || (ReputationThreshold = {}));
|
|
/**
|
|
* IP type classifications
|
|
*/
|
|
export var IPType;
|
|
(function (IPType) {
|
|
IPType["RESIDENTIAL"] = "residential";
|
|
IPType["DATACENTER"] = "datacenter";
|
|
IPType["PROXY"] = "proxy";
|
|
IPType["TOR"] = "tor";
|
|
IPType["VPN"] = "vpn";
|
|
IPType["UNKNOWN"] = "unknown";
|
|
})(IPType || (IPType = {}));
|
|
/**
|
|
* Class for checking IP reputation of inbound email senders
|
|
*/
|
|
export class IPReputationChecker {
|
|
static instance;
|
|
reputationCache;
|
|
options;
|
|
storageManager; // StorageManager instance
|
|
// Default DNSBL servers
|
|
static DEFAULT_DNSBL_SERVERS = [
|
|
'zen.spamhaus.org', // Spamhaus
|
|
'bl.spamcop.net', // SpamCop
|
|
'b.barracudacentral.org', // Barracuda
|
|
'spam.dnsbl.sorbs.net', // SORBS
|
|
'dnsbl.sorbs.net', // SORBS (expanded)
|
|
'cbl.abuseat.org', // Composite Blocking List
|
|
'xbl.spamhaus.org', // Spamhaus XBL
|
|
'pbl.spamhaus.org', // Spamhaus PBL
|
|
'dnsbl-1.uceprotect.net', // UCEPROTECT
|
|
'psbl.surriel.com' // PSBL
|
|
];
|
|
// Default options
|
|
static DEFAULT_OPTIONS = {
|
|
maxCacheSize: 10000,
|
|
cacheTTL: 24 * 60 * 60 * 1000, // 24 hours
|
|
dnsblServers: IPReputationChecker.DEFAULT_DNSBL_SERVERS,
|
|
highRiskThreshold: ReputationThreshold.HIGH_RISK,
|
|
mediumRiskThreshold: ReputationThreshold.MEDIUM_RISK,
|
|
lowRiskThreshold: ReputationThreshold.LOW_RISK,
|
|
enableLocalCache: true,
|
|
enableDNSBL: true,
|
|
enableIPInfo: true
|
|
};
|
|
/**
|
|
* Constructor for IPReputationChecker
|
|
* @param options Configuration options
|
|
* @param storageManager Optional StorageManager instance for persistence
|
|
*/
|
|
constructor(options = {}, storageManager) {
|
|
// Merge with default options
|
|
this.options = {
|
|
...IPReputationChecker.DEFAULT_OPTIONS,
|
|
...options
|
|
};
|
|
this.storageManager = storageManager;
|
|
// If no storage manager provided, log warning
|
|
if (!storageManager && this.options.enableLocalCache) {
|
|
logger.log('warn', '⚠️ WARNING: IPReputationChecker initialized without StorageManager.\n' +
|
|
' IP reputation cache will only be stored to filesystem.\n' +
|
|
' Consider passing a StorageManager instance for better storage flexibility.');
|
|
}
|
|
// Initialize reputation cache
|
|
this.reputationCache = new LRUCache({
|
|
max: this.options.maxCacheSize,
|
|
ttl: this.options.cacheTTL, // Cache TTL
|
|
});
|
|
// Load cache from disk if enabled
|
|
if (this.options.enableLocalCache) {
|
|
// Fire and forget the load operation
|
|
this.loadCache().catch(error => {
|
|
logger.log('error', `Failed to load IP reputation cache during initialization: ${error.message}`);
|
|
});
|
|
}
|
|
}
|
|
/**
|
|
* Get the singleton instance of the checker
|
|
* @param options Configuration options
|
|
* @param storageManager Optional StorageManager instance for persistence
|
|
* @returns Singleton instance
|
|
*/
|
|
static getInstance(options = {}, storageManager) {
|
|
if (!IPReputationChecker.instance) {
|
|
IPReputationChecker.instance = new IPReputationChecker(options, storageManager);
|
|
}
|
|
return IPReputationChecker.instance;
|
|
}
|
|
/**
|
|
* Check an IP address's reputation
|
|
* @param ip IP address to check
|
|
* @returns Reputation check result
|
|
*/
|
|
async checkReputation(ip) {
|
|
try {
|
|
// Validate IP address format
|
|
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;
|
|
}
|
|
// Initialize empty result
|
|
const result = {
|
|
score: 100, // Start with perfect score
|
|
isSpam: false,
|
|
isProxy: false,
|
|
isTor: false,
|
|
isVPN: false,
|
|
timestamp: Date.now()
|
|
};
|
|
// Check IP against DNS blacklists if enabled
|
|
if (this.options.enableDNSBL) {
|
|
const dnsblResult = await this.checkDNSBL(ip);
|
|
// Update result with DNSBL information
|
|
result.score -= dnsblResult.listCount * 10; // Subtract 10 points per blacklist
|
|
result.isSpam = dnsblResult.listCount > 0;
|
|
result.blacklists = dnsblResult.lists;
|
|
}
|
|
// Get additional IP information if enabled
|
|
if (this.options.enableIPInfo) {
|
|
const ipInfo = await this.getIPInfo(ip);
|
|
// Update result with IP info
|
|
result.country = ipInfo.country;
|
|
result.asn = ipInfo.asn;
|
|
result.org = ipInfo.org;
|
|
// Adjust score based on IP type
|
|
if (ipInfo.type === IPType.PROXY || ipInfo.type === IPType.TOR || ipInfo.type === IPType.VPN) {
|
|
result.score -= 30; // Subtract 30 points for proxies, Tor, VPNs
|
|
// Set proxy flags
|
|
result.isProxy = ipInfo.type === IPType.PROXY;
|
|
result.isTor = ipInfo.type === IPType.TOR;
|
|
result.isVPN = ipInfo.type === IPType.VPN;
|
|
}
|
|
}
|
|
// Ensure score is between 0 and 100
|
|
result.score = Math.max(0, Math.min(100, result.score));
|
|
// Update cache with result
|
|
this.reputationCache.set(ip, result);
|
|
// Save cache if enabled
|
|
if (this.options.enableLocalCache) {
|
|
// Fire and forget the save operation
|
|
this.saveCache().catch(error => {
|
|
logger.log('error', `Failed to save IP reputation cache: ${error.message}`);
|
|
});
|
|
}
|
|
// Log the reputation check
|
|
this.logReputationCheck(ip, result);
|
|
return result;
|
|
}
|
|
catch (error) {
|
|
logger.log('error', `Error checking IP reputation for ${ip}: ${error.message}`, {
|
|
ip,
|
|
stack: error.stack
|
|
});
|
|
return this.createErrorResult(ip, error.message);
|
|
}
|
|
}
|
|
/**
|
|
* Check an IP against DNS blacklists
|
|
* @param ip IP address to check
|
|
* @returns DNSBL check results
|
|
*/
|
|
async checkDNSBL(ip) {
|
|
try {
|
|
// Reverse the IP for DNSBL queries
|
|
const reversedIP = this.reverseIP(ip);
|
|
const results = await Promise.allSettled(this.options.dnsblServers.map(async (server) => {
|
|
try {
|
|
const lookupDomain = `${reversedIP}.${server}`;
|
|
await plugins.dns.promises.resolve(lookupDomain);
|
|
return server; // IP is listed in this DNSBL
|
|
}
|
|
catch (error) {
|
|
if (error.code === 'ENOTFOUND') {
|
|
return null; // IP is not listed in this DNSBL
|
|
}
|
|
throw error; // Other error
|
|
}
|
|
}));
|
|
// Extract successful lookups (listed in DNSBL)
|
|
const lists = results
|
|
.filter((result) => result.status === 'fulfilled' && result.value !== null)
|
|
.map(result => result.value);
|
|
return {
|
|
listCount: lists.length,
|
|
lists
|
|
};
|
|
}
|
|
catch (error) {
|
|
logger.log('error', `Error checking DNSBL for ${ip}: ${error.message}`);
|
|
return {
|
|
listCount: 0,
|
|
lists: []
|
|
};
|
|
}
|
|
}
|
|
/**
|
|
* Get information about an IP address
|
|
* @param ip IP address to check
|
|
* @returns IP information
|
|
*/
|
|
async getIPInfo(ip) {
|
|
try {
|
|
// In a real implementation, this would use an IP data service API
|
|
// For this implementation, we'll use a simplified approach
|
|
// Check if it's a known Tor exit node (simplified)
|
|
const isTor = ip.startsWith('171.25.') || ip.startsWith('185.220.') || ip.startsWith('95.216.');
|
|
// Check if it's a known VPN (simplified)
|
|
const isVPN = ip.startsWith('185.156.') || ip.startsWith('37.120.');
|
|
// Check if it's a known proxy (simplified)
|
|
const isProxy = ip.startsWith('34.92.') || ip.startsWith('34.206.');
|
|
// Determine IP type
|
|
let type = IPType.UNKNOWN;
|
|
if (isTor) {
|
|
type = IPType.TOR;
|
|
}
|
|
else if (isVPN) {
|
|
type = IPType.VPN;
|
|
}
|
|
else if (isProxy) {
|
|
type = IPType.PROXY;
|
|
}
|
|
else {
|
|
// Simple datacenters detection (major cloud providers)
|
|
if (ip.startsWith('13.') || // AWS
|
|
ip.startsWith('35.') || // Google Cloud
|
|
ip.startsWith('52.') || // AWS
|
|
ip.startsWith('34.') || // Google Cloud
|
|
ip.startsWith('104.') // Various providers
|
|
) {
|
|
type = IPType.DATACENTER;
|
|
}
|
|
else {
|
|
type = IPType.RESIDENTIAL;
|
|
}
|
|
}
|
|
// Return the information
|
|
return {
|
|
country: this.determineCountry(ip), // Simplified, would use geolocation service
|
|
asn: 'AS12345', // Simplified, would look up real ASN
|
|
org: this.determineOrg(ip), // Simplified, would use real org data
|
|
type
|
|
};
|
|
}
|
|
catch (error) {
|
|
logger.log('error', `Error getting IP info for ${ip}: ${error.message}`);
|
|
return {
|
|
type: IPType.UNKNOWN
|
|
};
|
|
}
|
|
}
|
|
/**
|
|
* Simplified method to determine country from IP
|
|
* In a real implementation, this would use a geolocation database or service
|
|
* @param ip IP address
|
|
* @returns Country code
|
|
*/
|
|
determineCountry(ip) {
|
|
// Simplified mapping for demo purposes
|
|
if (ip.startsWith('13.') || ip.startsWith('52.'))
|
|
return 'US';
|
|
if (ip.startsWith('35.') || ip.startsWith('34.'))
|
|
return 'US';
|
|
if (ip.startsWith('185.'))
|
|
return 'NL';
|
|
if (ip.startsWith('171.'))
|
|
return 'DE';
|
|
return 'XX'; // Unknown
|
|
}
|
|
/**
|
|
* Simplified method to determine organization from IP
|
|
* In a real implementation, this would use an IP-to-org database or service
|
|
* @param ip IP address
|
|
* @returns Organization name
|
|
*/
|
|
determineOrg(ip) {
|
|
// Simplified mapping for demo purposes
|
|
if (ip.startsWith('13.') || ip.startsWith('52.'))
|
|
return 'Amazon AWS';
|
|
if (ip.startsWith('35.') || ip.startsWith('34.'))
|
|
return 'Google Cloud';
|
|
if (ip.startsWith('185.156.'))
|
|
return 'NordVPN';
|
|
if (ip.startsWith('37.120.'))
|
|
return 'ExpressVPN';
|
|
if (ip.startsWith('185.220.'))
|
|
return 'Tor Exit Node';
|
|
return 'Unknown';
|
|
}
|
|
/**
|
|
* Reverse an IP address for DNSBL lookups (e.g., 1.2.3.4 -> 4.3.2.1)
|
|
* @param ip IP address to reverse
|
|
* @returns Reversed IP for DNSBL queries
|
|
*/
|
|
reverseIP(ip) {
|
|
return ip.split('.').reverse().join('.');
|
|
}
|
|
/**
|
|
* Create an error result for when reputation check fails
|
|
* @param ip IP address
|
|
* @param errorMessage Error message
|
|
* @returns Error result
|
|
*/
|
|
createErrorResult(ip, errorMessage) {
|
|
return {
|
|
score: 50, // Neutral score for errors
|
|
isSpam: false,
|
|
isProxy: false,
|
|
isTor: false,
|
|
isVPN: false,
|
|
timestamp: Date.now(),
|
|
error: errorMessage
|
|
};
|
|
}
|
|
/**
|
|
* Validate IP address format
|
|
* @param ip IP address to validate
|
|
* @returns Whether the IP is valid
|
|
*/
|
|
isValidIPAddress(ip) {
|
|
// IPv4 regex pattern
|
|
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);
|
|
}
|
|
/**
|
|
* Log reputation check to security logger
|
|
* @param ip IP address
|
|
* @param result Reputation result
|
|
*/
|
|
logReputationCheck(ip, result) {
|
|
// Determine log level based on reputation score
|
|
let logLevel = SecurityLogLevel.INFO;
|
|
if (result.score < this.options.highRiskThreshold) {
|
|
logLevel = SecurityLogLevel.WARN;
|
|
}
|
|
else if (result.score < this.options.mediumRiskThreshold) {
|
|
logLevel = SecurityLogLevel.INFO;
|
|
}
|
|
// Log the check
|
|
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
|
|
});
|
|
}
|
|
/**
|
|
* Save cache to disk or storage manager
|
|
*/
|
|
async saveCache() {
|
|
try {
|
|
// Convert cache entries to serializable array
|
|
const entries = Array.from(this.reputationCache.entries()).map(([ip, data]) => ({
|
|
ip,
|
|
data
|
|
}));
|
|
// Only save if we have entries
|
|
if (entries.length === 0) {
|
|
return;
|
|
}
|
|
const cacheData = JSON.stringify(entries);
|
|
// Save to storage manager if available
|
|
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 {
|
|
// Fall back to filesystem
|
|
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}`);
|
|
}
|
|
}
|
|
/**
|
|
* Load cache from disk or storage manager
|
|
*/
|
|
async loadCache() {
|
|
try {
|
|
let cacheData = null;
|
|
let fromFilesystem = false;
|
|
// Try to load from storage manager first
|
|
if (this.storageManager) {
|
|
try {
|
|
cacheData = await this.storageManager.get('/security/ip-reputation-cache.json');
|
|
if (!cacheData) {
|
|
// Check if data exists in filesystem and migrate it
|
|
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;
|
|
// Migrate to storage manager
|
|
await this.storageManager.set('/security/ip-reputation-cache.json', cacheData);
|
|
logger.log('info', 'IP reputation cache migrated to StorageManager successfully');
|
|
// Optionally delete the old file after successful migration
|
|
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 {
|
|
// No storage manager, load from filesystem
|
|
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;
|
|
}
|
|
}
|
|
// Parse and restore cache if data was found
|
|
if (cacheData) {
|
|
const entries = JSON.parse(cacheData);
|
|
// Validate and filter entries
|
|
const now = Date.now();
|
|
const validEntries = entries.filter(entry => {
|
|
const age = now - entry.data.timestamp;
|
|
return age < this.options.cacheTTL; // Only load entries that haven't expired
|
|
});
|
|
// Restore cache
|
|
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}`);
|
|
}
|
|
}
|
|
/**
|
|
* Get the risk level for a reputation score
|
|
* @param score Reputation score (0-100)
|
|
* @returns Risk level description
|
|
*/
|
|
static getRiskLevel(score) {
|
|
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';
|
|
}
|
|
}
|
|
/**
|
|
* Update the storage manager after instantiation
|
|
* This is useful when the storage manager is not available at construction time
|
|
* @param storageManager The StorageManager instance to use
|
|
*/
|
|
updateStorageManager(storageManager) {
|
|
this.storageManager = storageManager;
|
|
logger.log('info', 'IPReputationChecker storage manager updated');
|
|
// If cache is enabled and we have entries, save them to the new storage manager
|
|
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}`);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xhc3Nlcy5pcHJlcHV0YXRpb25jaGVja2VyLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vdHMvc2VjdXJpdHkvY2xhc3Nlcy5pcHJlcHV0YXRpb25jaGVja2VyLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sS0FBSyxPQUFPLE1BQU0sZUFBZSxDQUFDO0FBQ3pDLE9BQU8sS0FBSyxLQUFLLE1BQU0sYUFBYSxDQUFDO0FBQ3JDLE9BQU8sRUFBRSxNQUFNLEVBQUUsTUFBTSxjQUFjLENBQUM7QUFDdEMsT0FBTyxFQUFFLGNBQWMsRUFBRSxnQkFBZ0IsRUFBRSxpQkFBaUIsRUFBRSxNQUFNLDZCQUE2QixDQUFDO0FBQ2xHLE9BQU8sRUFBRSxRQUFRLEVBQUUsTUFBTSxXQUFXLENBQUM7QUFtQnJDOztHQUVHO0FBQ0gsTUFBTSxDQUFOLElBQVksbUJBSVg7QUFKRCxXQUFZLG1CQUFtQjtJQUM3Qix3RUFBYyxDQUFBO0lBQ2QsNEVBQWdCLENBQUE7SUFDaEIsc0VBQWEsQ0FBQSxDQUFRLDREQUE0RDtBQUNuRixDQUFDLEVBSlcsbUJBQW1CLEtBQW5CLG1CQUFtQixRQUk5QjtBQUVEOztHQUVHO0FBQ0gsTUFBTSxDQUFOLElBQVksTUFPWDtBQVBELFdBQVksTUFBTTtJQUNoQixxQ0FBMkIsQ0FBQTtJQUMzQixtQ0FBeUIsQ0FBQTtJQUN6Qix5QkFBZSxDQUFBO0lBQ2YscUJBQVcsQ0FBQTtJQUNYLHFCQUFXLENBQUE7SUFDWCw2QkFBbUIsQ0FBQTtBQUNyQixDQUFDLEVBUFcsTUFBTSxLQUFOLE1BQU0sUUFPakI7QUFpQkQ7O0dBRUc7QUFDSCxNQUFNLE9BQU8sbUJBQW1CO0lBQ3RCLE1BQU0sQ0FBQyxRQUFRLENBQXNCO0lBQ3JDLGVBQWUsQ0FBc0M7SUFDckQsT0FBTyxDQUFpQztJQUN4QyxjQUFjLENBQU8sQ0FBQywwQkFBMEI7SUFFeEQsd0JBQXdCO0lBQ2hCLE1BQU0sQ0FBVSxxQkFBcUIsR0FBRztRQUM5QyxrQkFB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
|