diff --git a/package.json b/package.json index c4a0075..3df0745 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "test": "npm run test:lighthouse && npm run test:playwright", "test:playwright": "npx playwright test", "test:lighthouse": "node tests/lighthouse.js", + "test:headers": "playwright test tests/headers.spec.js", "setup": "npx playwright install" }, "repository": { @@ -23,7 +24,13 @@ "license": "ISC", "devDependencies": { "@playwright/test": "^1.42.1", + "chrome-launcher": "^1.1.2", "lighthouse": "^11.4.0", "puppeteer": "^22.4.1" + }, + "dependencies": { + "express": "^4.18.2", + "lighthouse": "^11.6.0", + "playwright": "^1.42.1" } } diff --git a/tests/headers.spec.js b/tests/headers.spec.js new file mode 100644 index 0000000..1d65b97 --- /dev/null +++ b/tests/headers.spec.js @@ -0,0 +1,49 @@ +const { test, expect } = require('@playwright/test'); + +test.describe('Security Headers Tests', () => { + test('should have all required security headers', async ({ page }) => { + // Navigate to the page + await page.goto('http://localhost:8080'); + + // Get response headers + const response = await page.waitForResponse('http://localhost:8080'); + const headers = response.headers(); + + // Define required headers and their expected values + const requiredHeaders = { + 'Content-Security-Policy': expect.stringContaining("default-src 'self'"), + 'X-Content-Type-Options': 'nosniff', + 'X-Frame-Options': 'DENY', + 'X-XSS-Protection': '1; mode=block', + 'Referrer-Policy': 'strict-origin-when-cross-origin', + 'Permissions-Policy': expect.stringContaining('geolocation=()'), + 'Strict-Transport-Security': expect.stringContaining('max-age=31536000'), + }; + + // Check each required header + for (const [header, expectedValue] of Object.entries(requiredHeaders)) { + const headerValue = headers[header.toLowerCase()]; + expect(headerValue).toBeDefined(); + if (typeof expectedValue === 'string') { + expect(headerValue).toBe(expectedValue); + } else { + expect(headerValue).toMatch(expectedValue); + } + } + }); + + test('should have correct CSP directives', async ({ page }) => { + await page.goto('http://localhost:8080'); + const response = await page.waitForResponse('http://localhost:8080'); + const headers = response.headers(); + const csp = headers['content-security-policy']; + + // Check for essential CSP directives + expect(csp).toContain("default-src 'self'"); + expect(csp).toContain("script-src 'self' 'unsafe-inline'"); + expect(csp).toContain("style-src 'self' 'unsafe-inline'"); + expect(csp).toContain("img-src 'self' data: https: http:"); + expect(csp).toContain("font-src 'self'"); + expect(csp).toContain("connect-src 'self'"); + }); +}); \ No newline at end of file diff --git a/tests/server.js b/tests/server.js new file mode 100644 index 0000000..25f1211 --- /dev/null +++ b/tests/server.js @@ -0,0 +1,35 @@ +const express = require('express'); +const path = require('path'); +const app = express(); +const port = 8080; + +// Security headers middleware +app.use((req, res, next) => { + // Content Security Policy + res.setHeader( + 'Content-Security-Policy', + "default-src 'self'; " + + "script-src 'self' 'unsafe-inline'; " + + "style-src 'self' 'unsafe-inline'; " + + "img-src 'self' data: https: http:; " + + "font-src 'self'; " + + "connect-src 'self'" + ); + + // Other security headers + res.setHeader('X-Content-Type-Options', 'nosniff'); + res.setHeader('X-Frame-Options', 'DENY'); + res.setHeader('X-XSS-Protection', '1; mode=block'); + res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin'); + res.setHeader('Permissions-Policy', 'geolocation=(), microphone=(), camera=()'); + res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains'); + + next(); +}); + +// Serve static files from the docker/resume directory +app.use(express.static(path.join(__dirname, '../docker/resume'))); + +app.listen(port, () => { + console.log(`Local development server running at http://localhost:${port}`); +}); \ No newline at end of file