/** * HVAC Testing Framework - Secure Browser Management * * Provides secure browser configuration with hardened security settings, * SSL/TLS validation, and secure authentication handling for Playwright. * * Security Features: * - Hardened browser security configuration * - SSL/TLS certificate validation * - Secure authentication and session management * - Network request filtering and monitoring * - Screenshot and trace security controls * * @author Claude Code - Emergency Security Response * @version 1.0.0 * @security CRITICAL - Provides secure browser automation */ const { chromium, firefox, webkit } = require('playwright'); const fs = require('fs').promises; const path = require('path'); const { getCredentialManager } = require('./SecureCredentialManager'); class SecureBrowserManager { constructor() { this.credentialManager = getCredentialManager(); this.activeBrowsers = new Map(); this.activeContexts = new Map(); this.securityConfig = this.loadSecurityConfiguration(); } /** * Load security configuration from environment * @returns {Object} Security configuration */ loadSecurityConfiguration() { return { // Browser security settings headless: process.env.PLAYWRIGHT_HEADLESS !== 'false', slowMo: parseInt(process.env.PLAYWRIGHT_SLOW_MO) || 0, timeout: parseInt(process.env.PLAYWRIGHT_TIMEOUT) || 30000, // SSL/TLS settings tlsValidationMode: process.env.TLS_VALIDATION_MODE || 'strict', ignoreCertificateErrors: process.env.TLS_VALIDATION_MODE === 'permissive', // Security features enableNetworkTracing: process.env.ENABLE_NETWORK_TRACE === 'true', screenshotOnFailure: process.env.SCREENSHOT_ON_FAILURE !== 'false', // Resource limits maxMemoryMB: parseInt(process.env.MAX_BROWSER_MEMORY) || 512, maxDiskMB: parseInt(process.env.MAX_BROWSER_DISK) || 100, // Results directories resultsDir: process.env.TEST_RESULTS_DIR || './test-results', screenshotsDir: process.env.TEST_SCREENSHOTS_DIR || './test-screenshots' }; } /** * Create secure browser instance with hardened configuration * @param {string} browserType - Browser type (chromium, firefox, webkit) * @param {Object} options - Additional browser options * @returns {Promise} Browser instance with security metadata */ async createSecureBrowser(browserType = 'chromium', options = {}) { const browsers = { chromium, firefox, webkit }; const browserEngine = browsers[browserType]; if (!browserEngine) { throw new Error(`Unsupported browser type: ${browserType}`); } // Build secure launch options const launchOptions = this.buildSecureLaunchOptions(browserType, options); // Launch browser with security configuration const browser = await browserEngine.launch(launchOptions); const browserId = this.generateBrowserId(); // Store browser reference this.activeBrowsers.set(browserId, { browser, browserType, created: new Date().toISOString(), options: launchOptions }); // Log browser creation await this.credentialManager.auditLogger('BROWSER_CREATED', { browserId, browserType, headless: launchOptions.headless, securityFeatures: this.getSecurityFeaturesSummary(launchOptions) }); return { browser, browserId, createSecureContext: (contextOptions = {}) => this.createSecureContext(browserId, contextOptions) }; } /** * Build secure browser launch options * @param {string} browserType - Browser type * @param {Object} userOptions - User-provided options * @returns {Object} Secure launch options */ buildSecureLaunchOptions(browserType, userOptions) { const baseOptions = { headless: this.securityConfig.headless, slowMo: this.securityConfig.slowMo, timeout: this.securityConfig.timeout }; // Browser-specific security hardening if (browserType === 'chromium') { baseOptions.args = this.buildSecureChromiumArgs(); } else if (browserType === 'firefox') { baseOptions.firefoxUserPrefs = this.buildSecureFirefoxPrefs(); } // SSL/TLS configuration baseOptions.ignoreHTTPSErrors = this.securityConfig.ignoreCertificateErrors; // Merge with user options (security options take precedence) const mergedOptions = { ...userOptions, ...baseOptions }; // Validate security-critical options this.validateSecurityOptions(mergedOptions); return mergedOptions; } /** * Build secure Chromium arguments * @returns {Array} Secure Chrome arguments */ buildSecureChromiumArgs() { const secureArgs = [ // Security hardening '--disable-web-security=false', // Enable web security '--disable-features=TranslateUI', '--disable-ipc-flooding-protection=false', '--disable-renderer-backgrounding', '--disable-backgrounding-occluded-windows', '--disable-background-timer-throttling', '--disable-component-extensions-with-background-pages', // Memory and resource limits '--max-old-space-size=512', '--memory-pressure-off', // Network security '--no-proxy-server', '--disable-sync', '--disable-translate', // Content security '--disable-plugins', '--disable-flash-3d', '--disable-flash-stage3d' ]; // Add sandbox control based on environment if (process.env.CONTAINER_MODE === 'true') { // In containers, we need to disable sandbox due to user namespace issues secureArgs.push('--no-sandbox', '--disable-setuid-sandbox'); console.warn('⚠️ Running in container mode with reduced sandbox security'); } else { // In normal environments, keep sandbox enabled console.log('✅ Running with full sandbox security'); } return secureArgs; } /** * Build secure Firefox preferences * @returns {Object} Secure Firefox preferences */ buildSecureFirefoxPrefs() { return { // Security preferences 'security.tls.insecure_fallback_hosts': '', 'security.mixed_content.block_active_content': true, 'security.mixed_content.block_display_content': true, // Privacy preferences 'privacy.trackingprotection.enabled': true, 'privacy.donottrackheader.enabled': true, // Network preferences 'network.http.sendOriginHeader': 1, 'network.cookie.cookieBehavior': 1 }; } /** * Validate security-critical browser options * @param {Object} options - Browser options to validate */ validateSecurityOptions(options) { // Check for dangerous options const dangerousOptions = [ 'devtools', // Don't allow devtools in automated testing 'userDataDir' // Prevent data persistence ]; for (const dangerous of dangerousOptions) { if (options[dangerous]) { throw new Error(`Security violation: ${dangerous} option not allowed`); } } // Validate arguments for security if (options.args) { const dangerousArgs = [ '--disable-web-security', '--allow-running-insecure-content', '--disable-security-warnings' ]; for (const arg of options.args) { if (dangerousArgs.some(dangerous => arg.includes(dangerous))) { throw new Error(`Security violation: dangerous argument not allowed: ${arg}`); } } } } /** * Create secure browser context with authentication and security controls * @param {string} browserId - Browser ID * @param {Object} contextOptions - Context options * @returns {Promise} Secure context with authentication helpers */ async createSecureContext(browserId, contextOptions = {}) { const browserInfo = this.activeBrowsers.get(browserId); if (!browserInfo) { throw new Error('Browser not found'); } // Build secure context options const secureContextOptions = this.buildSecureContextOptions(contextOptions); // Create context const context = await browserInfo.browser.newContext(secureContextOptions); const contextId = this.generateContextId(); // Store context reference this.activeContexts.set(contextId, { context, browserId, created: new Date().toISOString(), options: secureContextOptions }); // Set up security monitoring await this.setupSecurityMonitoring(context, contextId); // Create authentication helpers const authHelpers = this.createAuthenticationHelpers(context, contextId); await this.credentialManager.auditLogger('BROWSER_CONTEXT_CREATED', { contextId, browserId, securityFeatures: this.getContextSecurityFeatures(secureContextOptions) }); return { context, contextId, ...authHelpers, createSecurePage: () => this.createSecurePage(contextId) }; } /** * Build secure context options * @param {Object} userOptions - User context options * @returns {Object} Secure context options */ buildSecureContextOptions(userOptions) { const baseOptions = { viewport: { width: 1280, height: 720 }, userAgent: 'HVAC-Testing-Framework/1.0 (Security-Hardened)', ignoreHTTPSErrors: this.securityConfig.ignoreCertificateErrors, // Permissions and security permissions: [], // No permissions by default geolocation: undefined, // No geolocation locale: 'en-US', // Recording options (secure) recordVideo: undefined, // No video recording for security recordHar: this.securityConfig.enableNetworkTracing ? { mode: 'minimal', content: 'omit' // Don't record response bodies } : undefined }; // Add base URL from secure configuration baseOptions.baseURL = this.credentialManager.getBaseUrl(); return { ...baseOptions, ...userOptions }; } /** * Set up security monitoring for browser context * @param {Object} context - Browser context * @param {string} contextId - Context ID */ async setupSecurityMonitoring(context, contextId) { // Monitor network requests for security context.on('request', async (request) => { const url = request.url(); // Block dangerous requests if (this.isBlockedRequest(url)) { await request.abort('blockedbyclient'); await this.credentialManager.auditLogger('REQUEST_BLOCKED', { contextId, url, reason: 'security_policy' }); return; } // Log sensitive requests if (this.isSensitiveRequest(url)) { await this.credentialManager.auditLogger('SENSITIVE_REQUEST', { contextId, url: this.sanitizeUrlForLogging(url), method: request.method() }); } }); // Monitor responses for errors context.on('response', async (response) => { if (response.status() >= 400) { await this.credentialManager.auditLogger('HTTP_ERROR', { contextId, url: this.sanitizeUrlForLogging(response.url()), status: response.status() }); } }); // Monitor console messages for security issues context.on('console', async (message) => { const text = message.text(); if (this.isSecurityRelevantConsoleMessage(text)) { await this.credentialManager.auditLogger('SECURITY_CONSOLE_MESSAGE', { contextId, type: message.type(), message: this.sanitizeConsoleMessage(text) }); } }); } /** * Create authentication helpers for secure login * @param {Object} context - Browser context * @param {string} contextId - Context ID * @returns {Object} Authentication helper methods */ createAuthenticationHelpers(context, contextId) { return { /** * Authenticate as a specific user role * @param {string} role - User role * @returns {Promise} Authentication result */ authenticateAs: async (role) => { const session = this.credentialManager.createSecureSession(role); const credentials = this.credentialManager.getSessionCredentials(session.sessionId); const page = await context.newPage(); try { // Navigate to login page const loginUrl = `${this.credentialManager.getBaseUrl()}/training-login/`; await page.goto(loginUrl, { waitUntil: 'networkidle' }); // Perform secure login await this.performSecureLogin(page, credentials, contextId); // Verify authentication await this.verifyAuthentication(page, credentials.role, contextId); await this.credentialManager.auditLogger('AUTHENTICATION_SUCCESS', { contextId, role: credentials.role, sessionId: session.sessionId }); return { success: true, role: credentials.role, sessionId: session.sessionId, page }; } catch (error) { await page.close(); this.credentialManager.destroySession(session.sessionId); await this.credentialManager.auditLogger('AUTHENTICATION_FAILED', { contextId, role, error: error.message }); throw error; } }, /** * Logout and clean up session * @param {string} sessionId - Session ID to destroy * @returns {Promise} */ logout: async (sessionId) => { if (sessionId) { this.credentialManager.destroySession(sessionId); await this.credentialManager.auditLogger('LOGOUT_COMPLETED', { contextId, sessionId }); } } }; } /** * Perform secure login with credential validation * @param {Object} page - Playwright page * @param {Object} credentials - User credentials * @param {string} contextId - Context ID */ async performSecureLogin(page, credentials, contextId) { // Look for login form elements const usernameSelectors = [ 'input[name="log"]', 'input[name="username"]', '#user_login', '#username' ]; const passwordSelectors = [ 'input[name="pwd"]', 'input[name="password"]', '#user_pass', '#password' ]; // Find and fill username let usernameField = null; for (const selector of usernameSelectors) { try { usernameField = page.locator(selector); if (await usernameField.count() > 0) { await usernameField.fill(credentials.username); break; } } catch (e) { continue; } } if (!usernameField || await usernameField.count() === 0) { throw new Error('Username field not found'); } // Find and fill password let passwordField = null; for (const selector of passwordSelectors) { try { passwordField = page.locator(selector); if (await passwordField.count() > 0) { await passwordField.fill(credentials.password); break; } } catch (e) { continue; } } if (!passwordField || await passwordField.count() === 0) { throw new Error('Password field not found'); } // Submit login form const submitSelectors = [ 'button[type="submit"]', 'input[type="submit"]', '#wp-submit', '.wp-submit' ]; let submitted = false; for (const selector of submitSelectors) { try { const submitButton = page.locator(selector); if (await submitButton.count() > 0) { await submitButton.click(); submitted = true; break; } } catch (e) { continue; } } if (!submitted) { throw new Error('Submit button not found'); } // Wait for navigation await page.waitForLoadState('networkidle', { timeout: 15000 }); } /** * Verify successful authentication * @param {Object} page - Playwright page * @param {string} expectedRole - Expected user role * @param {string} contextId - Context ID */ async verifyAuthentication(page, expectedRole, contextId) { // Check for authentication success indicators const currentUrl = page.url(); // Should not be on login page after successful authentication if (currentUrl.includes('/training-login/') || currentUrl.includes('/wp-login.php')) { throw new Error('Authentication failed - still on login page'); } // Role-specific URL verification const roleUrlPatterns = { 'hvac_master_trainer': /\/master-trainer\//, 'hvac_trainer': /\/trainer\//, 'administrator': /\/wp-admin\// }; if (roleUrlPatterns[expectedRole] && !roleUrlPatterns[expectedRole].test(currentUrl)) { console.warn(`Authentication successful but unexpected URL pattern for role ${expectedRole}: ${currentUrl}`); } // Additional verification by checking page content const bodyText = await page.textContent('body'); // Should not contain login-related error messages const errorIndicators = [ 'invalid username', 'invalid password', 'login failed', 'authentication error' ]; for (const indicator of errorIndicators) { if (bodyText.toLowerCase().includes(indicator)) { throw new Error(`Authentication failed: ${indicator} detected`); } } } /** * Create secure page with monitoring and security controls * @param {string} contextId - Context ID * @returns {Promise} Secure page with helpers */ async createSecurePage(contextId) { const contextInfo = this.activeContexts.get(contextId); if (!contextInfo) { throw new Error('Context not found'); } const page = await contextInfo.context.newPage(); // Set security headers await page.setExtraHTTPHeaders({ 'X-Security-Test': 'HVAC-Framework', 'Cache-Control': 'no-cache, no-store' }); return { page, secureGoto: (url, options = {}) => this.securePageGoto(page, url, options, contextId), secureScreenshot: (options = {}) => this.secureScreenshot(page, options, contextId) }; } /** * Secure page navigation with URL validation * @param {Object} page - Playwright page * @param {string} url - URL to navigate to * @param {Object} options - Navigation options * @param {string} contextId - Context ID */ async securePageGoto(page, url, options, contextId) { // Validate URL if (!this.isAllowedUrl(url)) { throw new Error(`URL not allowed: ${url}`); } // Navigate with security monitoring const response = await page.goto(url, { waitUntil: 'networkidle', timeout: this.securityConfig.timeout, ...options }); // Log navigation await this.credentialManager.auditLogger('PAGE_NAVIGATION', { contextId, url: this.sanitizeUrlForLogging(url), status: response ? response.status() : 'unknown' }); return response; } /** * Take secure screenshot with metadata * @param {Object} page - Playwright page * @param {Object} options - Screenshot options * @param {string} contextId - Context ID */ async secureScreenshot(page, options, contextId) { const filename = options.path || path.join(this.securityConfig.screenshotsDir, `screenshot-${contextId}-${Date.now()}.png`); // Ensure screenshots directory exists await fs.mkdir(path.dirname(filename), { recursive: true }); const screenshot = await page.screenshot({ fullPage: true, ...options, path: filename }); await this.credentialManager.auditLogger('SCREENSHOT_TAKEN', { contextId, filename, size: screenshot.length }); return screenshot; } /** * Utility methods for security validation */ generateBrowserId() { return `browser-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; } generateContextId() { return `context-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; } isBlockedRequest(url) { const blockedPatterns = [ /facebook\.com/, /google-analytics\.com/, /googletagmanager\.com/, /doubleclick\.net/, /\.ads\./ ]; return blockedPatterns.some(pattern => pattern.test(url)); } isSensitiveRequest(url) { return url.includes('login') || url.includes('auth') || url.includes('password'); } isSecurityRelevantConsoleMessage(message) { const securityKeywords = [ 'security', 'error', 'warning', 'blocked', 'cors', 'csp', 'mixed content', 'certificate', 'ssl', 'tls' ]; const lowerMessage = message.toLowerCase(); return securityKeywords.some(keyword => lowerMessage.includes(keyword)); } isAllowedUrl(url) { const baseUrl = this.credentialManager.getBaseUrl(); return url.startsWith(baseUrl) || url.startsWith('data:') || url.startsWith('about:'); } sanitizeUrlForLogging(url) { return url.replace(/[?&](password|pwd|token|key)=[^&]+/gi, '$1=***'); } sanitizeConsoleMessage(message) { return message.replace(/password[=:]\s*[^\s]+/gi, 'password=***') .replace(/token[=:]\s*[^\s]+/gi, 'token=***'); } getSecurityFeaturesSummary(options) { return { headless: options.headless, sandboxEnabled: !options.args?.includes('--no-sandbox'), tlsValidation: !options.ignoreHTTPSErrors }; } getContextSecurityFeatures(options) { return { httpsOnly: !options.ignoreHTTPSErrors, permissionsLimited: options.permissions?.length === 0, networkMonitoring: !!options.recordHar }; } /** * Clean up all browsers and contexts */ async cleanup() { for (const [contextId, contextInfo] of this.activeContexts) { try { await contextInfo.context.close(); } catch (error) { console.warn(`Failed to close context ${contextId}:`, error.message); } } this.activeContexts.clear(); for (const [browserId, browserInfo] of this.activeBrowsers) { try { await browserInfo.browser.close(); } catch (error) { console.warn(`Failed to close browser ${browserId}:`, error.message); } } this.activeBrowsers.clear(); await this.credentialManager.auditLogger('BROWSER_CLEANUP_COMPLETED'); } } // Singleton instance let browserManagerInstance = null; /** * Get singleton instance of SecureBrowserManager * @returns {SecureBrowserManager} */ function getBrowserManager() { if (!browserManagerInstance) { browserManagerInstance = new SecureBrowserManager(); } return browserManagerInstance; } module.exports = { SecureBrowserManager, getBrowserManager };