javascript

exercises

exercises.js⚡
/**
 * 21.1 Security Fundamentals - Exercises
 *
 * Practice implementing security features
 */

// ============================================
// EXERCISE 1: Build a Complete XSS Filter
// ============================================

/**
 * Create an XSS filter that:
 * - Escapes HTML entities
 * - Removes dangerous tags
 * - Sanitizes URLs
 * - Handles nested attacks
 * - Preserves safe formatting
 */

class XSSFilter {
  // Your implementation here
}

/*
// SOLUTION:
class XSSFilter {
    constructor(options = {}) {
        this.allowedTags = new Set(options.allowedTags || [
            'p', 'br', 'b', 'i', 'u', 'strong', 'em', 
            'ul', 'ol', 'li', 'a', 'span', 'div'
        ]);
        
        this.allowedAttributes = new Map(Object.entries(options.allowedAttributes || {
            'a': ['href', 'title'],
            'span': ['class'],
            'div': ['class'],
            '*': ['class', 'id']
        }));
        
        this.allowedProtocols = new Set(options.allowedProtocols || [
            'http:', 'https:', 'mailto:'
        ]);
    }
    
    // Main sanitize function
    sanitize(html) {
        if (typeof html !== 'string') return '';
        
        // Decode entities first to catch encoded attacks
        html = this.decodeEntities(html);
        
        // Remove script tags and content
        html = html.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '');
        
        // Remove style tags and content
        html = html.replace(/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi, '');
        
        // Remove HTML comments
        html = html.replace(/<!--[\s\S]*?-->/g, '');
        
        // Process remaining HTML
        html = this.processHTML(html);
        
        return html;
    }
    
    // Decode HTML entities
    decodeEntities(str) {
        // Decode common entities
        const entities = {
            '&lt;': '<',
            '&gt;': '>',
            '&amp;': '&',
            '&quot;': '"',
            '&#39;': "'",
            '&#x27;': "'",
            '&#x2F;': '/',
            '&#60;': '<',
            '&#62;': '>'
        };
        
        return str.replace(/&(?:#x?[0-9a-f]+|[a-z]+);/gi, entity => {
            return entities[entity.toLowerCase()] || entity;
        });
    }
    
    // Process HTML tags
    processHTML(html) {
        // Match HTML tags
        const tagRegex = /<\/?([a-zA-Z][a-zA-Z0-9]*)\b([^>]*)>/g;
        
        return html.replace(tagRegex, (match, tagName, attributes) => {
            const tag = tagName.toLowerCase();
            
            // Check if tag is allowed
            if (!this.allowedTags.has(tag)) {
                return ''; // Remove disallowed tags
            }
            
            // Check if closing tag
            if (match.startsWith('</')) {
                return `</${tag}>`;
            }
            
            // Process attributes
            const safeAttrs = this.processAttributes(tag, attributes);
            
            // Self-closing tags
            if (['br', 'hr', 'img'].includes(tag)) {
                return `<${tag}${safeAttrs} />`;
            }
            
            return `<${tag}${safeAttrs}>`;
        });
    }
    
    // Process attributes
    processAttributes(tag, attrString) {
        if (!attrString || !attrString.trim()) return '';
        
        const allowedForTag = this.allowedAttributes.get(tag) || [];
        const allowedGlobal = this.allowedAttributes.get('*') || [];
        const allowed = [...allowedForTag, ...allowedGlobal];
        
        const safeAttrs = [];
        
        // Match attributes
        const attrRegex = /([a-zA-Z][a-zA-Z0-9-]*)\s*=\s*(?:"([^"]*)"|'([^']*)'|(\S+))/g;
        let match;
        
        while ((match = attrRegex.exec(attrString)) !== null) {
            const attrName = match[1].toLowerCase();
            const attrValue = match[2] || match[3] || match[4] || '';
            
            // Skip event handlers
            if (attrName.startsWith('on')) continue;
            
            // Skip javascript: in any attribute
            if (attrValue.toLowerCase().includes('javascript:')) continue;
            
            // Skip data: in src attributes
            if (['src', 'href'].includes(attrName) && 
                attrValue.toLowerCase().startsWith('data:') &&
                !attrValue.toLowerCase().startsWith('data:image/')) continue;
            
            // Validate href/src URLs
            if (['href', 'src'].includes(attrName)) {
                if (!this.isValidUrl(attrValue)) continue;
            }
            
            // Check if attribute is allowed
            if (allowed.includes(attrName)) {
                safeAttrs.push(`${attrName}="${this.escapeAttribute(attrValue)}"`);
            }
        }
        
        return safeAttrs.length ? ' ' + safeAttrs.join(' ') : '';
    }
    
    // Validate URL
    isValidUrl(url) {
        if (!url) return false;
        
        // Check for dangerous protocols
        const lower = url.toLowerCase().trim();
        if (lower.startsWith('javascript:')) return false;
        if (lower.startsWith('vbscript:')) return false;
        if (lower.startsWith('data:') && !lower.startsWith('data:image/')) return false;
        
        // Check protocol whitelist for absolute URLs
        try {
            const parsed = new URL(url, 'https://example.com');
            if (url.includes('://') && !this.allowedProtocols.has(parsed.protocol)) {
                return false;
            }
        } catch {
            return false;
        }
        
        return true;
    }
    
    // Escape attribute value
    escapeAttribute(value) {
        return value
            .replace(/&/g, '&amp;')
            .replace(/"/g, '&quot;')
            .replace(/'/g, '&#x27;')
            .replace(/</g, '&lt;')
            .replace(/>/g, '&gt;');
    }
    
    // Escape HTML text
    escapeText(text) {
        return text
            .replace(/&/g, '&amp;')
            .replace(/</g, '&lt;')
            .replace(/>/g, '&gt;')
            .replace(/"/g, '&quot;')
            .replace(/'/g, '&#x27;');
    }
}

// Test XSS Filter
function testXSSFilter() {
    console.log('=== XSS Filter Tests ===\n');
    
    const filter = new XSSFilter();
    
    const testCases = [
        {
            name: 'Script tag',
            input: '<script>alert("XSS")</script>',
            expected: ''
        },
        {
            name: 'Event handler',
            input: '<img src="x" onerror="alert(1)">',
            expected: ''
        },
        {
            name: 'JavaScript URL',
            input: '<a href="javascript:alert(1)">Click</a>',
            expected: '<a>Click</a>'
        },
        {
            name: 'Safe link',
            input: '<a href="https://example.com">Safe</a>',
            expected: '<a href="https://example.com">Safe</a>'
        },
        {
            name: 'Encoded attack',
            input: '&lt;script&gt;alert(1)&lt;/script&gt;',
            expected: ''
        },
        {
            name: 'Nested attack',
            input: '<scr<script>ipt>alert(1)</sc</script>ript>',
            expected: ''
        },
        {
            name: 'Safe formatting',
            input: '<p><strong>Bold</strong> and <em>italic</em></p>',
            expected: '<p><strong>Bold</strong> and <em>italic</em></p>'
        }
    ];
    
    for (const tc of testCases) {
        const result = filter.sanitize(tc.input);
        const passed = result === tc.expected;
        console.log(`${passed ? '✓' : '✗'} ${tc.name}`);
        if (!passed) {
            console.log(`  Input:    ${tc.input}`);
            console.log(`  Expected: ${tc.expected}`);
            console.log(`  Got:      ${result}`);
        }
    }
    
    console.log('\n=== XSS Filter Tests Complete ===\n');
}
*/

