Resource Auditing Techniques

Resource Auditing Techniques

Comprehensive resource auditing forms the foundation of effective CSP implementation. Use automated tools and manual inspection to create a complete inventory of your application's resource requirements.

Browser-based audit script:

// Comprehensive Resource Auditor
class ResourceAuditor {
    constructor() {
        this.resources = {
            scripts: new Map(),
            styles: new Map(),
            images: new Map(),
            fonts: new Map(),
            media: new Map(),
            frames: new Map(),
            connects: new Map(),
            inlineScripts: [],
            inlineStyles: []
        };
        
        this.startAudit();
    }
    
    startAudit() {
        // Monitor resource loading
        this.observeResources();
        
        // Scan existing resources
        this.scanExistingResources();
        
        // Monitor dynamic resource loading
        this.monitorDynamicLoading();
        
        // Check for inline content
        this.scanInlineContent();
    }
    
    observeResources() {
        const observer = new PerformanceObserver((list) => {
            for (const entry of list.getEntries()) {
                this.categorizeResource(entry);
            }
        });
        
        observer.observe({ entryTypes: ['resource'] });
    }
    
    categorizeResource(entry) {
        const url = new URL(entry.name);
        const origin = url.origin;
        
        if (entry.initiatorType === 'script') {
            this.resources.scripts.set(entry.name, {
                origin: origin,
                loadTime: entry.responseEnd - entry.startTime,
                size: entry.transferSize
            });
        } else if (entry.initiatorType === 'css' || entry.initiatorType === 'link') {
            this.resources.styles.set(entry.name, {
                origin: origin,
                loadTime: entry.responseEnd - entry.startTime
            });
        } else if (entry.initiatorType === 'img') {
            this.resources.images.set(entry.name, {
                origin: origin,
                type: this.getImageType(entry.name)
            });
        }
    }
    
    scanExistingResources() {
        // Scripts
        document.querySelectorAll('script').forEach(script => {
            if (script.src) {
                const url = new URL(script.src, window.location.origin);
                this.resources.scripts.set(url.href, {
                    origin: url.origin,
                    async: script.async,
                    defer: script.defer
                });
            } else if (script.textContent.trim()) {
                this.resources.inlineScripts.push({
                    content: script.textContent.substring(0, 100) + '...',
                    hash: this.calculateHash(script.textContent)
                });
            }
        });
        
        // Stylesheets
        document.querySelectorAll('link[rel="stylesheet"]').forEach(link => {
            const url = new URL(link.href, window.location.origin);
            this.resources.styles.set(url.href, {
                origin: url.origin,
                media: link.media
            });
        });
        
        // Inline styles
        document.querySelectorAll('style').forEach(style => {
            this.resources.inlineStyles.push({
                content: style.textContent.substring(0, 100) + '...',
                hash: this.calculateHash(style.textContent)
            });
        });
    }
    
    monitorDynamicLoading() {
        // Override fetch to monitor API calls
        const originalFetch = window.fetch;
        window.fetch = (...args) => {
            const url = args[0];
            if (typeof url === 'string') {
                const urlObj = new URL(url, window.location.origin);
                this.resources.connects.set(urlObj.origin, {
                    type: 'fetch',
                    count: (this.resources.connects.get(urlObj.origin)?.count || 0) + 1
                });
            }
            return originalFetch.apply(this, args);
        };
        
        // Monitor XMLHttpRequest
        const originalXHR = window.XMLHttpRequest.prototype.open;
        window.XMLHttpRequest.prototype.open = function(method, url) {
            const urlObj = new URL(url, window.location.origin);
            this.resources.connects.set(urlObj.origin, {
                type: 'xhr',
                count: (this.resources.connects.get(urlObj.origin)?.count || 0) + 1
            });
            return originalXHR.apply(this, arguments);
        };
    }
    
    calculateHash(content) {
        // Simple hash calculation for demo - use crypto.subtle in production
        return 'sha256-' + btoa(content).substring(0, 20) + '...';
    }
    
    generateReport() {
        const report = {
            summary: {
                totalScripts: this.resources.scripts.size,
                totalStyles: this.resources.styles.size,
                inlineScripts: this.resources.inlineScripts.length,
                inlineStyles: this.resources.inlineStyles.length,
                uniqueOrigins: this.getUniqueOrigins().size
            },
            suggestedPolicy: this.generateSuggestedPolicy(),
            details: this.resources
        };
        
        return report;
    }
    
    generateSuggestedPolicy() {
        const policy = {
            'default-src': ["'self'"],
            'script-src': ["'self'"],
            'style-src': ["'self'"],
            'img-src': ["'self'"],
            'font-src': ["'self'"],
            'connect-src': ["'self'"]
        };
        
        // Add discovered origins
        this.resources.scripts.forEach((info, url) => {
            if (info.origin !== window.location.origin) {
                policy['script-src'].push(info.origin);
            }
        });
        
        // Handle inline content
        if (this.resources.inlineScripts.length > 0) {
            policy['script-src'].push("'unsafe-inline'"); // To be replaced with nonces
        }
        
        if (this.resources.inlineStyles.length > 0) {
            policy['style-src'].push("'unsafe-inline'"); // To be replaced with nonces
        }
        
        return this.formatPolicy(policy);
    }
    
    formatPolicy(policy) {
        return Object.entries(policy)
            .map(([directive, sources]) => `${directive} ${sources.join(' ')}`)
            .join('; ');
    }
}

// Usage
const auditor = new ResourceAuditor();
setTimeout(() => {
    console.log('CSP Audit Report:', auditor.generateReport());
}, 5000);