upskill-event-manager/lib/security/SecureBrowserManager.js
Ben c3e7fe9140 feat: comprehensive HVAC plugin development framework and modernization
## Major Enhancements

### 🏗️ Architecture & Infrastructure
- Implement comprehensive Docker testing infrastructure with hermetic environment
- Add Forgejo Actions CI/CD pipeline for automated deployments
- Create Page Object Model (POM) testing architecture reducing test duplication by 90%
- Establish security-first development patterns with input validation and output escaping

### 🧪 Testing Framework Modernization
- Migrate 146+ tests from 80 duplicate files to centralized architecture
- Add comprehensive E2E test suites for all user roles and workflows
- Implement WordPress error detection with automatic site health monitoring
- Create robust browser lifecycle management with proper cleanup

### 📚 Documentation & Guides
- Add comprehensive development best practices guide
- Create detailed administrator setup documentation
- Establish user guides for trainers and master trainers
- Document security incident reports and migration guides

### 🔧 Core Plugin Features
- Enhance trainer profile management with certification system
- Improve find trainer functionality with advanced filtering
- Strengthen master trainer area with content management
- Add comprehensive venue and organizer management

### 🛡️ Security & Reliability
- Implement security-first patterns throughout codebase
- Add comprehensive input validation and output escaping
- Create secure credential management system
- Establish proper WordPress role-based access control

### 🎯 WordPress Integration
- Strengthen singleton pattern implementation across all classes
- Enhance template hierarchy with proper WordPress integration
- Improve page manager with hierarchical URL structure
- Add comprehensive shortcode and menu system

### 🔍 Developer Experience
- Add extensive debugging and troubleshooting tools
- Create comprehensive test data seeding scripts
- Implement proper error handling and logging
- Establish consistent code patterns and standards

### 📊 Performance & Optimization
- Optimize database queries and caching strategies
- Improve asset loading and script management
- Enhance template rendering performance
- Streamline user experience across all interfaces

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-29 11:26:10 -03:00

767 lines
No EOL
26 KiB
JavaScript

/**
* 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<Object>} 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<Object>} 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<Object>} 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<void>}
*/
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<Object>} 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
};