merge: integrate claude/read-repo-YMu21 branch
Merged changes from the other session: - Ko-fi support link in options footer - PPI → PII renaming across codebase - Legal/licensing updates (stronger disclaimer, non-commercial only) - Custom secret/auto-redact patterns - Internal refactoring of secret scanner to auto-redact naming - Custom domains in popup Options tab - Additional AI services (Perplexity, DeepSeek, HuggingChat, Poe) - Developer sites (GitHub, GitLab, Reddit, StackOverflow, Pastebin) - Dynamic content script registration for custom domains Resolved conflicts in options.html and popup.html (kept newer "Auto Redact" naming with custom patterns description). https://claude.ai/code/session_01SWSwDfMVij53bCTNSCLMwn
This commit is contained in:
@@ -35,7 +35,7 @@
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* Auto-detect PPI warning banner */
|
||||
/* Auto-detect PII warning banner */
|
||||
.ss-autodetect-warning {
|
||||
position: fixed;
|
||||
top: 16px;
|
||||
@@ -136,7 +136,7 @@
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Pre-send PPI warning (spellcheck-style, appears while typing) */
|
||||
/* Pre-send PII warning (spellcheck-style, appears while typing) */
|
||||
.ss-presend-warning {
|
||||
position: fixed;
|
||||
top: 16px;
|
||||
|
||||
+47
-36
@@ -287,8 +287,8 @@
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Combined substitution: smart patterns + explicit + secret scan
|
||||
// + auto-detect warning for unconfigured PPI
|
||||
// Combined substitution: smart patterns + explicit + auto-redact
|
||||
// + auto-detect warning for unconfigured PII
|
||||
// ============================================================
|
||||
function substituteAll(text) {
|
||||
const allReplacements = [];
|
||||
@@ -301,20 +301,20 @@
|
||||
const explicit = substitute(smart.text, mappings);
|
||||
allReplacements.push(...explicit.replacements);
|
||||
|
||||
// 3. Secret scanner (API keys, tokens, SSNs, credit cards, etc.)
|
||||
// 3. Auto Redact (API keys, tokens, SSNs, credit cards, custom patterns, etc.)
|
||||
let finalText = explicit.text;
|
||||
if (settings.secretScanning !== false) {
|
||||
const secrets = scanAndRedactSecrets(finalText);
|
||||
allReplacements.push(...secrets.redactions);
|
||||
finalText = secrets.text;
|
||||
if (settings.autoRedact !== false) {
|
||||
const redacted = runAutoRedact(finalText);
|
||||
allReplacements.push(...redacted.redactions);
|
||||
finalText = redacted.text;
|
||||
}
|
||||
|
||||
// 4. Auto-detect: scan the FINAL text for unconfigured PPI
|
||||
// 4. Auto-detect: scan the FINAL text for unconfigured PII
|
||||
// Auto-redact if enabled, otherwise just warn
|
||||
if (settings.autoDetect !== false) {
|
||||
const warnings = autoDetectPPI(finalText, identity);
|
||||
const warnings = autoDetectPII(finalText, identity);
|
||||
if (warnings.length > 0) {
|
||||
// Auto-redact detected PPI in the outbound text
|
||||
// Auto-redact detected PII in the outbound text
|
||||
if (settings.autoRedactDetected !== false) {
|
||||
for (let i = warnings.length - 1; i >= 0; i--) {
|
||||
const w = warnings[i];
|
||||
@@ -343,9 +343,9 @@
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Auto-Detect PPI Scanner (inline for page world)
|
||||
// Auto-Detect PII Scanner (inline for page world)
|
||||
// ============================================================
|
||||
const PPI_PATTERNS = [
|
||||
const PII_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' },
|
||||
@@ -574,7 +574,7 @@
|
||||
});
|
||||
}
|
||||
|
||||
function autoDetectPPI(text, ident) {
|
||||
function autoDetectPII(text, ident) {
|
||||
if (!text || text.length < 5) return [];
|
||||
const hasContext = CONTEXT_WORDS_RE.test(text);
|
||||
|
||||
@@ -595,7 +595,7 @@
|
||||
}
|
||||
|
||||
const findings = [];
|
||||
for (const pat of PPI_PATTERNS) {
|
||||
for (const pat of PII_PATTERNS) {
|
||||
if (pat.contextRequired && !hasContext) continue;
|
||||
pat.re.lastIndex = 0;
|
||||
let m;
|
||||
@@ -649,7 +649,7 @@
|
||||
|
||||
safeHTML(warningEl, `
|
||||
<div class="ss-ad-header">
|
||||
<strong>Silent Send detected potential PPI that may not be substituted:</strong>
|
||||
<strong>Silent Send detected potential PII that may not be substituted:</strong>
|
||||
<button class="ss-ad-close">×</button>
|
||||
</div>
|
||||
${items}
|
||||
@@ -672,10 +672,11 @@
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Secret Scanner (inline for page world)
|
||||
// Detects API keys, tokens, passwords, SSNs, credit cards, etc.
|
||||
// Auto Redact (inline for page world)
|
||||
// Detects API keys, tokens, passwords, SSNs, credit cards,
|
||||
// plus user-defined custom patterns from settings.
|
||||
// ============================================================
|
||||
const SECRET_PATTERNS = [
|
||||
const REDACT_PATTERNS = [
|
||||
// OpenAI
|
||||
{ name: 'OpenAI Key', re: /\bsk-[A-Za-z0-9]{20,}\b/g, to: '[REDACTED-OPENAI-KEY]' },
|
||||
{ name: 'OpenAI Project Key', re: /\bsk-proj-[A-Za-z0-9_-]{20,}\b/g, to: '[REDACTED-OPENAI-KEY]' },
|
||||
@@ -713,11 +714,21 @@
|
||||
{ name: 'Credit Card', re: /\b(?:4\d{3}|5[1-5]\d{2}|3[47]\d{2}|6(?:011|5\d{2}))[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}\b/g, to: '[REDACTED-CARD]' },
|
||||
];
|
||||
|
||||
function scanAndRedactSecrets(text) {
|
||||
function runAutoRedact(text) {
|
||||
const redactions = [];
|
||||
let result = text;
|
||||
|
||||
for (const pat of SECRET_PATTERNS) {
|
||||
// Combine built-in + custom patterns
|
||||
const allPatterns = [...REDACT_PATTERNS];
|
||||
const custom = settings.customRedactPatterns || [];
|
||||
for (const cp of custom) {
|
||||
if (!cp.enabled || !cp.pattern) continue;
|
||||
try {
|
||||
allPatterns.push({ name: cp.name, re: new RegExp(cp.pattern, 'g'), to: cp.redact });
|
||||
} catch { /* invalid regex — skip */ }
|
||||
}
|
||||
|
||||
for (const pat of allPatterns) {
|
||||
pat.re.lastIndex = 0;
|
||||
const matches = [];
|
||||
let m;
|
||||
@@ -735,7 +746,7 @@
|
||||
redactions.push({
|
||||
original: match.value.slice(0, 8) + '...', // Don't log the full secret
|
||||
replaced: replacement,
|
||||
category: 'secret',
|
||||
category: 'redact',
|
||||
pattern: pat.name,
|
||||
});
|
||||
result =
|
||||
@@ -1036,7 +1047,7 @@
|
||||
// ============================================================
|
||||
// Document Upload Processing
|
||||
//
|
||||
// Scans files in FormData uploads for PPI. Supports PDF, DOCX,
|
||||
// Scans files in FormData uploads for PII. Supports PDF, DOCX,
|
||||
// XLSX, and text files. Shows preview for binary formats.
|
||||
// ============================================================
|
||||
|
||||
@@ -1108,8 +1119,8 @@
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan a document file for PPI. Strategy: extract text from any
|
||||
* format, substitute PPI, upload as plaintext. The AI extracts text
|
||||
* Scan a document file for PII. Strategy: extract text from any
|
||||
* format, substitute PII, upload as plaintext. The AI extracts text
|
||||
* from files anyway — no need to preserve formatting in a file
|
||||
* the user never gets back. Original stays untouched on disk.
|
||||
*
|
||||
@@ -1366,7 +1377,7 @@
|
||||
|
||||
safeHTML(docPreviewEl, `
|
||||
<div class="ss-dp-header">
|
||||
<strong>PPI found in ${esc(filename)}</strong>
|
||||
<strong>PII found in ${esc(filename)}</strong>
|
||||
<span class="ss-dp-count">${preview.replacementCount} item(s)</span>
|
||||
</div>
|
||||
<div class="ss-dp-note">${preview.note || ''}</div>
|
||||
@@ -1495,7 +1506,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Also add auto-detect and secret scanner substitutions from this session
|
||||
// Also add auto-detect and auto-redact substitutions from this session
|
||||
for (const [key, entry] of sessionSubstitutions) {
|
||||
if (!pairs.some(p => p.from.toLowerCase() === key)) {
|
||||
pairs.push({ from: entry.replaced, to: entry.original });
|
||||
@@ -1815,7 +1826,7 @@
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Pre-Send PPI Detection — scans as you type/paste (spellcheck style)
|
||||
// Pre-Send PII Detection — scans as you type/paste (spellcheck style)
|
||||
// ============================================================
|
||||
|
||||
// Generate obviously-fake values using reserved/standard ranges
|
||||
@@ -1879,7 +1890,7 @@
|
||||
|
||||
safeHTML(preSendWarningEl, `
|
||||
<div class="ss-ad-header">
|
||||
<strong>Potential PPI detected — not yet configured:</strong>
|
||||
<strong>Potential PII detected — not yet configured:</strong>
|
||||
<button class="ss-ad-close">×</button>
|
||||
</div>
|
||||
${items}
|
||||
@@ -1918,12 +1929,12 @@
|
||||
// Persist via storage bridge (handles encryption transparently)
|
||||
setStorageData('ss_mappings', mappings);
|
||||
|
||||
// Replace the PPI value in the current input right now
|
||||
// Replace the PII value in the current input right now
|
||||
if (inputEl) {
|
||||
replaceInInput(inputEl, real, fake);
|
||||
// Re-scan — will dismiss warning if no more PPI remains
|
||||
// Re-scan — will dismiss warning if no more PII remains
|
||||
if (inputScanTimer) clearTimeout(inputScanTimer);
|
||||
inputScanTimer = setTimeout(() => scanInputForPPI(inputEl), 150);
|
||||
inputScanTimer = setTimeout(() => scanInputForPII(inputEl), 150);
|
||||
}
|
||||
|
||||
// Visual feedback
|
||||
@@ -1946,7 +1957,7 @@
|
||||
// Re-scan to update warning
|
||||
if (inputEl) {
|
||||
if (inputScanTimer) clearTimeout(inputScanTimer);
|
||||
inputScanTimer = setTimeout(() => scanInputForPPI(inputEl), 150);
|
||||
inputScanTimer = setTimeout(() => scanInputForPII(inputEl), 150);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1997,14 +2008,14 @@
|
||||
// Scan input on type and paste
|
||||
let inputScanTimer = null;
|
||||
|
||||
function scanInputForPPI(target) {
|
||||
function scanInputForPII(target) {
|
||||
const text = target.textContent || target.value || '';
|
||||
if (!text || text.length < 5) {
|
||||
if (preSendWarningEl) preSendWarningEl.classList.remove('visible');
|
||||
return;
|
||||
}
|
||||
|
||||
const warnings = autoDetectPPI(text, identity);
|
||||
const warnings = autoDetectPII(text, identity);
|
||||
if (warnings.length > 0) {
|
||||
showPreSendWarning(warnings, target);
|
||||
} else if (preSendWarningEl) {
|
||||
@@ -2018,7 +2029,7 @@
|
||||
if (target.matches?.('[contenteditable], textarea, input[type="text"]')) {
|
||||
// Debounce — don't scan on every keystroke
|
||||
if (inputScanTimer) clearTimeout(inputScanTimer);
|
||||
inputScanTimer = setTimeout(() => scanInputForPPI(target), 800);
|
||||
inputScanTimer = setTimeout(() => scanInputForPII(target), 800);
|
||||
}
|
||||
}, true);
|
||||
|
||||
@@ -2028,7 +2039,7 @@
|
||||
if (target.matches?.('[contenteditable], textarea, input[type="text"]') ||
|
||||
target.closest?.('[contenteditable]')) {
|
||||
// Scan shortly after paste completes
|
||||
setTimeout(() => scanInputForPPI(target.closest?.('[contenteditable]') || target), 200);
|
||||
setTimeout(() => scanInputForPII(target.closest?.('[contenteditable]') || target), 200);
|
||||
}
|
||||
}, true);
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
/**
|
||||
* 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 —
|
||||
* Scans text for potential PII that the user hasn't configured.
|
||||
* This catches things the identity and auto-redact 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 = [
|
||||
const PII_PATTERNS = [
|
||||
// --- Network ---
|
||||
{
|
||||
name: 'Private IP Address',
|
||||
@@ -21,7 +21,7 @@ const PPI_PATTERNS = [
|
||||
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 common non-PII 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)$/,
|
||||
},
|
||||
{
|
||||
@@ -126,12 +126,12 @@ const PPI_PATTERNS = [
|
||||
},
|
||||
];
|
||||
|
||||
// Context words that make ambiguous patterns more likely to be PPI
|
||||
// Context words that make ambiguous patterns more likely to be PII
|
||||
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.
|
||||
* Scan text for potential unconfigured PII.
|
||||
* Pass in identity so we can skip values the user already configured.
|
||||
*
|
||||
* Returns array of { name, value, hint, category, index }
|
||||
@@ -167,7 +167,7 @@ const AutoDetect = {
|
||||
}
|
||||
}
|
||||
|
||||
for (const pattern of PPI_PATTERNS) {
|
||||
for (const pattern of PII_PATTERNS) {
|
||||
// Skip context-dependent patterns if no context words present
|
||||
if (pattern.contextRequired && !hasContext) continue;
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
/**
|
||||
* Silent Send - Secret Scanner
|
||||
* Silent Send - Auto Redact
|
||||
*
|
||||
* Detects common secret/credential patterns in text and either
|
||||
* warns or auto-redacts them. This catches things the identity-based
|
||||
* smart patterns can't: API keys, tokens, passwords, SSNs, credit
|
||||
* cards, private keys, connection strings, etc.
|
||||
*
|
||||
* Supports user-defined custom patterns for proprietary token formats.
|
||||
*
|
||||
* Each pattern has:
|
||||
* - name: human-readable label
|
||||
* - regex: detection pattern
|
||||
@@ -13,7 +15,7 @@
|
||||
* - severity: 'critical' (always redact) or 'warning' (flag but allow)
|
||||
*/
|
||||
|
||||
const SECRET_PATTERNS = [
|
||||
const REDACT_PATTERNS = [
|
||||
// --- API Keys ---
|
||||
{
|
||||
name: 'OpenAI API Key',
|
||||
@@ -161,14 +163,39 @@ const SECRET_PATTERNS = [
|
||||
},
|
||||
];
|
||||
|
||||
const SecretScanner = {
|
||||
const AutoRedact = {
|
||||
/**
|
||||
* Build the full pattern list (built-in + custom).
|
||||
* Custom patterns come from settings.customRedactPatterns.
|
||||
*/
|
||||
_buildPatterns(customPatterns) {
|
||||
const all = [...REDACT_PATTERNS];
|
||||
if (Array.isArray(customPatterns)) {
|
||||
for (const cp of customPatterns) {
|
||||
if (!cp.enabled || !cp.pattern) continue;
|
||||
try {
|
||||
all.push({
|
||||
name: cp.name || 'Custom Pattern',
|
||||
regex: new RegExp(cp.pattern, 'g'),
|
||||
redact: cp.redact || '[REDACTED-CUSTOM]',
|
||||
severity: 'critical',
|
||||
});
|
||||
} catch { /* invalid regex — skip */ }
|
||||
}
|
||||
}
|
||||
return all;
|
||||
},
|
||||
|
||||
/**
|
||||
* Scan text for secrets. Returns list of findings.
|
||||
* @param {string} text
|
||||
* @param {Array} [customPatterns] — from settings.customRedactPatterns
|
||||
*/
|
||||
scan(text) {
|
||||
scan(text, customPatterns) {
|
||||
const findings = [];
|
||||
const patterns = this._buildPatterns(customPatterns);
|
||||
|
||||
for (const pattern of SECRET_PATTERNS) {
|
||||
for (const pattern of patterns) {
|
||||
// Reset regex lastIndex
|
||||
pattern.regex.lastIndex = 0;
|
||||
let match;
|
||||
@@ -204,9 +231,11 @@ const SecretScanner = {
|
||||
/**
|
||||
* Redact all critical secrets in text. Warnings are not auto-redacted.
|
||||
* Returns { text, redactions[] }
|
||||
* @param {string} text
|
||||
* @param {Array} [customPatterns] — from settings.customRedactPatterns
|
||||
*/
|
||||
redact(text) {
|
||||
const findings = this.scan(text);
|
||||
redact(text, customPatterns) {
|
||||
const findings = this.scan(text, customPatterns);
|
||||
const redactions = [];
|
||||
let result = text;
|
||||
|
||||
@@ -220,7 +249,7 @@ const SecretScanner = {
|
||||
redactions.push({
|
||||
original: f.value,
|
||||
replaced: f.redactTo,
|
||||
category: 'secret',
|
||||
category: 'redact',
|
||||
pattern: f.name,
|
||||
});
|
||||
}
|
||||
@@ -237,7 +266,7 @@ const SecretScanner = {
|
||||
};
|
||||
|
||||
if (typeof globalThis !== 'undefined') {
|
||||
globalThis.SecretScanner = SecretScanner;
|
||||
globalThis.AutoRedact = AutoRedact;
|
||||
}
|
||||
|
||||
export default SecretScanner;
|
||||
export default AutoRedact;
|
||||
@@ -1,11 +1,11 @@
|
||||
/**
|
||||
* Silent Send - Document Scanner
|
||||
*
|
||||
* Scans uploaded documents for PPI and substitutes/redacts before
|
||||
* Scans uploaded documents for PII and substitutes/redacts before
|
||||
* the file reaches the AI service.
|
||||
*
|
||||
* Supported formats:
|
||||
* - PDF: Extract text, scan for PPI, create sanitized plaintext version
|
||||
* - PDF: Extract text, scan for PII, create sanitized plaintext version
|
||||
* (PDFs can't be reliably edited in-place without breaking layout)
|
||||
* - DOCX: Parse XML, find-replace text, repackage ZIP (layout preserved)
|
||||
* - XLSX: Parse cells, find-replace values, repackage (formatting preserved)
|
||||
@@ -13,7 +13,7 @@
|
||||
*
|
||||
* Modes:
|
||||
* - Silent: substitute and upload (default for text files)
|
||||
* - Preview: show PPI findings, let user confirm before upload (default for PDF/DOCX/XLSX)
|
||||
* - Preview: show PII findings, let user confirm before upload (default for PDF/DOCX/XLSX)
|
||||
*
|
||||
* Integration:
|
||||
* - The fetch interceptor in content.js detects multipart/form-data uploads
|
||||
@@ -70,9 +70,9 @@ const DocumentScanner = {
|
||||
},
|
||||
|
||||
/**
|
||||
* PDF: extract text, scan for PPI, create sanitized text file.
|
||||
* PDF: extract text, scan for PII, create sanitized text file.
|
||||
* PDFs can't be reliably edited in-place, so we extract text,
|
||||
* substitute PPI, and send the clean text instead.
|
||||
* substitute PII, and send the clean text instead.
|
||||
*/
|
||||
async _processPDF(file, filename, substituteAll, options) {
|
||||
const text = await this._extractPDFText(file);
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
* - Policy updates are applied automatically
|
||||
*
|
||||
* Privacy: the org admin can check compliance (are required fields
|
||||
* configured?) but CANNOT see individual PPI values.
|
||||
* configured?) but CANNOT see individual PII values.
|
||||
*/
|
||||
|
||||
import api from './browser-polyfill.js';
|
||||
@@ -198,11 +198,11 @@ const OrgPolicy = {
|
||||
},
|
||||
|
||||
/**
|
||||
* Get org-required secret scanner patterns.
|
||||
* Get org-required auto-redact patterns.
|
||||
*
|
||||
* @returns {Array} additional patterns to add to the secret scanner
|
||||
* @returns {Array} additional patterns to add to auto-redact
|
||||
*/
|
||||
async getOrgSecretPatterns() {
|
||||
async getOrgRedactPatterns() {
|
||||
const policy = await this.getPolicy();
|
||||
if (!policy?.requiredSecretPatterns?.length) return [];
|
||||
|
||||
@@ -220,7 +220,7 @@ const OrgPolicy = {
|
||||
|
||||
/**
|
||||
* Check if the user's configuration meets org policy requirements.
|
||||
* Returns compliance status WITHOUT revealing actual PPI values.
|
||||
* Returns compliance status WITHOUT revealing actual PII values.
|
||||
*
|
||||
* @returns {{ compliant: boolean, missing: string[], configured: string[] }}
|
||||
*/
|
||||
|
||||
+3
-2
@@ -23,7 +23,7 @@ const KEYS = {
|
||||
SETTINGS: 'ss_settings',
|
||||
};
|
||||
|
||||
// Keys that contain sensitive PPI and should be encrypted at rest
|
||||
// Keys that contain sensitive PII and should be encrypted at rest
|
||||
// All user data keys are encrypted at rest — settings included since
|
||||
// custom domains and configuration can reveal what services the user
|
||||
// accesses. Only ss_sync_encryption (salt, verification blob) and
|
||||
@@ -34,12 +34,13 @@ const DEFAULT_SETTINGS = {
|
||||
enabled: true,
|
||||
showHighlights: false,
|
||||
revealMode: false,
|
||||
secretScanning: true,
|
||||
autoRedact: true,
|
||||
autoDetect: true,
|
||||
autoRedactDetected: true,
|
||||
autoAddDetected: true,
|
||||
maxLogEntries: 100,
|
||||
customDomains: [],
|
||||
customRedactPatterns: [],
|
||||
categories: ['name', 'email', 'phone', 'address', 'ssn', 'dob', 'domain', 'password', 'general'],
|
||||
browserSync: false,
|
||||
};
|
||||
|
||||
@@ -49,17 +49,38 @@
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<div>
|
||||
<label>Auto-redact secrets</label>
|
||||
<p class="setting-desc">Automatically redact API keys, tokens, passwords, SSNs, credit card numbers</p>
|
||||
<label>Auto Redact</label>
|
||||
<p class="setting-desc">Automatically detect and redact API keys, tokens, passwords, SSNs, credit card numbers, and custom patterns</p>
|
||||
</div>
|
||||
<label class="toggle">
|
||||
<input type="checkbox" id="secretScanning" checked>
|
||||
<input type="checkbox" id="autoRedactToggle" checked>
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Custom Secret Patterns -->
|
||||
<div style="margin:12px 0;padding:12px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px">
|
||||
<h3 style="font-size:13px;font-weight:600;margin:0 0 4px">Custom Redact Patterns</h3>
|
||||
<p class="setting-desc" style="margin-bottom:8px">
|
||||
Define your own patterns to catch proprietary tokens, internal URLs with keys, or any format the built-in scanner doesn't cover.
|
||||
</p>
|
||||
<div id="customRedactList"></div>
|
||||
<div style="display:flex;flex-direction:column;gap:6px;margin-top:8px">
|
||||
<div style="display:flex;gap:6px;flex-wrap:wrap">
|
||||
<input type="text" id="newRedactName" placeholder="Name (e.g. ControlD Token)" style="flex:1;min-width:140px;font-size:12px;padding:5px 8px;border:1px solid #d1d5db;border-radius:6px">
|
||||
<input type="text" id="newRedactPattern" placeholder="Regex or prefix (e.g. ctrl_[A-Za-z0-9]{20,})" style="flex:2;min-width:200px;font-size:12px;font-family:monospace;padding:5px 8px;border:1px solid #d1d5db;border-radius:6px">
|
||||
</div>
|
||||
<div style="display:flex;gap:6px;align-items:center">
|
||||
<input type="text" id="newRedactReplacement" placeholder="Replacement (default: [REDACTED-NAME])" style="flex:1;font-size:12px;padding:5px 8px;border:1px solid #d1d5db;border-radius:6px">
|
||||
<button class="btn btn-primary btn-sm" id="btnAddRedactPattern">Add Pattern</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="setting-desc" style="margin-top:6px;margin-bottom:0">
|
||||
<strong>Tip:</strong> For a URL like <code>https://dns.example.com/abc123</code>, use a pattern like <code>dns\.example\.com/[A-Za-z0-9;]+</code> to match the secret path segment.
|
||||
</p>
|
||||
</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">
|
||||
@@ -69,8 +90,8 @@
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<div>
|
||||
<label>Auto-redact detected PPI on send</label>
|
||||
<p class="setting-desc">Automatically replace detected PPI with generic placeholders (192.0.2.1, 123 Example Street, etc.) when sending</p>
|
||||
<label>Auto-redact detected PII on send</label>
|
||||
<p class="setting-desc">Automatically replace detected PII with generic placeholders (192.0.2.1, 123 Example Street, etc.) when sending</p>
|
||||
</div>
|
||||
<label class="toggle">
|
||||
<input type="checkbox" id="autoRedactDetected" checked>
|
||||
@@ -79,8 +100,8 @@
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<div>
|
||||
<label>Offer to auto-add detected PPI</label>
|
||||
<p class="setting-desc">Show a + button on detected PPI to instantly create a mapping with a suggested fake value</p>
|
||||
<label>Offer to auto-add detected PII</label>
|
||||
<p class="setting-desc">Show a + button on detected PII to instantly create a mapping with a suggested fake value</p>
|
||||
</div>
|
||||
<label class="toggle">
|
||||
<input type="checkbox" id="autoAddDetected" checked>
|
||||
@@ -611,6 +632,13 @@
|
||||
|
||||
<footer>
|
||||
<p>Silent Send v0.9.0</p>
|
||||
<p style="font-size:11px;color:#9ca3af;margin-top:6px;max-width:600px">
|
||||
Silent Send is a convenience tool, not a security guarantee. Third-party sites may change how they send data at any time, which can cause missed substitutions without warning. You are responsible for verifying your data before sending. See the <a href="https://github.com/outis1one/silent-send/blob/main/LICENSE" target="_blank" style="color:#6b7280">LICENSE</a> for full terms.
|
||||
</p>
|
||||
<p style="font-size:11px;color:#9ca3af;margin-top:8px">
|
||||
Find Silent Send useful? No obligation, but if you'd like to help keep it going:
|
||||
<a href="https://ko-fi.com/YOUR_KOFI_USERNAME" target="_blank" style="color:#6b7280">Buy me a coffee</a>
|
||||
</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
|
||||
+94
-3
@@ -26,7 +26,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
|
||||
// Apply settings to UI
|
||||
$('#showHighlights').checked = settings.showHighlights || false;
|
||||
$('#secretScanning').checked = settings.secretScanning !== false;
|
||||
$('#autoRedactToggle').checked = settings.autoRedact !== false;
|
||||
$('#autoDetect').checked = settings.autoDetect !== false;
|
||||
$('#autoRedactDetected').checked = settings.autoRedactDetected !== false;
|
||||
$('#autoAddDetected').checked = settings.autoAddDetected !== false;
|
||||
@@ -299,8 +299,15 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
await Storage.saveSettings({ showHighlights: e.target.checked });
|
||||
});
|
||||
|
||||
$('#secretScanning').addEventListener('change', async (e) => {
|
||||
await Storage.saveSettings({ secretScanning: e.target.checked });
|
||||
$('#autoRedactToggle').addEventListener('change', async (e) => {
|
||||
await Storage.saveSettings({ autoRedact: e.target.checked });
|
||||
});
|
||||
|
||||
// Custom redact patterns
|
||||
renderCustomRedactPatterns();
|
||||
$('#btnAddRedactPattern').addEventListener('click', addCustomRedactPattern);
|
||||
$('#newRedactPattern').addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') addCustomRedactPattern();
|
||||
});
|
||||
|
||||
$('#autoDetect').addEventListener('change', async (e) => {
|
||||
@@ -804,6 +811,90 @@ function renderDomains() {
|
||||
});
|
||||
}
|
||||
|
||||
// --- Custom Redact Patterns ---
|
||||
|
||||
function addCustomRedactPattern() {
|
||||
const name = $('#newRedactName').value.trim();
|
||||
const pattern = $('#newRedactPattern').value.trim();
|
||||
const redact = $('#newRedactReplacement').value.trim();
|
||||
|
||||
if (!pattern) { alert('Pattern is required.'); return; }
|
||||
|
||||
// Validate regex
|
||||
try {
|
||||
new RegExp(pattern, 'g');
|
||||
} catch (e) {
|
||||
alert('Invalid regex: ' + e.message);
|
||||
return;
|
||||
}
|
||||
|
||||
const label = name || 'Custom Pattern';
|
||||
const replacement = redact || `[REDACTED-${label.toUpperCase().replace(/\s+/g, '-')}]`;
|
||||
|
||||
const patterns = settings.customRedactPatterns || [];
|
||||
patterns.push({
|
||||
id: crypto.randomUUID(),
|
||||
name: label,
|
||||
pattern,
|
||||
redact: replacement,
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
settings.customRedactPatterns = patterns;
|
||||
Storage.saveSettings({ customRedactPatterns: patterns });
|
||||
renderCustomRedactPatterns();
|
||||
|
||||
$('#newRedactName').value = '';
|
||||
$('#newRedactPattern').value = '';
|
||||
$('#newRedactReplacement').value = '';
|
||||
}
|
||||
|
||||
function renderCustomRedactPatterns() {
|
||||
const list = $('#customRedactList');
|
||||
if (!list) return;
|
||||
const patterns = settings.customRedactPatterns || [];
|
||||
|
||||
if (patterns.length === 0) {
|
||||
safeHTML(list, '<div style="font-size:12px;color:#9ca3af;padding:6px 0">No custom patterns defined. Built-in patterns cover common API keys, tokens, and credentials.</div>');
|
||||
return;
|
||||
}
|
||||
|
||||
safeHTML(list, patterns.map((p, i) => `
|
||||
<div style="display:flex;align-items:center;gap:6px;padding:6px 8px;background:#fff;border:1px solid #e5e7eb;border-radius:6px;margin-bottom:4px;flex-wrap:wrap">
|
||||
<label style="display:flex;align-items:center;gap:4px;cursor:pointer;min-width:0">
|
||||
<input type="checkbox" class="redact-toggle" data-index="${i}" ${p.enabled ? 'checked' : ''}>
|
||||
</label>
|
||||
<span style="font-size:12px;font-weight:500;white-space:nowrap">${escapeHtml(p.name)}</span>
|
||||
<code style="font-size:11px;color:#6b7280;background:#f3f4f6;padding:1px 5px;border-radius:3px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:250px" title="${escapeHtml(p.pattern)}">${escapeHtml(p.pattern)}</code>
|
||||
<span style="font-size:11px;color:#9ca3af;margin-left:auto;white-space:nowrap">→ ${escapeHtml(p.redact)}</span>
|
||||
<button class="btn btn-sm btn-danger btn-remove-redact" data-index="${i}" style="padding:2px 6px">×</button>
|
||||
</div>
|
||||
`).join(''));
|
||||
|
||||
// Toggle handlers
|
||||
list.querySelectorAll('.redact-toggle').forEach(toggle => {
|
||||
toggle.addEventListener('change', async () => {
|
||||
const idx = parseInt(toggle.dataset.index, 10);
|
||||
const patterns = settings.customRedactPatterns || [];
|
||||
patterns[idx].enabled = toggle.checked;
|
||||
settings.customRedactPatterns = patterns;
|
||||
await Storage.saveSettings({ customRedactPatterns: patterns });
|
||||
});
|
||||
});
|
||||
|
||||
// Remove handlers
|
||||
list.querySelectorAll('.btn-remove-redact').forEach(btn => {
|
||||
btn.addEventListener('click', async () => {
|
||||
const idx = parseInt(btn.dataset.index, 10);
|
||||
const patterns = settings.customRedactPatterns || [];
|
||||
patterns.splice(idx, 1);
|
||||
settings.customRedactPatterns = patterns;
|
||||
await Storage.saveSettings({ customRedactPatterns: patterns });
|
||||
renderCustomRedactPatterns();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// --- Transfer Data (Export/Import All) ---
|
||||
|
||||
async function getAllData() {
|
||||
|
||||
+14
-10
@@ -198,15 +198,15 @@
|
||||
<section class="tab-content" id="tab-options">
|
||||
<div class="setting-item">
|
||||
<div class="setting-label">
|
||||
<strong>Auto-redact secrets</strong>
|
||||
<span class="setting-desc">Automatically redact API keys, tokens, passwords, SSNs, credit cards</span>
|
||||
<strong>Auto Redact</strong>
|
||||
<span class="setting-desc">Automatically redact API keys, tokens, passwords, SSNs, credit cards, and custom patterns</span>
|
||||
</div>
|
||||
<label class="toggle"><input type="checkbox" id="optSecretScanning" checked><span class="toggle-slider"></span></label>
|
||||
<label class="toggle"><input type="checkbox" id="optAutoRedact" checked><span class="toggle-slider"></span></label>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<div class="setting-label">
|
||||
<strong>Auto-detect PPI</strong>
|
||||
<strong>Auto-detect PII</strong>
|
||||
<span class="setting-desc">Warn about unconfigured personal data (IPs, addresses, paths)</span>
|
||||
</div>
|
||||
<label class="toggle"><input type="checkbox" id="optAutoDetect" checked><span class="toggle-slider"></span></label>
|
||||
@@ -214,8 +214,8 @@
|
||||
|
||||
<div class="setting-item">
|
||||
<div class="setting-label">
|
||||
<strong>Auto-redact detected PPI</strong>
|
||||
<span class="setting-desc">Replace detected PPI with placeholders on send</span>
|
||||
<strong>Auto-redact detected PII</strong>
|
||||
<span class="setting-desc">Replace detected PII with placeholders on send</span>
|
||||
</div>
|
||||
<label class="toggle"><input type="checkbox" id="optAutoRedact" checked><span class="toggle-slider"></span></label>
|
||||
</div>
|
||||
@@ -231,7 +231,7 @@
|
||||
<div class="setting-item">
|
||||
<div class="setting-label">
|
||||
<strong>Document scan preview</strong>
|
||||
<span class="setting-desc">Show PPI findings before uploading documents</span>
|
||||
<span class="setting-desc">Show PII findings before uploading documents</span>
|
||||
</div>
|
||||
<label class="toggle"><input type="checkbox" id="optDocPreview" checked><span class="toggle-slider"></span></label>
|
||||
</div>
|
||||
@@ -271,9 +271,13 @@
|
||||
AES-256 encrypted at rest — unreadable without your password.</span>
|
||||
</div>
|
||||
<div class="privacy-note" style="color:#b45309;background:#fef3c7;padding:6px 8px;border-radius:4px;margin-bottom:6px">
|
||||
Silent Send is a convenience tool, not a security guarantee. It can miss
|
||||
PPI in images, file uploads, unusual name forms, or data you forgot to
|
||||
configure. Always verify sensitive messages before sending.
|
||||
Silent Send is a convenience tool, not a security guarantee. It reduces
|
||||
but cannot eliminate the risk of sharing personal data. It may miss PII
|
||||
in images, unusual formats, or data you haven't configured. Third-party
|
||||
sites may change how they send data at any time, which can cause missed
|
||||
substitutions without warning. Always verify sensitive messages before
|
||||
sending. By using this extension, you accept full responsibility for
|
||||
verifying your data is protected.
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
+22
-22
@@ -1,6 +1,6 @@
|
||||
import SubstitutionEngine from '../lib/substitution-engine.js';
|
||||
import SmartPatterns from '../lib/smart-patterns.js';
|
||||
import SecretScanner from '../lib/secret-scanner.js';
|
||||
import AutoRedact from '../lib/auto-redact.js';
|
||||
import AutoDetect from '../lib/auto-detect.js';
|
||||
import Storage from '../lib/storage.js';
|
||||
import SilentSendSync from '../lib/sync.js';
|
||||
@@ -214,7 +214,7 @@ async function initUnlockedUI() {
|
||||
});
|
||||
|
||||
// Load options tab settings
|
||||
$('#optSecretScanning').checked = settings.secretScanning !== false;
|
||||
$('#optAutoRedact').checked = settings.autoRedact !== false;
|
||||
$('#optAutoDetect').checked = settings.autoDetect !== false;
|
||||
$('#optAutoRedact').checked = settings.autoRedactDetected !== false;
|
||||
$('#optHighlights').checked = settings.showHighlights || false;
|
||||
@@ -223,7 +223,7 @@ async function initUnlockedUI() {
|
||||
|
||||
// Options tab change handlers
|
||||
const optHandlers = [
|
||||
['optSecretScanning', 'secretScanning'],
|
||||
['optAutoRedact', 'autoRedact'],
|
||||
['optAutoDetect', 'autoDetect'],
|
||||
['optAutoRedact', 'autoRedactDetected'],
|
||||
['optHighlights', 'showHighlights'],
|
||||
@@ -715,16 +715,16 @@ function renderTestDiff() {
|
||||
|
||||
const smartResult = SmartPatterns.substitute(input, identity);
|
||||
const explicitResult = SubstitutionEngine.substitute(smartResult.text, mappings);
|
||||
const secretResult = SecretScanner.redact(explicitResult.text);
|
||||
const redactResult = AutoRedact.redact(explicitResult.text, settings.customRedactPatterns);
|
||||
|
||||
const allReplacements = [
|
||||
...smartResult.replacements,
|
||||
...explicitResult.replacements,
|
||||
...secretResult.redactions,
|
||||
...redactResult.redactions,
|
||||
];
|
||||
const finalText = secretResult.text;
|
||||
const finalText = redactResult.text;
|
||||
|
||||
if (finalText === input && secretResult.warnings.length === 0) {
|
||||
if (finalText === input && redactResult.warnings.length === 0) {
|
||||
output.textContent = input;
|
||||
stats.textContent = 'No substitutions detected';
|
||||
return;
|
||||
@@ -740,8 +740,8 @@ function renderTestDiff() {
|
||||
`<span class="sub-highlight" title="Was: ${escapeHtml(r.original)} [${r.pattern || r.category}]">${escapedReplaced}</span>`
|
||||
);
|
||||
}
|
||||
// Highlight secret redactions in red
|
||||
for (const r of secretResult.redactions) {
|
||||
// Highlight auto-redactions in red
|
||||
for (const r of redactResult.redactions) {
|
||||
const escapedReplaced = escapeHtml(r.replaced);
|
||||
html = html.replace(
|
||||
escapedReplaced,
|
||||
@@ -752,28 +752,28 @@ function renderTestDiff() {
|
||||
|
||||
const smartCount = smartResult.replacements.length;
|
||||
const explicitCount = explicitResult.replacements.length;
|
||||
const secretCount = secretResult.redactions.length;
|
||||
const warnCount = secretResult.warnings.length;
|
||||
const redactCount = redactResult.redactions.length;
|
||||
const warnCount = redactResult.warnings.length;
|
||||
const parts = [];
|
||||
if (smartCount > 0) parts.push(`${smartCount} smart`);
|
||||
if (explicitCount > 0) parts.push(`${explicitCount} explicit`);
|
||||
if (secretCount > 0) parts.push(`${secretCount} secrets redacted`);
|
||||
if (redactCount > 0) parts.push(`${redactCount} auto-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`);
|
||||
// Auto-detect unconfigured PII in the final text
|
||||
const piiWarnings = AutoDetect.scan(finalText, identity);
|
||||
if (piiWarnings.length > 0) parts.push(`${piiWarnings.length} PII detected`);
|
||||
|
||||
stats.textContent = `${allReplacements.length} substitution${allReplacements.length !== 1 ? 's' : ''} (${parts.join(', ')})`;
|
||||
|
||||
// Show PPI warnings below stats
|
||||
if (ppiWarnings.length > 0) {
|
||||
const ppiDiv = document.createElement('div');
|
||||
safeHTML(ppiDiv, `<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('')}
|
||||
// Show PII warnings below stats
|
||||
if (piiWarnings.length > 0) {
|
||||
const piiDiv = document.createElement('div');
|
||||
safeHTML(piiDiv, `<div style="margin-top:6px;padding:6px 8px;background:#fef3c7;border-radius:4px;color:#92400e;font-size:11px">
|
||||
<strong>Unconfigured PII detected:</strong>
|
||||
${piiWarnings.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>`);
|
||||
stats.appendChild(ppiDiv);
|
||||
stats.appendChild(piiDiv);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user