From e6b3ef7e392fa044bb3888d18cac073e73bb417b Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 26 Mar 2026 04:38:15 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20auto-detect=20unconfigured=20PPI=20?= =?UTF-8?q?=E2=80=94=20warns=20before=20sending?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Scans outbound messages AFTER all substitutions for personal data the user forgot to configure: - Private/public IP addresses (skips 127.0.0.1, 8.8.8.8, etc.) - MAC addresses - Street addresses ("123 Main St") - GPS coordinates - Dates (possible DOBs) - EIN/tax IDs - Home directory paths not caught by smart patterns - Shell prompts (user@host) - Git remotes (reveals username/org) - Environment variable assignments (HOME=, USER=, etc.) Shows a floating dark warning banner (top-right, auto-dismisses after 15s) listing each detected item with its type, value, and hint. Skips values already in the user's identity config. Also shows PPI warnings in the popup Test tab and adds toggle in Options to disable auto-detect. https://claude.ai/code/session_01Dvgwe7XMoSxnWXkih8p1Cw --- src/content/content.css | 101 ++++++++++++++++++ src/content/content.js | 138 +++++++++++++++++++++++-- src/lib/auto-detect.js | 215 +++++++++++++++++++++++++++++++++++++++ src/lib/storage.js | 1 + src/options/options.html | 10 ++ src/options/options.js | 5 + src/popup/popup.js | 14 +++ 7 files changed, 477 insertions(+), 7 deletions(-) create mode 100644 src/lib/auto-detect.js diff --git a/src/content/content.css b/src/content/content.css index ae765b1..b457e4b 100644 --- a/src/content/content.css +++ b/src/content/content.css @@ -35,6 +35,107 @@ border-radius: 2px; } +/* Auto-detect PPI warning banner */ +.ss-autodetect-warning { + position: fixed; + top: 16px; + right: 16px; + max-width: 400px; + background: #1a1a1a; + color: #e5e7eb; + border: 1px solid #f59e0b; + border-radius: 10px; + padding: 12px 16px; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + font-size: 12px; + z-index: 999999; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4); + opacity: 0; + transform: translateY(-10px); + transition: opacity 0.2s, transform 0.2s; + pointer-events: none; +} + +.ss-autodetect-warning.visible { + opacity: 1; + transform: translateY(0); + pointer-events: auto; +} + +.ss-ad-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 8px; + margin-bottom: 8px; + color: #f59e0b; + font-size: 11px; + line-height: 1.4; +} + +.ss-ad-close { + background: none; + border: none; + color: #6b7280; + font-size: 18px; + cursor: pointer; + padding: 0; + line-height: 1; + flex-shrink: 0; +} + +.ss-ad-close:hover { color: #fff; } + +.ss-ad-item { + display: flex; + align-items: center; + gap: 8px; + padding: 4px 0; + border-bottom: 1px solid #333; +} + +.ss-ad-item:last-of-type { border-bottom: none; } + +.ss-ad-type { + font-size: 10px; + font-weight: 600; + color: #f59e0b; + min-width: 70px; + text-transform: uppercase; +} + +.ss-ad-value { + font-family: 'SF Mono', Monaco, monospace; + font-size: 11px; + color: #4ade80; + background: #0a0a0a; + padding: 2px 6px; + border-radius: 4px; + max-width: 180px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.ss-ad-hint { + font-size: 10px; + color: #9ca3af; + flex: 1; +} + +.ss-ad-more { + font-size: 10px; + color: #6b7280; + padding: 4px 0; +} + +.ss-ad-footer { + margin-top: 8px; + font-size: 10px; + color: #6b7280; + font-style: italic; +} + /* Floating reveal mode indicator */ .ss-reveal-badge { position: fixed; diff --git a/src/content/content.js b/src/content/content.js index 59cec12..5ab2d35 100644 --- a/src/content/content.js +++ b/src/content/content.js @@ -254,6 +254,7 @@ // ============================================================ // Combined substitution: smart patterns + explicit + secret scan + // + auto-detect warning for unconfigured PPI // ============================================================ function substituteAll(text) { const allReplacements = []; @@ -267,23 +268,146 @@ allReplacements.push(...explicit.replacements); // 3. Secret scanner (API keys, tokens, SSNs, credit cards, etc.) + let finalText = explicit.text; if (settings.secretScanning !== false) { - const secrets = scanAndRedactSecrets(explicit.text); + const secrets = scanAndRedactSecrets(finalText); allReplacements.push(...secrets.redactions); - return { - text: secrets.text, - replacements: allReplacements, - modified: allReplacements.length > 0, - }; + finalText = secrets.text; + } + + // 4. Auto-detect: scan the FINAL text for unconfigured PPI + if (settings.autoDetect !== false) { + const warnings = autoDetectPPI(finalText, identity); + if (warnings.length > 0) { + showAutoDetectWarning(warnings); + } } return { - text: explicit.text, + text: finalText, replacements: allReplacements, modified: allReplacements.length > 0, }; } + // ============================================================ + // Auto-Detect PPI Scanner (inline for page world) + // ============================================================ + const PPI_PATTERNS = [ + // Network + { name: 'Private IP', re: /\b(?:10\.\d{1,3}\.\d{1,3}\.\d{1,3}|172\.(?:1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}|192\.168\.\d{1,3}\.\d{1,3})\b/g, + hint: 'Private IP address', cat: 'network' }, + { name: 'Public IP', re: /\b(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\b/g, + hint: 'IP address — could identify your network', cat: 'network', + skip: /^(?:127\.0\.0\.1|0\.0\.0\.0|255\.255\.255\.\d+|8\.8\.[84]\.[84]|1\.1\.1\.1)$/ }, + { name: 'MAC Address', re: /\b(?:[0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}\b/g, + hint: 'MAC address — identifies hardware', cat: 'network' }, + // Location + { name: 'Street Address', re: /\b\d{1,5}\s+(?:[A-Z][a-z]+\s+){1,3}(?:St|Street|Ave|Avenue|Blvd|Boulevard|Dr|Drive|Ln|Lane|Rd|Road|Way|Ct|Court|Pl|Place)\.?\b/gi, + hint: 'Street address', cat: 'address' }, + { name: 'GPS Coordinates', re: /\b-?\d{1,3}\.\d{4,},\s*-?\d{1,3}\.\d{4,}\b/g, + hint: 'GPS coordinates — pinpoints a location', cat: 'address' }, + // Personal + { name: 'Date (possible DOB)', re: /\b(?:(?:0[1-9]|1[0-2])[-/](?:0[1-9]|[12]\d|3[01])[-/](?:19|20)\d{2}|(?:19|20)\d{2}[-/](?:0[1-9]|1[0-2])[-/](?:0[1-9]|[12]\d|3[01]))\b/g, + hint: 'Date — could be a birthday', cat: 'personal' }, + { name: 'EIN / Tax ID', re: /\b\d{2}-\d{7}\b/g, + hint: 'Could be a tax ID', cat: 'document' }, + // Paths not caught by smart patterns + { name: 'Home Path', re: /(?:\/home\/|\/Users\/|C:\\Users\\)[a-zA-Z0-9._-]+/g, + hint: 'Home directory — reveals username', cat: 'path' }, + // Shell prompts + { name: 'Shell Prompt', re: /[a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+[:\$#%>]\s/g, + hint: 'Shell prompt — reveals user@host', cat: 'prompt' }, + // Git remotes + { name: 'Git Remote', re: /(?:git@|https:\/\/)(?:github|gitlab|bitbucket)\.[a-z]+[:/][^\s]+/gi, + hint: 'Git remote — may reveal username/org', cat: 'url' }, + // Env vars + { name: 'Env Variable', re: /\b(?:HOME|USER|USERNAME|LOGNAME|HOSTNAME|COMPUTERNAME|EMAIL)=[^\s]+/gi, + hint: 'Env variable with personal data', cat: 'env' }, + ]; + + function autoDetectPPI(text, ident) { + if (!text || text.length < 5) return []; + + // Build skip set from configured values + const configured = new Set(); + if (ident) { + const addAll = (arr, key) => (arr || []).forEach(item => { + if (item.real) configured.add(item.real.toLowerCase()); + if (item.substitute) configured.add(item.substitute.toLowerCase()); + }); + addAll(ident.names); addAll(ident.emails); + addAll(ident.usernames); addAll(ident.hostnames); addAll(ident.phones); + } + + const findings = []; + for (const pat of PPI_PATTERNS) { + pat.re.lastIndex = 0; + let m; + while ((m = pat.re.exec(text)) !== null) { + const val = m[0]; + if (configured.has(val.toLowerCase())) continue; + if (pat.skip && pat.skip.test(val)) continue; + findings.push({ name: pat.name, value: val, hint: pat.hint, category: pat.cat }); + } + } + + // Deduplicate by value + const seen = new Set(); + return findings.filter(f => { + if (seen.has(f.value)) return false; + seen.add(f.value); + return true; + }); + } + + // ============================================================ + // Auto-Detect Warning UI — floating banner + // ============================================================ + let warningEl = null; + let warningTimeout = null; + + function showAutoDetectWarning(warnings) { + if (!warningEl) { + warningEl = document.createElement('div'); + warningEl.className = 'ss-autodetect-warning'; + document.body.appendChild(warningEl); + } + + const items = warnings.slice(0, 5).map(w => + `
+ ${w.name} + ${w.value.length > 30 ? w.value.slice(0, 27) + '...' : w.value} + ${w.hint} +
` + ).join(''); + + const more = warnings.length > 5 ? `
+${warnings.length - 5} more
` : ''; + + warningEl.innerHTML = ` +
+ Silent Send detected potential PPI that may not be substituted: + +
+ ${items} + ${more} + + `; + + warningEl.classList.add('visible'); + + // Close button + warningEl.querySelector('.ss-ad-close').addEventListener('click', () => { + warningEl.classList.remove('visible'); + }); + + // Auto-dismiss after 15 seconds + if (warningTimeout) clearTimeout(warningTimeout); + warningTimeout = setTimeout(() => { + warningEl.classList.remove('visible'); + }, 15000); + } + // ============================================================ // Secret Scanner (inline for page world) // Detects API keys, tokens, passwords, SSNs, credit cards, etc. diff --git a/src/lib/auto-detect.js b/src/lib/auto-detect.js new file mode 100644 index 0000000..57cbbc3 --- /dev/null +++ b/src/lib/auto-detect.js @@ -0,0 +1,215 @@ +/** + * Silent Send - Auto-Detect + * + * Scans text for potential PPI that the user hasn't configured. + * This catches things the identity and secret scanner can't — + * because the user forgot or didn't know to configure them. + * + * Returns warnings (not auto-redactions) so the user can decide. + */ + +const PPI_PATTERNS = [ + // --- Network --- + { + name: 'Private IP Address', + regex: /\b(?:10\.\d{1,3}\.\d{1,3}\.\d{1,3}|172\.(?:1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}|192\.168\.\d{1,3}\.\d{1,3})\b/g, + category: 'network', + hint: 'Private/local IP address', + }, + { + name: 'Public IP Address', + regex: /\b(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\b/g, + category: 'network', + hint: 'IP address — could identify your network', + // Exclude common non-PPI IPs + exclude: /^(?:127\.0\.0\.1|0\.0\.0\.0|255\.255\.255\.\d+|8\.8\.[84]\.[84]|1\.1\.1\.1|1\.0\.0\.1)$/, + }, + { + name: 'IPv6 Address', + regex: /\b(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}\b/g, + category: 'network', + hint: 'IPv6 address', + }, + { + name: 'MAC Address', + regex: /\b(?:[0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}\b/g, + category: 'network', + hint: 'MAC address — identifies your hardware', + }, + + // --- Location / Address --- + { + name: 'US Street Address', + regex: /\b\d{1,5}\s+(?:[A-Z][a-z]+\s+){1,3}(?:St|Street|Ave|Avenue|Blvd|Boulevard|Dr|Drive|Ln|Lane|Rd|Road|Way|Ct|Court|Pl|Place|Cir|Circle)\.?\b/gi, + category: 'address', + hint: 'Looks like a street address', + }, + { + name: 'US Zip Code', + regex: /\b\d{5}(?:-\d{4})?\b/g, + category: 'address', + hint: 'Could be a zip code', + // Only flag if near address-like context + contextRequired: true, + }, + { + name: 'GPS Coordinates', + regex: /\b-?\d{1,3}\.\d{4,},\s*-?\d{1,3}\.\d{4,}\b/g, + category: 'address', + hint: 'GPS coordinates — pinpoints a location', + }, + + // --- Identity Documents --- + { + name: 'US Passport Number', + regex: /\b[A-Z]\d{8}\b/g, + category: 'document', + hint: 'Could be a passport number', + contextRequired: true, + }, + { + name: 'US Driver License', + regex: /\b[A-Z]\d{7,14}\b/g, + category: 'document', + hint: 'Could be a driver license number', + contextRequired: true, + }, + { + name: 'Date of Birth Pattern', + regex: /\b(?:(?:0[1-9]|1[0-2])[-/](?:0[1-9]|[12]\d|3[01])[-/](?:19|20)\d{2}|(?:19|20)\d{2}[-/](?:0[1-9]|1[0-2])[-/](?:0[1-9]|[12]\d|3[01]))\b/g, + category: 'personal', + hint: 'Date — could be a birthday or other personal date', + }, + { + name: 'EIN / Tax ID', + regex: /\b\d{2}-\d{7}\b/g, + category: 'document', + hint: 'Could be an EIN or tax ID number', + }, + + // --- URLs with usernames --- + { + name: 'URL with Username', + regex: /https?:\/\/[^\s]*(?:user|profile|account|member)[^\s]*/gi, + category: 'url', + hint: 'URL that may contain your identity', + }, + { + name: 'Git Remote with Username', + regex: /(?:git@|https:\/\/)(?:github|gitlab|bitbucket)\.[a-z]+[:/][^\s]+/gi, + category: 'url', + hint: 'Git remote — may reveal your username/org', + }, + + // --- File paths with home dirs (if not already caught by smart patterns) --- + { + name: 'Home Directory Path', + regex: /(?:\/home\/|\/Users\/|C:\\Users\\)[a-zA-Z0-9._-]+/g, + category: 'path', + hint: 'Home directory path — reveals your username', + }, + + // --- Environment Variables with Sensitive Values --- + { + name: 'Env Variable Assignment', + regex: /\b(?:HOME|USER|USERNAME|LOGNAME|HOSTNAME|COMPUTERNAME|EMAIL)=\S+/gi, + category: 'env', + hint: 'Environment variable with personal data', + }, + + // --- Shell Prompts --- + { + name: 'Shell Prompt', + regex: /[a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+[:\$#%>]\s/g, + category: 'prompt', + hint: 'Shell prompt — reveals username and hostname', + }, +]; + +// Context words that make ambiguous patterns more likely to be PPI +const CONTEXT_WORDS = /\b(?:born|birthday|dob|birth|passport|license|driver|ssn|social\s*security|address|home|live|lives|reside|zip|postal)\b/i; + +const AutoDetect = { + /** + * Scan text for potential unconfigured PPI. + * Pass in identity so we can skip values the user already configured. + * + * Returns array of { name, value, hint, category, index } + */ + scan(text, identity) { + if (!text || text.length < 5) return []; + + const hasContext = CONTEXT_WORDS.test(text); + const findings = []; + + // Build a set of already-configured values to skip + const configured = new Set(); + if (identity) { + for (const n of (identity.names || [])) { + if (n.real) configured.add(n.real.toLowerCase()); + if (n.substitute) configured.add(n.substitute.toLowerCase()); + } + for (const e of (identity.emails || [])) { + if (e.real) configured.add(e.real.toLowerCase()); + if (e.substitute) configured.add(e.substitute.toLowerCase()); + } + for (const u of (identity.usernames || [])) { + if (u.real) configured.add(u.real.toLowerCase()); + if (u.substitute) configured.add(u.substitute.toLowerCase()); + } + for (const h of (identity.hostnames || [])) { + if (h.real) configured.add(h.real.toLowerCase()); + if (h.substitute) configured.add(h.substitute.toLowerCase()); + } + for (const p of (identity.phones || [])) { + if (p.real) configured.add(p.real.toLowerCase()); + if (p.substitute) configured.add(p.substitute.toLowerCase()); + } + } + + for (const pattern of PPI_PATTERNS) { + // Skip context-dependent patterns if no context words present + if (pattern.contextRequired && !hasContext) continue; + + pattern.regex.lastIndex = 0; + let match; + + while ((match = pattern.regex.exec(text)) !== null) { + const value = match[0]; + + // Skip if already configured + if (configured.has(value.toLowerCase())) continue; + + // Skip excluded values (like 127.0.0.1) + if (pattern.exclude && pattern.exclude.test(value)) continue; + + findings.push({ + name: pattern.name, + value, + hint: pattern.hint, + category: pattern.category, + index: match.index, + }); + } + } + + // Deduplicate overlapping matches + findings.sort((a, b) => a.index - b.index); + const deduped = []; + let lastEnd = -1; + for (const f of findings) { + if (f.index >= lastEnd) { + deduped.push(f); + lastEnd = f.index + f.value.length; + } + } + + return deduped; + }, +}; + +if (typeof globalThis !== 'undefined') { + globalThis.AutoDetect = AutoDetect; +} + +export default AutoDetect; diff --git a/src/lib/storage.js b/src/lib/storage.js index 70916d1..e9b08de 100644 --- a/src/lib/storage.js +++ b/src/lib/storage.js @@ -19,6 +19,7 @@ const DEFAULT_SETTINGS = { showHighlights: false, revealMode: false, secretScanning: true, + autoDetect: true, maxLogEntries: 200, customDomains: [], categories: ['name', 'email', 'phone', 'address', 'ssn', 'dob', 'domain', 'general'], diff --git a/src/options/options.html b/src/options/options.html index 94d84fc..91b5f63 100644 --- a/src/options/options.html +++ b/src/options/options.html @@ -57,6 +57,16 @@ +
+
+ +

Warn when potential personal data (IPs, addresses, paths) is detected that you haven't configured

+
+ +
diff --git a/src/options/options.js b/src/options/options.js index 0f599a6..fad2243 100644 --- a/src/options/options.js +++ b/src/options/options.js @@ -14,6 +14,7 @@ document.addEventListener('DOMContentLoaded', async () => { // Apply settings to UI $('#showHighlights').checked = settings.showHighlights || false; $('#secretScanning').checked = settings.secretScanning !== false; + $('#autoDetect').checked = settings.autoDetect !== false; $('#maxLogEntries').value = settings.maxLogEntries || 200; renderMappings(); @@ -41,6 +42,10 @@ document.addEventListener('DOMContentLoaded', async () => { await Storage.saveSettings({ secretScanning: e.target.checked }); }); + $('#autoDetect').addEventListener('change', async (e) => { + await Storage.saveSettings({ autoDetect: e.target.checked }); + }); + $('#maxLogEntries').addEventListener('change', async (e) => { await Storage.saveSettings({ maxLogEntries: parseInt(e.target.value, 10) || 200 }); }); diff --git a/src/popup/popup.js b/src/popup/popup.js index 4b14db4..b651a60 100644 --- a/src/popup/popup.js +++ b/src/popup/popup.js @@ -1,6 +1,7 @@ import SubstitutionEngine from '../lib/substitution-engine.js'; import SmartPatterns from '../lib/smart-patterns.js'; import SecretScanner from '../lib/secret-scanner.js'; +import AutoDetect from '../lib/auto-detect.js'; import Storage from '../lib/storage.js'; import api from '../lib/browser-polyfill.js'; @@ -607,7 +608,20 @@ function renderTestDiff() { if (explicitCount > 0) parts.push(`${explicitCount} explicit`); if (secretCount > 0) parts.push(`${secretCount} secrets redacted`); if (warnCount > 0) parts.push(`${warnCount} warnings`); + + // Auto-detect unconfigured PPI in the final text + const ppiWarnings = AutoDetect.scan(finalText, identity); + if (ppiWarnings.length > 0) parts.push(`${ppiWarnings.length} PPI detected`); + stats.textContent = `${allReplacements.length} substitution${allReplacements.length !== 1 ? 's' : ''} (${parts.join(', ')})`; + + // Show PPI warnings below stats + if (ppiWarnings.length > 0) { + stats.innerHTML += `
+ Unconfigured PPI detected: + ${ppiWarnings.map(w => `
${escapeHtml(w.value)} — ${w.hint}
`).join('')} +
`; + } } // --- Reveal Diff (fake → real) ---