// ============================================
// EXERCISE 2: Implement Rate Limiter
// ============================================

/**
 * Create a rate limiter that:
 * - Limits requests per time window
 * - Supports different limits per endpoint
 * - Uses sliding window algorithm
 * - Returns appropriate headers
 */

class RateLimiter {
  // Your implementation here
}

/*
// SOLUTION:
class RateLimiter {
    constructor(options = {}) {
        this.windowMs = options.windowMs || 60000; // 1 minute
        this.maxRequests = options.maxRequests || 100;
        this.store = new Map();
        this.endpointLimits = new Map(Object.entries(options.endpointLimits || {}));
    }
    
    // Get limit for endpoint
    getLimit(endpoint) {
        return this.endpointLimits.get(endpoint) || this.maxRequests;
    }
    
    // Generate key
    getKey(identifier, endpoint = '') {
        return `${identifier}:${endpoint}`;
    }
    
    // Check and update rate limit
    check(identifier, endpoint = '') {
        const key = this.getKey(identifier, endpoint);
        const now = Date.now();
        const windowStart = now - this.windowMs;
        const limit = this.getLimit(endpoint);
        
        // Get or create record
        let record = this.store.get(key);
        
        if (!record) {
            record = { timestamps: [] };
            this.store.set(key, record);
        }
        
        // Remove expired timestamps (sliding window)
        record.timestamps = record.timestamps.filter(ts => ts > windowStart);
        
        // Check if limit exceeded
        const remaining = limit - record.timestamps.length;
        
        if (remaining <= 0) {
            // Calculate retry after
            const oldestInWindow = record.timestamps[0];
            const retryAfter = Math.ceil((oldestInWindow + this.windowMs - now) / 1000);
            
            return {
                allowed: false,
                remaining: 0,
                limit,
                retryAfter,
                headers: {
                    'X-RateLimit-Limit': limit,
                    'X-RateLimit-Remaining': 0,
                    'X-RateLimit-Reset': Math.ceil((oldestInWindow + this.windowMs) / 1000),
                    'Retry-After': retryAfter
                }
            };
        }
        
        // Add timestamp
        record.timestamps.push(now);
        
        return {
            allowed: true,
            remaining: remaining - 1,
            limit,
            headers: {
                'X-RateLimit-Limit': limit,
                'X-RateLimit-Remaining': remaining - 1,
                'X-RateLimit-Reset': Math.ceil((now + this.windowMs) / 1000)
            }
        };
    }
    
    // Reset for identifier
    reset(identifier, endpoint = '') {
        const key = this.getKey(identifier, endpoint);
        this.store.delete(key);
    }
    
    // Middleware for Express
    middleware(options = {}) {
        const getIdentifier = options.getIdentifier || (req => req.ip);
        const getEndpoint = options.getEndpoint || (req => req.path);
        
        return (req, res, next) => {
            const identifier = getIdentifier(req);
            const endpoint = getEndpoint(req);
            
            const result = this.check(identifier, endpoint);
            
            // Set headers
            for (const [header, value] of Object.entries(result.headers)) {
                res.setHeader(header, value);
            }
            
            if (!result.allowed) {
                return res.status(429).json({
                    error: 'Too Many Requests',
                    retryAfter: result.retryAfter
                });
            }
            
            next();
        };
    }
    
    // Cleanup old entries
    cleanup() {
        const cutoff = Date.now() - this.windowMs;
        
        for (const [key, record] of this.store) {
            record.timestamps = record.timestamps.filter(ts => ts > cutoff);
            if (record.timestamps.length === 0) {
                this.store.delete(key);
            }
        }
    }
    
    // Get stats
    getStats() {
        let totalRequests = 0;
        let activeKeys = 0;
        
        for (const record of this.store.values()) {
            if (record.timestamps.length > 0) {
                activeKeys++;
                totalRequests += record.timestamps.length;
            }
        }
        
        return {
            activeClients: activeKeys,
            totalRequests,
            windowMs: this.windowMs,
            defaultLimit: this.maxRequests
        };
    }
}

// Test Rate Limiter
function testRateLimiter() {
    console.log('=== Rate Limiter Tests ===\n');
    
    const limiter = new RateLimiter({
        windowMs: 1000,  // 1 second for testing
        maxRequests: 5,
        endpointLimits: {
            '/login': 3
        }
    });
    
    const userId = 'user-123';
    
    // Test normal endpoint
    console.log('Testing normal endpoint (limit: 5):');
    for (let i = 0; i < 7; i++) {
        const result = limiter.check(userId);
        console.log(`  Request ${i + 1}: ${result.allowed ? '✓ Allowed' : '✗ Blocked'}, remaining: ${result.remaining}`);
    }
    
    // Test limited endpoint
    console.log('\nTesting /login endpoint (limit: 3):');
    limiter.reset(userId, '/login');
    for (let i = 0; i < 5; i++) {
        const result = limiter.check(userId, '/login');
        console.log(`  Request ${i + 1}: ${result.allowed ? '✓ Allowed' : '✗ Blocked'}, remaining: ${result.remaining}`);
    }
    
    // Test sliding window (wait and retry)
    console.log('\nTesting sliding window (waiting 500ms):');
    setTimeout(() => {
        const result = limiter.check(userId);
        console.log(`  After wait: ${result.allowed ? '✓ Allowed' : '✗ Blocked'}, remaining: ${result.remaining}`);
        
        console.log('\nStats:', limiter.getStats());
        console.log('\n=== Rate Limiter Tests Complete ===\n');
    }, 500);
}
*/

