637 lines
49 KiB
JavaScript
637 lines
49 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 { 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;
|
||
|
|
// Predefined patterns for common threats
|
||
|
|
static MALICIOUS_PATTERNS = {
|
||
|
|
// Phishing patterns
|
||
|
|
phishing: [
|
||
|
|
/(?:verify|confirm|update|login).*(?:account|password|details)/i,
|
||
|
|
/urgent.*(?:action|attention|required)/i,
|
||
|
|
/(?:paypal|apple|microsoft|amazon|google|bank).*(?:verify|confirm|suspend)/i,
|
||
|
|
/your.*(?:account).*(?:suspended|compromised|locked)/i,
|
||
|
|
/\b(?:password reset|security alert|security notice)\b/i
|
||
|
|
],
|
||
|
|
// Spam indicators
|
||
|
|
spam: [
|
||
|
|
/\b(?:viagra|cialis|enlargement|diet pill|lose weight fast|cheap meds)\b/i,
|
||
|
|
/\b(?:million dollars|lottery winner|prize claim|inheritance|rich widow)\b/i,
|
||
|
|
/\b(?:earn from home|make money fast|earn \$\d{3,}\/day)\b/i,
|
||
|
|
/\b(?:limited time offer|act now|exclusive deal|only \d+ left)\b/i,
|
||
|
|
/\b(?:forex|stock tip|investment opportunity|cryptocurrency|bitcoin)\b/i
|
||
|
|
],
|
||
|
|
// Malware indicators in text
|
||
|
|
malware: [
|
||
|
|
/(?:attached file|see attachment).*(?:invoice|receipt|statement|document)/i,
|
||
|
|
/open.*(?:the attached|this attachment)/i,
|
||
|
|
/(?:enable|allow).*(?:macros|content|editing)/i,
|
||
|
|
/download.*(?:attachment|file|document)/i,
|
||
|
|
/\b(?:ransomware protection|virus alert|malware detected)\b/i
|
||
|
|
],
|
||
|
|
// Suspicious links
|
||
|
|
suspiciousLinks: [
|
||
|
|
/https?:\/\/bit\.ly\//i,
|
||
|
|
/https?:\/\/goo\.gl\//i,
|
||
|
|
/https?:\/\/t\.co\//i,
|
||
|
|
/https?:\/\/tinyurl\.com\//i,
|
||
|
|
/https?:\/\/(?:\d{1,3}\.){3}\d{1,3}/i, // IP address URLs
|
||
|
|
/https?:\/\/.*\.(?:xyz|top|club|gq|cf)\//i, // Suspicious TLDs
|
||
|
|
/(?:login|account|signin|auth).*\.(?!gov|edu|com|org|net)\w+\.\w+/i, // Login pages on unusual domains
|
||
|
|
],
|
||
|
|
// XSS and script injection
|
||
|
|
scriptInjection: [
|
||
|
|
/<script.*>.*<\/script>/is,
|
||
|
|
/javascript:/i,
|
||
|
|
/on(?:click|load|mouse|error|focus|blur)=".*"/i,
|
||
|
|
/document\.(?:cookie|write|location)/i,
|
||
|
|
/eval\s*\(/i
|
||
|
|
],
|
||
|
|
// Sensitive data patterns
|
||
|
|
sensitiveData: [
|
||
|
|
/\b(?:\d{3}-\d{2}-\d{4}|\d{9})\b/, // SSN
|
||
|
|
/\b\d{13,16}\b/, // Credit card numbers
|
||
|
|
/\b(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{4})\b/ // Possible Base64
|
||
|
|
]
|
||
|
|
};
|
||
|
|
// Common executable extensions
|
||
|
|
static EXECUTABLE_EXTENSIONS = [
|
||
|
|
'.exe', '.dll', '.bat', '.cmd', '.msi', '.ts', '.vbs', '.ps1',
|
||
|
|
'.sh', '.jar', '.py', '.com', '.scr', '.pif', '.hta', '.cpl',
|
||
|
|
'.reg', '.vba', '.lnk', '.wsf', '.msi', '.msp', '.mst'
|
||
|
|
];
|
||
|
|
// Document formats that may contain macros
|
||
|
|
static MACRO_DOCUMENT_EXTENSIONS = [
|
||
|
|
'.doc', '.docm', '.xls', '.xlsm', '.ppt', '.pptm', '.dotm', '.xlsb', '.ppam', '.potm'
|
||
|
|
];
|
||
|
|
/**
|
||
|
|
* 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
|
||
|
|
* @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;
|
||
|
|
}
|
||
|
|
// Initialize scan result
|
||
|
|
const result = {
|
||
|
|
isClean: true,
|
||
|
|
threatScore: 0,
|
||
|
|
scannedElements: [],
|
||
|
|
timestamp: Date.now()
|
||
|
|
};
|
||
|
|
// List of scan promises
|
||
|
|
const scanPromises = [];
|
||
|
|
// Scan subject
|
||
|
|
if (this.options.scanSubject && email.subject) {
|
||
|
|
scanPromises.push(this.scanSubject(email.subject, result));
|
||
|
|
}
|
||
|
|
// Scan body content
|
||
|
|
if (this.options.scanBody) {
|
||
|
|
if (email.text) {
|
||
|
|
scanPromises.push(this.scanTextContent(email.text, result));
|
||
|
|
}
|
||
|
|
if (email.html) {
|
||
|
|
scanPromises.push(this.scanHtmlContent(email.html, result));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
// Scan attachments
|
||
|
|
if (this.options.scanAttachments && email.attachments && email.attachments.length > 0) {
|
||
|
|
for (const attachment of email.attachments) {
|
||
|
|
scanPromises.push(this.scanAttachment(attachment, result));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
// Run all scans in parallel
|
||
|
|
await Promise.all(scanPromises);
|
||
|
|
// 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, // Let it pass if scanner fails (configure as desired)
|
||
|
|
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 email subject for threats
|
||
|
|
* @param subject The subject to scan
|
||
|
|
* @param result The scan result to update
|
||
|
|
*/
|
||
|
|
async scanSubject(subject, result) {
|
||
|
|
result.scannedElements.push('subject');
|
||
|
|
// Check against phishing patterns
|
||
|
|
for (const pattern of ContentScanner.MALICIOUS_PATTERNS.phishing) {
|
||
|
|
if (pattern.test(subject)) {
|
||
|
|
result.threatScore += 25;
|
||
|
|
result.threatType = ThreatCategory.PHISHING;
|
||
|
|
result.threatDetails = `Subject contains potential phishing indicators: ${subject}`;
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
// Check against spam patterns
|
||
|
|
for (const pattern of ContentScanner.MALICIOUS_PATTERNS.spam) {
|
||
|
|
if (pattern.test(subject)) {
|
||
|
|
result.threatScore += 15;
|
||
|
|
result.threatType = ThreatCategory.SPAM;
|
||
|
|
result.threatDetails = `Subject contains potential spam indicators: ${subject}`;
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
// Check custom rules
|
||
|
|
for (const rule of this.options.customRules) {
|
||
|
|
const pattern = rule.pattern instanceof RegExp ? rule.pattern : new RegExp(rule.pattern, 'i');
|
||
|
|
if (pattern.test(subject)) {
|
||
|
|
result.threatScore += rule.score;
|
||
|
|
result.threatType = rule.type;
|
||
|
|
result.threatDetails = rule.description;
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Scan plain text content for threats
|
||
|
|
* @param text The text content to scan
|
||
|
|
* @param result The scan result to update
|
||
|
|
*/
|
||
|
|
async scanTextContent(text, result) {
|
||
|
|
result.scannedElements.push('text');
|
||
|
|
// Check suspicious links
|
||
|
|
for (const pattern of ContentScanner.MALICIOUS_PATTERNS.suspiciousLinks) {
|
||
|
|
if (pattern.test(text)) {
|
||
|
|
result.threatScore += 20;
|
||
|
|
if (!result.threatType || result.threatScore > (result.threatType === ThreatCategory.SUSPICIOUS_LINK ? 0 : 20)) {
|
||
|
|
result.threatType = ThreatCategory.SUSPICIOUS_LINK;
|
||
|
|
result.threatDetails = `Text contains suspicious links`;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
// Check phishing
|
||
|
|
for (const pattern of ContentScanner.MALICIOUS_PATTERNS.phishing) {
|
||
|
|
if (pattern.test(text)) {
|
||
|
|
result.threatScore += 25;
|
||
|
|
if (!result.threatType || result.threatScore > (result.threatType === ThreatCategory.PHISHING ? 0 : 25)) {
|
||
|
|
result.threatType = ThreatCategory.PHISHING;
|
||
|
|
result.threatDetails = `Text contains potential phishing indicators`;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
// Check spam
|
||
|
|
for (const pattern of ContentScanner.MALICIOUS_PATTERNS.spam) {
|
||
|
|
if (pattern.test(text)) {
|
||
|
|
result.threatScore += 15;
|
||
|
|
if (!result.threatType || result.threatScore > (result.threatType === ThreatCategory.SPAM ? 0 : 15)) {
|
||
|
|
result.threatType = ThreatCategory.SPAM;
|
||
|
|
result.threatDetails = `Text contains potential spam indicators`;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
// Check malware indicators
|
||
|
|
for (const pattern of ContentScanner.MALICIOUS_PATTERNS.malware) {
|
||
|
|
if (pattern.test(text)) {
|
||
|
|
result.threatScore += 30;
|
||
|
|
if (!result.threatType || result.threatScore > (result.threatType === ThreatCategory.MALWARE ? 0 : 30)) {
|
||
|
|
result.threatType = ThreatCategory.MALWARE;
|
||
|
|
result.threatDetails = `Text contains potential malware indicators`;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
// Check sensitive data
|
||
|
|
for (const pattern of ContentScanner.MALICIOUS_PATTERNS.sensitiveData) {
|
||
|
|
if (pattern.test(text)) {
|
||
|
|
result.threatScore += 25;
|
||
|
|
if (!result.threatType || result.threatScore > (result.threatType === ThreatCategory.SENSITIVE_DATA ? 0 : 25)) {
|
||
|
|
result.threatType = ThreatCategory.SENSITIVE_DATA;
|
||
|
|
result.threatDetails = `Text contains potentially sensitive data patterns`;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
// Check custom rules
|
||
|
|
for (const rule of this.options.customRules) {
|
||
|
|
const pattern = rule.pattern instanceof RegExp ? rule.pattern : new RegExp(rule.pattern, 'i');
|
||
|
|
if (pattern.test(text)) {
|
||
|
|
result.threatScore += rule.score;
|
||
|
|
if (!result.threatType || result.threatScore > 20) {
|
||
|
|
result.threatType = rule.type;
|
||
|
|
result.threatDetails = rule.description;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Scan HTML content for threats
|
||
|
|
* @param html The HTML content to scan
|
||
|
|
* @param result The scan result to update
|
||
|
|
*/
|
||
|
|
async scanHtmlContent(html, result) {
|
||
|
|
result.scannedElements.push('html');
|
||
|
|
// Check for script injection
|
||
|
|
for (const pattern of ContentScanner.MALICIOUS_PATTERNS.scriptInjection) {
|
||
|
|
if (pattern.test(html)) {
|
||
|
|
result.threatScore += 40;
|
||
|
|
if (!result.threatType || result.threatType !== ThreatCategory.XSS) {
|
||
|
|
result.threatType = ThreatCategory.XSS;
|
||
|
|
result.threatDetails = `HTML contains potentially malicious script content`;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
// Extract text content from HTML for further scanning
|
||
|
|
const textContent = this.extractTextFromHtml(html);
|
||
|
|
if (textContent) {
|
||
|
|
// We'll leverage the text scanning but not double-count threat score
|
||
|
|
const tempResult = {
|
||
|
|
isClean: true,
|
||
|
|
threatScore: 0,
|
||
|
|
scannedElements: [],
|
||
|
|
timestamp: Date.now()
|
||
|
|
};
|
||
|
|
await this.scanTextContent(textContent, tempResult);
|
||
|
|
// Only add additional threat types if they're more severe
|
||
|
|
if (tempResult.threatType && tempResult.threatScore > 0) {
|
||
|
|
// Add half of the text content score to avoid double counting
|
||
|
|
result.threatScore += Math.floor(tempResult.threatScore / 2);
|
||
|
|
// Adopt the threat type if more severe or no existing type
|
||
|
|
if (!result.threatType || tempResult.threatScore > result.threatScore) {
|
||
|
|
result.threatType = tempResult.threatType;
|
||
|
|
result.threatDetails = tempResult.threatDetails;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
// Extract and check links from HTML
|
||
|
|
const links = this.extractLinksFromHtml(html);
|
||
|
|
if (links.length > 0) {
|
||
|
|
// Check for suspicious links
|
||
|
|
let suspiciousLinks = 0;
|
||
|
|
for (const link of links) {
|
||
|
|
for (const pattern of ContentScanner.MALICIOUS_PATTERNS.suspiciousLinks) {
|
||
|
|
if (pattern.test(link)) {
|
||
|
|
suspiciousLinks++;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (suspiciousLinks > 0) {
|
||
|
|
// Add score based on percentage of suspicious links
|
||
|
|
const suspiciousPercentage = (suspiciousLinks / links.length) * 100;
|
||
|
|
const additionalScore = Math.min(40, Math.floor(suspiciousPercentage / 2.5));
|
||
|
|
result.threatScore += additionalScore;
|
||
|
|
if (!result.threatType || additionalScore > 20) {
|
||
|
|
result.threatType = ThreatCategory.SUSPICIOUS_LINK;
|
||
|
|
result.threatDetails = `HTML contains ${suspiciousLinks} suspicious links out of ${links.length} total links`;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Scan an attachment for threats
|
||
|
|
* @param attachment The attachment to scan
|
||
|
|
* @param result The scan result to update
|
||
|
|
*/
|
||
|
|
async scanAttachment(attachment, result) {
|
||
|
|
const filename = attachment.filename.toLowerCase();
|
||
|
|
result.scannedElements.push(`attachment:${filename}`);
|
||
|
|
// Skip large attachments if configured
|
||
|
|
if (attachment.content && attachment.content.length > this.options.maxAttachmentSizeToScan) {
|
||
|
|
logger.log('info', `Skipping scan of large attachment: ${filename} (${attachment.content.length} bytes)`);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
// Check filename for executable extensions
|
||
|
|
if (this.options.blockExecutables) {
|
||
|
|
for (const ext of ContentScanner.EXECUTABLE_EXTENSIONS) {
|
||
|
|
if (filename.endsWith(ext)) {
|
||
|
|
result.threatScore += 70; // High score for executable attachments
|
||
|
|
result.threatType = ThreatCategory.EXECUTABLE;
|
||
|
|
result.threatDetails = `Attachment has a potentially dangerous extension: ${filename}`;
|
||
|
|
return; // No need to scan contents if filename already flagged
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
// Check for Office documents with macros
|
||
|
|
if (this.options.blockMacros) {
|
||
|
|
for (const ext of ContentScanner.MACRO_DOCUMENT_EXTENSIONS) {
|
||
|
|
if (filename.endsWith(ext)) {
|
||
|
|
// For Office documents, check if they contain macros
|
||
|
|
// This is a simplified check - a real implementation would use specialized libraries
|
||
|
|
// to detect macros in Office documents
|
||
|
|
if (attachment.content && this.likelyContainsMacros(attachment)) {
|
||
|
|
result.threatScore += 60;
|
||
|
|
result.threatType = ThreatCategory.MALICIOUS_MACRO;
|
||
|
|
result.threatDetails = `Attachment appears to contain macros: ${filename}`;
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
// Perform basic content analysis if we have content buffer
|
||
|
|
if (attachment.content) {
|
||
|
|
// Convert to string for scanning, with a limit to prevent memory issues
|
||
|
|
const textContent = this.extractTextFromBuffer(attachment.content);
|
||
|
|
if (textContent) {
|
||
|
|
// Scan for malicious patterns in attachment content
|
||
|
|
for (const category in ContentScanner.MALICIOUS_PATTERNS) {
|
||
|
|
const patterns = ContentScanner.MALICIOUS_PATTERNS[category];
|
||
|
|
for (const pattern of patterns) {
|
||
|
|
if (pattern.test(textContent)) {
|
||
|
|
result.threatScore += 30;
|
||
|
|
if (!result.threatType) {
|
||
|
|
result.threatType = this.mapCategoryToThreatType(category);
|
||
|
|
result.threatDetails = `Attachment content contains suspicious patterns: ${filename}`;
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
// Check for PE headers (Windows executables)
|
||
|
|
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}`;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Extract links from HTML content
|
||
|
|
* @param html HTML content
|
||
|
|
* @returns Array of extracted links
|
||
|
|
*/
|
||
|
|
extractLinksFromHtml(html) {
|
||
|
|
const links = [];
|
||
|
|
// Simple regex-based extraction - a real implementation might use a proper HTML parser
|
||
|
|
const matches = html.match(/href=["'](https?:\/\/[^"']+)["']/gi);
|
||
|
|
if (matches) {
|
||
|
|
for (const match of matches) {
|
||
|
|
const linkMatch = match.match(/href=["'](https?:\/\/[^"']+)["']/i);
|
||
|
|
if (linkMatch && linkMatch[1]) {
|
||
|
|
links.push(linkMatch[1]);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return links;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Extract plain text from HTML
|
||
|
|
* @param html HTML content
|
||
|
|
* @returns Extracted text
|
||
|
|
*/
|
||
|
|
extractTextFromHtml(html) {
|
||
|
|
// Remove HTML tags and decode entities - simplified version
|
||
|
|
return html
|
||
|
|
.replace(/<style[^>]*>.*?<\/style>/gs, '')
|
||
|
|
.replace(/<script[^>]*>.*?<\/script>/gs, '')
|
||
|
|
.replace(/<[^>]+>/g, ' ')
|
||
|
|
.replace(/ /g, ' ')
|
||
|
|
.replace(/</g, '<')
|
||
|
|
.replace(/>/g, '>')
|
||
|
|
.replace(/&/g, '&')
|
||
|
|
.replace(/"/g, '"')
|
||
|
|
.replace(/'/g, "'")
|
||
|
|
.replace(/\s+/g, ' ')
|
||
|
|
.trim();
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* 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
|
||
|
|
* This is a simplified check - real implementation would use specialized libraries
|
||
|
|
* @param attachment The attachment to check
|
||
|
|
* @returns Whether the file likely contains macros
|
||
|
|
*/
|
||
|
|
likelyContainsMacros(attachment) {
|
||
|
|
// Simple heuristic: look for VBA/macro related strings
|
||
|
|
// This is a simplified approach and not comprehensive
|
||
|
|
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;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Map a pattern category to a threat type
|
||
|
|
* @param category The pattern category
|
||
|
|
* @returns The corresponding threat type
|
||
|
|
*/
|
||
|
|
mapCategoryToThreatType(category) {
|
||
|
|
switch (category) {
|
||
|
|
case 'phishing': return ThreatCategory.PHISHING;
|
||
|
|
case 'spam': return ThreatCategory.SPAM;
|
||
|
|
case 'malware': return ThreatCategory.MALWARE;
|
||
|
|
case 'suspiciousLinks': return ThreatCategory.SUSPICIOUS_LINK;
|
||
|
|
case 'scriptInjection': return ThreatCategory.XSS;
|
||
|
|
case 'sensitiveData': return ThreatCategory.SENSITIVE_DATA;
|
||
|
|
default: return ThreatCategory.BLACKLISTED_CONTENT;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* 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,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xhc3Nlcy5jb250ZW50c2Nhbm5lci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3RzL3NlY3VyaXR5L2NsYXNzZXMuY29udGVudHNjYW5uZXIudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxLQUFLLE9BQU8sTUFBTSxlQUFlLENBQUM7QUFDekMsT0FBTyxLQUFLLEtBQUssTUFBTSxhQUFhLENBQUM7QUFDckMsT0FBTyxFQUFFLE1BQU0sRUFBRSxNQUFNLGNBQWMsQ0FBQztBQUN0QyxPQUFPLEVBQUUsS0FBSyxFQUFFLE1BQU0sK0JBQStCLENBQUM7QUFFdEQsT0FBTyxFQUFFLGNBQWMsRUFBRSxnQkFBZ0IsRUFBRSxpQkFBaUIsRUFBRSxNQUFNLDZCQUE2QixDQUFDO0FBQ2xHLE9BQU8sRUFBRSxRQUFRLEVBQUUsTUFBTSxXQUFXLENBQUM7QUFxQ3JDOztHQUVHO0FBQ0gsTUFBTSxDQUFOLElBQVksY0FXWDtBQVhELFdBQVksY0FBYztJQUN4QiwrQkFBYSxDQUFBO0lBQ2IsdUNBQXFCLENBQUE7SUFDckIscUNBQW1CLENBQUE7SUFDbkIsMkNBQXlCLENBQUE7SUFDekIscURBQW1DLENBQUE7SUFDbkMscURBQW1DLENBQUE7SUFDbkMsNkJBQVcsQ0FBQTtJQUNYLG1EQUFpQyxDQUFBO0lBQ2pDLDZEQUEyQyxDQUFBO0lBQzNDLDZDQUEyQixDQUFBO0FBQzdCLENBQUMsRUFYVyxjQUFjLEtBQWQsY0FBYyxRQVd6QjtBQUVEOztHQUVHO0FBQ0gsTUFBTSxPQUFPLGNBQWM7SUFDakIsTUFBTSxDQUFDLFFBQVEsQ0FBaUI7SUFDaEMsU0FBUyxDQUFnQztJQUN6QyxPQUFPLENBQW1DO0lBRWxELHlDQUF5QztJQUNqQyxNQUFNLENBQVUsa0JBQWtCLEdBQUc7UUFDM0Msb0JBQW9CO1FBQ3BCLFFBQVEsRUFBRTtZQUNSLGdFQUFnRTtZQUNoRSx3Q0FBd0M7WUFDeEMsNEVBQTRFO1lBQzVFLHNEQUFzRDtZQUN0RCx3REFBd0Q7U0FDekQ7UUFFRCxrQkFBa0I7UUFDbEIsSUFBSSxFQUFFO1lBQ0osMEVBQTBFO1lBQzFFLDRFQUE0RTtZQUM1RSw0REFBNEQ7WUFDNUQsa0VBQWtFO1lBQ2xFLHdFQUF3RTtTQUN6RTtRQUVELDZCQUE2QjtRQUM3QixPQUFPLEVBQUU7WUFDUCwyRUFBMkU7WUFDM0UseUNBQXlDO1lBQ3pDLCtDQUErQztZQUMvQyx5Q0FBeUM7WUFDekMsNkRBQTZEO1NBQzlEO1FBRUQsbUJBQW1CO1FBQ25CLGVBQWUsRUFBRTtZQUNmLHVCQUF1QjtZQUN2Qix1QkFBdUI7WUFDdkIscUJBQXFCO1lBQ3JCLDRCQUE0QjtZQUM1QixxQ0FBcUMsRUFBRSxrQkFBa0I7WUFDekQsMENBQTBDLEVBQUUsa0JBQWtCO1lBQzlELG1FQUFtRSxFQUFFLGlDQUFpQztTQUN2RztRQUVELDJCQUEyQjtRQUMzQixlQUFlLEVBQUU7WUFDZiwwQkFBMEI7WUFDMUIsY0FBYztZQUNkLCtDQUErQztZQUMvQyxzQ0FBc0M7WUFDdEMsWUFBWTtTQUNiO1FBRUQsMEJBQTBCO1FBQzFCLGFBQWEsRUFBRTtZQUNiLGlDQUFpQyxFQUFFLE1BQU07WUFDekMsZUFBZSxFQUFFLHNCQUFzQjtZQUN2QyxvRkFBb0YsQ0FBQyxrQkFBa0I7U0FDeEc7S0FDRixDQUFDO0lBRUYsK0JBQStCO0lBQ3ZCLE1BQU0sQ0FBVSxxQkFBcUIsR0FBRztRQUM5QyxNQUFNLEVBQUUsTUFBTSxFQUFFLE1BQU0sRUFBRSxNQUFNLEVBQUUsTUFBTSxFQUFFLEtBQUssRUFBRSxNQUFNLEVBQUUsTUFBTTtRQUM3RCxLQUFLLEVBQUUsTUFBTSxFQUFFLEtBQUssRUFBRSxNQUFNLEVBQUUsTUFBTSxFQUFFLE1BQU0sRUFBRSxNQUFNLEVBQUUsTUFBTTtRQUM1RCxNQUFNLEVBQUUsTUFBTSxFQUFFLE1BQU0sRUFBRSxNQUFNLEVBQUUsTUFBTSxFQUFFLE1BQU0sRUFBRSxNQUFNO0tBQ3ZELENBQUM7SUFFRiwyQ0FBMkM7SUFDbkMsTUFBTSxDQUFVLHlCQUF5QixHQUFHO1FBQ2xELE1BQU0sRUFBRSxPQUFPLEVBQUUsTUFBTSxFQUFFLE9BQU8sRUFBRSxNQUFNLEVBQUUsT0FBTyxFQUFFLE9BQU8sRUFBRSxPQUFPLEVBQUUsT0FBTyxFQUFFLE9BQU87S0FDdEYsQ0FBQztJQUVGOztPQUVHO0lBQ0ssTUFBTSxDQUFVLGVBQWUsR0FBcUM7UUFDMUUsWUFBWSxFQUFFLEtBQUs7UUFDbkIsUUFBUSxFQUFFLEVBQUUsR0FBRyxFQUFFLEdBQUcsRUFBRSxHQUFHLElBQUksRUFBRSxXQUFXO1FBQzFDLFdBQVcsRUFBRSxJQUFJO1FBQ2pCLFFBQVEsRUFBRSxJQUFJO1FBQ2QsZUFBZSxFQUFFLElBQUk7UUFDckIsdUJBQXVCLEVBQUUsRUFBRSxHQUFHLElBQUksR0FBRyxJQUFJLEVBQUUsT0FBTztRQUNsRCxtQkFBbUIsRUFBRSxJQUFJO1FBQ3pCLGdCQUFnQixFQUFFLElBQUk7UUFDdEIsV0FBVyxFQUFFLElBQUk7UUFDakIsV0FBVyxFQUFFLEVBQUU7UUFDZixjQUFjLEVBQUUsRUFBRSxFQUFFLGdEQUFnRDtRQUNwRSxlQUFlLEVBQUUsRUFBRSxDQUFFLHNEQUFzRDtLQUM1RSxDQUFDO0lBRUY7OztPQUdHO0lBQ0gsWUFBWSxVQUFrQyxFQUFFO1FBQzlDLDZCQUE2QjtRQUM3QixJQUFJLENBQUMsT0FBTyxHQUFHO1lBQ2IsR0FBRyxjQUFjLENBQUMsZUFBZTtZQUNqQyxHQUFHLE9BQU87U0FDWCxDQUFDO1FBRUYsbUJBQW1CO1FBQ25CLElBQUksQ0FBQyxTQUFTLEdBQUcsSUFBSSxRQUFRLENBQXNCO1lBQ2pELEdBQUcsRUFBRSxJQUFJLENBQUMsT0FBTyxDQUFDLFlBQVk7WUFDOUIsR0FBRyxFQUFFLElBQUksQ0FBQyxPQUFPLENBQUMsUUFBUTtTQUMzQixDQUFDLENBQUM7UUFFSCxNQUFNLENBQUMsR0FBRyxDQUFDLE1BQU0sRUFBRSw0QkFBNEIsQ0FBQyxDQUFDO0lBQ25ELENBQUM7SUFFRDs7OztPQUlHO0lBQ0ksTUFBTSxDQUFDLFdBQVcsQ0FBQyxVQUFrQyxFQUFFO1FBQzVELElBQUksQ0FBQyxjQUFjLENBQUMsUUFBUSxFQUFFLENBQUM7WUFDN0IsY0FBYyxDQUFDLFFBQVEsR0FBRyxJQUFJLGNBQWMsQ0FBQyxPQUFPLENBQUMsQ0FBQztRQUN4RCxDQUFDO1FBQ0QsT0FBTyxjQUFjLENBQUMsUUFBUSxDQUFDO0lBQ2pDLENBQUM7SUFFRDs7OztPQUlHO0lBQ0ksS0FBSyxDQUFDLFNBQVMsQ0FBQyxLQUFZO1FBQ2pDLElBQUksQ0FBQztZQUNILHNDQUFzQztZQUN0QyxNQUFNLFFBQVEsR0FBRyxJQUFJLENBQUMsZ0JBQWdCLENBQUMsS0FBSyxDQUFDLENBQUM7WUFFOUMsb0JBQW9CO1lBQ3BCLE1BQU0sWUFBWSxHQUFHLElBQUksQ0FBQyxTQUFTLENBQUMsR0FBRyxDQUFDLFFBQ
|