/** * @fileoverview Malware Detection Utilities * @module rules/malware/utils * * Core utility functions for malware detection including entropy calculation, * code normalization, obfuscation detection, and pattern matching helpers. */ import { SupportedLanguage, PatternMatch, SourceLocation, MalwarePattern, PatternType, RegexPattern } from '../types'; import { ENTROPY_THRESHOLDS, OBFUSCATION_INDICATORS, LIMITS } from '../constants'; // ============================================================================ // ENTROPY CALCULATION // ============================================================================ /** * Calculate Shannon entropy of a string * Higher entropy indicates more randomness/potential obfuscation * * @param content - String to analyze * @returns Entropy value (0-8 for ASCII) */ export function calculateEntropy(content: string): number { if (!content || content.length === 0) { return 0; } const charFrequency: Map = new Map(); for (const char of content) { charFrequency.set(char, (charFrequency.get(char) || 0) + 1); } let entropy = 0; const length = content.length; for (const count of Array.from(charFrequency.values())) { const probability = count / length; entropy -= probability * Math.log2(probability); } return entropy; } /** * Calculate entropy per line and detect anomalies * * @param content - Source code content * @returns Object with average entropy and lines with high entropy */ export function analyzeEntropyByLine(content: string): { averageEntropy: number; maxEntropy: number; highEntropyLines: Array<{ line: number; entropy: number; content: string }>; } { const lines = content.split('\n'); const entropies = lines.map(line => calculateEntropy(line)); const averageEntropy = entropies.reduce((a, b) => a + b, 0) / entropies.length; const maxEntropy = Math.max(...entropies); const highEntropyLines = lines .map((line, index) => ({ line: index + 1, entropy: entropies[index], content: line.substring(0, LIMITS.MAX_SNIPPET_LENGTH) })) .filter(item => item.entropy > ENTROPY_THRESHOLDS.SUSPICIOUS); return { averageEntropy, maxEntropy, highEntropyLines }; } // ============================================================================ // CODE NORMALIZATION // ============================================================================ /** * Normalize code for analysis by removing common obfuscation patterns * IMPORTANT: This does NOT execute any code * * @param content - Source code to normalize * @param language - Programming language * @returns Normalized code */ export function normalizeCode( content: string, language: SupportedLanguage ): string { let normalized = content; // Remove comments based on language normalized = removeComments(normalized, language); // Normalize whitespace normalized = normalizeWhitespace(normalized); // Decode simple escape sequences (safe, no execution) normalized = decodeEscapeSequences(normalized); // Normalize string concatenations normalized = normalizeStringConcatenation(normalized); return normalized; } /** * Remove comments from code based on language */ function removeComments(content: string, language: SupportedLanguage): string { let result = content; switch (language) { case SupportedLanguage.JAVASCRIPT: case SupportedLanguage.TYPESCRIPT: case SupportedLanguage.JAVA: case SupportedLanguage.CSHARP: case SupportedLanguage.C: case SupportedLanguage.CPP: // Remove single-line comments result = result.replace(/\/\/.*$/gm, ''); // Remove multi-line comments result = result.replace(/\/\*[\s\S]*?\*\//g, ''); break; case SupportedLanguage.PYTHON: case SupportedLanguage.RUBY: // Remove single-line comments result = result.replace(/#.*$/gm, ''); // Remove docstrings (Python) result = result.replace(/"""[\s\S]*?"""/g, ''); result = result.replace(/'''[\s\S]*?'''/g, ''); break; case SupportedLanguage.PHP: // Remove single-line comments result = result.replace(/\/\/.*$/gm, ''); result = result.replace(/#.*$/gm, ''); // Remove multi-line comments result = result.replace(/\/\*[\s\S]*?\*\//g, ''); break; case SupportedLanguage.SHELL: case SupportedLanguage.POWERSHELL: result = result.replace(/#.*$/gm, ''); break; } return result; } /** * Normalize whitespace */ function normalizeWhitespace(content: string): string { return content .replace(/\r\n/g, '\n') .replace(/[ \t]+/g, ' ') .replace(/\n{3,}/g, '\n\n'); } /** * Safely decode escape sequences without execution */ function decodeEscapeSequences(content: string): string { let result = content; // Decode hex escapes (\x41 -> A) result = result.replace(/\\x([0-9a-fA-F]{2})/g, (_, hex) => { const charCode = parseInt(hex, 16); if (charCode >= 32 && charCode < 127) { return String.fromCharCode(charCode); } return _; }); // Decode unicode escapes (\u0041 -> A) result = result.replace(/\\u([0-9a-fA-F]{4})/g, (_, hex) => { const charCode = parseInt(hex, 16); if (charCode >= 32 && charCode < 127) { return String.fromCharCode(charCode); } return _; }); return result; } /** * Normalize string concatenation * "e" + "v" + "a" + "l" -> "eval" */ function normalizeStringConcatenation(content: string): string { // Match patterns like 'a' + 'b' + 'c' return content.replace( /(['"])(\w)\1\s*\+\s*(['"])(\w)\3(?:\s*\+\s*(['"])(\w)\5)*/g, (match) => { const chars = match.match(/['"](\w)['"]/g); if (chars) { const combined = chars.map(c => c[1]).join(''); return `"${combined}"`; } return match; } ); } // ============================================================================ // OBFUSCATION DETECTION // ============================================================================ /** * Detect obfuscation level in code * * @param content - Source code to analyze * @param language - Programming language * @returns Obfuscation score (0-1) */ export function detectObfuscationLevel( content: string, language: SupportedLanguage ): number { let score = 0; let maxScore = 0; // Check each obfuscation indicator for (const [name, indicator] of Object.entries(OBFUSCATION_INDICATORS)) { maxScore += (indicator as { weight: number }).weight; if ('pattern' in indicator) { const matches = content.match(indicator.pattern as RegExp); if (matches && matches.length > 0) { // Scale by match density const density = Math.min(matches.length / (content.length / 100), 1); score += indicator.weight * density; } } else if ('threshold' in indicator) { if (name === 'LONG_LINES') { const lines = content.split('\n'); const longLines = lines.filter(l => l.length > (indicator.threshold as number)); if (longLines.length / lines.length > 0.1) { score += indicator.weight; } } else if (name === 'CHAR_DIVERSITY') { const uniqueChars = new Set(content).size; const diversity = uniqueChars / Math.min(content.length, 256); if (diversity < (indicator.threshold as number)) { score += indicator.weight; } } } } // Check entropy const entropy = calculateEntropy(content); if (entropy > ENTROPY_THRESHOLDS.HIGH_OBFUSCATION) { score += 0.3; maxScore += 0.3; } else if (entropy > ENTROPY_THRESHOLDS.SUSPICIOUS) { score += 0.15; maxScore += 0.15; } // Language-specific checks score += detectLanguageSpecificObfuscation(content, language); maxScore += 0.3; return Math.min(score / maxScore, 1); } /** * Language-specific obfuscation detection */ function detectLanguageSpecificObfuscation( content: string, language: SupportedLanguage ): number { let score = 0; switch (language) { case SupportedLanguage.JAVASCRIPT: case SupportedLanguage.TYPESCRIPT: // JSFuck patterns if (/\[\]!\+/.test(content)) score += 0.15; // JavaScript packer if (/eval\(function\(p,a,c,k,e,/.test(content)) score += 0.15; // Obfuscator.io patterns if (/_0x[a-f0-9]{4,}/gi.test(content)) score += 0.1; break; case SupportedLanguage.PHP: // Base64 + eval/gzinflate if (/eval\s*\(\s*(?:base64_decode|gzinflate|str_rot13)/i.test(content)) score += 0.15; // Variable function calls if (/\$[a-z]+\s*\(/gi.test(content)) score += 0.05; break; case SupportedLanguage.PYTHON: // exec(compile(...)) if (/exec\s*\(\s*compile/i.test(content)) score += 0.15; // __import__ obfuscation if (/__import__\s*\([^)]+\)\s*\./gi.test(content)) score += 0.1; break; } return score; } // ============================================================================ // ANTI-DEBUGGING DETECTION // ============================================================================ /** * Detect anti-debugging techniques */ export function detectAntiDebugging( content: string, language: SupportedLanguage ): { detected: boolean; techniques: string[] } { const techniques: string[] = []; // JavaScript anti-debugging if (language === SupportedLanguage.JAVASCRIPT || language === SupportedLanguage.TYPESCRIPT) { // Debugger statements if (/\bdebugger\b/.test(content)) { techniques.push('debugger-statement'); } // Console detection if (/console\.(log|warn|error)\s*=/.test(content)) { techniques.push('console-override'); } // DevTools detection if (/devtools|firebug/i.test(content)) { techniques.push('devtools-detection'); } // Performance timing checks if (/performance\.now\s*\(\s*\)|Date\.now\s*\(\s*\)/.test(content) && /setTimeout|setInterval/.test(content)) { techniques.push('timing-check'); } // Function.toString detection if (/toString\s*\(\s*\)\s*\.(?:indexOf|includes|match)/.test(content)) { techniques.push('toString-check'); } } // Python anti-debugging if (language === SupportedLanguage.PYTHON) { if (/sys\.(settrace|gettrace)/.test(content)) { techniques.push('trace-detection'); } if (/pydevd|pdb\.set_trace/.test(content)) { techniques.push('debugger-detection'); } } return { detected: techniques.length > 0, techniques }; } // ============================================================================ // ENVIRONMENT CHECKS DETECTION // ============================================================================ /** * Detect environment-dependent activation (time bombs, sandbox evasion) */ export function detectEnvironmentChecks(content: string): { detected: boolean; checks: string[]; } { const checks: string[] = []; // Time-based activation if (/new Date\s*\(\s*\)|Date\.now\s*\(\s*\)/.test(content)) { if (/getFullYear|getMonth|getDate|getHours/.test(content)) { checks.push('time-based-activation'); } } // Environment variable checks if (/process\.env|os\.environ|getenv|Environment\.GetEnvironmentVariable/.test(content)) { checks.push('environment-variable-check'); } // CI/CD detection if (/CI|TRAVIS|JENKINS|GITHUB_ACTIONS|GITLAB_CI|CIRCLECI/.test(content)) { checks.push('ci-cd-detection'); } // Sandbox/VM detection if (/vmware|virtualbox|sandbox|qemu|xen|hypervisor/i.test(content)) { checks.push('sandbox-detection'); } // Production environment checks if (/NODE_ENV|RAILS_ENV|APP_ENV|ENVIRONMENT/.test(content) && /production|prod|live/i.test(content)) { checks.push('production-check'); } // User/hostname checks if (/os\.getlogin|getpass\.getuser|socket\.gethostname|Environment\.UserName/.test(content)) { checks.push('user-hostname-check'); } return { detected: checks.length > 0, checks }; } // ============================================================================ // PATTERN MATCHING UTILITIES // ============================================================================ /** * Safe regex matching with timeout protection */ export async function matchWithTimeout( content: string, pattern: RegExp, timeout: number = LIMITS.REGEX_TIMEOUT ): Promise { return new Promise((resolve) => { const timer = setTimeout(() => { resolve(null); }, timeout); try { const result = content.match(pattern); clearTimeout(timer); resolve(result); } catch { clearTimeout(timer); resolve(null); } }); } /** * Apply regex pattern with safety limits */ export function safeRegexMatch( content: string, pattern: RegexPattern ): PatternMatch[] { const matches: PatternMatch[] = []; try { const regex = new RegExp(pattern.pattern, pattern.flags || 'g'); const maxMatches = pattern.maxMatches || LIMITS.MAX_MATCHES_PER_PATTERN; let match: RegExpExecArray | null; let count = 0; while ((match = regex.exec(content)) !== null && count < maxMatches) { const location = getLocationFromIndex(content, match.index, match[0].length); matches.push({ pattern, matchedText: match[0].substring(0, LIMITS.MAX_SNIPPET_LENGTH), location, captures: match.slice(1) }); count++; // Prevent infinite loops for zero-width matches if (match[0].length === 0) { regex.lastIndex++; } } } catch (error) { // Log error but don't throw - invalid regex should be handled gracefully console.error(`Invalid regex pattern: ${pattern.pattern}`, error); } return matches; } /** * Convert string index to line/column location */ export function getLocationFromIndex( content: string, startIndex: number, length: number ): SourceLocation { const lines = content.substring(0, startIndex).split('\n'); const startLine = lines.length; const startColumn = lines[lines.length - 1].length; const matchedContent = content.substring(startIndex, startIndex + length); const matchedLines = matchedContent.split('\n'); const endLine = startLine + matchedLines.length - 1; const endColumn = matchedLines.length === 1 ? startColumn + length : matchedLines[matchedLines.length - 1].length; return { filePath: '', // Will be set by caller startLine, endLine, startColumn, endColumn }; } /** * Extract code snippet with context */ export function extractSnippet( content: string, location: SourceLocation, contextLines: number = LIMITS.CONTEXT_LINES ): string { const lines = content.split('\n'); const startLine = Math.max(0, location.startLine - contextLines - 1); const endLine = Math.min(lines.length, location.endLine + contextLines); return lines .slice(startLine, endLine) .join('\n') .substring(0, LIMITS.MAX_SNIPPET_LENGTH); } // ============================================================================ // BASE64 UTILITIES // ============================================================================ /** * Detect and analyze base64 encoded content * Does NOT decode potentially malicious content */ export function analyzeBase64Content(content: string): { found: boolean; count: number; longestLength: number; locations: SourceLocation[]; } { const base64Pattern = /['"]([A-Za-z0-9+/]{50,}={0,2})['"]/g; const locations: SourceLocation[] = []; let longestLength = 0; let count = 0; let match; while ((match = base64Pattern.exec(content)) !== null) { count++; longestLength = Math.max(longestLength, match[1].length); locations.push(getLocationFromIndex(content, match.index, match[0].length)); } return { found: count > 0, count, longestLength, locations }; } // ============================================================================ // STRING ANALYSIS // ============================================================================ /** * Extract suspicious strings from code */ export function extractSuspiciousStrings(content: string): { urls: string[]; ips: string[]; emails: string[]; paths: string[]; commands: string[]; } { const urls: string[] = []; const ips: string[] = []; const emails: string[] = []; const paths: string[] = []; const commands: string[] = []; // URLs const urlPattern = /https?:\/\/[^\s'"<>]+/gi; let match; while ((match = urlPattern.exec(content)) !== null) { urls.push(match[0]); } // IP addresses const ipPattern = /\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g; while ((match = ipPattern.exec(content)) !== null) { ips.push(match[0]); } // Email addresses const emailPattern = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g; while ((match = emailPattern.exec(content)) !== null) { emails.push(match[0]); } // File paths const pathPattern = /(?:\/[\w.-]+)+|(?:[A-Z]:\\[\w\\.-]+)/gi; while ((match = pathPattern.exec(content)) !== null) { paths.push(match[0]); } // Shell commands const cmdPattern = /(?:(?:sh|bash|cmd|powershell)\s+-c\s+['"]?|`)[^'"`]+/gi; while ((match = cmdPattern.exec(content)) !== null) { commands.push(match[0]); } return { urls, ips, emails, paths, commands }; } // ============================================================================ // EXPORTS // ============================================================================ export { removeComments, normalizeWhitespace, decodeEscapeSequences, normalizeStringConcatenation };