// ============================================
// EXERCISE 3: Build Security Audit Logger
// ============================================

/**
 * Create a security audit logger that:
 * - Logs security events
 * - Detects suspicious patterns
 * - Masks sensitive data
 * - Generates alerts
 */

class SecurityAuditLogger {
  // Your implementation here
}

/*
// SOLUTION:
class SecurityAuditLogger {
    constructor(options = {}) {
        this.logs = [];
        this.maxLogs = options.maxLogs || 10000;
        this.alertHandlers = [];
        this.suspiciousPatterns = options.suspiciousPatterns || [
            { name: 'rapid-requests', threshold: 50, window: 60000 },
            { name: 'failed-logins', threshold: 5, window: 300000 },
            { name: 'unusual-hours', hours: [0, 1, 2, 3, 4, 5] }
        ];
        this.eventCounts = new Map();
    }
    
    // Log security event
    log(event) {
        const entry = {
            id: this.generateId(),
            timestamp: Date.now(),
            isoTime: new Date().toISOString(),
            type: event.type,
            severity: event.severity || 'info',
            userId: event.userId,
            ip: event.ip,
            userAgent: this.maskSensitive(event.userAgent),
            action: event.action,
            resource: event.resource,
            result: event.result,
            details: this.maskSensitive(event.details),
            metadata: event.metadata
        };
        
        this.logs.push(entry);
        
        // Trim old logs
        if (this.logs.length > this.maxLogs) {
            this.logs = this.logs.slice(-this.maxLogs);
        }
        
        // Check for suspicious patterns
        this.checkPatterns(entry);
        
        // Output to console
        this.output(entry);
        
        return entry;
    }
    
    generateId() {
        return 'audit_' + Date.now().toString(36) + Math.random().toString(36).substr(2, 9);
    }
    
    // Mask sensitive data
    maskSensitive(data) {
        if (typeof data !== 'object' || data === null) {
            return data;
        }
        
        const sensitiveFields = ['password', 'token', 'secret', 'key', 'authorization', 'cookie'];
        const masked = Array.isArray(data) ? [] : {};
        
        for (const [key, value] of Object.entries(data)) {
            const lowerKey = key.toLowerCase();
            
            if (sensitiveFields.some(f => lowerKey.includes(f))) {
                masked[key] = '[REDACTED]';
            } else if (typeof value === 'object' && value !== null) {
                masked[key] = this.maskSensitive(value);
            } else {
                masked[key] = value;
            }
        }
        
        return masked;
    }
    
    // Check for suspicious patterns
    checkPatterns(entry) {
        const key = `${entry.type}:${entry.userId || entry.ip}`;
        
        // Track event counts
        if (!this.eventCounts.has(key)) {
            this.eventCounts.set(key, []);
        }
        
        const counts = this.eventCounts.get(key);
        counts.push(entry.timestamp);
        
        // Check patterns
        for (const pattern of this.suspiciousPatterns) {
            switch (pattern.name) {
                case 'rapid-requests': {
                    const windowStart = Date.now() - pattern.window;
                    const recent = counts.filter(t => t > windowStart);
                    
                    if (recent.length > pattern.threshold) {
                        this.triggerAlert({
                            pattern: pattern.name,
                            entry,
                            count: recent.length,
                            threshold: pattern.threshold
                        });
                    }
                    break;
                }
                
                case 'failed-logins': {
                    if (entry.type === 'login' && entry.result === 'failure') {
                        const windowStart = Date.now() - pattern.window;
                        const recent = this.logs.filter(l => 
                            l.type === 'login' && 
                            l.result === 'failure' &&
                            (l.userId === entry.userId || l.ip === entry.ip) &&
                            l.timestamp > windowStart
                        );
                        
                        if (recent.length >= pattern.threshold) {
                            this.triggerAlert({
                                pattern: pattern.name,
                                entry,
                                count: recent.length,
                                threshold: pattern.threshold
                            });
                        }
                    }
                    break;
                }
                
                case 'unusual-hours': {
                    const hour = new Date().getHours();
                    if (pattern.hours.includes(hour)) {
                        this.triggerAlert({
                            pattern: pattern.name,
                            entry,
                            hour
                        });
                    }
                    break;
                }
            }
        }
    }
    
    // Trigger alert
    triggerAlert(alert) {
        const alertEntry = {
            id: this.generateId(),
            timestamp: Date.now(),
            isoTime: new Date().toISOString(),
            ...alert
        };
        
        // Call alert handlers
        for (const handler of this.alertHandlers) {
            try {
                handler(alertEntry);
            } catch (e) {
                console.error('Alert handler error:', e);
            }
        }
    }
    
    // Register alert handler
    onAlert(handler) {
        this.alertHandlers.push(handler);
        return () => {
            const idx = this.alertHandlers.indexOf(handler);
            if (idx !== -1) this.alertHandlers.splice(idx, 1);
        };
    }
    
    // Output log entry
    output(entry) {
        const severitySymbols = {
            debug: '🔍',
            info: 'â„šī¸',
            warn: 'âš ī¸',
            error: '❌',
            critical: '🚨'
        };
        
        const symbol = severitySymbols[entry.severity] || 'â„šī¸';
        console.log(`${symbol} [${entry.isoTime}] ${entry.type}: ${entry.action} - ${entry.result}`);
        
        if (entry.details && Object.keys(entry.details).length > 0) {
            console.log(`   Details:`, JSON.stringify(entry.details));
        }
    }
    
    // Query logs
    query(filter = {}) {
        let results = [...this.logs];
        
        if (filter.type) {
            results = results.filter(l => l.type === filter.type);
        }
        
        if (filter.userId) {
            results = results.filter(l => l.userId === filter.userId);
        }
        
        if (filter.ip) {
            results = results.filter(l => l.ip === filter.ip);
        }
        
        if (filter.severity) {
            results = results.filter(l => l.severity === filter.severity);
        }
        
        if (filter.since) {
            results = results.filter(l => l.timestamp >= filter.since);
        }
        
        if (filter.until) {
            results = results.filter(l => l.timestamp <= filter.until);
        }
        
        return results;
    }
    
    // Generate report
    generateReport(options = {}) {
        const since = options.since || Date.now() - 86400000; // Last 24 hours
        const logs = this.query({ since });
        
        const byType = {};
        const byResult = {};
        const bySeverity = {};
        const byUser = {};
        const byIp = {};
        
        for (const log of logs) {
            byType[log.type] = (byType[log.type] || 0) + 1;
            byResult[log.result] = (byResult[log.result] || 0) + 1;
            bySeverity[log.severity] = (bySeverity[log.severity] || 0) + 1;
            
            if (log.userId) {
                byUser[log.userId] = (byUser[log.userId] || 0) + 1;
            }
            if (log.ip) {
                byIp[log.ip] = (byIp[log.ip] || 0) + 1;
            }
        }
        
        return {
            period: {
                start: new Date(since).toISOString(),
                end: new Date().toISOString()
            },
            totalEvents: logs.length,
            byType,
            byResult,
            bySeverity,
            topUsers: Object.entries(byUser)
                .sort((a, b) => b[1] - a[1])
                .slice(0, 10)
                .map(([user, count]) => ({ user, count })),
            topIPs: Object.entries(byIp)
                .sort((a, b) => b[1] - a[1])
                .slice(0, 10)
                .map(([ip, count]) => ({ ip, count }))
        };
    }
}

// Test Security Audit Logger
function testSecurityAuditLogger() {
    console.log('=== Security Audit Logger Tests ===\n');
    
    const logger = new SecurityAuditLogger({
        suspiciousPatterns: [
            { name: 'failed-logins', threshold: 3, window: 60000 }
        ]
    });
    
    // Register alert handler
    logger.onAlert(alert => {
        console.log('🚨 ALERT:', alert.pattern, alert);
    });
    
    // Log some events
    logger.log({
        type: 'login',
        userId: 'user-123',
        ip: '192.168.1.1',
        action: 'authenticate',
        result: 'success',
        details: { password: 'secret123' }
    });
    
    // Simulate failed login attempts (should trigger alert)
    for (let i = 0; i < 4; i++) {
        logger.log({
            type: 'login',
            userId: 'hacker',
            ip: '10.0.0.100',
            action: 'authenticate',
            result: 'failure',
            severity: 'warn',
            details: { reason: 'Invalid password' }
        });
    }
    
    // API access
    logger.log({
        type: 'api',
        userId: 'user-123',
        ip: '192.168.1.1',
        action: 'read',
        resource: '/api/users',
        result: 'success'
    });
    
    // Generate report
    console.log('\nSecurity Report:');
    console.log(JSON.stringify(logger.generateReport(), null, 2));
    
    console.log('\n=== Security Audit Logger Tests Complete ===\n');
}
*/

