feat: auto-detect unconfigured PPI — warns before sending
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
This commit is contained in:
@@ -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;
|
||||
|
||||
+131
-7
@@ -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 =>
|
||||
`<div class="ss-ad-item">
|
||||
<span class="ss-ad-type">${w.name}</span>
|
||||
<code class="ss-ad-value">${w.value.length > 30 ? w.value.slice(0, 27) + '...' : w.value}</code>
|
||||
<span class="ss-ad-hint">${w.hint}</span>
|
||||
</div>`
|
||||
).join('');
|
||||
|
||||
const more = warnings.length > 5 ? `<div class="ss-ad-more">+${warnings.length - 5} more</div>` : '';
|
||||
|
||||
warningEl.innerHTML = `
|
||||
<div class="ss-ad-header">
|
||||
<strong>Silent Send detected potential PPI that may not be substituted:</strong>
|
||||
<button class="ss-ad-close">×</button>
|
||||
</div>
|
||||
${items}
|
||||
${more}
|
||||
<div class="ss-ad-footer">These were sent as-is. Consider adding them to your identity or mappings.</div>
|
||||
`;
|
||||
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user