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.
|
||||
|
||||
@@ -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;
|
||||
@@ -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'],
|
||||
|
||||
@@ -57,6 +57,16 @@
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<div>
|
||||
<label>Auto-detect unconfigured PPI</label>
|
||||
<p class="setting-desc">Warn when potential personal data (IPs, addresses, paths) is detected that you haven't configured</p>
|
||||
</div>
|
||||
<label class="toggle">
|
||||
<input type="checkbox" id="autoDetect" checked>
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<div>
|
||||
<label>Max log entries</label>
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
|
||||
@@ -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 += `<div style="margin-top:6px;padding:6px 8px;background:#fef3c7;border-radius:4px;color:#92400e;font-size:11px">
|
||||
<strong>Unconfigured PPI detected:</strong>
|
||||
${ppiWarnings.map(w => `<div style="margin-top:3px"><code style="background:#fff;padding:1px 4px;border-radius:2px;color:#b45309">${escapeHtml(w.value)}</code> — ${w.hint}</div>`).join('')}
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Reveal Diff (fake → real) ---
|
||||
|
||||
Reference in New Issue
Block a user