// ============================================
// EXERCISE 4: Implement Secure Token Manager
// ============================================

/**
 * Create a secure token manager that:
 * - Generates cryptographically secure tokens
 * - Manages token lifecycle
 * - Supports token refresh
 * - Implements token revocation
 */

class SecureTokenManager {
  // Your implementation here
}

/*
// SOLUTION:
class SecureTokenManager {
    constructor(options = {}) {
        this.tokens = new Map();
        this.revokedTokens = new Set();
        this.accessTokenTTL = options.accessTokenTTL || 900000;    // 15 minutes
        this.refreshTokenTTL = options.refreshTokenTTL || 604800000; // 7 days
        this.tokenLength = options.tokenLength || 32;
    }
    
    // Generate secure random token
    generateToken(length = this.tokenLength) {
        const array = new Uint8Array(length);
        
        if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
            crypto.getRandomValues(array);
        } else {
            const nodeCrypto = require('crypto');
            const buffer = nodeCrypto.randomBytes(length);
            array.set(buffer);
        }
        
        return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
    }
    
    // Generate token pair (access + refresh)
    createTokenPair(userId, metadata = {}) {
        const accessToken = this.generateToken();
        const refreshToken = this.generateToken();
        const now = Date.now();
        
        const tokenData = {
            userId,
            accessToken,
            refreshToken,
            accessTokenExpiry: now + this.accessTokenTTL,
            refreshTokenExpiry: now + this.refreshTokenTTL,
            createdAt: now,
            metadata
        };
        
        // Store tokens
        this.tokens.set(accessToken, { ...tokenData, type: 'access' });
        this.tokens.set(refreshToken, { ...tokenData, type: 'refresh' });
        
        return {
            accessToken,
            refreshToken,
            accessTokenExpiresIn: this.accessTokenTTL,
            refreshTokenExpiresIn: this.refreshTokenTTL
        };
    }
    
    // Validate access token
    validateAccessToken(token) {
        // Check if revoked
        if (this.revokedTokens.has(token)) {
            return { valid: false, reason: 'Token revoked' };
        }
        
        const data = this.tokens.get(token);
        
        if (!data) {
            return { valid: false, reason: 'Token not found' };
        }
        
        if (data.type !== 'access') {
            return { valid: false, reason: 'Not an access token' };
        }
        
        if (Date.now() > data.accessTokenExpiry) {
            return { valid: false, reason: 'Token expired' };
        }
        
        return {
            valid: true,
            userId: data.userId,
            metadata: data.metadata,
            expiresIn: data.accessTokenExpiry - Date.now()
        };
    }
    
    // Refresh tokens
    refreshTokens(refreshToken) {
        // Check if revoked
        if (this.revokedTokens.has(refreshToken)) {
            return { success: false, reason: 'Refresh token revoked' };
        }
        
        const data = this.tokens.get(refreshToken);
        
        if (!data) {
            return { success: false, reason: 'Refresh token not found' };
        }
        
        if (data.type !== 'refresh') {
            return { success: false, reason: 'Not a refresh token' };
        }
        
        if (Date.now() > data.refreshTokenExpiry) {
            return { success: false, reason: 'Refresh token expired' };
        }
        
        // Revoke old tokens
        this.revoke(data.accessToken);
        this.revoke(refreshToken);
        
        // Create new token pair
        const newTokens = this.createTokenPair(data.userId, data.metadata);
        
        return {
            success: true,
            ...newTokens
        };
    }
    
    // Revoke token
    revoke(token) {
        this.revokedTokens.add(token);
        this.tokens.delete(token);
        return true;
    }
    
    // Revoke all tokens for user
    revokeAllForUser(userId) {
        let count = 0;
        
        for (const [token, data] of this.tokens) {
            if (data.userId === userId) {
                this.revoke(token);
                count++;
            }
        }
        
        return count;
    }
    
    // Get token info
    getTokenInfo(token) {
        const data = this.tokens.get(token);
        
        if (!data) return null;
        
        return {
            userId: data.userId,
            type: data.type,
            createdAt: new Date(data.createdAt).toISOString(),
            expiresAt: new Date(
                data.type === 'access' ? data.accessTokenExpiry : data.refreshTokenExpiry
            ).toISOString(),
            isExpired: Date.now() > (data.type === 'access' ? data.accessTokenExpiry : data.refreshTokenExpiry),
            isRevoked: this.revokedTokens.has(token)
        };
    }
    
    // Cleanup expired tokens
    cleanup() {
        const now = Date.now();
        let cleaned = 0;
        
        for (const [token, data] of this.tokens) {
            const expiry = data.type === 'access' ? data.accessTokenExpiry : data.refreshTokenExpiry;
            if (now > expiry) {
                this.tokens.delete(token);
                cleaned++;
            }
        }
        
        // Clean old revoked tokens (keep for 24 hours)
        // In production, use a more sophisticated cleanup
        
        return cleaned;
    }
    
    // Get stats
    getStats() {
        let accessTokens = 0;
        let refreshTokens = 0;
        let expired = 0;
        const now = Date.now();
        
        for (const data of this.tokens.values()) {
            if (data.type === 'access') accessTokens++;
            else refreshTokens++;
            
            const expiry = data.type === 'access' ? data.accessTokenExpiry : data.refreshTokenExpiry;
            if (now > expiry) expired++;
        }
        
        return {
            totalTokens: this.tokens.size,
            accessTokens,
            refreshTokens,
            revokedTokens: this.revokedTokens.size,
            expired
        };
    }
}

// Test Secure Token Manager
function testSecureTokenManager() {
    console.log('=== Secure Token Manager Tests ===\n');
    
    const manager = new SecureTokenManager({
        accessTokenTTL: 5000,    // 5 seconds for testing
        refreshTokenTTL: 10000   // 10 seconds for testing
    });
    
    // Create token pair
    const tokens = manager.createTokenPair('user-123', { role: 'admin' });
    console.log('Token pair created:');
    console.log('  Access token:', tokens.accessToken.substring(0, 20) + '...');
    console.log('  Refresh token:', tokens.refreshToken.substring(0, 20) + '...');
    
    // Validate access token
    const validation = manager.validateAccessToken(tokens.accessToken);
    console.log('\nAccess token validation:', validation);
    
    // Get token info
    console.log('\nToken info:', manager.getTokenInfo(tokens.accessToken));
    
    // Refresh tokens
    const refreshed = manager.refreshTokens(tokens.refreshToken);
    console.log('\nRefreshed tokens:', refreshed.success);
    
    // Old token should be invalid
    const oldValidation = manager.validateAccessToken(tokens.accessToken);
    console.log('Old token valid:', oldValidation.valid);
    
    // New token should be valid
    const newValidation = manager.validateAccessToken(refreshed.accessToken);
    console.log('New token valid:', newValidation.valid);
    
    // Stats
    console.log('\nStats:', manager.getStats());
    
    console.log('\n=== Secure Token Manager Tests Complete ===\n');
}
*/

// ============================================
// RUN EXERCISES
// ============================================

console.log('=== Security Fundamentals Exercises ===\n');
console.log('Implement the following exercises:');
console.log('1. XSSFilter - Complete XSS filtering and sanitization');
console.log('2. RateLimiter - Sliding window rate limiting');
console.log('3. SecurityAuditLogger - Security event logging and alerting');
console.log('4. SecureTokenManager - Token lifecycle management');
console.log('');
console.log('Uncomment solutions to verify your implementations.');

// Export
if (typeof module !== 'undefined' && module.exports) {
  module.exports = {
    XSSFilter,
    RateLimiter,
    SecurityAuditLogger,
    SecureTokenManager,
  };
}
Exercises - JavaScript Tutorial | DeepML