263 lines
22 KiB
JavaScript
263 lines
22 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 { RustSecurityBridge } from './classes.rustsecuritybridge.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 = {}));
|
|
/**
|
|
* IP reputation checker — delegates DNSBL lookups to the Rust security bridge.
|
|
* Retains LRU caching and disk persistence in TypeScript.
|
|
*/
|
|
export class IPReputationChecker {
|
|
static instance;
|
|
reputationCache;
|
|
options;
|
|
storageManager;
|
|
static DEFAULT_OPTIONS = {
|
|
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 = {}, storageManager) {
|
|
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}`);
|
|
});
|
|
}
|
|
}
|
|
static getInstance(options = {}, storageManager) {
|
|
if (!IPReputationChecker.instance) {
|
|
IPReputationChecker.instance = new IPReputationChecker(options, storageManager);
|
|
}
|
|
return IPReputationChecker.instance;
|
|
}
|
|
/**
|
|
* Check an IP address's reputation via the Rust bridge
|
|
*/
|
|
async checkReputation(ip) {
|
|
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 = {
|
|
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;
|
|
}
|
|
}
|
|
createErrorResult(ip, errorMessage) {
|
|
return {
|
|
score: 50,
|
|
isSpam: false,
|
|
isProxy: false,
|
|
isTor: false,
|
|
isVPN: false,
|
|
timestamp: Date.now(),
|
|
error: errorMessage
|
|
};
|
|
}
|
|
isValidIPAddress(ip) {
|
|
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);
|
|
}
|
|
logReputationCheck(ip, result) {
|
|
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
|
|
});
|
|
}
|
|
async saveCache() {
|
|
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}`);
|
|
}
|
|
}
|
|
async loadCache() {
|
|
try {
|
|
let cacheData = 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}`);
|
|
}
|
|
}
|
|
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';
|
|
}
|
|
}
|
|
updateStorageManager(storageManager) {
|
|
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}`);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xhc3Nlcy5pcHJlcHV0YXRpb25jaGVja2VyLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vdHMvc2VjdXJpdHkvY2xhc3Nlcy5pcHJlcHV0YXRpb25jaGVja2VyLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sS0FBSyxPQUFPLE1BQU0sZUFBZSxDQUFDO0FBQ3pDLE9BQU8sS0FBSyxLQUFLLE1BQU0sYUFBYSxDQUFDO0FBQ3JDLE9BQU8sRUFBRSxNQUFNLEVBQUUsTUFBTSxjQUFjLENBQUM7QUFDdEMsT0FBTyxFQUFFLGNBQWMsRUFBRSxnQkFBZ0IsRUFBRSxpQkFBaUIsRUFBRSxNQUFNLDZCQUE2QixDQUFDO0FBQ2xHLE9BQU8sRUFBRSxrQkFBa0IsRUFBRSxNQUFNLGlDQUFpQyxDQUFDO0FBQ3JFLE9BQU8sRUFBRSxRQUFRLEVBQUUsTUFBTSxXQUFXLENBQUM7QUFtQnJDOztHQUVHO0FBQ0gsTUFBTSxDQUFOLElBQVksbUJBSVg7QUFKRCxXQUFZLG1CQUFtQjtJQUM3Qix3RUFBYyxDQUFBO0lBQ2QsNEVBQWdCLENBQUE7SUFDaEIsc0VBQWEsQ0FBQSxDQUFRLDREQUE0RDtBQUNuRixDQUFDLEVBSlcsbUJBQW1CLEtBQW5CLG1CQUFtQixRQUk5QjtBQUVEOztHQUVHO0FBQ0gsTUFBTSxDQUFOLElBQVksTUFPWDtBQVBELFdBQVksTUFBTTtJQUNoQixxQ0FBMkIsQ0FBQTtJQUMzQixtQ0FBeUIsQ0FBQTtJQUN6Qix5QkFBZSxDQUFBO0lBQ2YscUJBQVcsQ0FBQTtJQUNYLHFCQUFXLENBQUE7SUFDWCw2QkFBbUIsQ0FBQTtBQUNyQixDQUFDLEVBUFcsTUFBTSxLQUFOLE1BQU0sUUFPakI7QUFpQkQ7OztHQUdHO0FBQ0gsTUFBTSxPQUFPLG1CQUFtQjtJQUN0QixNQUFNLENBQUMsUUFBUSxDQUFzQjtJQUNyQyxlQUFlLENBQXNDO0lBQ3JELE9BQU8sQ0FBaUM7SUFDeEMsY0FBYyxDQUFPO0lBRXJCLE1BQU0sQ0FBVSxl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
|