diff options
| author | Tadas Vilkeliskis <vilkeliskis.t@gmail.com> | 2026-02-08 17:02:29 -0500 |
|---|---|---|
| committer | Tadas Vilkeliskis <vilkeliskis.t@gmail.com> | 2026-02-08 17:02:29 -0500 |
| commit | 5307d1c45fcee63602b8d079956c4dc44d3ac08b (patch) | |
| tree | 33c12839d47f5030e0f4ff8d69a39ec8f583769f /redaction-checker.html | |
| parent | 8d752518ef6942e6fe2f1b4fc97229266f1ca114 (diff) | |
add redaction checker
Diffstat (limited to 'redaction-checker.html')
| -rw-r--r-- | redaction-checker.html | 374 |
1 files changed, 374 insertions, 0 deletions
diff --git a/redaction-checker.html b/redaction-checker.html new file mode 100644 index 0000000..9ae0f25 --- /dev/null +++ b/redaction-checker.html @@ -0,0 +1,374 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Redaction Checker | Just Redact</title> + <meta name="description" content="Check if your PDF is truly redacted. Detect hidden text layers, metadata, and unflattened elements. 100% local and private."> + + <script src="https://cdn.tailwindcss.com"></script> + <script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script> + + <style> + @import url('https://fonts.googleapis.com/css2?family=Crimson+Pro:ital,wght@0,400;0,600;0,700;1,400&family=Inter:wght@400;500;600&display=swap'); + + body { + font-family: 'Inter', sans-serif; + color: #1f2937; + background-color: #f9fafb; + -webkit-font-smoothing: antialiased; + } + + h1, h2, h3, h4 { + font-family: 'Crimson Pro', serif; + } + + .drop-zone--over { + border-color: #10b981 !important; + background-color: #ecfdf5 !important; + } + </style> +</head> +<body class="flex flex-col min-h-screen"> + + <!-- Header --> + <header class="max-w-5xl mx-auto w-full px-6 py-8 flex justify-between items-center"> + <a href="index.html" class="text-xl font-bold tracking-tight text-gray-900 font-serif flex items-center gap-2"> + Just Redact. + </a> + <div class="flex gap-4"> + <a href="redact.html" class="text-sm font-medium text-white bg-gray-900 hover:bg-gray-800 px-4 py-2 rounded-lg transition">Securely Redact Now</a> + </div> + </header> + + <main class="flex-grow max-w-4xl mx-auto w-full px-6 pb-24"> + + <section class="text-center mt-8 mb-12"> + <h1 class="text-4xl md:text-5xl font-semibold text-gray-900 mb-4"> + Is your PDF actually redacted? + </h1> + <p class="text-lg text-gray-600 max-w-2xl mx-auto"> + Hidden data often lurks beneath black boxes. Use our local checker to find searchable text, metadata, and unflattened layers. + </p> + </section> + + <!-- Upload Zone --> + <div id="drop-zone" class="border-2 border-dashed border-gray-300 rounded-2xl p-12 text-center transition-all bg-white cursor-pointer hover:border-gray-400"> + <input type="file" id="file-input" class="hidden" accept="application/pdf"> + <div class="flex flex-col items-center"> + <svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 text-gray-400 mb-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /> + </svg> + <p class="text-xl font-medium text-gray-900 mb-1">Drop your PDF here</p> + <p class="text-sm text-gray-500">or click to browse from your device</p> + <p class="mt-4 text-xs text-gray-400 uppercase tracking-widest font-semibold">100% Local • No Uploads</p> + </div> + </div> + + <!-- Analysis State --> + <div id="loading-state" class="hidden mt-12 text-center"> + <div class="inline-block animate-spin rounded-full h-8 w-8 border-4 border-gray-300 border-t-gray-900 mb-4"></div> + <p class="text-gray-600 font-medium">Analyzing document safety...</p> + </div> + + <!-- Results Dashboard --> + <div id="results-dashboard" class="hidden mt-12 animate-in fade-in duration-500"> + + <div class="grid md:grid-cols-3 gap-8"> + + <!-- Verdict Card --> + <div class="md:col-span-1 bg-white p-8 rounded-2xl shadow-sm border border-gray-100 flex flex-col items-center justify-center text-center"> + <h2 class="text-sm font-bold text-gray-400 uppercase tracking-widest mb-6">Security Verdict</h2> + <div id="verdict-icon" class="mb-4"> + <!-- Icon injected by JS --> + </div> + <p id="verdict-text" class="text-3xl font-bold mb-2"></p> + <p id="health-score" class="text-sm text-gray-500"></p> + </div> + + <!-- Details Column --> + <div class="md:col-span-2 space-y-6"> + + <!-- Searchable Text --> + <div class="bg-white p-6 rounded-2xl shadow-sm border border-gray-100"> + <div class="flex justify-between items-center mb-4"> + <h3 class="font-bold text-gray-900 flex items-center gap-2"> + <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"> + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM10 7v3m0 0v3m0-3h3m-3 0H7" /> + </svg> + Searchable Text Layer + </h3> + <span id="text-badge" class="px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider"></span> + </div> + <p id="text-description" class="text-sm text-gray-600 leading-relaxed"> + Searching for hidden text that remains selectable or searchable even if covered by black boxes. + </p> + </div> + + <!-- Metadata --> + <div class="bg-white p-6 rounded-2xl shadow-sm border border-gray-100"> + <div class="flex justify-between items-center mb-4"> + <h3 class="font-bold text-gray-900 flex items-center gap-2"> + <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"> + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> + </svg> + Document Metadata + </h3> + <span id="metadata-badge" class="px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider"></span> + </div> + <div id="metadata-list" class="space-y-2 mb-4 hidden"> + <!-- Table injected by JS --> + </div> + <p id="metadata-description" class="text-sm text-gray-600 leading-relaxed"> + Checking for hidden properties like Author, Producer, or Tool used that can leak identity or workflow details. + </p> + </div> + + <!-- Annotations --> + <div class="bg-white p-6 rounded-2xl shadow-sm border border-gray-100"> + <div class="flex justify-between items-center mb-4"> + <h3 class="font-bold text-gray-900 flex items-center gap-2"> + <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"> + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" /> + </svg> + Unflattened Annotations + </h3> + <span id="annotations-badge" class="px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider"></span> + </div> + <p id="annotations-description" class="text-sm text-gray-600 leading-relaxed"> + Detecting shapes, highlights, or comments that are "objects" on top of text rather than being part of the image. + </p> + </div> + + </div> + </div> + + <!-- Call to Action --> + <div id="cta-unsafe" class="mt-12 p-8 bg-gray-900 rounded-3xl text-center text-white hidden"> + <h3 class="text-2xl font-bold mb-4 font-serif">Hidden data detected. Fix it now.</h3> + <p class="text-gray-400 mb-8 max-w-xl mx-auto"> + Your PDF isn't fully redacted. Use Just Redact to permanently flatten this document into a secure image. + </p> + <a href="redact.html" class="inline-block bg-green-600 hover:bg-green-700 text-white font-bold py-4 px-10 rounded-full shadow-lg transition-transform transform hover:-translate-y-0.5"> + Launch Secure Redactor + </a> + </div> + + <div id="cta-safe" class="mt-12 p-8 bg-green-50 border border-green-100 rounded-3xl text-center hidden"> + <h3 class="text-2xl font-bold text-green-900 mb-4 font-serif">This document is secure.</h3> + <p class="text-green-700 mb-0"> + No text layers or hidden metadata were found. This PDF appears to be properly flattened. + </p> + </div> + + </div> + + </main> + + <!-- Footer --> + <footer class="bg-white py-12 border-t border-gray-100"> + <div class="max-w-4xl mx-auto px-6 text-center text-sm text-gray-500"> + © 2026 Just Redact. All processing happens 100% in your browser. + </div> + </footer> + + <script> + pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js'; + + const fileInput = document.getElementById('file-input'); + const dropZone = document.getElementById('drop-zone'); + const resultsDashboard = document.getElementById('results-dashboard'); + const loadingState = document.getElementById('loading-state'); + + // UI Elements for Results + const verdictIcon = document.getElementById('verdict-icon'); + const verdictText = document.getElementById('verdict-text'); + const healthScore = document.getElementById('health-score'); + const textBadge = document.getElementById('text-badge'); + const textDescription = document.getElementById('text-description'); + const metadataBadge = document.getElementById('metadata-badge'); + const metadataList = document.getElementById('metadata-list'); + const metadataDescription = document.getElementById('metadata-description'); + const annotationsBadge = document.getElementById('annotations-badge'); + const annotationsDescription = document.getElementById('annotations-description'); + const ctaUnsafe = document.getElementById('cta-unsafe'); + const ctaSafe = document.getElementById('cta-safe'); + + dropZone.addEventListener('click', () => fileInput.click()); + + dropZone.addEventListener('dragover', (e) => { + e.preventDefault(); + dropZone.classList.add('drop-zone--over'); + }); + + ['dragleave', 'dragend'].forEach(type => { + dropZone.addEventListener(type, () => { + dropZone.classList.remove('drop-zone--over'); + }); + }); + + dropZone.addEventListener('drop', (e) => { + e.preventDefault(); + dropZone.classList.remove('drop-zone--over'); + if (e.dataTransfer.files.length) { + fileInput.files = e.dataTransfer.files; + handleFile(e.dataTransfer.files[0]); + } + }); + + fileInput.addEventListener('change', (e) => { + if (fileInput.files.length) { + handleFile(fileInput.files[0]); + } + }); + + async function handleFile(file) { + if (file.type !== 'application/pdf') { + alert('Please upload a PDF file.'); + return; + } + + // UI Transition + resultsDashboard.classList.add('hidden'); + loadingState.classList.remove('hidden'); + dropZone.classList.add('opacity-50', 'pointer-events-none'); + + const reader = new FileReader(); + reader.onload = async function() { + const typedarray = new Uint8Array(this.result); + await analyzePDF(typedarray); + }; + reader.readAsArrayBuffer(file); + } + + async function analyzePDF(data) { + try { + const pdfDoc = await pdfjsLib.getDocument(data).promise; + + let findings = { + wordCount: 0, + annotationCount: 0, + metadata: {}, + metadataFound: false + }; + + // 1. Metadata Check + const { info, metadata } = await pdfDoc.getMetadata(); + const sensitiveKeys = ['Author', 'Creator', 'Producer', 'Title', 'Keywords']; + sensitiveKeys.forEach(key => { + if (info[key] && info[key].trim().length > 0) { + findings.metadata[key] = info[key]; + findings.metadataFound = true; + } + }); + + // 2. Page Analysis + for (let i = 1; i <= pdfDoc.numPages; i++) { + const page = await pdfDoc.getPage(i); + + // Text Layer + const textContent = await page.getTextContent(); + textContent.items.forEach(item => { + if (item.str && item.str.trim().length > 0) { + findings.wordCount += item.str.trim().split(/\s+/).length; + } + }); + + // Annotations + const annotations = await page.getAnnotations(); + findings.annotationCount += annotations.length; + } + + renderResults(findings); + + } catch (err) { + console.error("Analysis failed:", err); + alert("Could not analyze PDF. It might be password protected or corrupted."); + loadingState.classList.add('hidden'); + dropZone.classList.remove('opacity-50', 'pointer-events-none'); + } + } + + function renderResults(findings) { + loadingState.classList.add('hidden'); + resultsDashboard.classList.remove('hidden'); + dropZone.classList.remove('opacity-50', 'pointer-events-none'); + + let score = 100; + if (findings.wordCount > 0) score -= 50; + + // Only penalize for personally identifiable metadata + const piiKeys = ['Author', 'Title', 'Keywords']; + const hasPII = piiKeys.some(key => findings.metadata[key] && findings.metadata[key].trim().length > 0); + if (hasPII) score -= 20; + + if (findings.annotationCount > 0) score -= 30; + + // Limit score + score = Math.max(0, score); + + const isSafe = score === 100; + + // 1. Verdict UI + verdictText.innerText = isSafe ? 'Safe' : 'Unsafe'; + verdictText.className = `text-3xl font-bold mb-2 ${isSafe ? 'text-green-600' : 'text-red-600'}`; + healthScore.innerText = `Redaction Health Score: ${score}/100`; + + verdictIcon.innerHTML = isSafe + ? `<div class="bg-green-100 p-4 rounded-full"><svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7" /></svg></div>` + : `<div class="bg-red-100 p-4 rounded-full"><svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 text-red-600" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /></svg></div>`; + + // 2. Text Layer UI + if (findings.wordCount > 0) { + textBadge.innerText = 'Leaking'; + textBadge.className = 'px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider bg-red-100 text-red-700'; + textDescription.innerHTML = `<strong>Danger:</strong> Found <span class="text-red-600 font-bold">${findings.wordCount}</span> searchable words. This document has not been flattened.`; + } else { + textBadge.innerText = 'Clean'; + textBadge.className = 'px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider bg-green-100 text-green-700'; + textDescription.innerText = 'No searchable text found. The document is likely flattened into an image.'; + } + + // 3. Metadata UI + if (findings.metadataFound) { + metadataBadge.innerText = 'Visible'; + metadataBadge.className = 'px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider bg-yellow-100 text-yellow-700'; + metadataList.classList.remove('hidden'); + + let listHtml = `<table class="w-full text-sm text-left border-collapse">`; + for (const [key, value] of Object.entries(findings.metadata)) { + listHtml += `<tr class="border-b border-gray-50"><td class="py-2 font-medium text-gray-500 w-24">${key}</td><td class="py-2 text-gray-900 truncate max-w-xs">${value}</td></tr>`; + } + listHtml += `</table>`; + metadataList.innerHTML = listHtml; + metadataDescription.innerHTML = `<strong>Warning:</strong> Document properties contain potentially identifying information.`; + } else { + metadataBadge.innerText = 'Hidden'; + metadataBadge.className = 'px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider bg-green-100 text-green-700'; + metadataList.classList.add('hidden'); + metadataDescription.innerText = 'No common sensitive metadata keys were found.'; + } + + // 4. Annotations UI + if (findings.annotationCount > 0) { + annotationsBadge.innerText = 'Found'; + annotationsBadge.className = 'px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider bg-yellow-100 text-yellow-700'; + annotationsDescription.innerHTML = `<strong>Warning:</strong> <span class="font-bold">${findings.annotationCount}</span> unflattened objects found. These might be shapes or highlights used for redaction.`; + } else { + annotationsBadge.innerText = 'None'; + annotationsBadge.className = 'px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider bg-green-100 text-green-700'; + annotationsDescription.innerText = 'No unflattened annotations or interactive elements detected.'; + } + + // 5. CTA Visibility + if (isSafe) { + ctaSafe.classList.remove('hidden'); + ctaUnsafe.classList.add('hidden'); + } else { + ctaSafe.classList.add('hidden'); + ctaUnsafe.classList.remove('hidden'); + } + } + </script> +</body> +</html> |
