Files
smartmta/dist_ts/security/classes.contentscanner.js

338 lines
24 KiB
JavaScript

import * as plugins from '../plugins.js';
import * as paths from '../paths.js';
import { logger } from '../logger.js';
import { Email } from '../mail/core/classes.email.js';
import { SecurityLogger, SecurityLogLevel, SecurityEventType } from './classes.securitylogger.js';
import { RustSecurityBridge } from './classes.rustsecuritybridge.js';
import { LRUCache } from 'lru-cache';
/**
* Threat categories
*/
export var ThreatCategory;
(function (ThreatCategory) {
ThreatCategory["SPAM"] = "spam";
ThreatCategory["PHISHING"] = "phishing";
ThreatCategory["MALWARE"] = "malware";
ThreatCategory["EXECUTABLE"] = "executable";
ThreatCategory["SUSPICIOUS_LINK"] = "suspicious_link";
ThreatCategory["MALICIOUS_MACRO"] = "malicious_macro";
ThreatCategory["XSS"] = "xss";
ThreatCategory["SENSITIVE_DATA"] = "sensitive_data";
ThreatCategory["BLACKLISTED_CONTENT"] = "blacklisted_content";
ThreatCategory["CUSTOM_RULE"] = "custom_rule";
})(ThreatCategory || (ThreatCategory = {}));
/**
* Content Scanner for detecting malicious email content
*/
export class ContentScanner {
static instance;
scanCache;
options;
/**
* Default options for the content scanner
*/
static DEFAULT_OPTIONS = {
maxCacheSize: 10000,
cacheTTL: 24 * 60 * 60 * 1000, // 24 hours
scanSubject: true,
scanBody: true,
scanAttachments: true,
maxAttachmentSizeToScan: 10 * 1024 * 1024, // 10MB
scanAttachmentNames: true,
blockExecutables: true,
blockMacros: true,
customRules: [],
minThreatScore: 30, // Minimum score to consider content as a threat
highThreatScore: 70 // Score above which content is considered high threat
};
/**
* Constructor for the ContentScanner
* @param options Configuration options
*/
constructor(options = {}) {
// Merge with default options
this.options = {
...ContentScanner.DEFAULT_OPTIONS,
...options
};
// Initialize cache
this.scanCache = new LRUCache({
max: this.options.maxCacheSize,
ttl: this.options.cacheTTL,
});
logger.log('info', 'ContentScanner initialized');
}
/**
* Get the singleton instance of the scanner
* @param options Configuration options
* @returns Singleton scanner instance
*/
static getInstance(options = {}) {
if (!ContentScanner.instance) {
ContentScanner.instance = new ContentScanner(options);
}
return ContentScanner.instance;
}
/**
* Scan an email for malicious content.
* Delegates text/subject/html/filename pattern scanning to Rust.
* Binary attachment scanning (PE headers, VBA macros) stays in TS.
* @param email The email to scan
* @returns Scan result
*/
async scanEmail(email) {
try {
// Generate a cache key from the email
const cacheKey = this.generateCacheKey(email);
// Check cache first
const cachedResult = this.scanCache.get(cacheKey);
if (cachedResult) {
logger.log('info', `Using cached scan result for email ${email.getMessageId()}`);
return cachedResult;
}
// Delegate text/subject/html/filename scanning to Rust
const bridge = RustSecurityBridge.getInstance();
const rustResult = await bridge.scanContent({
subject: this.options.scanSubject ? email.subject : undefined,
textBody: this.options.scanBody ? email.text : undefined,
htmlBody: this.options.scanBody ? email.html : undefined,
attachmentNames: this.options.scanAttachmentNames
? email.attachments?.map(a => a.filename) ?? []
: [],
});
const result = {
isClean: true,
threatScore: rustResult.threatScore,
threatType: rustResult.threatType ?? undefined,
threatDetails: rustResult.threatDetails ?? undefined,
scannedElements: rustResult.scannedElements,
timestamp: Date.now(),
};
// Attachment binary scanning stays in TS (PE headers, macro detection)
if (this.options.scanAttachments && email.attachments?.length > 0) {
for (const attachment of email.attachments) {
this.scanAttachmentBinary(attachment, result);
}
}
// Apply custom rules (TS-only, runtime-configured)
this.applyCustomRules(email, result);
// Determine if the email is clean based on threat score
result.isClean = result.threatScore < this.options.minThreatScore;
// Save to cache
this.scanCache.set(cacheKey, result);
// Log high threat findings
if (result.threatScore >= this.options.highThreatScore) {
this.logHighThreatFound(email, result);
}
else if (!result.isClean) {
this.logThreatFound(email, result);
}
return result;
}
catch (error) {
logger.log('error', `Error scanning email: ${error.message}`, {
messageId: email.getMessageId(),
error: error.stack
});
// Return a safe default with error indication
return {
isClean: true,
threatScore: 0,
scannedElements: ['error'],
timestamp: Date.now(),
threatType: 'scan_error',
threatDetails: `Scan error: ${error.message}`
};
}
}
/**
* Generate a cache key from an email
* @param email The email to generate a key for
* @returns Cache key
*/
generateCacheKey(email) {
// Use message ID if available
if (email.getMessageId()) {
return `email:${email.getMessageId()}`;
}
// Fallback to a hash of key content
const contentToHash = [
email.from,
email.subject || '',
email.text?.substring(0, 1000) || '',
email.html?.substring(0, 1000) || '',
email.attachments?.length || 0
].join(':');
return `email:${plugins.crypto.createHash('sha256').update(contentToHash).digest('hex')}`;
}
/**
* Scan attachment binary content for PE headers and VBA macros.
* This stays in TS because it accesses raw Buffer data (too large for IPC).
* @param attachment The attachment to scan
* @param result The scan result to update
*/
scanAttachmentBinary(attachment, result) {
if (!attachment.content) {
return;
}
// Skip large attachments
if (attachment.content.length > this.options.maxAttachmentSizeToScan) {
return;
}
const filename = attachment.filename.toLowerCase();
// Check for PE headers (Windows executables disguised with non-.exe extensions)
if (attachment.content.length > 64 &&
attachment.content[0] === 0x4D &&
attachment.content[1] === 0x5A) { // 'MZ' header
result.threatScore += 80;
result.threatType = ThreatCategory.EXECUTABLE;
result.threatDetails = `Attachment contains executable code: ${filename}`;
return;
}
// Check for VBA macro indicators in Office documents
if (this.options.blockMacros && this.likelyContainsMacros(attachment)) {
result.threatScore += 60;
result.threatType = ThreatCategory.MALICIOUS_MACRO;
result.threatDetails = `Attachment appears to contain macros: ${filename}`;
}
}
/**
* Apply custom rules (runtime-configured patterns) to the email.
* These stay in TS because they are configured at runtime.
* @param email The email to check
* @param result The scan result to update
*/
applyCustomRules(email, result) {
if (!this.options.customRules.length) {
return;
}
const textsToCheck = [];
if (email.subject)
textsToCheck.push(email.subject);
if (email.text)
textsToCheck.push(email.text);
if (email.html)
textsToCheck.push(email.html);
for (const rule of this.options.customRules) {
const pattern = rule.pattern instanceof RegExp ? rule.pattern : new RegExp(rule.pattern, 'i');
for (const text of textsToCheck) {
if (pattern.test(text)) {
result.threatScore += rule.score;
result.threatType = rule.type;
result.threatDetails = rule.description;
return;
}
}
}
}
/**
* Extract text from a binary buffer for scanning
* @param buffer Binary content
* @returns Extracted text (may be partial)
*/
extractTextFromBuffer(buffer) {
try {
// Limit the amount we convert to avoid memory issues
const sampleSize = Math.min(buffer.length, 100 * 1024); // 100KB max sample
const sample = buffer.slice(0, sampleSize);
// Try to convert to string, filtering out non-printable chars
return sample.toString('utf8')
.replace(/[\x00-\x09\x0B-\x1F\x7F-\x9F]/g, '') // Remove control chars
.replace(/\uFFFD/g, ''); // Remove replacement char
}
catch (error) {
logger.log('warn', `Error extracting text from buffer: ${error.message}`);
return '';
}
}
/**
* Check if an Office document likely contains macros
* @param attachment The attachment to check
* @returns Whether the file likely contains macros
*/
likelyContainsMacros(attachment) {
const content = this.extractTextFromBuffer(attachment.content);
const macroIndicators = [
/vbaProject\.bin/i,
/Microsoft VBA/i,
/\bVBA\b/,
/Auto_Open/i,
/AutoExec/i,
/DocumentOpen/i,
/AutoOpen/i,
/\bExecute\(/i,
/\bShell\(/i,
/\bCreateObject\(/i
];
for (const indicator of macroIndicators) {
if (indicator.test(content)) {
return true;
}
}
return false;
}
/**
* Log a high threat finding to the security logger
* @param email The email containing the threat
* @param result The scan result
*/
logHighThreatFound(email, result) {
SecurityLogger.getInstance().logEvent({
level: SecurityLogLevel.ERROR,
type: SecurityEventType.MALWARE,
message: `High threat content detected in email from ${email.from} to ${email.to.join(', ')}`,
details: {
messageId: email.getMessageId(),
threatType: result.threatType,
threatDetails: result.threatDetails,
threatScore: result.threatScore,
scannedElements: result.scannedElements,
subject: email.subject
},
success: false,
domain: email.getFromDomain()
});
}
/**
* Log a threat finding to the security logger
* @param email The email containing the threat
* @param result The scan result
*/
logThreatFound(email, result) {
SecurityLogger.getInstance().logEvent({
level: SecurityLogLevel.WARN,
type: SecurityEventType.SPAM,
message: `Suspicious content detected in email from ${email.from} to ${email.to.join(', ')}`,
details: {
messageId: email.getMessageId(),
threatType: result.threatType,
threatDetails: result.threatDetails,
threatScore: result.threatScore,
scannedElements: result.scannedElements,
subject: email.subject
},
success: false,
domain: email.getFromDomain()
});
}
/**
* Get threat level description based on score
* @param score Threat score
* @returns Threat level description
*/
static getThreatLevel(score) {
if (score < 20) {
return 'none';
}
else if (score < 40) {
return 'low';
}
else if (score < 70) {
return 'medium';
}
else {
return 'high';
}
}
}
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xhc3Nlcy5jb250ZW50c2Nhbm5lci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3RzL3NlY3VyaXR5L2NsYXNzZXMuY29udGVudHNjYW5uZXIudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxLQUFLLE9BQU8sTUFBTSxlQUFlLENBQUM7QUFDekMsT0FBTyxLQUFLLEtBQUssTUFBTSxhQUFhLENBQUM7QUFDckMsT0FBTyxFQUFFLE1BQU0sRUFBRSxNQUFNLGNBQWMsQ0FBQztBQUN0QyxPQUFPLEVBQUUsS0FBSyxFQUFFLE1BQU0sK0JBQStCLENBQUM7QUFFdEQsT0FBTyxFQUFFLGNBQWMsRUFBRSxnQkFBZ0IsRUFBRSxpQkFBaUIsRUFBRSxNQUFNLDZCQUE2QixDQUFDO0FBQ2xHLE9BQU8sRUFBRSxrQkFBa0IsRUFBRSxNQUFNLGlDQUFpQyxDQUFDO0FBQ3JFLE9BQU8sRUFBRSxRQUFRLEVBQUUsTUFBTSxXQUFXLENBQUM7QUFxQ3JDOztHQUVHO0FBQ0gsTUFBTSxDQUFOLElBQVksY0FXWDtBQVhELFdBQVksY0FBYztJQUN4QiwrQkFBYSxDQUFBO0lBQ2IsdUNBQXFCLENBQUE7SUFDckIscUNBQW1CLENBQUE7SUFDbkIsMkNBQXlCLENBQUE7SUFDekIscURBQW1DLENBQUE7SUFDbkMscURBQW1DLENBQUE7SUFDbkMsNkJBQVcsQ0FBQTtJQUNYLG1EQUFpQyxDQUFBO0lBQ2pDLDZEQUEyQyxDQUFBO0lBQzNDLDZDQUEyQixDQUFBO0FBQzdCLENBQUMsRUFYVyxjQUFjLEtBQWQsY0FBYyxRQVd6QjtBQUVEOztHQUVHO0FBQ0gsTUFBTSxPQUFPLGNBQWM7SUFDakIsTUFBTSxDQUFDLFFBQVEsQ0FBaUI7SUFDaEMsU0FBUyxDQUFnQztJQUN6QyxPQUFPLENBQW1DO0lBRWxEOztPQUVHO0lBQ0ssTUFBTSxDQUFVLGVBQWUsR0FBcUM7UUFDMUUsWUFBWSxFQUFFLEtBQUs7UUFDbkIsUUFBUSxFQUFFLEVBQUUsR0FBRyxFQUFF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