import { useState, useEffect } from 'react'; import { Plus, X, AlertCircle, Check, Code } from 'lucide-react'; import { Button } from './ui/Button'; import { Input } from './ui/Input'; import { NativeSelect } from './ui/NativeSelect'; import { Card } from './ui/Card'; import { Badge } from './ui/Badge'; import { Alert } from './ui/Alert'; import type { CSPDirective } from '../api/securityHeaders'; interface CSPBuilderProps { value: string; // JSON string of CSPDirective[] onChange: (value: string) => void; onValidate?: (valid: boolean, errors: string[]) => void; } const CSP_DIRECTIVES = [ 'default-src', 'script-src', 'style-src', 'img-src', 'font-src', 'connect-src', 'frame-src', 'object-src', 'media-src', 'worker-src', 'form-action', 'base-uri', 'frame-ancestors', 'manifest-src', 'prefetch-src', ]; const CSP_VALUES = [ "'self'", "'none'", "'unsafe-inline'", "'unsafe-eval'", 'data:', 'https:', 'http:', 'blob:', 'filesystem:', "'strict-dynamic'", "'report-sample'", "'unsafe-hashes'", ]; const CSP_PRESETS: Record = { 'Strict Default': [ { directive: 'default-src', values: ["'self'"] }, { directive: 'script-src', values: ["'self'"] }, { directive: 'style-src', values: ["'self'"] }, { directive: 'img-src', values: ["'self'", 'data:', 'https:'] }, { directive: 'font-src', values: ["'self'", 'data:'] }, { directive: 'connect-src', values: ["'self'"] }, { directive: 'frame-src', values: ["'none'"] }, { directive: 'object-src', values: ["'none'"] }, ], 'Allow Inline Styles': [ { directive: 'default-src', values: ["'self'"] }, { directive: 'script-src', values: ["'self'"] }, { directive: 'style-src', values: ["'self'", "'unsafe-inline'"] }, { directive: 'img-src', values: ["'self'", 'data:', 'https:'] }, { directive: 'font-src', values: ["'self'", 'data:'] }, ], 'Development Mode': [ { directive: 'default-src', values: ["'self'"] }, { directive: 'script-src', values: ["'self'", "'unsafe-inline'", "'unsafe-eval'"] }, { directive: 'style-src', values: ["'self'", "'unsafe-inline'"] }, { directive: 'img-src', values: ["'self'", 'data:', 'https:', 'http:'] }, ], }; export function CSPBuilder({ value, onChange, onValidate }: CSPBuilderProps) { const [directives, setDirectives] = useState([]); const [newDirective, setNewDirective] = useState('default-src'); const [newValue, setNewValue] = useState(''); const [validationErrors, setValidationErrors] = useState([]); const [showPreview, setShowPreview] = useState(false); // Parse initial value useEffect(() => { try { if (value) { const parsed = JSON.parse(value) as CSPDirective[]; setDirectives(parsed); } else { setDirectives([]); } } catch { setDirectives([]); } }, [value]); // Generate CSP string preview const generateCSPString = (dirs: CSPDirective[]): string => { return dirs .map((dir) => `${dir.directive} ${dir.values.join(' ')}`) .join('; '); }; const cspString = generateCSPString(directives); // Update parent component const updateDirectives = (newDirectives: CSPDirective[]) => { setDirectives(newDirectives); onChange(JSON.stringify(newDirectives)); validateCSP(newDirectives); }; const validateCSP = (dirs: CSPDirective[]) => { const errors: string[] = []; // Check for duplicate directives const directiveNames = dirs.map((d) => d.directive); const duplicates = directiveNames.filter((name, index) => directiveNames.indexOf(name) !== index); if (duplicates.length > 0) { errors.push(`Duplicate directives found: ${duplicates.join(', ')}`); } // Check for dangerous combinations const hasUnsafeInline = dirs.some((d) => d.values.some((v) => v === "'unsafe-inline'" || v === "'unsafe-eval'") ); if (hasUnsafeInline) { errors.push('Using unsafe-inline or unsafe-eval weakens CSP protection'); } // Check if default-src is set const hasDefaultSrc = dirs.some((d) => d.directive === 'default-src'); if (!hasDefaultSrc && dirs.length > 0) { errors.push('Consider setting default-src as a fallback for all directives'); } setValidationErrors(errors); onValidate?.(errors.length === 0, errors); }; const handleAddDirective = () => { if (!newValue.trim()) return; const existingIndex = directives.findIndex((d) => d.directive === newDirective); let updated: CSPDirective[]; if (existingIndex >= 0) { // Add to existing directive const existing = directives[existingIndex]; if (!existing.values.includes(newValue.trim())) { const updatedDirective = { ...existing, values: [...existing.values, newValue.trim()], }; updated = [ ...directives.slice(0, existingIndex), updatedDirective, ...directives.slice(existingIndex + 1), ]; } else { return; // Value already exists } } else { // Create new directive updated = [...directives, { directive: newDirective, values: [newValue.trim()] }]; } updateDirectives(updated); setNewValue(''); }; const handleRemoveDirective = (directive: string) => { updateDirectives(directives.filter((d) => d.directive !== directive)); }; const handleRemoveValue = (directive: string, value: string) => { updateDirectives( directives.map((d) => d.directive === directive ? { ...d, values: d.values.filter((v) => v !== value) } : d ).filter((d) => d.values.length > 0) ); }; const handleApplyPreset = (presetName: string) => { const preset = CSP_PRESETS[presetName]; if (preset) { updateDirectives(preset); } }; return (

Content Security Policy Builder

{/* Preset Buttons */}
Quick Presets: {Object.keys(CSP_PRESETS).map((presetName) => ( ))}
{/* Add Directive Form */}
setNewDirective(e.target.value)} className="w-48" > {CSP_DIRECTIVES.map((dir) => ( ))}
setNewValue(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && handleAddDirective()} placeholder="Enter value or select from suggestions..." list="csp-values" /> {CSP_VALUES.map((val) => (
{/* Current Directives */}
{directives.length === 0 ? ( No CSP directives configured. Add directives above to build your policy. ) : ( directives.map((dir) => (
{dir.directive}
{dir.values.map((val) => ( handleRemoveValue(dir.directive, val)} > {val} ))}
)) )}
{/* Validation Errors */} {validationErrors.length > 0 && (

CSP Validation Warnings:

    {validationErrors.map((error, index) => (
  • {error}
  • ))}
)} {validationErrors.length === 0 && directives.length > 0 && ( CSP configuration looks good! )} {/* CSP String Preview */} {showPreview && cspString && (
            {cspString || '(empty)'}
          
)}
); }