feat: custom secret patterns + rename Secret Scanner → Auto Redact
Users can now define custom regex patterns for proprietary token formats, internal URLs with keys, or any secret the built-in scanner doesn't cover. Patterns are added/toggled/removed from the Options page and apply to both the live interception (content.js) and the Test tab (popup.js). Renamed all user-facing "Secret scanning" labels to "Auto Redact" across popup and options. Internal variable names (secretScanning, SecretScanner) kept for backwards compatibility with stored settings. https://claude.ai/code/session_01KF4i7Ra7zCEDskxDBaNtcT
This commit is contained in:
+15
-4
@@ -301,7 +301,7 @@
|
|||||||
const explicit = substitute(smart.text, mappings);
|
const explicit = substitute(smart.text, mappings);
|
||||||
allReplacements.push(...explicit.replacements);
|
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;
|
let finalText = explicit.text;
|
||||||
if (settings.secretScanning !== false) {
|
if (settings.secretScanning !== false) {
|
||||||
const secrets = scanAndRedactSecrets(finalText);
|
const secrets = scanAndRedactSecrets(finalText);
|
||||||
@@ -665,8 +665,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Secret Scanner (inline for page world)
|
// Auto Redact — Secret Scanner (inline for page world)
|
||||||
// Detects API keys, tokens, passwords, SSNs, credit cards, etc.
|
// Detects API keys, tokens, passwords, SSNs, credit cards,
|
||||||
|
// plus user-defined custom patterns from settings.
|
||||||
// ============================================================
|
// ============================================================
|
||||||
const SECRET_PATTERNS = [
|
const SECRET_PATTERNS = [
|
||||||
// OpenAI
|
// OpenAI
|
||||||
@@ -710,7 +711,17 @@
|
|||||||
const redactions = [];
|
const redactions = [];
|
||||||
let result = text;
|
let result = text;
|
||||||
|
|
||||||
for (const pat of SECRET_PATTERNS) {
|
// Combine built-in + custom patterns
|
||||||
|
const allPatterns = [...SECRET_PATTERNS];
|
||||||
|
const custom = settings.customSecretPatterns || [];
|
||||||
|
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;
|
pat.re.lastIndex = 0;
|
||||||
const matches = [];
|
const matches = [];
|
||||||
let m;
|
let m;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* Silent Send - Auto-Detect
|
* Silent Send - Auto-Detect
|
||||||
*
|
*
|
||||||
* Scans text for potential PPI that the user hasn't configured.
|
* Scans text for potential PPI that the user hasn't configured.
|
||||||
* This catches things the identity and secret scanner can't —
|
* This catches things the identity and auto-redact scanner can't —
|
||||||
* because the user forgot or didn't know to configure them.
|
* because the user forgot or didn't know to configure them.
|
||||||
*
|
*
|
||||||
* Returns warnings (not auto-redactions) so the user can decide.
|
* Returns warnings (not auto-redactions) so the user can decide.
|
||||||
|
|||||||
@@ -198,9 +198,9 @@ 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 getOrgSecretPatterns() {
|
||||||
const policy = await this.getPolicy();
|
const policy = await this.getPolicy();
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
/**
|
/**
|
||||||
* Silent Send - Secret Scanner
|
* Silent Send - Auto Redact (Secret Scanner)
|
||||||
*
|
*
|
||||||
* Detects common secret/credential patterns in text and either
|
* Detects common secret/credential patterns in text and either
|
||||||
* warns or auto-redacts them. This catches things the identity-based
|
* warns or auto-redacts them. This catches things the identity-based
|
||||||
* smart patterns can't: API keys, tokens, passwords, SSNs, credit
|
* smart patterns can't: API keys, tokens, passwords, SSNs, credit
|
||||||
* cards, private keys, connection strings, etc.
|
* cards, private keys, connection strings, etc.
|
||||||
*
|
*
|
||||||
|
* Supports user-defined custom patterns for proprietary token formats.
|
||||||
|
*
|
||||||
* Each pattern has:
|
* Each pattern has:
|
||||||
* - name: human-readable label
|
* - name: human-readable label
|
||||||
* - regex: detection pattern
|
* - regex: detection pattern
|
||||||
@@ -163,12 +165,37 @@ const SECRET_PATTERNS = [
|
|||||||
|
|
||||||
const SecretScanner = {
|
const SecretScanner = {
|
||||||
/**
|
/**
|
||||||
* Scan text for secrets. Returns list of findings.
|
* Build the full pattern list (built-in + custom).
|
||||||
|
* Custom patterns come from settings.customSecretPatterns.
|
||||||
*/
|
*/
|
||||||
scan(text) {
|
_buildPatterns(customPatterns) {
|
||||||
const findings = [];
|
const all = [...SECRET_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;
|
||||||
|
},
|
||||||
|
|
||||||
for (const pattern of SECRET_PATTERNS) {
|
/**
|
||||||
|
* Scan text for secrets. Returns list of findings.
|
||||||
|
* @param {string} text
|
||||||
|
* @param {Array} [customPatterns] — from settings.customSecretPatterns
|
||||||
|
*/
|
||||||
|
scan(text, customPatterns) {
|
||||||
|
const findings = [];
|
||||||
|
const patterns = this._buildPatterns(customPatterns);
|
||||||
|
|
||||||
|
for (const pattern of patterns) {
|
||||||
// Reset regex lastIndex
|
// Reset regex lastIndex
|
||||||
pattern.regex.lastIndex = 0;
|
pattern.regex.lastIndex = 0;
|
||||||
let match;
|
let match;
|
||||||
@@ -204,9 +231,11 @@ const SecretScanner = {
|
|||||||
/**
|
/**
|
||||||
* Redact all critical secrets in text. Warnings are not auto-redacted.
|
* Redact all critical secrets in text. Warnings are not auto-redacted.
|
||||||
* Returns { text, redactions[] }
|
* Returns { text, redactions[] }
|
||||||
|
* @param {string} text
|
||||||
|
* @param {Array} [customPatterns] — from settings.customSecretPatterns
|
||||||
*/
|
*/
|
||||||
redact(text) {
|
redact(text, customPatterns) {
|
||||||
const findings = this.scan(text);
|
const findings = this.scan(text, customPatterns);
|
||||||
const redactions = [];
|
const redactions = [];
|
||||||
let result = text;
|
let result = text;
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ const DEFAULT_SETTINGS = {
|
|||||||
autoAddDetected: true,
|
autoAddDetected: true,
|
||||||
maxLogEntries: 100,
|
maxLogEntries: 100,
|
||||||
customDomains: [],
|
customDomains: [],
|
||||||
|
customSecretPatterns: [],
|
||||||
categories: ['name', 'email', 'phone', 'address', 'ssn', 'dob', 'domain', 'password', 'general'],
|
categories: ['name', 'email', 'phone', 'address', 'ssn', 'dob', 'domain', 'password', 'general'],
|
||||||
browserSync: false,
|
browserSync: false,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -49,17 +49,38 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="setting-row">
|
<div class="setting-row">
|
||||||
<div>
|
<div>
|
||||||
<label>Secret scanning</label>
|
<label>Auto Redact</label>
|
||||||
<p class="setting-desc">Auto-detect and redact API keys, tokens, passwords, SSNs, credit card numbers</p>
|
<p class="setting-desc">Automatically detect and redact API keys, tokens, passwords, SSNs, credit card numbers, and custom patterns</p>
|
||||||
</div>
|
</div>
|
||||||
<label class="toggle">
|
<label class="toggle">
|
||||||
<input type="checkbox" id="secretScanning" checked>
|
<input type="checkbox" id="secretScanning" checked>
|
||||||
<span class="toggle-slider"></span>
|
<span class="toggle-slider"></span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</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 Secret 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="customSecretList"></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="newSecretName" 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="newSecretPattern" 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="newSecretRedact" 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="btnAddSecretPattern">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 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>
|
<p class="setting-desc">Warn when potential personal data (IPs, addresses, paths) is detected that you haven't configured</p>
|
||||||
</div>
|
</div>
|
||||||
<label class="toggle">
|
<label class="toggle">
|
||||||
@@ -359,7 +380,7 @@
|
|||||||
<!-- Organization -->
|
<!-- Organization -->
|
||||||
<section class="section">
|
<section class="section">
|
||||||
<h2>Organization</h2>
|
<h2>Organization</h2>
|
||||||
<p class="section-desc">Join an organization to receive required substitution rules and secret scanner patterns from your admin. Org rules merge with your personal rules and cannot be disabled.</p>
|
<p class="section-desc">Join an organization to receive required substitution rules and auto-redact patterns from your admin. Org rules merge with your personal rules and cannot be disabled.</p>
|
||||||
|
|
||||||
<div id="orgNotJoined">
|
<div id="orgNotJoined">
|
||||||
<div style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:8px">
|
<div style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:8px">
|
||||||
|
|||||||
@@ -297,6 +297,13 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
await Storage.saveSettings({ secretScanning: e.target.checked });
|
await Storage.saveSettings({ secretScanning: e.target.checked });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Custom secret patterns
|
||||||
|
renderCustomSecrets();
|
||||||
|
$('#btnAddSecretPattern').addEventListener('click', addCustomSecret);
|
||||||
|
$('#newSecretPattern').addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Enter') addCustomSecret();
|
||||||
|
});
|
||||||
|
|
||||||
$('#autoDetect').addEventListener('change', async (e) => {
|
$('#autoDetect').addEventListener('change', async (e) => {
|
||||||
await Storage.saveSettings({ autoDetect: e.target.checked });
|
await Storage.saveSettings({ autoDetect: e.target.checked });
|
||||||
});
|
});
|
||||||
@@ -798,6 +805,90 @@ function renderDomains() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Custom Secret Patterns ---
|
||||||
|
|
||||||
|
function addCustomSecret() {
|
||||||
|
const name = $('#newSecretName').value.trim();
|
||||||
|
const pattern = $('#newSecretPattern').value.trim();
|
||||||
|
const redact = $('#newSecretRedact').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.customSecretPatterns || [];
|
||||||
|
patterns.push({
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
name: label,
|
||||||
|
pattern,
|
||||||
|
redact: replacement,
|
||||||
|
enabled: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
settings.customSecretPatterns = patterns;
|
||||||
|
Storage.saveSettings({ customSecretPatterns: patterns });
|
||||||
|
renderCustomSecrets();
|
||||||
|
|
||||||
|
$('#newSecretName').value = '';
|
||||||
|
$('#newSecretPattern').value = '';
|
||||||
|
$('#newSecretRedact').value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderCustomSecrets() {
|
||||||
|
const list = $('#customSecretList');
|
||||||
|
if (!list) return;
|
||||||
|
const patterns = settings.customSecretPatterns || [];
|
||||||
|
|
||||||
|
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="secret-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-secret" data-index="${i}" style="padding:2px 6px">×</button>
|
||||||
|
</div>
|
||||||
|
`).join(''));
|
||||||
|
|
||||||
|
// Toggle handlers
|
||||||
|
list.querySelectorAll('.secret-toggle').forEach(toggle => {
|
||||||
|
toggle.addEventListener('change', async () => {
|
||||||
|
const idx = parseInt(toggle.dataset.index, 10);
|
||||||
|
const patterns = settings.customSecretPatterns || [];
|
||||||
|
patterns[idx].enabled = toggle.checked;
|
||||||
|
settings.customSecretPatterns = patterns;
|
||||||
|
await Storage.saveSettings({ customSecretPatterns: patterns });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove handlers
|
||||||
|
list.querySelectorAll('.btn-remove-secret').forEach(btn => {
|
||||||
|
btn.addEventListener('click', async () => {
|
||||||
|
const idx = parseInt(btn.dataset.index, 10);
|
||||||
|
const patterns = settings.customSecretPatterns || [];
|
||||||
|
patterns.splice(idx, 1);
|
||||||
|
settings.customSecretPatterns = patterns;
|
||||||
|
await Storage.saveSettings({ customSecretPatterns: patterns });
|
||||||
|
renderCustomSecrets();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// --- Transfer Data (Export/Import All) ---
|
// --- Transfer Data (Export/Import All) ---
|
||||||
|
|
||||||
async function getAllData() {
|
async function getAllData() {
|
||||||
|
|||||||
@@ -198,8 +198,8 @@
|
|||||||
<section class="tab-content" id="tab-options">
|
<section class="tab-content" id="tab-options">
|
||||||
<div class="setting-item">
|
<div class="setting-item">
|
||||||
<div class="setting-label">
|
<div class="setting-label">
|
||||||
<strong>Secret scanning</strong>
|
<strong>Auto Redact</strong>
|
||||||
<span class="setting-desc">Auto-redact API keys, tokens, passwords, SSNs, credit cards</span>
|
<span class="setting-desc">Automatically redact API keys, tokens, passwords, SSNs, credit cards, and custom patterns</span>
|
||||||
</div>
|
</div>
|
||||||
<label class="toggle"><input type="checkbox" id="optSecretScanning" checked><span class="toggle-slider"></span></label>
|
<label class="toggle"><input type="checkbox" id="optSecretScanning" checked><span class="toggle-slider"></span></label>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+1
-1
@@ -713,7 +713,7 @@ function renderTestDiff() {
|
|||||||
|
|
||||||
const smartResult = SmartPatterns.substitute(input, identity);
|
const smartResult = SmartPatterns.substitute(input, identity);
|
||||||
const explicitResult = SubstitutionEngine.substitute(smartResult.text, mappings);
|
const explicitResult = SubstitutionEngine.substitute(smartResult.text, mappings);
|
||||||
const secretResult = SecretScanner.redact(explicitResult.text);
|
const secretResult = SecretScanner.redact(explicitResult.text, settings.customSecretPatterns);
|
||||||
|
|
||||||
const allReplacements = [
|
const allReplacements = [
|
||||||
...smartResult.replacements,
|
...smartResult.replacements,
|
||||||
|
|||||||
Reference in New Issue
Block a user