Some checks are pending
HVAC Plugin CI/CD Pipeline / Security Analysis (push) Waiting to run
HVAC Plugin CI/CD Pipeline / Code Quality & Standards (push) Waiting to run
HVAC Plugin CI/CD Pipeline / Unit Tests (push) Waiting to run
HVAC Plugin CI/CD Pipeline / Integration Tests (push) Waiting to run
HVAC Plugin CI/CD Pipeline / Deploy to Staging (push) Blocked by required conditions
HVAC Plugin CI/CD Pipeline / Deploy to Production (push) Blocked by required conditions
HVAC Plugin CI/CD Pipeline / Notification (push) Blocked by required conditions
Security Monitoring & Compliance / Dependency Vulnerability Scan (push) Waiting to run
Security Monitoring & Compliance / Secrets & Credential Scan (push) Waiting to run
Security Monitoring & Compliance / WordPress Security Analysis (push) Waiting to run
Security Monitoring & Compliance / Static Code Security Analysis (push) Waiting to run
Security Monitoring & Compliance / Security Compliance Validation (push) Waiting to run
Security Monitoring & Compliance / Security Summary Report (push) Blocked by required conditions
Security Monitoring & Compliance / Security Team Notification (push) Blocked by required conditions
- Add 90+ test files including E2E, unit, and integration tests - Implement Page Object Model (POM) architecture - Add Docker testing environment with comprehensive services - Include modernized test framework with error recovery - Add specialized test suites for master trainer and trainer workflows - Update .gitignore to properly track test infrastructure 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
605 lines
No EOL
20 KiB
JavaScript
605 lines
No EOL
20 KiB
JavaScript
/**
|
|
* Security Test Framework
|
|
* Comprehensive security testing utilities for WordPress HVAC plugin
|
|
*/
|
|
|
|
const { getBrowserManager } = require('../../framework/browser/BrowserManager');
|
|
const { getAuthManager } = require('../../framework/authentication/AuthManager');
|
|
const BasePage = require('../../framework/base/BasePage');
|
|
|
|
class SecurityTestFramework {
|
|
constructor(config = {}) {
|
|
this.config = {
|
|
baseUrl: 'https://upskill-staging.measurequick.com',
|
|
timeout: 30000,
|
|
screenshotOnFailure: true,
|
|
...config
|
|
};
|
|
|
|
this.browserManager = getBrowserManager();
|
|
this.authManager = getAuthManager();
|
|
this.testResults = [];
|
|
}
|
|
|
|
/**
|
|
* Initialize security test framework
|
|
* @param {Object} config - Configuration object
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async initialize(config = {}) {
|
|
this.config = { ...this.config, ...config };
|
|
await this.browserManager.initialize(this.config);
|
|
await this.authManager.initialize(this.config);
|
|
}
|
|
|
|
/**
|
|
* Test authentication redirects for protected URLs
|
|
* @param {string[]} protectedUrls - Array of URLs that should require authentication
|
|
* @returns {Promise<Object>}
|
|
*/
|
|
async testAuthenticationRedirects(protectedUrls) {
|
|
console.log('🔒 Testing authentication redirects...');
|
|
|
|
const results = {
|
|
passed: 0,
|
|
failed: 0,
|
|
tests: []
|
|
};
|
|
|
|
for (const url of protectedUrls) {
|
|
const testResult = await this.testSingleUrlAuthRedirect(url);
|
|
results.tests.push(testResult);
|
|
|
|
if (testResult.passed) {
|
|
results.passed++;
|
|
} else {
|
|
results.failed++;
|
|
}
|
|
}
|
|
|
|
console.log(` ✅ Passed: ${results.passed}, ❌ Failed: ${results.failed}`);
|
|
return results;
|
|
}
|
|
|
|
/**
|
|
* Test single URL for proper authentication redirect
|
|
* @param {string} url - URL to test
|
|
* @returns {Promise<Object>}
|
|
* @private
|
|
*/
|
|
async testSingleUrlAuthRedirect(url) {
|
|
const fullUrl = url.startsWith('http') ? url : this.config.baseUrl + url;
|
|
const testResult = {
|
|
url: url,
|
|
fullUrl: fullUrl,
|
|
passed: false,
|
|
statusCode: null,
|
|
redirectUrl: null,
|
|
error: null
|
|
};
|
|
|
|
try {
|
|
console.log(` 🔍 Testing: ${url}`);
|
|
|
|
const page = this.browserManager.getCurrentPage();
|
|
|
|
// Navigate to protected URL without authentication
|
|
const response = await page.goto(fullUrl, {
|
|
waitUntil: 'networkidle',
|
|
timeout: this.config.timeout
|
|
});
|
|
|
|
testResult.statusCode = response.status();
|
|
testResult.redirectUrl = page.url();
|
|
|
|
// Check if properly redirected to login
|
|
const isRedirected = this.isRedirectedToLogin(testResult.redirectUrl, fullUrl);
|
|
|
|
// Check for proper status codes
|
|
const hasValidStatus = response.status() === 302 ||
|
|
response.status() === 200 && isRedirected;
|
|
|
|
testResult.passed = isRedirected && hasValidStatus;
|
|
|
|
if (testResult.passed) {
|
|
console.log(` ✅ Properly redirected to login`);
|
|
} else {
|
|
console.log(` ❌ Not properly protected - Status: ${response.status()}, URL: ${page.url()}`);
|
|
|
|
if (this.config.screenshotOnFailure) {
|
|
await this.takeSecurityScreenshot(`auth-fail-${url.replace(/[^a-zA-Z0-9]/g, '-')}.png`);
|
|
}
|
|
}
|
|
|
|
} catch (error) {
|
|
testResult.error = error.message;
|
|
console.log(` ❌ Error testing ${url}: ${error.message}`);
|
|
}
|
|
|
|
return testResult;
|
|
}
|
|
|
|
/**
|
|
* Check if URL is redirected to login page
|
|
* @param {string} currentUrl - Current page URL
|
|
* @param {string} originalUrl - Original requested URL
|
|
* @returns {boolean}
|
|
* @private
|
|
*/
|
|
isRedirectedToLogin(currentUrl, originalUrl) {
|
|
// If still on the same URL, not redirected
|
|
if (currentUrl === originalUrl) {
|
|
return false;
|
|
}
|
|
|
|
// Check for login page indicators
|
|
const loginIndicators = [
|
|
'community-login',
|
|
'wp-login.php',
|
|
'login',
|
|
'auth',
|
|
'signin'
|
|
];
|
|
|
|
return loginIndicators.some(indicator =>
|
|
currentUrl.toLowerCase().includes(indicator.toLowerCase())
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Test role-based access control
|
|
* @param {Object} testScenarios - Test scenarios with roles and expected access
|
|
* @returns {Promise<Object>}
|
|
*/
|
|
async testRoleBasedAccess(testScenarios) {
|
|
console.log('👥 Testing role-based access control...');
|
|
|
|
const results = {
|
|
passed: 0,
|
|
failed: 0,
|
|
tests: []
|
|
};
|
|
|
|
for (const scenario of testScenarios) {
|
|
const testResult = await this.testRoleAccess(scenario);
|
|
results.tests.push(testResult);
|
|
|
|
if (testResult.passed) {
|
|
results.passed++;
|
|
} else {
|
|
results.failed++;
|
|
}
|
|
}
|
|
|
|
console.log(` ✅ Passed: ${results.passed}, ❌ Failed: ${results.failed}`);
|
|
return results;
|
|
}
|
|
|
|
/**
|
|
* Test access control for specific role
|
|
* @param {Object} scenario - Test scenario
|
|
* @returns {Promise<Object>}
|
|
* @private
|
|
*/
|
|
async testRoleAccess(scenario) {
|
|
const { user, url, expectedAccess } = scenario;
|
|
const testResult = {
|
|
user: user.username,
|
|
role: user.role,
|
|
url: url,
|
|
expectedAccess: expectedAccess,
|
|
actualAccess: false,
|
|
passed: false,
|
|
error: null
|
|
};
|
|
|
|
try {
|
|
console.log(` 🔍 Testing ${user.role} access to ${url}`);
|
|
|
|
// Login as specified user
|
|
await this.authManager.login(user);
|
|
|
|
const page = this.browserManager.getCurrentPage();
|
|
const fullUrl = url.startsWith('http') ? url : this.config.baseUrl + url;
|
|
|
|
// Navigate to URL
|
|
const response = await page.goto(fullUrl, {
|
|
waitUntil: 'networkidle',
|
|
timeout: this.config.timeout
|
|
});
|
|
|
|
// Determine if user has access
|
|
testResult.actualAccess = this.determineAccess(response, page.url(), fullUrl);
|
|
testResult.passed = testResult.actualAccess === expectedAccess;
|
|
|
|
if (testResult.passed) {
|
|
console.log(` ✅ Access control working correctly`);
|
|
} else {
|
|
console.log(` ❌ Access control failed - Expected: ${expectedAccess}, Actual: ${testResult.actualAccess}`);
|
|
|
|
if (this.config.screenshotOnFailure) {
|
|
await this.takeSecurityScreenshot(`role-fail-${user.role}-${url.replace(/[^a-zA-Z0-9]/g, '-')}.png`);
|
|
}
|
|
}
|
|
|
|
// Logout
|
|
await this.authManager.logout();
|
|
|
|
} catch (error) {
|
|
testResult.error = error.message;
|
|
console.log(` ❌ Error testing role access: ${error.message}`);
|
|
}
|
|
|
|
return testResult;
|
|
}
|
|
|
|
/**
|
|
* Determine if user has access based on response and URL
|
|
* @param {Response} response - Page response
|
|
* @param {string} currentUrl - Current page URL
|
|
* @param {string} requestedUrl - Originally requested URL
|
|
* @returns {boolean}
|
|
* @private
|
|
*/
|
|
determineAccess(response, currentUrl, requestedUrl) {
|
|
// If redirected to login, no access
|
|
if (this.isRedirectedToLogin(currentUrl, requestedUrl)) {
|
|
return false;
|
|
}
|
|
|
|
// If status is 403 (Forbidden), no access
|
|
if (response.status() === 403) {
|
|
return false;
|
|
}
|
|
|
|
// If status is 404 (Not Found), might indicate access control
|
|
if (response.status() === 404) {
|
|
return false;
|
|
}
|
|
|
|
// If status is 200 and on the same URL, has access
|
|
if (response.status() === 200 && currentUrl === requestedUrl) {
|
|
return true;
|
|
}
|
|
|
|
// Default to no access for safety
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Test for common WordPress security vulnerabilities
|
|
* @returns {Promise<Object>}
|
|
*/
|
|
async testCommonVulnerabilities() {
|
|
console.log('🛡️ Testing common security vulnerabilities...');
|
|
|
|
const results = {
|
|
passed: 0,
|
|
failed: 0,
|
|
tests: []
|
|
};
|
|
|
|
// Test directory browsing
|
|
const directoryTest = await this.testDirectoryBrowsing();
|
|
results.tests.push(directoryTest);
|
|
|
|
// Test file inclusion vulnerabilities
|
|
const fileInclusionTest = await this.testFileInclusion();
|
|
results.tests.push(fileInclusionTest);
|
|
|
|
// Test WordPress version disclosure
|
|
const versionDisclosureTest = await this.testVersionDisclosure();
|
|
results.tests.push(versionDisclosureTest);
|
|
|
|
// Test admin access without authentication
|
|
const adminAccessTest = await this.testUnauthenticatedAdminAccess();
|
|
results.tests.push(adminAccessTest);
|
|
|
|
// Count results
|
|
results.tests.forEach(test => {
|
|
if (test.passed) results.passed++;
|
|
else results.failed++;
|
|
});
|
|
|
|
console.log(` ✅ Passed: ${results.passed}, ❌ Failed: ${results.failed}`);
|
|
return results;
|
|
}
|
|
|
|
/**
|
|
* Test directory browsing vulnerabilities
|
|
* @returns {Promise<Object>}
|
|
* @private
|
|
*/
|
|
async testDirectoryBrowsing() {
|
|
const testResult = {
|
|
testName: 'Directory Browsing',
|
|
passed: true,
|
|
vulnerabilities: []
|
|
};
|
|
|
|
const directoriesToTest = [
|
|
'/wp-content/',
|
|
'/wp-content/plugins/',
|
|
'/wp-content/themes/',
|
|
'/wp-content/uploads/',
|
|
'/wp-includes/',
|
|
'/wp-admin/'
|
|
];
|
|
|
|
for (const dir of directoriesToTest) {
|
|
try {
|
|
const page = this.browserManager.getCurrentPage();
|
|
const response = await page.goto(this.config.baseUrl + dir, { timeout: 10000 });
|
|
|
|
if (response.status() === 200) {
|
|
const content = await page.content();
|
|
if (content.includes('Index of') || content.includes('Directory listing')) {
|
|
testResult.passed = false;
|
|
testResult.vulnerabilities.push(`Directory browsing enabled for ${dir}`);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
// Directory might be properly protected - continue
|
|
}
|
|
}
|
|
|
|
return testResult;
|
|
}
|
|
|
|
/**
|
|
* Test file inclusion vulnerabilities
|
|
* @returns {Promise<Object>}
|
|
* @private
|
|
*/
|
|
async testFileInclusion() {
|
|
const testResult = {
|
|
testName: 'File Inclusion',
|
|
passed: true,
|
|
vulnerabilities: []
|
|
};
|
|
|
|
const payloads = [
|
|
'../../../../etc/passwd',
|
|
'..\\..\\..\\..\\windows\\system32\\drivers\\etc\\hosts',
|
|
'php://filter/read=convert.base64-encode/resource=wp-config.php'
|
|
];
|
|
|
|
for (const payload of payloads) {
|
|
try {
|
|
const page = this.browserManager.getCurrentPage();
|
|
const testUrl = `${this.config.baseUrl}/?file=${encodeURIComponent(payload)}`;
|
|
const response = await page.goto(testUrl, { timeout: 10000 });
|
|
|
|
if (response.status() === 200) {
|
|
const content = await page.content();
|
|
if (content.includes('root:') || content.includes('# localhost')) {
|
|
testResult.passed = false;
|
|
testResult.vulnerabilities.push(`File inclusion vulnerability with payload: ${payload}`);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
// Error might indicate protection - continue
|
|
}
|
|
}
|
|
|
|
return testResult;
|
|
}
|
|
|
|
/**
|
|
* Test WordPress version disclosure
|
|
* @returns {Promise<Object>}
|
|
* @private
|
|
*/
|
|
async testVersionDisclosure() {
|
|
const testResult = {
|
|
testName: 'Version Disclosure',
|
|
passed: true,
|
|
disclosures: []
|
|
};
|
|
|
|
try {
|
|
const page = this.browserManager.getCurrentPage();
|
|
await page.goto(this.config.baseUrl, { timeout: 10000 });
|
|
|
|
const content = await page.content();
|
|
|
|
// Check for WordPress version in meta tags
|
|
const versionRegex = /<meta name="generator" content="WordPress (\d+\.\d+(?:\.\d+)?)"/i;
|
|
const versionMatch = content.match(versionRegex);
|
|
|
|
if (versionMatch) {
|
|
testResult.passed = false;
|
|
testResult.disclosures.push(`WordPress version disclosed in meta tag: ${versionMatch[1]}`);
|
|
}
|
|
|
|
// Check readme.html file
|
|
try {
|
|
await page.goto(`${this.config.baseUrl}/readme.html`, { timeout: 10000 });
|
|
const readmeContent = await page.content();
|
|
if (readmeContent.includes('WordPress') && readmeContent.includes('Version')) {
|
|
testResult.passed = false;
|
|
testResult.disclosures.push('WordPress version disclosed in readme.html');
|
|
}
|
|
} catch (error) {
|
|
// Readme might be removed - good practice
|
|
}
|
|
} catch (error) {
|
|
console.warn(`Version disclosure test error: ${error.message}`);
|
|
}
|
|
|
|
return testResult;
|
|
}
|
|
|
|
/**
|
|
* Test unauthenticated admin access
|
|
* @returns {Promise<Object>}
|
|
* @private
|
|
*/
|
|
async testUnauthenticatedAdminAccess() {
|
|
const testResult = {
|
|
testName: 'Unauthenticated Admin Access',
|
|
passed: true,
|
|
vulnerabilities: []
|
|
};
|
|
|
|
try {
|
|
const page = this.browserManager.getCurrentPage();
|
|
const response = await page.goto(`${this.config.baseUrl}/wp-admin/`, { timeout: 10000 });
|
|
|
|
// Should be redirected to login
|
|
if (!this.isRedirectedToLogin(page.url(), `${this.config.baseUrl}/wp-admin/`)) {
|
|
testResult.passed = false;
|
|
testResult.vulnerabilities.push('Admin area accessible without authentication');
|
|
}
|
|
} catch (error) {
|
|
console.warn(`Admin access test error: ${error.message}`);
|
|
}
|
|
|
|
return testResult;
|
|
}
|
|
|
|
/**
|
|
* Test CSRF protection
|
|
* @param {Object} formData - Form data to test
|
|
* @returns {Promise<Object>}
|
|
*/
|
|
async testCSRFProtection(formData) {
|
|
console.log('🔐 Testing CSRF protection...');
|
|
|
|
const testResult = {
|
|
testName: 'CSRF Protection',
|
|
passed: false,
|
|
error: null
|
|
};
|
|
|
|
try {
|
|
// Login first
|
|
await this.authManager.loginAsMasterTrainer();
|
|
|
|
const page = this.browserManager.getCurrentPage();
|
|
await page.goto(formData.formUrl);
|
|
|
|
// Try to submit form without CSRF token
|
|
const response = await page.evaluate(async (data) => {
|
|
const form = new FormData();
|
|
Object.keys(data.fields).forEach(key => {
|
|
form.append(key, data.fields[key]);
|
|
});
|
|
|
|
return await fetch(data.submitUrl, {
|
|
method: 'POST',
|
|
body: form
|
|
});
|
|
}, formData);
|
|
|
|
// If request is rejected (403/400), CSRF protection is working
|
|
testResult.passed = response.status === 403 || response.status === 400;
|
|
|
|
} catch (error) {
|
|
testResult.error = error.message;
|
|
}
|
|
|
|
return testResult;
|
|
}
|
|
|
|
/**
|
|
* Take security-related screenshot
|
|
* @param {string} filename - Screenshot filename
|
|
* @returns {Promise<string>}
|
|
* @private
|
|
*/
|
|
async takeSecurityScreenshot(filename) {
|
|
try {
|
|
return await this.browserManager.takeScreenshot(`security-${filename}`);
|
|
} catch (error) {
|
|
console.warn(`Failed to take security screenshot: ${error.message}`);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate comprehensive security test report
|
|
* @param {Object} allTestResults - All test results
|
|
* @returns {Object}
|
|
*/
|
|
generateSecurityReport(allTestResults) {
|
|
const report = {
|
|
timestamp: new Date().toISOString(),
|
|
baseUrl: this.config.baseUrl,
|
|
totalTests: 0,
|
|
totalPassed: 0,
|
|
totalFailed: 0,
|
|
securityScore: 0,
|
|
categories: {},
|
|
recommendations: [],
|
|
criticalIssues: []
|
|
};
|
|
|
|
// Process results by category
|
|
Object.keys(allTestResults).forEach(category => {
|
|
const categoryResults = allTestResults[category];
|
|
report.categories[category] = {
|
|
tests: categoryResults.tests || [categoryResults],
|
|
passed: categoryResults.passed || (categoryResults.passed ? 1 : 0),
|
|
failed: categoryResults.failed || (categoryResults.passed ? 0 : 1)
|
|
};
|
|
|
|
report.totalTests += report.categories[category].tests.length;
|
|
report.totalPassed += report.categories[category].passed;
|
|
report.totalFailed += report.categories[category].failed;
|
|
});
|
|
|
|
// Calculate security score (0-100)
|
|
report.securityScore = report.totalTests > 0
|
|
? Math.round((report.totalPassed / report.totalTests) * 100)
|
|
: 0;
|
|
|
|
// Generate recommendations based on failures
|
|
this.generateSecurityRecommendations(report, allTestResults);
|
|
|
|
return report;
|
|
}
|
|
|
|
/**
|
|
* Generate security recommendations
|
|
* @param {Object} report - Security report
|
|
* @param {Object} testResults - Test results
|
|
* @private
|
|
*/
|
|
generateSecurityRecommendations(report, testResults) {
|
|
if (report.securityScore < 100) {
|
|
report.recommendations.push('Review and strengthen security configurations');
|
|
}
|
|
|
|
if (testResults.authRedirects && testResults.authRedirects.failed > 0) {
|
|
report.criticalIssues.push('Some protected URLs are not properly secured');
|
|
report.recommendations.push('Implement proper authentication checks for all protected pages');
|
|
}
|
|
|
|
if (testResults.roleAccess && testResults.roleAccess.failed > 0) {
|
|
report.criticalIssues.push('Role-based access control has issues');
|
|
report.recommendations.push('Review and fix user role permissions');
|
|
}
|
|
|
|
if (testResults.vulnerabilities) {
|
|
testResults.vulnerabilities.tests.forEach(test => {
|
|
if (!test.passed && test.vulnerabilities) {
|
|
test.vulnerabilities.forEach(vuln => {
|
|
report.criticalIssues.push(vuln);
|
|
});
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clean up security test framework
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async cleanup() {
|
|
await this.authManager.cleanup();
|
|
await this.browserManager.cleanup();
|
|
}
|
|
}
|
|
|
|
module.exports = SecurityTestFramework; |