UNPKG

@profullstack/scanner

Version:

A comprehensive CLI and Node.js module for web application security scanning with OWASP compliance, supporting multiple scanning tools and detailed vulnerability reporting

400 lines (352 loc) 10.2 kB
import { URL } from 'url'; /** * Validate a target URL or IP address * @param {string} target - Target to validate * @returns {Object} Validation result with valid boolean and error message */ export function validateTarget(target) { if (!target || typeof target !== 'string') { return { valid: false, error: 'Target must be a non-empty string' }; } target = target.trim(); // Check if it's an IP address if (isValidIP(target)) { return { valid: true, type: 'ip', target: target }; } // Check if it's a URL if (isValidUrl(target)) { return { valid: true, type: 'url', target: target }; } // If it already has a protocol but isn't HTTP/HTTPS, reject it if (target.includes('://') && !target.startsWith('http://') && !target.startsWith('https://')) { return { valid: false, error: 'Only HTTP and HTTPS protocols are supported' }; } // Try to add protocol and validate const withProtocol = target.startsWith('http') ? target : `http://${target}`; if (isValidUrl(withProtocol)) { return { valid: true, type: 'url', target: withProtocol }; } // Check if it looks like a domain name (has at least one dot and valid format) if (target.includes('.') && !target.includes(' ') && target.length > 3) { // Basic domain validation - should have valid characters and structure const domainRegex = /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/; if (domainRegex.test(target)) { const withHttp = `http://${target}`; if (isValidUrl(withHttp)) { return { valid: true, type: 'url', target: withHttp }; } } } return { valid: false, error: 'Target must be a valid URL or IP address' }; } /** * Check if a string is a valid URL * @param {string} urlString - URL string to validate * @returns {boolean} True if valid URL */ export function isValidUrl(urlString) { try { const url = new URL(urlString); return (url.protocol === 'http:' || url.protocol === 'https:') && url.hostname.length > 0; } catch (error) { return false; } } /** * Check if a string is a valid IP address (IPv4 or IPv6) * @param {string} ip - IP address string to validate * @returns {boolean} True if valid IP address */ export function isValidIP(ip) { // IPv4 regex const ipv4Regex = /^(?:(?: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]?)$/; // IPv6 regex (simplified) const ipv6Regex = /^(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$|^::1$|^::$/; return ipv4Regex.test(ip) || ipv6Regex.test(ip); } /** * Parse a URL and extract components * @param {string} urlString - URL string to parse * @returns {Object} Parsed URL components */ export function parseUrl(urlString) { try { const url = new URL(urlString); return { protocol: url.protocol, hostname: url.hostname, port: url.port || (url.protocol === 'https:' ? '443' : '80'), pathname: url.pathname, search: url.search, hash: url.hash, origin: url.origin, href: url.href }; } catch (error) { return null; } } /** * Sanitize a string for use in file names * @param {string} str - String to sanitize * @returns {string} Sanitized string */ export function sanitizeFilename(str) { return str .replace(/[^a-z0-9]/gi, '_') .replace(/_+/g, '_') .replace(/^_|_$/g, '') .toLowerCase(); } /** * Format duration in seconds to human readable format * @param {number} seconds - Duration in seconds * @returns {string} Formatted duration */ export function formatDuration(seconds) { if (seconds < 60) { return `${seconds}s`; } const minutes = Math.floor(seconds / 60); const remainingSeconds = seconds % 60; if (minutes < 60) { return remainingSeconds > 0 ? `${minutes}m ${remainingSeconds}s` : `${minutes}m`; } const hours = Math.floor(minutes / 60); const remainingMinutes = minutes % 60; let result = `${hours}h`; if (remainingMinutes > 0) { result += ` ${remainingMinutes}m`; } if (remainingSeconds > 0) { result += ` ${remainingSeconds}s`; } return result; } /** * Format file size in bytes to human readable format * @param {number} bytes - Size in bytes * @returns {string} Formatted size */ export function formatFileSize(bytes) { const units = ['B', 'KB', 'MB', 'GB', 'TB']; let size = bytes; let unitIndex = 0; while (size >= 1024 && unitIndex < units.length - 1) { size /= 1024; unitIndex++; } return `${size.toFixed(unitIndex === 0 ? 0 : 1)}${units[unitIndex]}`; } /** * Get severity color for console output * @param {string} severity - Severity level * @returns {string} ANSI color code */ export function getSeverityColor(severity) { const colors = { critical: '\x1b[41m\x1b[37m', // Red background, white text high: '\x1b[31m', // Red text medium: '\x1b[33m', // Yellow text low: '\x1b[36m', // Cyan text info: '\x1b[37m', // White text reset: '\x1b[0m' // Reset }; return colors[severity?.toLowerCase()] || colors.info; } /** * Get severity emoji * @param {string} severity - Severity level * @returns {string} Emoji representing severity */ export function getSeverityEmoji(severity) { const emojis = { critical: '🔴', high: '🟠', medium: '🟡', low: '🔵', info: '⚪' }; return emojis[severity?.toLowerCase()] || emojis.info; } /** * Create a progress bar string * @param {number} current - Current progress * @param {number} total - Total items * @param {number} width - Width of progress bar * @returns {string} Progress bar string */ export function createProgressBar(current, total, width = 20) { const percentage = Math.min(current / total, 1); const filled = Math.floor(percentage * width); const empty = width - filled; const bar = '█'.repeat(filled) + '░'.repeat(empty); const percent = Math.floor(percentage * 100); return `[${bar}] ${percent}% (${current}/${total})`; } /** * Debounce function to limit function calls * @param {Function} func - Function to debounce * @param {number} wait - Wait time in milliseconds * @returns {Function} Debounced function */ export function debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } /** * Deep merge two objects * @param {Object} target - Target object * @param {Object} source - Source object * @returns {Object} Merged object */ export function deepMerge(target, source) { const result = { ...target }; for (const key in source) { if (source.hasOwnProperty(key)) { if (typeof source[key] === 'object' && source[key] !== null && !Array.isArray(source[key])) { result[key] = deepMerge(result[key] || {}, source[key]); } else { result[key] = source[key]; } } } return result; } /** * Generate a unique scan ID * @returns {string} Unique scan ID */ export function generateScanId() { const timestamp = Date.now(); const random = Math.random().toString(36).substr(2, 9); return `scan-${timestamp}-${random}`; } /** * Check if a port is commonly used for web services * @param {number|string} port - Port number * @returns {boolean} True if it's a common web port */ export function isWebPort(port) { const webPorts = [80, 443, 8080, 8443, 3000, 5000, 8000, 8888, 9000]; return webPorts.includes(parseInt(port)); } /** * Extract domain from URL or hostname * @param {string} input - URL or hostname * @returns {string} Domain name */ export function extractDomain(input) { try { if (input.includes('://')) { const url = new URL(input); return url.hostname; } else { // Assume it's already a hostname return input.split(':')[0]; // Remove port if present } } catch (error) { return input; } } /** * Check if running with sufficient privileges for certain scans * @returns {boolean} True if running as root/admin */ export function hasElevatedPrivileges() { return process.getuid && process.getuid() === 0; } /** * Escape shell arguments to prevent injection * @param {string} arg - Argument to escape * @returns {string} Escaped argument */ export function escapeShellArg(arg) { return `'${arg.replace(/'/g, "'\"'\"'")}'`; } /** * Parse command line style arguments from a string * @param {string} str - String containing arguments * @returns {Array} Array of parsed arguments */ export function parseArgs(str) { const args = []; let current = ''; let inQuotes = false; let quoteChar = ''; for (let i = 0; i < str.length; i++) { const char = str[i]; if ((char === '"' || char === "'") && !inQuotes) { inQuotes = true; quoteChar = char; } else if (char === quoteChar && inQuotes) { inQuotes = false; quoteChar = ''; } else if (char === ' ' && !inQuotes) { if (current) { args.push(current); current = ''; } } else { current += char; } } if (current) { args.push(current); } return args; } /** * Retry a function with exponential backoff * @param {Function} fn - Function to retry * @param {number} maxRetries - Maximum number of retries * @param {number} baseDelay - Base delay in milliseconds * @returns {Promise} Promise that resolves with function result */ export async function retryWithBackoff(fn, maxRetries = 3, baseDelay = 1000) { let lastError; for (let attempt = 0; attempt <= maxRetries; attempt++) { try { return await fn(); } catch (error) { lastError = error; if (attempt === maxRetries) { throw lastError; } const delay = baseDelay * Math.pow(2, attempt); await new Promise(resolve => setTimeout(resolve, delay)); } } }