Automated Testing Frameworks

Automated Testing Frameworks

Node.js Security Header Testing Suite

const axios = require('axios');
const chalk = require('chalk');

class SecurityHeaderTester {
    constructor(baseUrl) {
        this.baseUrl = baseUrl;
        this.results = {
            passed: [],
            failed: [],
            warnings: []
        };
    }
    
    async runAllTests() {
        console.log(chalk.blue(`\nTesting security headers for: ${this.baseUrl}\n`));
        
        await this.testEndpoint('/', 'Homepage');
        await this.testEndpoint('/api', 'API Endpoint');
        await this.testEndpoint('/admin', 'Admin Panel');
        await this.testEndpoint('/login', 'Login Page');
        
        this.generateReport();
    }
    
    async testEndpoint(path, description) {
        console.log(chalk.yellow(`Testing ${description} (${path})...`));
        
        try {
            const response = await axios.get(this.baseUrl + path, {
                validateStatus: () => true // Don't throw on any status
            });
            
            await this.validateSecurityHeaders(response.headers, path);
            await this.validateCookieSecurity(response.headers['set-cookie'], path);
            await this.validateCSPPolicy(response.headers['content-security-policy'], path);
            
        } catch (error) {
            this.results.failed.push({
                path,
                test: 'Connection',
                message: error.message
            });
        }
    }
    
    async validateSecurityHeaders(headers, path) {
        const requiredHeaders = {
            'content-security-policy': {
                required: true,
                validator: (value) => !value.includes('unsafe-inline') || 'Contains unsafe-inline'
            },
            'x-content-type-options': {
                required: true,
                expectedValue: 'nosniff'
            },
            'x-frame-options': {
                required: true,
                expectedValues: ['DENY', 'SAMEORIGIN']
            },
            'strict-transport-security': {
                required: true,
                validator: (value) => {
                    const maxAge = value.match(/max-age=(\d+)/);
                    return maxAge && parseInt(maxAge[1]) >= 31536000 || 'Max-age less than 1 year';
                }
            },
            'referrer-policy': {
                required: true,
                expectedValues: ['no-referrer', 'strict-origin-when-cross-origin', 'same-origin']
            },
            'permissions-policy': {
                required: false,
                validator: (value) => value.includes('geolocation=()') || 'Geolocation not restricted'
            }
        };
        
        Object.entries(requiredHeaders).forEach(([header, config]) => {
            const value = headers[header];
            
            if (!value && config.required) {
                this.results.failed.push({
                    path,
                    test: header,
                    message: 'Header not set'
                });
            } else if (value) {
                if (config.expectedValue && value !== config.expectedValue) {
                    this.results.failed.push({
                        path,
                        test: header,
                        message: `Expected "${config.expectedValue}", got "${value}"`
                    });
                } else if (config.expectedValues && !config.expectedValues.includes(value)) {
                    this.results.failed.push({
                        path,
                        test: header,
                        message: `Invalid value: "${value}"`
                    });
                } else if (config.validator) {
                    const validationResult = config.validator(value);
                    if (validationResult !== true) {
                        this.results.warnings.push({
                            path,
                            test: header,
                            message: validationResult
                        });
                    } else {
                        this.results.passed.push({
                            path,
                            test: header,
                            value
                        });
                    }
                } else {
                    this.results.passed.push({
                        path,
                        test: header,
                        value
                    });
                }
            }
        });
    }
    
    async validateCookieSecurity(cookies, path) {
        if (!cookies) return;
        
        cookies.forEach(cookie => {
            const hasSecure = cookie.includes('Secure');
            const hasHttpOnly = cookie.includes('HttpOnly');
            const hasSameSite = cookie.includes('SameSite');
            
            const cookieName = cookie.split('=')[0];
            
            if (!hasSecure) {
                this.results.failed.push({
                    path,
                    test: `Cookie: ${cookieName}`,
                    message: 'Missing Secure attribute'
                });
            }
            
            if (!hasHttpOnly && cookieName.toLowerCase().includes('session')) {
                this.results.warnings.push({
                    path,
                    test: `Cookie: ${cookieName}`,
                    message: 'Session cookie missing HttpOnly'
                });
            }
            
            if (!hasSameSite) {
                this.results.warnings.push({
                    path,
                    test: `Cookie: ${cookieName}`,
                    message: 'Missing SameSite attribute'
                });
            }
        });
    }
    
    async validateCSPPolicy(csp, path) {
        if (!csp) return;
        
        const problematicDirectives = {
            'unsafe-inline': 'script-src or style-src contains unsafe-inline',
            'unsafe-eval': 'script-src contains unsafe-eval',
            '*': 'Wildcard source in use',
            'http:': 'Allows HTTP resources'
        };
        
        Object.entries(problematicDirectives).forEach(([pattern, message]) => {
            if (csp.includes(pattern)) {
                this.results.warnings.push({
                    path,
                    test: 'CSP Policy',
                    message
                });
            }
        });
    }
    
    generateReport() {
        console.log(chalk.blue('\n========== Security Headers Test Report ==========\n'));
        
        if (this.results.passed.length > 0) {
            console.log(chalk.green(`✓ Passed Tests: ${this.results.passed.length}`));
            this.results.passed.forEach(result => {
                console.log(chalk.green(`  ✓ ${result.path} - ${result.test}`));
            });
        }
        
        if (this.results.warnings.length > 0) {
            console.log(chalk.yellow(`\n⚠ Warnings: ${this.results.warnings.length}`));
            this.results.warnings.forEach(result => {
                console.log(chalk.yellow(`  ⚠ ${result.path} - ${result.test}: ${result.message}`));
            });
        }
        
        if (this.results.failed.length > 0) {
            console.log(chalk.red(`\n✗ Failed Tests: ${this.results.failed.length}`));
            this.results.failed.forEach(result => {
                console.log(chalk.red(`  ✗ ${result.path} - ${result.test}: ${result.message}`));
            });
        }
        
        const score = this.calculateSecurityScore();
        console.log(chalk.blue(`\nSecurity Score: ${score}/100`));
    }
    
    calculateSecurityScore() {
        const total = this.results.passed.length + this.results.failed.length;
        const passRate = this.results.passed.length / total;
        const warningPenalty = this.results.warnings.length * 2;
        
        return Math.max(0, Math.round(passRate * 100 - warningPenalty));
    }
}

// Usage
const tester = new SecurityHeaderTester('https://example.com');
tester.runAllTests();