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) ---